Skip to content


Subversion checkout URL

You can clone with
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

540 lines (488 sloc) 14.5 KB
;;; gen-html-docs.clj: Generate HTML documentation for Clojure libs
;; by Craig Andera,,
;; February 13th, 2009
;; Copyright (c) Craig Andera, 2009. All rights reserved. The use
;; and distribution terms for this software are covered by the Eclipse
;; Public License 1.0 (
;; which can be found in the file epl-v10.html at the root of this
;; distribution. By using this software in any fashion, you are
;; agreeing to be bound by the terms of this license. You must not
;; remove this notice, or any other, from this software.
;; Generates a single HTML page that contains the documentation for
;; one or more Clojure libraries. See the comments section at the end
;; of this file for usage.
;; * Make symbols in the source hyperlinks to the appropriate section
;; of the documentation.
;; * Investigate issue with miglayout mentioned here:
;; * Move to clojure.contrib
;; * Change namespace
;; * Change license as appropriate
;; * Double-check doc strings
;; * Remove doc strings from source code
;; * Add collapse/expand functionality for all namespaces
;; * Add collapse/expand functionality for each namespace
;; * See if converting to use clojure.contrib.prxml is possible
;; * Figure out why the source doesn't show up for most things
;; * Add collapsible source
;; * Add links at the top to jump to each namespace
;; * Add object type (var, function, whatever)
;; * Add argument lists for functions
;; * Add links at the top of each namespace to jump to members
;; * Add license statement
;; * Remove the whojure dependency
#^{:author "Craig Andera",
:doc "Generates a single HTML page that contains the documentation for
one or more Clojure libraries."}
(:require [ :as duck-streams])
(:use [clojure.contrib seq-utils str-utils repl-utils def prxml])
(:import [java.lang Exception]
[java.util.regex Pattern]))
;; Doc generation constants
(def *script* " // <![CDATA[
function getElem(id)
if( document.getElementById )
return document.getElementById( id )
else if ( document.all )
return eval( 'document.all.' + id )
return false;
function setDisplayStyle(id,displayStyle)
var elem = getElem (id)
if (elem)
{ = displayStyle
function setLinkToggleText (id, text)
var elem = getElem (id)
if (elem)
elem.innerHTML = text
function collapse(id)
setDisplayStyle (id, 'none')
function expand (id)
setDisplayStyle (id, 'block')
function toggleSource( id )
toggle(id, 'linkto-' + id, 'Hide Source', 'Show Source')
function toggle(targetid, linkid, textWhenOpen, textWhenClosed)
var elem = getElem (targetid)
var link = getElem (linkid)
if (elem && link)
var isOpen = false
if ( == '')
isOpen = link.innerHTML == textWhenOpen
else if( == 'block' )
isOpen = true
if (isOpen)
{ = 'none'
link.innerHTML = textWhenClosed
{ = 'block'
link.innerHTML = textWhenOpen
(def *style* "
padding: 0.5em 0 0 0
font-size: small;
.all-libs-toggle a,.library-contents-toggle a
color: white
white-space: pre
font-size: small;
margin-top: 0.5em
display: none;
border-left: solid lightblue
font-family: monospace
font-weight: bold;
font-size: small;
font-style: italic;
color: darkred
margin: 0 0 1em 0
color: white;
background: darkgreen;
width: 100%
color: white;
background: darkblue;
width: 100%
color: darkred;
margin: 0 0 1em 0
list-style: none
font-weight: bold;
font-size: 105%
(defn- extract-documentation
"Pulls the documentation for a var v out and turns it into HTML"
(if-let [docs (:doc (meta v))]
(fn [l]
[:div {:class "library-member-doc-line"}
(if (= 0 (count l))
[:span {:class "library-member-doc-whitespace"} " "] ; We need something here to make the blank line show up
(re-split #"\n" docs))
(defn- member-type
"Figures out for a var x whether it's a macro, function, var or multifunction"
(let [dx (deref x)]
(:macro (meta x)) :macro
(fn? dx) :fn
(= clojure.lang.MultiFn (:tag (meta x))) :multi
true :var))
(catch Exception e
(defn- anchor-for-member
"Returns a suitable HTML anchor name given a library id and a member
[libid memberid]
(str "member-" libid "-" memberid))
(defn- id-for-member-source
"Returns a suitable HTML id for a source listing given a library and
a member"
[libid memberid]
(str "membersource-" libid "-" memberid))
(defn- id-for-member-source-link
"Returns a suitable HTML id for a link to a source listing given a
library and a member"
[libid memberid]
(str "linkto-membersource-" libid "-" memberid))
(defn- symbol-for
"Given a namespace object ns and a namespaceless symbol memberid
naming a member of that namespace, returns a namespaced symbol that
identifies that member."
[ns memberid]
(symbol (name (ns-name ns)) (name memberid)))
(defn- elide-to-one-line
"Elides a string down to one line."
(re-sub #"(\n.*)+" "..." s))
(defn- elide-string
"Returns a string that is at most the first limit characters of s"
[s limit]
(if (< (- limit 3) (count s))
(str (subs s 0 (- limit 3)) "...")
(defn- doc-elided-src
"Returns the src with the docs elided."
[docs src]
(re-sub (re-pattern (str "\"" (Pattern/quote docs) "\""))
(str "\""
(elide-to-one-line docs)
;; (elide-string docs 10)
;; "..."
(defn- format-source [libid memberid v]
(let [docs (:doc (meta v))
src (if-let [ns (find-ns libid)]
(get-source (symbol-for ns memberid)))]
(if (and src docs)
(doc-elided-src docs src)
(catch Exception ex
(defn- generate-lib-member [libid [n v]]
[:li {:class "library-member"}
[:a {:name (anchor-for-member libid n)}]
[:dl {:class "library-member-table"}
[:dt {:class "library-member-name"}
(str n)]
[:div {:class "library-member-info"}
[:span {:class "library-member-type"} (name (member-type v))]
" "
[:span {:class "library-member-arglists"} (str (:arglists (meta v)))]]
(into [:div {:class "library-member-docs"}] (extract-documentation v))
(let [member-source-id (id-for-member-source libid n)
member-source-link-id (id-for-member-source-link libid n)]
(if-let [member-source (format-source libid n v)]
[:div {:class "library-member-source-section"}
[:div {:class "library-member-source-toggle"}
"[ "
[:a {:href (format "javascript:toggleSource('%s')" member-source-id)
:id member-source-link-id} "Show Source"]
" ]"]
[:div {:class "library-member-source" :id member-source-id}
[:pre member-source]]]))]]])
(defn- anchor-for-library
"Given a symbol id identifying a namespace, returns an identifier
suitable for use as the name attribute of an HTML anchor tag."
(str "library-" id))
(defn- generate-lib-member-link
"Emits a hyperlink to a member of a namespace given libid (a symbol
identifying the namespace) and the vector [n v], where n is the symbol
naming the member in question and v is the var pointing to the
[libid [n v]]
[:a {:class "lib-member-link"
:href (str "#" (anchor-for-member libid n))} (name n)])
(defn- anchor-for-library-contents
"Returns an HTML ID that identifies the element that holds the
documentation contents for the specified library."
(str "library-contents-" lib))
(defn- anchor-for-library-contents-toggle
"Returns an HTML ID that identifies the element that toggles the
visibility of the library contents."
(str "library-contents-toggle-" lib))
(defn- generate-lib-doc
"Emits the HTML that documents the namespace identified by the
symbol lib."
[:div {:class "library"}
[:a {:name (anchor-for-library lib)}]
[:div {:class "library-name"}
[:span {:class "library-contents-toggle"}
"[ "
[:a {:id (anchor-for-library-contents-toggle lib)
:href (format "javascript:toggle('%s', '%s', '-', '+')"
(anchor-for-library-contents lib)
(anchor-for-library-contents-toggle lib))}
" ] "]
(name lib)]
(let [ns (find-ns lib)]
(if ns
(let [lib-members (sort (ns-publics ns))]
[:a {:name (anchor-for-library lib)}]
[:div {:class "library-contents" :id (anchor-for-library-contents lib)}
(into [:div {:class "library-member-links"}]
(interpose " " (map #(generate-lib-member-link lib %) lib-members)))
(into [:ol {:class "library-members"}]
(map #(generate-lib-member lib %) lib-members))])
[:div {:class "missing-library library-contents" :id (anchor-for-library-contents lib)} "Could not load library"]))])
(defn- load-lib
"Calls require on the library identified by lib, eating any
(require lib)
(catch java.lang.Exception x
(defn- generate-lib-link
"Generates a hyperlink to the documentation for a namespace given
lib, a symbol identifying that namespace."
(let [ns (find-ns lib)]
(if ns
[:a {:class "lib-link" :href (str "#" (anchor-for-library lib))} (str (ns-name ns))])))
(defn- generate-lib-links
"Generates the list of hyperlinks to each namespace, given libs, a
vector of symbols naming namespaces."
(into [:div {:class "lib-links"}
[:div {:class "lib-link-header"} "Namespaces"
[:span {:class "all-libs-toggle"}
" [ "
[:a {:href "javascript:expandAllNamespaces()"}
"Expand All"]
" ] [ "
[:a {:href "javascript:collapseAllNamespaces()"}
"Collapse All"]
" ]"]]]
(interpose " " (map generate-lib-link libs))))
(defn generate-toggle-namespace-script
[action toggle-text lib]
(str (format "%s('%s');\n" action (anchor-for-library-contents lib))
(format "setLinkToggleText('%s', '%s');\n" (anchor-for-library-contents-toggle lib) toggle-text)))
(defn generate-all-namespaces-action-script
[action toggle-text libs]
(str (format "function %sAllNamespaces()" action)
(reduce str (map #(generate-toggle-namespace-script action toggle-text %) libs))
(defn generate-documentation
"Returns a string which is the HTML documentation for the libraries
named by libs. Libs is a vector of symbols identifying Clojure
(dorun (map load-lib libs))
(let [writer (new]
(binding [*out* writer]
[:html {:xmlns ""}
[:title "Clojure documentation browser"]
[:style *style*]
[:script {:language "JavaScript" :type "text/javascript"} [:raw! *script*]]
[:script {:language "JavaScript" :type "text/javascript"}
[:raw! "// <![CDATA[!" \newline]
(generate-all-namespaces-action-script "expand" "-" libs)
(generate-all-namespaces-action-script "collapse" "+" libs)
[:raw! \newline "// ]]>"]]]
(let [lib-vec (sort libs)]
(into [:body (generate-lib-links lib-vec)]
(map generate-lib-doc lib-vec)))]))
(.toString writer)))
(defn generate-documentation-to-file
"Calls generate-documentation on the libraries named by libs and
emits the generated HTML to the path named by path."
[path libs]
(duck-streams/spit path (generate-documentation libs)))
(defn gen-all-docs []
Jump to Line
Something went wrong with that request. Please try again.