;;; gen-html-docs.clj: Generate HTML documentation for Clojure libs
;; by Craig Andera, http://pluralsight.com/craig, candera@wangdera.com
;; 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 (http://opensource.org/licenses/eclipse-1.0.php)
;; 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.
;; TODO
;;
;; * Make symbols in the source hyperlinks to the appropriate section
;; of the documentation.
;; * Investigate issue with miglayout mentioned here:
;; http://groups.google.com/group/clojure/browse_thread/thread/5a0c4395e44f5a79/3ae483100366bd3d?lnk=gst&q=documentation+browser#3ae483100366bd3d
;;
;; DONE
;;
;; * 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
(ns
#^{:author "Craig Andera",
:doc "Generates a single HTML page that contains the documentation for
one or more Clojure libraries."}
clojure.contrib.gen-html-docs
(:require [clojure.contrib.duck-streams :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* " //
")
(def *style* "
.library
{
padding: 0.5em 0 0 0
}
.all-libs-toggle,.library-contents-toggle
{
font-size: small;
}
.all-libs-toggle a,.library-contents-toggle a
{
color: white
}
.library-member-doc-whitespace
{
white-space: pre
}
.library-member-source-toggle
{
font-size: small;
margin-top: 0.5em
}
.library-member-source
{
display: none;
border-left: solid lightblue
}
.library-member-docs
{
font-family:monospace
}
.library-member-arglists
{
font-family: monospace
}
.library-member-type
{
font-weight: bold;
font-size: small;
font-style: italic;
color: darkred
}
.lib-links
{
margin: 0 0 1em 0
}
.lib-link-header
{
color: white;
background: darkgreen;
width: 100%
}
.library-name
{
color: white;
background: darkblue;
width: 100%
}
.missing-library
{
color: darkred;
margin: 0 0 1em 0
}
.library-members
{
list-style: none
}
.library-member-name
{
font-weight: bold;
font-size: 105%
}")
(defn- extract-documentation
"Pulls the documentation for a var v out and turns it into HTML"
[v]
(if-let [docs (:doc (meta v))]
(map
(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
l)])
(re-split #"\n" docs))
""))
(defn- member-type
"Figures out for a var x whether it's a macro, function, var or multifunction"
[x]
(try
(let [dx (deref x)]
(cond
(:macro (meta x)) :macro
(fn? dx) :fn
(= clojure.lang.MultiFn (:tag (meta x))) :multi
true :var))
(catch Exception e
:unknown)))
(defn- anchor-for-member
"Returns a suitable HTML anchor name given a library id and a member
id"
[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."
[s]
(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)) "...")
s))
(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)
;; "..."
"\"")
src))
(defn- format-source [libid memberid v]
(try
(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)
src))
(catch Exception ex
nil)))
(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)]
[:dd
[: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."
[id]
(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
member."
[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."
[lib]
(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."
[lib]
(str "library-contents-toggle-" lib))
(defn- generate-lib-doc
"Emits the HTML that documents the namespace identified by the
symbol lib."
[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
exceptions."
[lib]
(try
(require lib)
(catch java.lang.Exception x
nil)))
(defn- generate-lib-link
"Generates a hyperlink to the documentation for a namespace given
lib, a symbol identifying that namespace."
[lib]
(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."
[libs]
(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)
\newline
"{"
\newline
(reduce str (map #(generate-toggle-namespace-script action toggle-text %) libs))
\newline
"}"))
(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
libraries."
[libs]
(dorun (map load-lib libs))
(let [writer (new java.io.StringWriter)]
(binding [*out* writer]
(prxml
[:html {:xmlns "http://www.w3.org/1999/xhtml"}
[:head
[:title "Clojure documentation browser"]
[:style *style*]
[:script {:language "JavaScript" :type "text/javascript"} [:raw! *script*]]
[:script {:language "JavaScript" :type "text/javascript"}
[:raw! "// "]]]
(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)))
(comment
(generate-documentation-to-file
"C:/TEMP/CLJ-DOCS.HTML"
['clojure.contrib.accumulators])
(defn gen-all-docs []
(generate-documentation-to-file
"C:/temp/clj-libs.html"
[
'clojure.set
'clojure.main
'clojure.core
'clojure.zip
'clojure.xml
'clojure.contrib.accumulators
'clojure.contrib.apply-macro
'clojure.contrib.auto-agent
'clojure.contrib.combinatorics
'clojure.contrib.command-line
'clojure.contrib.complex-numbers
'clojure.contrib.cond
'clojure.contrib.condt
'clojure.contrib.def
'clojure.contrib.duck-streams
'clojure.contrib.enum
'clojure.contrib.error-kit
'clojure.contrib.except
'clojure.contrib.fcase
'clojure.contrib.generic
'clojure.contrib.generic.arithmetic
'clojure.contrib.generic.collection
'clojure.contrib.generic.comparison
'clojure.contrib.generic.functor
'clojure.contrib.generic.math-functions
'clojure.contrib.import-static
'clojure.contrib.javadoc
'clojure.contrib.javalog
'clojure.contrib.lazy-seqs
'clojure.contrib.lazy-xml
'clojure.contrib.macro-utils
'clojure.contrib.macros
'clojure.contrib.math
'clojure.contrib.miglayout
'clojure.contrib.mmap
'clojure.contrib.monads
'clojure.contrib.ns-utils
'clojure.contrib.prxml
'clojure.contrib.repl-ln
'clojure.contrib.repl-utils
'clojure.contrib.seq-utils
'clojure.contrib.server-socket
'clojure.contrib.shell-out
'clojure.contrib.sql
'clojure.contrib.stacktrace
'clojure.contrib.stream-utils
'clojure.contrib.str-utils
'clojure.contrib.template
'clojure.contrib.test-clojure
'clojure.contrib.test-contrib
'clojure.contrib.test-is
'clojure.contrib.trace
'clojure.contrib.types
'clojure.contrib.walk
'clojure.contrib.zip-filter
'clojure.contrib.javadoc.browse
'clojure.contrib.json.read
'clojure.contrib.json.write
'clojure.contrib.lazy-xml.with-pull
'clojure.contrib.miglayout.internal
'clojure.contrib.probabilities.finite-distributions
'clojure.contrib.probabilities.monte-carlo
'clojure.contrib.probabilities.random-numbers
'clojure.contrib.sql.internal
'clojure.contrib.test-clojure.evaluation
'clojure.contrib.test-clojure.for
'clojure.contrib.test-clojure.numbers
'clojure.contrib.test-clojure.printer
'clojure.contrib.test-clojure.reader
'clojure.contrib.test-clojure.sequences
'clojure.contrib.test-contrib.shell-out
'clojure.contrib.test-contrib.str-utils
'clojure.contrib.zip-filter.xml
]))
)