view src/clojure/contrib/gen_html_docs.clj @ 10:ef7dbbd6452c

added clojure source goodness
author Robert McIntyre <rlm@mit.edu>
date Sat, 21 Aug 2010 06:25:44 -0400
parents
children
line wrap: on
line source
1 ;;; gen-html-docs.clj: Generate HTML documentation for Clojure libs
3 ;; by Craig Andera, http://pluralsight.com/craig, candera@wangdera.com
4 ;; February 13th, 2009
6 ;; Copyright (c) Craig Andera, 2009. All rights reserved. The use
7 ;; and distribution terms for this software are covered by the Eclipse
8 ;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
9 ;; which can be found in the file epl-v10.html at the root of this
10 ;; distribution. By using this software in any fashion, you are
11 ;; agreeing to be bound by the terms of this license. You must not
12 ;; remove this notice, or any other, from this software.
14 ;; Generates a single HTML page that contains the documentation for
15 ;; one or more Clojure libraries. See the comments section at the end
16 ;; of this file for usage.
18 ;; TODO
19 ;;
20 ;; * Make symbols in the source hyperlinks to the appropriate section
21 ;; of the documentation.
22 ;; * Investigate issue with miglayout mentioned here:
23 ;; http://groups.google.com/group/clojure/browse_thread/thread/5a0c4395e44f5a79/3ae483100366bd3d?lnk=gst&q=documentation+browser#3ae483100366bd3d
24 ;;
25 ;; DONE
26 ;;
27 ;; * Move to clojure.contrib
28 ;; * Change namespace
29 ;; * Change license as appropriate
30 ;; * Double-check doc strings
31 ;; * Remove doc strings from source code
32 ;; * Add collapse/expand functionality for all namespaces
33 ;; * Add collapse/expand functionality for each namespace
34 ;; * See if converting to use clojure.contrib.prxml is possible
35 ;; * Figure out why the source doesn't show up for most things
36 ;; * Add collapsible source
37 ;; * Add links at the top to jump to each namespace
38 ;; * Add object type (var, function, whatever)
39 ;; * Add argument lists for functions
40 ;; * Add links at the top of each namespace to jump to members
41 ;; * Add license statement
42 ;; * Remove the whojure dependency
44 (ns
45 ^{:author "Craig Andera",
46 :doc "Generates a single HTML page that contains the documentation for
47 one or more Clojure libraries."}
48 clojure.contrib.gen-html-docs
49 (:require [clojure.contrib.io :as io]
50 [clojure.contrib.string :as s])
51 (:use [clojure.contrib repl-utils def prxml])
52 (:import [java.lang Exception]
53 [java.util.regex Pattern]))
55 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
56 ;; Doc generation constants
57 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
59 (def *script* " // <![CDATA[
61 function getElem(id)
62 {
63 if( document.getElementById )
64 {
65 return document.getElementById( id )
66 }
67 else if ( document.all )
68 {
69 return eval( 'document.all.' + id )
70 }
71 else
72 return false;
73 }
75 function setDisplayStyle(id,displayStyle)
76 {
77 var elem = getElem (id)
78 if (elem)
79 {
80 elem.style.display = displayStyle
81 }
83 }
85 function setLinkToggleText (id, text)
86 {
87 var elem = getElem (id)
88 if (elem)
89 {
90 elem.innerHTML = text
91 }
92 }
94 function collapse(id)
95 {
96 setDisplayStyle (id, 'none')
97 }
99 function expand (id)
100 {
101 setDisplayStyle (id, 'block')
102 }
104 function toggleSource( id )
105 {
106 toggle(id, 'linkto-' + id, 'Hide Source', 'Show Source')
107 }
109 function toggle(targetid, linkid, textWhenOpen, textWhenClosed)
110 {
111 var elem = getElem (targetid)
112 var link = getElem (linkid)
114 if (elem && link)
115 {
116 var isOpen = false
117 if (elem.style.display == '')
118 {
119 isOpen = link.innerHTML == textWhenOpen
120 }
121 else if( elem.style.display == 'block' )
122 {
123 isOpen = true
124 }
126 if (isOpen)
127 {
128 elem.style.display = 'none'
129 link.innerHTML = textWhenClosed
130 }
131 else
132 {
133 elem.style.display = 'block'
134 link.innerHTML = textWhenOpen
135 }
136 }
137 }
139 //]]>
140 ")
142 (def *style* "
143 .library
144 {
145 padding: 0.5em 0 0 0
146 }
147 .all-libs-toggle,.library-contents-toggle
148 {
149 font-size: small;
150 }
151 .all-libs-toggle a,.library-contents-toggle a
152 {
153 color: white
154 }
155 .library-member-doc-whitespace
156 {
157 white-space: pre
158 }
159 .library-member-source-toggle
160 {
161 font-size: small;
162 margin-top: 0.5em
163 }
164 .library-member-source
165 {
166 display: none;
167 border-left: solid lightblue
168 }
169 .library-member-docs
170 {
171 font-family:monospace
172 }
173 .library-member-arglists
174 {
175 font-family: monospace
176 }
177 .library-member-type
178 {
179 font-weight: bold;
180 font-size: small;
181 font-style: italic;
182 color: darkred
183 }
184 .lib-links
185 {
186 margin: 0 0 1em 0
187 }
189 .lib-link-header
190 {
191 color: white;
192 background: darkgreen;
193 width: 100%
194 }
196 .library-name
197 {
198 color: white;
199 background: darkblue;
200 width: 100%
201 }
203 .missing-library
204 {
205 color: darkred;
206 margin: 0 0 1em 0
207 }
209 .library-members
210 {
211 list-style: none
212 }
214 .library-member-name
215 {
216 font-weight: bold;
217 font-size: 105%
218 }")
220 (defn- extract-documentation
221 "Pulls the documentation for a var v out and turns it into HTML"
222 [v]
223 (if-let [docs (:doc (meta v))]
224 (map
225 (fn [l]
226 [:div {:class "library-member-doc-line"}
227 (if (= 0 (count l))
228 [:span {:class "library-member-doc-whitespace"} " "] ; We need something here to make the blank line show up
229 l)])
230 (s/split #"\n" docs))
231 ""))
233 (defn- member-type
234 "Figures out for a var x whether it's a macro, function, var or multifunction"
235 [x]
236 (try
237 (let [dx (deref x)]
238 (cond
239 (:macro (meta x)) :macro
240 (fn? dx) :fn
241 (= clojure.lang.MultiFn (:tag (meta x))) :multi
242 true :var))
243 (catch Exception e
244 :unknown)))
246 (defn- anchor-for-member
247 "Returns a suitable HTML anchor name given a library id and a member
248 id"
249 [libid memberid]
250 (str "member-" libid "-" memberid))
252 (defn- id-for-member-source
253 "Returns a suitable HTML id for a source listing given a library and
254 a member"
255 [libid memberid]
256 (str "membersource-" libid "-" memberid))
258 (defn- id-for-member-source-link
259 "Returns a suitable HTML id for a link to a source listing given a
260 library and a member"
261 [libid memberid]
262 (str "linkto-membersource-" libid "-" memberid))
264 (defn- symbol-for
265 "Given a namespace object ns and a namespaceless symbol memberid
266 naming a member of that namespace, returns a namespaced symbol that
267 identifies that member."
268 [ns memberid]
269 (symbol (name (ns-name ns)) (name memberid)))
271 (defn- elide-to-one-line
272 "Elides a string down to one line."
273 [s]
274 (s/replace-re #"(\n.*)+" "..." s))
276 (defn- elide-string
277 "Returns a string that is at most the first limit characters of s"
278 [s limit]
279 (if (< (- limit 3) (count s))
280 (str (subs s 0 (- limit 3)) "...")
281 s))
283 (defn- doc-elided-src
284 "Returns the src with the docs elided."
285 [docs src]
286 (s/replace-re (re-pattern (str "\"" (Pattern/quote docs) "\""))
287 (str "\""
288 (elide-to-one-line docs)
289 ;; (elide-string docs 10)
290 ;; "..."
291 "\"")
292 src))
294 (defn- format-source [libid memberid v]
295 (try
296 (let [docs (:doc (meta v))
297 src (if-let [ns (find-ns libid)]
298 (get-source (symbol-for ns memberid)))]
299 (if (and src docs)
300 (doc-elided-src docs src)
301 src))
302 (catch Exception ex
303 nil)))
305 (defn- generate-lib-member [libid [n v]]
306 [:li {:class "library-member"}
307 [:a {:name (anchor-for-member libid n)}]
308 [:dl {:class "library-member-table"}
309 [:dt {:class "library-member-name"}
310 (str n)]
311 [:dd
312 [:div {:class "library-member-info"}
313 [:span {:class "library-member-type"} (name (member-type v))]
314 " "
315 [:span {:class "library-member-arglists"} (str (:arglists (meta v)))]]
316 (into [:div {:class "library-member-docs"}] (extract-documentation v))
317 (let [member-source-id (id-for-member-source libid n)
318 member-source-link-id (id-for-member-source-link libid n)]
319 (if-let [member-source (format-source libid n v)]
320 [:div {:class "library-member-source-section"}
321 [:div {:class "library-member-source-toggle"}
322 "[ "
323 [:a {:href (format "javascript:toggleSource('%s')" member-source-id)
324 :id member-source-link-id} "Show Source"]
325 " ]"]
326 [:div {:class "library-member-source" :id member-source-id}
327 [:pre member-source]]]))]]])
329 (defn- anchor-for-library
330 "Given a symbol id identifying a namespace, returns an identifier
331 suitable for use as the name attribute of an HTML anchor tag."
332 [id]
333 (str "library-" id))
335 (defn- generate-lib-member-link
336 "Emits a hyperlink to a member of a namespace given libid (a symbol
337 identifying the namespace) and the vector [n v], where n is the symbol
338 naming the member in question and v is the var pointing to the
339 member."
340 [libid [n v]]
341 [:a {:class "lib-member-link"
342 :href (str "#" (anchor-for-member libid n))} (name n)])
344 (defn- anchor-for-library-contents
345 "Returns an HTML ID that identifies the element that holds the
346 documentation contents for the specified library."
347 [lib]
348 (str "library-contents-" lib))
350 (defn- anchor-for-library-contents-toggle
351 "Returns an HTML ID that identifies the element that toggles the
352 visibility of the library contents."
353 [lib]
354 (str "library-contents-toggle-" lib))
356 (defn- generate-lib-doc
357 "Emits the HTML that documents the namespace identified by the
358 symbol lib."
359 [lib]
360 [:div {:class "library"}
361 [:a {:name (anchor-for-library lib)}]
362 [:div {:class "library-name"}
363 [:span {:class "library-contents-toggle"}
364 "[ "
365 [:a {:id (anchor-for-library-contents-toggle lib)
366 :href (format "javascript:toggle('%s', '%s', '-', '+')"
367 (anchor-for-library-contents lib)
368 (anchor-for-library-contents-toggle lib))}
369 "-"]
370 " ] "]
371 (name lib)]
372 (let [ns (find-ns lib)]
373 (if ns
374 (let [lib-members (sort (ns-publics ns))]
375 [:a {:name (anchor-for-library lib)}]
376 [:div {:class "library-contents" :id (anchor-for-library-contents lib)}
377 (into [:div {:class "library-member-links"}]
378 (interpose " " (map #(generate-lib-member-link lib %) lib-members)))
379 (into [:ol {:class "library-members"}]
380 (map #(generate-lib-member lib %) lib-members))])
381 [:div {:class "missing-library library-contents" :id (anchor-for-library-contents lib)} "Could not load library"]))])
383 (defn- load-lib
384 "Calls require on the library identified by lib, eating any
385 exceptions."
386 [lib]
387 (try
388 (require lib)
389 (catch java.lang.Exception x
390 nil)))
392 (defn- generate-lib-link
393 "Generates a hyperlink to the documentation for a namespace given
394 lib, a symbol identifying that namespace."
395 [lib]
396 (let [ns (find-ns lib)]
397 (if ns
398 [:a {:class "lib-link" :href (str "#" (anchor-for-library lib))} (str (ns-name ns))])))
400 (defn- generate-lib-links
401 "Generates the list of hyperlinks to each namespace, given libs, a
402 vector of symbols naming namespaces."
403 [libs]
404 (into [:div {:class "lib-links"}
405 [:div {:class "lib-link-header"} "Namespaces"
406 [:span {:class "all-libs-toggle"}
407 " [ "
408 [:a {:href "javascript:expandAllNamespaces()"}
409 "Expand All"]
410 " ] [ "
411 [:a {:href "javascript:collapseAllNamespaces()"}
412 "Collapse All"]
413 " ]"]]]
414 (interpose " " (map generate-lib-link libs))))
416 (defn generate-toggle-namespace-script
417 [action toggle-text lib]
418 (str (format "%s('%s');\n" action (anchor-for-library-contents lib))
419 (format "setLinkToggleText('%s', '%s');\n" (anchor-for-library-contents-toggle lib) toggle-text)))
421 (defn generate-all-namespaces-action-script
422 [action toggle-text libs]
423 (str (format "function %sAllNamespaces()" action)
424 \newline
425 "{"
426 \newline
427 (reduce str (map #(generate-toggle-namespace-script action toggle-text %) libs))
428 \newline
429 "}"))
431 (defn generate-documentation
432 "Returns a string which is the HTML documentation for the libraries
433 named by libs. Libs is a vector of symbols identifying Clojure
434 libraries."
435 [libs]
436 (dorun (map load-lib libs))
437 (let [writer (new java.io.StringWriter)]
438 (binding [*out* writer]
439 (prxml
440 [:html {:xmlns "http://www.w3.org/1999/xhtml"}
441 [:head
442 [:title "Clojure documentation browser"]
443 [:style *style*]
444 [:script {:language "JavaScript" :type "text/javascript"} [:raw! *script*]]
446 [:script {:language "JavaScript" :type "text/javascript"}
447 [:raw! "// <![CDATA[!" \newline]
448 (generate-all-namespaces-action-script "expand" "-" libs)
449 (generate-all-namespaces-action-script "collapse" "+" libs)
450 [:raw! \newline "// ]]>"]]]
451 (let [lib-vec (sort libs)]
452 (into [:body (generate-lib-links lib-vec)]
453 (map generate-lib-doc lib-vec)))]))
454 (.toString writer)))
457 (defn generate-documentation-to-file
458 "Calls generate-documentation on the libraries named by libs and
459 emits the generated HTML to the path named by path."
460 [path libs]
461 (io/spit path (generate-documentation libs)))
463 (comment
464 (generate-documentation-to-file
465 "C:/TEMP/CLJ-DOCS.HTML"
466 ['clojure.contrib.accumulators])
468 (defn gen-all-docs []
469 (generate-documentation-to-file
470 "C:/temp/clj-libs.html"
471 [
472 'clojure.set
473 'clojure.main
474 'clojure.core
475 'clojure.zip
476 'clojure.xml
477 'clojure.contrib.accumulators
478 'clojure.contrib.apply-macro
479 'clojure.contrib.auto-agent
480 'clojure.contrib.combinatorics
481 'clojure.contrib.command-line
482 'clojure.contrib.complex-numbers
483 'clojure.contrib.cond
484 'clojure.contrib.def
485 'clojure.contrib.io
486 'clojure.contrib.enum
487 'clojure.contrib.error-kit
488 'clojure.contrib.except
489 'clojure.contrib.fcase
490 'clojure.contrib.generic
491 'clojure.contrib.generic.arithmetic
492 'clojure.contrib.generic.collection
493 'clojure.contrib.generic.comparison
494 'clojure.contrib.generic.functor
495 'clojure.contrib.generic.math-functions
496 'clojure.contrib.import-static
497 'clojure.contrib.javadoc
498 'clojure.contrib.javalog
499 'clojure.contrib.lazy-seqs
500 'clojure.contrib.lazy-xml
501 'clojure.contrib.macro-utils
502 'clojure.contrib.macros
503 'clojure.contrib.math
504 'clojure.contrib.miglayout
505 'clojure.contrib.mmap
506 'clojure.contrib.monads
507 'clojure.contrib.ns-utils
508 'clojure.contrib.prxml
509 'clojure.contrib.repl-ln
510 'clojure.contrib.repl-utils
511 'clojure.contrib.seq
512 'clojure.contrib.server-socket
513 'clojure.contrib.shell
514 'clojure.contrib.sql
515 'clojure.contrib.stream-utils
516 'clojure.contrib.string
517 'clojure.contrib.test-contrib
518 'clojure.contrib.trace
519 'clojure.contrib.types
520 'clojure.contrib.zip-filter
521 'clojure.contrib.javadoc.browse
522 'clojure.contrib.json.read
523 'clojure.contrib.json.write
524 'clojure.contrib.lazy-xml.with-pull
525 'clojure.contrib.miglayout.internal
526 'clojure.contrib.probabilities.finite-distributions
527 'clojure.contrib.probabilities.monte-carlo
528 'clojure.contrib.probabilities.random-numbers
529 'clojure.contrib.sql.internal
530 'clojure.contrib.test-clojure.evaluation
531 'clojure.contrib.test-clojure.for
532 'clojure.contrib.test-clojure.numbers
533 'clojure.contrib.test-clojure.printer
534 'clojure.contrib.test-clojure.reader
535 'clojure.contrib.test-clojure.sequences
536 'clojure.contrib.test-contrib.shell
537 'clojure.contrib.test-contrib.string
538 'clojure.contrib.zip-filter.xml
539 ]))
540 )