Merge branch 'master' into more_marginalia

This merges Fogus's changes Jan 7 - Jan 25 into goodmike's banch for
  additional comments.
Conflicts:
	src/marginalia/core.clj
This commit is contained in:
Michael Harrison 2011-02-01 09:18:32 -05:00
commit 2a654c2b85
7 changed files with 405 additions and 228 deletions

View file

@ -4,10 +4,7 @@ Marginalia
Marginalia is a source documentation too that parses Clojure code and outputs an side-by-side source view with appropriate comments and docstrings aligned. Marginalia is a source documentation too that parses Clojure code and outputs an side-by-side source view with appropriate comments and docstrings aligned.
To get a quick look at what marginalia output looks like: To get a quick look at what marginalia output looks like, then [visit the official site](http://fogus.me/fun/marginalia/).
1. `git clone https://github.com/fogus/marginalia.git`
2. `open ./marginalia/example-output/uberdoc.html` (or [look here](http://fogus.me/fun/marginalia/))
Usage Usage
----- -----
@ -59,7 +56,6 @@ TODO
---- ----
* paragraph anchors * paragraph anchors
* options for non-uber-docs * options for non-uber-docs
* new docstring/comment reader
* Maven generation support * Maven generation support
* POM parsing * POM parsing

View file

@ -1,18 +1,18 @@
(defproject marginalia "0.3.2" (defproject marginalia "0.5.0-alpha"
:description "lightweight literate programming for clojure -- inspired by [docco](http://jashkenas.github.com/docco/)" :description "lightweight literate programming for clojure -- inspired by [docco](http://jashkenas.github.com/docco/)"
:main marginalia.core :main marginalia.core
:eval-in-leiningen true
:dependencies :dependencies
[[org.clojure/clojure "1.2.0"] [[org.clojure/clojure "1.2.0"]
[org.clojars.nakkaya/markdownj "1.0.2b4"] [hiccup "0.3.0"]
[hiccup "0.3.0"]] [org.markdownj/markdownj "0.3.0-1.0.2b4"]]
:dev-dependencies :dev-dependencies
[[lein-clojars "0.5.0-SNAPSHOT"] [[lein-clojars "0.5.0-SNAPSHOT"]
[jline "0.9.94"] [jline "0.9.94"]
[swank-clojure "1.2.1"] [swank-clojure "1.2.1"]
;;Needed for testing lein plugin ;;Needed for testing lein plugin
[hiccup "0.3.0"] [hiccup "0.3.0"]
[org.clojars.nakkaya/markdownj "1.0.2b4"] [org.markdownj/markdownj "0.3.0-1.0.2b4"]]
[marginalia "0.3.2"]]
;;Needed for testing cake plugin ;;Needed for testing cake plugin
:tasks [marginalia.tasks] :tasks [marginalia.tasks]
;;Needed for testing Latex equation formatting. You must download ;;Needed for testing Latex equation formatting. You must download

View file

@ -1,27 +1,15 @@
(ns leiningen.marg (ns leiningen.marg
"# Leiningen plugin for running marginalia against your project. "Run Marginalia against your project source files."
(:use [leiningen.compile :only [eval-in-project]]
## Usage marginalia.core))
1. Add `[marginalia \"<current version number>\"]` to your project.clj's `:dev-dependencies` section.
2. run `lein marg` from your project's root directory. "
(:use [marginalia.core]))
(defn marg [project & args] (defn marg [project & args]
(run-marginalia args)) (eval-in-project project
`(marginalia.core/run-marginalia (list ~@args))
;; You can pass a file, directory, multiple files and/or directories to marginalia like so: nil
;; nil
;; $ lein marg # runs marginalia on all the cljs found in your ./src dir. '(require 'marginalia.core)))
;; $ lein marg ./path/to/files # runs marginalia on all cljs found in ./path/to/files
;; $ lein marg ./path/to/file.clj # runs marginalia on ./path/to/file.clj only.
;; $ lein marg ./path/to/one.clj ./path/to/another.clj
;; $ lein marg ./path/to/dir ./path/to/some/random.clj
;;
;; This allows you to control the order in which sections appear in the generated
;; documentation. For example, in marginalia's docs, the leiningen.marg namespace
;; forced to the bottom of the namespace ordering by using this command:
;;
;; $ lein marg ./src/marginalia ./src/leiningen
(.setMeta #'marg
(assoc (.meta #'marg)
:doc (with-out-str (run-marginalia (list "-h")))))

View file

@ -1,18 +1,51 @@
;; ## A new way to think about programs
;;
;; What if your code and its documentation were one and the same?
;;
;; Much of the philosophy guiding literate programming is the realization of the answer to this question.
;; However, if literate programming stands as a comprehensive programming methodology at one of end of the
;; spectrum and no documentation stands as its antithesis, then Marginalia falls somewhere between. That is,
;; you should always aim for comprehensive documentation, but the shortest path to a useful subset is the
;; commented source code itself.
;;
;; ## The art of Marginalia
;;
;; If youre fervently writing code that is heavily documented, then using Marginalia for your Clojure projects
;; is as simple as running it on your codebase. However, if youre unaccustomed to documenting your source, then
;; the guidelines herein will help you make the most out of Marginalia for true-power documentation.
;;
;; Following the guidelines will work to make your code not only easier to follow it will make it better.
;; The very process of using Marginalia will help to crystalize your understanding of problem and its solution(s).
;;
;; The quality of the prose in your documentation will often reflect the quality of the code itself thus highlighting
;; problem areas. The elimination of problem areas will solidify your code and its accompanying prose. Marginalia
;; provides a virtuous circle spiraling inward toward maximal code quality.
;;
;; ## The one true way
;;
;; 1. Start by running Marginalia against your code
;; 2. Cringe at the sad state of your code commentary
;; 3. Add docstrings and code comments as appropriate
;; 4. Generate the documentation again
;; 5. Read the resulting documentation
;; 6. Make changes to code and documentation so that the “dialog” flows sensibly
;; 7. Repeat from step #4 until complete
;;
(ns marginalia.core (ns marginalia.core
"**Core** provides all of the functionality around parsing clojure source files
into an easily consumable format."
(:require [clojure.java.io :as io] (:require [clojure.java.io :as io]
[clojure.string :as str]) [clojure.string :as str])
(:use [marginalia.html :only (uberdoc-html)] (:use [marginalia
[clojure.contrib.find-namespaces :only (read-file-ns-decl)]) [html :only (uberdoc-html)]
[parser :only (parse-file)]]
[clojure.contrib
[find-namespaces :only (read-file-ns-decl)]
[command-line :only (print-help with-command-line)]])
(:gen-class)) (:gen-class))
(def *test* "./src/cljojo/core.clj") (def ^{:dynamic true} *test* "src/marginalia/core.clj")
(def *docs* "docs") (def ^{:dynamic true} *docs* "./docs")
(def *comment* #"^\s*;;\s?") (def ^{:dynamic true} *comment* #"^\s*;;\s?")
(def *divider-text* "\n;;DIVIDER\n")
(def *divider-html* #"\n*<span class=\"c[1]?\">;;DIVIDER</span>\n*")
;; ## File System Utilities ;; ## File System Utilities
@ -53,13 +86,11 @@
;; ## Project Info Parsing ;; ## Project Info Parsing
;; Marginalia will parse info out of your project.clj to display in ;; Marginalia will parse info out of your project.clj to display in
;; the generated html file's header. ;; the generated html file's header.
;;
;; ![TODO](http://images.fogus.me/badges/todo.png "POM") add pom.xml support.
(defn parse-project-form (defn parse-project-file
"Pulls apart the seq of project information and assembles it into a map of "Parses a project.clj file and returns a map in the following form
the following form
{:name {:name
:version :version
:dependencies :dependencies
@ -91,14 +122,6 @@
;; ## Source File Analysis ;; ## Source File Analysis
;; Marginalia will parse your code to extract doc strings for display in the
;; generated html file.
(defn parse [src]
(for [line (line-seq src)]
(if (re-find *comment* line)
{:docs-text (str (str/replace line *comment* ""))}
{:code-text (str line)})))
(defn end-of-block? [cur-group groups lines] (defn end-of-block? [cur-group groups lines]
(let [line (first lines) (let [line (first lines)
@ -111,9 +134,15 @@
(defn merge-line [line m] (defn merge-line [line m]
(cond (cond
(:docstring-text line) (assoc m :docs (conj (get m :docs []) line)) (:docstring-text line) (assoc m
(:code-text line) (assoc m :codes (conj (get m :codes []) line)) :docs
(:docs-text line) (assoc m :docs (conj (get m :docs []) line)))) (conj (get m :docs []) line))
(:code-text line) (assoc m
:codes
(conj (get m :codes []) line))
(:docs-text line) (assoc m
:docs
(conj (get m :docs []) line))))
(defn group-lines [doc-lines] (defn group-lines [doc-lines]
(loop [cur-group {} (loop [cur-group {}
@ -127,96 +156,12 @@
:else (recur (merge-line (first lines) cur-group) groups (rest lines))))) :else (recur (merge-line (first lines) cur-group) groups (rest lines)))))
;; Hacktastic, these ad-hoc checks should be replaced with something
;; more robust.
(defn docstring-line? [line sections]
(let [l (last sections)
last-code-text (get l :code-text "")]
(try
(or
;; Last line contain defn &&
;; last line not contain what looks like a param vector &&
;; current line start with a quote
(and (re-find #"\(defn" last-code-text)
(not (re-find #"\[.*\]" last-code-text))
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text a deftask, and does the
;; current line start with a quote?
(and (re-find #"^\(deftask" (str/trim last-code-text))
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text the start of a ns
;; decl, and does the current line start with a quote?
(and (re-find #"^\(ns" last-code-text)
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text the start of a defprotocol,
;; and does the current line start with a quote?
(and (re-find #"^\(defprotocol" last-code-text)
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text the start of a defmulti,
;; and does the current line start with a quote?
(and (re-find #"^\(defmulti" last-code-text)
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text the start of a defmethod,
;; and does the current line start with a quote?
(and (re-find #"^\(defmethod" last-code-text)
(re-find #"^\"" (str/trim (str line))))
;; Is the last line's code-text the start of a defmacro,
;; and does the current line start with a quote?
(and (re-find #"^\(defmacro" last-code-text)
(re-find #"^\"" (str/trim (str line))))
;; Is the prev line a docstring, prev line not end with a quote,
;; and the current line empty?
(and (:docstring-text l)
(not (re-find #"\"$" (str/trim (:docstring-text l)))))
;; Is the prev line a docstring, the prev line not end with a quote,
;; and the current line not an empty string?
(and (:docstring-text l)
(not (re-find #"[^\\]\"$" (str/trim (:docstring-text l))))
(= "" (str/trim (str line)))))
(catch Exception e nil))))
(defn parse [src]
(loop [[line & more] (line-seq src) cnum 1 dnum 0 sections []]
(if line
(if (re-find *comment* line)
(recur more
cnum
(inc dnum)
(conj sections {:docs-text (str (str/replace line *comment* "")) :line (+ cnum dnum)}))
(recur more
(inc cnum)
0
(if (docstring-line? (str line) sections)
(conj sections {:docstring-text (str line) :line cnum})
(conj sections {:code-text (str line) :line cnum}))))
sections)))
;; How is this handled?
;; I wonder?
;; No idea ne
(defn gen-doc! [path]
(println "Generating documentation for " path)
(with-open [src (io/reader (io/file path))]
(doseq [section (parse src)]
;; and this?
(println section))))
(defn gen-doc! [path]
(with-open [src (io/reader (io/file path))]
(parse src)))
(re-find *comment* " ;; this is a comment")
(defn path-to-doc [fn] (defn path-to-doc [fn]
(let [ns (-> (java.io.File. fn) (let [ns (-> (java.io.File. fn)
(read-file-ns-decl) (read-file-ns-decl)
(second) (second)
(str)) (str))
groups (->> fn groups (parse-file fn)]
(gen-doc!)
(group-lines))]
{:ns ns {:ns ns
:groups groups})) :groups groups}))
@ -233,8 +178,7 @@
- :version - :version
" "
[output-file-name files-to-analyze props] [output-file-name files-to-analyze props]
(spit output-file-name (let [source (uberdoc-html
(uberdoc-html
output-file-name output-file-name
(map path-to-doc files-to-analyze) (map path-to-doc files-to-analyze)
props))) props)))
@ -270,27 +214,34 @@
using the found source files and a project file expected to be in its default location. using the found source files and a project file expected to be in its default location.
If no source files are found, complain with a usage message." If no source files are found, complain with a usage message."
[sources] [args]
(let [sources (format-sources sources)] (with-command-line args
(str "Leiningen plugin for running marginalia against your project.\n\n"
"Usage: lein marg <options?> <src1> ... <src-n>\n")
[[dir d "Directory into which the documentation will be written" "./docs"]
[file f "File into which the documentation will be written" "uberdoc.html"]
src]
(let [sources (format-sources (seq src))]
(if-not sources (if-not sources
(do (do
(println "Wrong number of arguments passed to marginalia.") (println "Wrong number of arguments passed to marginalia.")
(println "Please present paths to source files as follows:") (print-help))
(usage)) (binding [*docs* dir]
(do
(println "Generating uberdoc for the following source files:") (println "Generating uberdoc for the following source files:")
(doseq [s sources] (doseq [s sources]
(println " " s)) (println " " s))
(println) (println)
(ensure-directory! "./docs") (ensure-directory! *docs*)
(uberdoc! "./docs/uberdoc.html" sources (parse-project-file)) (uberdoc! (str *docs* "/" file) sources (parse-project-file))
(println "Done generating your docs, please see ./docs/uberdoc.html") (println "Done generating your documentation, please see"
(println))))) (str *docs* "/" file))
(println ""))))))
(defn -main (defn -main
"The main entry point into Marginalia." "The main entry point into Marginalia."
[& sources] [& sources]
(run-marginalia sources)) (binding [marginalia.html/*resources* ""]
(run-marginalia sources)))
;; # Example Usage ;; # Example Usage

View file

@ -1,20 +1,11 @@
(ns marginalia.html (ns marginalia.html
"# Utilities for converting parse results into html. "Utilities for converting parse results into html."
## Plus a few other goodies.
Here's a random code block (println \"hi!\")
Like I said:
* utils for docs -> html
* other goodies
hello world"
(:use [hiccup.core :only (html escape-html)] (:use [hiccup.core :only (html escape-html)]
[hiccup.page-helpers :only (doctype)]) [hiccup.page-helpers :only (doctype)])
(:require [clojure.string :as str]) (:require [clojure.string :as str])
(:import [com.petebevin.markdown MarkdownProcessor])) (:import [com.petebevin.markdown MarkdownProcessor]))
(def ^{:dynamic true} *resources* "./resources/")
(defn css-rule [rule] (defn css-rule [rule]
(let [sels (reverse (rest (reverse rule))) (let [sels (reverse (rest (reverse rule)))
@ -26,6 +17,7 @@
"Quick and dirty dsl for inline css rules, similar to hiccup. "Quick and dirty dsl for inline css rules, similar to hiccup.
ex. `(css [:h1 {:color \"blue\"}] [:div.content p {:text-indent \"1em\"}])` ex. `(css [:h1 {:color \"blue\"}] [:div.content p {:text-indent \"1em\"}])`
-> `h1 {color: blue;} div.content p {text-indent: 1em;}`" -> `h1 {color: blue;} div.content p {text-indent: 1em;}`"
[& rules] [& rules]
(html [:style {:type "text/css"} (html [:style {:type "text/css"}
@ -59,8 +51,13 @@
(def mdp (com.petebevin.markdown.MarkdownProcessor.)) (def mdp (com.petebevin.markdown.MarkdownProcessor.))
(defn md (defn md
"Markdown string to html converter. Translates strings like \"# header!\" "Markdown string to html converter. Translates strings like:
-> \"<h1>header!</h1>"
\"# header!\" -> `\"<h1>header!</h1>\"`
\"## header!\" -> `\"<h2>header!</h2>\"`
..."
[s] [s]
(.markdown mdp s)) (.markdown mdp s))
@ -68,13 +65,14 @@
"Inserts super-fancy characters into the doc section." "Inserts super-fancy characters into the doc section."
[s] [s]
(-> s (-> s
(str/replace #"-&gt;" "&rarr;") (str/replace #"->" "&rarr;")
(str/replace #"&quot;" "\""))) (str/replace #"&quot;" "\"")))
;; As a result of docifying then grouping, you'll end up with a seq like this one: ;; As a result of docifying then grouping, you'll end up with a seq like this one:
;; ;; <pre><code>[...
;; [{:docs [{:docs-text "Some doc text"}] ;; {:docs [{:docs-text "Some doc text"}]
;; :codes [{:code-text "(def something \"hi\")"}]}] ;; :codes [{:code-text "(def something \"hi\")"}]}
;; ...]</code></pre>
;; ;;
;; `docs-to-html` and `codes-to-html` convert their respective entries into html, ;; `docs-to-html` and `codes-to-html` convert their respective entries into html,
;; and `group-to-html` calls them on each seq item to do so. ;; and `group-to-html` calls them on each seq item to do so.
@ -98,33 +96,29 @@
"Converts a docs section to html by threading each doc line through the forms "Converts a docs section to html by threading each doc line through the forms
outlined above. outlined above.
ex. `(docs-to-html [{:doc-text \"#hello world!\"} {:docstring-text \"I'm a docstring!}]) ex. (docs-to-html [{:doc-text \"# hello world!\"} {:docstring-text \"I'm a docstring!}])
-> \"<h1>hello world!</h1><br />\"`
-> `\"<h1>hello world!</h1><br />\"`
" "
[docs] [docs]
(->> docs (-> docs
(map #(if (:docs-text %) str
(prep-docs-text (:docs-text %)) prep-docs-text
(prep-docstring-text (:docstring-text %)))) replace-special-chars
(map replace-special-chars)
(interpose "\n")
(apply str)
(md))) (md)))
(defn codes-to-html [code-block]
(html [:pre {:class "brush: clojure"} code-block]))
(defn codes-to-html [codes] (defn section-to-html [section]
(html [:pre {:class "brush: clojure"} (html [:tr
(->> codes [:td {:class "docs"} (docs-to-html
(map :code-text) (if (= (:type section) :comment)
(map escape-html) (:raw section)
(interpose "\n") (:docstring section)))]
(apply str))])) [:td {:class "codes"}] (if (= (:type section) :code)
(codes-to-html (:raw section))
(defn group-to-html [group] "")]))
(html
[:tr
[:td {:class "docs"} (docs-to-html (:docs group))]
[:td {:class "codes"} (codes-to-html (:codes group))]]))
(defn dependencies-html [deps & header-name] (defn dependencies-html [deps & header-name]
(let [header-name (or header-name "dependencies")] (let [header-name (or header-name "dependencies")]
@ -170,7 +164,7 @@
(when css (when css
(map #(vector :link {:tyle "text/css" :rel "stylesheet" :href %}) css)))))) (map #(vector :link {:tyle "text/css" :rel "stylesheet" :href %}) css))))))
;; Is <h1/> overloaded? Maybe we should consider redistributing ;; Is &lt;h1/&gt; overloaded? Maybe we should consider redistributing
;; header numbers instead of adding classes to all the h1 tags. ;; header numbers instead of adding classes to all the h1 tags.
(defn header-html [project-info] (defn header-html [project-info]
(html (html
@ -221,7 +215,7 @@
[:a {:href "#toc" :class "toc-link"} [:a {:href "#toc" :class "toc-link"}
"toc"]]]] "toc"]]]]
[:td {:class "codes"}]] [:td {:class "codes"}]]
(map group-to-html (:groups doc)) (map section-to-html (:groups doc))
[:tr [:tr
[:td {:class "spacer docs"} "&nbsp;"] [:td {:class "spacer docs"} "&nbsp;"]
[:td {:class "codes"}]])) [:td {:class "codes"}]]))
@ -358,16 +352,16 @@
[:head [:head
[:meta {:http-equiv "Content-Type" :content "text/html" :charset "utf-8"}] [:meta {:http-equiv "Content-Type" :content "text/html" :charset "utf-8"}]
[:meta {:name "description" :content (:description project-metadata)}] [:meta {:name "description" :content (:description project-metadata)}]
(inline-js "jquery-1.4.4.min.js") (inline-js (str *resources* "jquery-1.4.4.min.js"))
(inline-js "xregexp-min.js") (inline-js (str *resources* "xregexp-min.js"))
(inline-js "shCore.js") (inline-js (str *resources* "shCore.js"))
(inline-js "shBrushClojure.js") (inline-js (str *resources* "shBrushClojure.js"))
(inline-js "app.js") (inline-js (str *resources* "app.js"))
#_[:script {:type "text/javascript" :src "./../resources/app.js"}] #_[:script {:type "text/javascript" :src "./../resources/app.js"}]
(inline-css "shCore.css") (inline-css (str *resources* "shCore.css"))
(css (css
[:.syntaxhighlighter {:overflow "hidden !important"}]) [:.syntaxhighlighter {:overflow "hidden !important"}])
(inline-css "shThemeEclipse.css") (inline-css (str *resources* "shThemeEclipse.css"))
reset-css reset-css
header-css header-css
floating-toc-css floating-toc-css

233
src/marginalia/parser.clj Normal file
View file

@ -0,0 +1,233 @@
;; This file contains the complete Marginalia parser.
;; It leverages the Clojure reader instead of implementing a complete
;; Clojure parsing solution.
(ns marginalia.parser
"Provides the parsing facilities for Marginalia."
(:refer-clojure :exclude [replace])
(:use [clojure.contrib [reflect :only (get-field)]]
[clojure [string :only (join replace)]]))
(defrecord Comment [content])
(defmethod print-method Comment [comment ^String out]
(.write out (str \" (.content comment) \")))
(def top-level-comments (atom []))
(def sub-level-comments (atom []))
(def *comments* nil)
(defn read-comment [reader semicolon]
(let [sb (StringBuilder.)]
(.append sb semicolon)
(loop [c (.read reader)]
(let [ch (char c)]
(if (or (= ch \newline)
(= ch \return))
(let [line (dec (.getLineNumber reader))]
(swap! *comments* conj
{:form (Comment. (.toString sb))
:start line
:end line})
reader)
(do
(.append sb (Character/toString ch))
(recur (.read reader))))))))
(defn set-comment-reader [reader]
(aset (get-field clojure.lang.LispReader :macros nil)
(int \;)
reader))
(defn skip-spaces-and-comments [rdr]
(loop [c (.read rdr)]
(cond (= c -1) nil
(= (char c) \;)
(do (read-comment rdr \;)
(recur (.read rdr)))
(#{\space \tab \return \newline \,} (char c))
(recur (.read rdr))
:else (.unread rdr c))))
(defn parse* [reader]
(take-while
:form
(flatten
(repeatedly
(fn []
(binding [*comments* top-level-comments]
(skip-spaces-and-comments reader))
(let [start (.getLineNumber reader)
form (binding [*comments* sub-level-comments]
(. clojure.lang.LispReader
(read reader false nil false)))
end (.getLineNumber reader)
code {:form form :start start :end end}
comments @top-level-comments]
(swap! top-level-comments (constantly []))
(if (empty? comments)
[code]
(vec (concat comments [code])))))))))
(defn strip-docstring [docstring raw]
(-> raw
(replace (str \" (-> docstring
str
(replace "\"" "\\\""))
\")
"")
(replace #"\n\s*\n" "\n")
(replace #"\n\s*\)" ")")))
(defn get-var-docstring [nspace-sym sym]
(try
(-> `(var ~(symbol (str nspace-sym) (str sym))) eval meta :doc)
;; HACK: to handle types
(catch Exception _)))
(defmulti dispatch-form (fn [form _ _] (first form)))
(defn- extract-common-docstring
[form raw nspace-sym]
(let [sym (second form)
_ (if (= 'ns (first form))
(try (require sym)
(catch Exception _)))
nspace (find-ns sym)]
(let [docstring (if nspace
(-> nspace meta :doc)
(get-var-docstring nspace-sym sym))]
[docstring
(strip-docstring docstring raw)
(if nspace sym nspace-sym)])))
(defmethod dispatch-form 'ns
[form raw nspace-sym]
(let [[ds r s] (extract-common-docstring form raw nspace-sym)]
(let [[_ _ ds & _] form
ds (when (string? ds) ds)]
[ds
(strip-docstring ds r)
s])))
(defmethod dispatch-form 'def
[form raw nspace-sym]
(extract-common-docstring form raw nspace-sym))
(defmethod dispatch-form 'defn
[form raw nspace-sym]
(extract-common-docstring form raw nspace-sym))
(defmethod dispatch-form 'defprotocol
[form raw nspace-sym]
;; this needs some work to extract embedded docstrings
(extract-common-docstring form raw nspace-sym))
(defmethod dispatch-form 'defmulti
[form raw nspace-sym]
(extract-common-docstring form raw nspace-sym))
(defmethod dispatch-form 'defmethod
[form raw nspace-sym]
(let [ds (nth form 3)
docstring (when (string? ds) ds)]
[docstring
(strip-docstring docstring raw)
nspace-sym]))
(defmethod dispatch-form :default
[form raw nspace-sym]
(if (re-find #"^def" (-> form first name))
(extract-common-docstring form raw nspace-sym)
[nil raw nspace-sym]))
(defn extract-docstring [m raw nspace-sym]
(let [raw (join "\n" (subvec raw (-> m :start dec) (:end m)))
form (:form m)]
(dispatch-form form raw nspace-sym)))
(defn- ->str [m]
(replace (-> m :form .content) #"^;+\s*" ""))
(defn merge-comments [f s]
{:form (Comment. (str (->str f) "\n" (->str s)))
:start (:start f)
:end (:end s)})
(defn comment? [o]
(->> o :form (instance? Comment)))
(defn code? [o]
(and (->> o :form (instance? Comment) not)
(->> o :form nil? not)))
(defn adjacent? [f s]
(= (-> f :end) (-> s :start dec)))
(defn arrange-in-sections [parsed-code raw-code]
(loop [sections []
f (first parsed-code)
s (second parsed-code)
nn (nnext parsed-code)
nspace nil]
(if f
(cond
;; ignore comments with only one semicolon
(and (comment? f) (re-find #"^;\s" (-> f :form .content)))
(recur sections s (first nn) (next nn) nspace)
;; merging comments block
(and (comment? f) (comment? s) (adjacent? f s))
(recur sections (merge-comments f s)
(first nn) (next nn)
nspace)
;; merging adjacent code blocks
(and (code? f) (code? s) (adjacent? f s))
(let [[fdoc fcode nspace] (extract-docstring f raw-code nspace)
[sdoc scode _] (extract-docstring s raw-code nspace)]
(recur sections (assoc s
:type :code
:raw (str (or (:raw f) fcode) "\n" scode)
:docstring (str (or (:docstring f) fdoc) "\n\n" sdoc))
(first nn) (next nn) nspace))
;; adjacent comments are added as extra documentation to code block
(and (comment? f) (code? s) (adjacent? f s))
(let [[doc code nspace] (extract-docstring s raw-code nspace)]
(recur sections (assoc s
:type :code
:raw code
:docstring (str doc "\n\n" (->str f)))
(first nn) (next nn) nspace))
;; adding comment section
(comment? f)
(recur (conj sections (assoc f :type :comment :raw (->str f)))
s
(first nn) (next nn)
nspace)
;; adding code section
:else
(let [[doc code nspace] (extract-docstring f raw-code nspace)]
(recur (conj sections (if (= (:type f) :code)
f
{:type :code
:raw code
:docstring doc}))
s (first nn) (next nn) nspace)))
sections)))
(defn parse [source-string]
(let [make-reader #(java.io.BufferedReader.
(java.io.StringReader. (str source-string "\n")))
lines (vec (line-seq (make-reader)))
reader (clojure.lang.LineNumberingPushbackReader. (make-reader))
old-cmt-rdr (aget (get-field clojure.lang.LispReader :macros nil) (int \;))]
(try
(set-comment-reader read-comment)
(let [parsed-code (-> reader parse* doall)]
(set-comment-reader old-cmt-rdr)
(arrange-in-sections parsed-code lines))
(catch Exception e
(set-comment-reader old-cmt-rdr)
(throw e)))))
(defn parse-file [file]
(parse (slurp file)))

View file

@ -6,7 +6,7 @@
;; Should have only this comment in the left margin. ;; Should have only this comment in the left margin.
;; See [https://github.com/fogus/marginalia/issues/#issue/4](https://github.com/fogus/marginalia/issues/#issue/4) ;; See [https://github.com/fogus/marginalia/issues/#issue/4](https://github.com/fogus/marginalia/issues/#issue/4)
(defn parse-bool [v] (condp = (.trim (text v)) (defn parse-bool [v] (condp = (.trim (str v))
"0" false "0" false
"1" true "1" true
"throw exception here")) "throw exception here"))
@ -21,10 +21,17 @@
"Here is just a string. It should be to the right." "Here is just a string. It should be to the right."
(* x x)) (* x x))
(defmacro foobar (defprotocol Relation
"This is a macro docstring. It should be on the left." (select [this predicate]
[& body] "Confines the query to rows for which the predicate is true
`~body)
Ex. (select (table :users) (where (= :id 5)))")
(join [this table2 join_on]
"Joins two tables on join_on
Ex. (join (table :one) (table :two) :id)
(join (table :one) (table :two)
(where (= :one.col :two.col)))"))
(defmulti bazfoo (defmulti bazfoo
"This is a defmulti docstring, it should also be on the left" "This is a defmulti docstring, it should also be on the left"
@ -59,6 +66,7 @@
; Define single-character indicator rules. ; Define single-character indicator rules.
; I use `clojure.template/do-template` to reduce repetition. ; I use `clojure.template/do-template` to reduce repetition.
(comment
(do-template [rule-name token] (do-template [rule-name token]
(h/defrule rule-name (h/defrule rule-name
"Padded on the front with optional whitespace." "Padded on the front with optional whitespace."
@ -70,4 +78,11 @@
<array-start> \[ <array-start> \[
<array-end> \] <array-end> \]
<object-start> \{ <object-start> \{
<object-end> \}) <object-end> \}))
(defmulti kompile identity)
(defmethod kompile [::standard AutoIncClause]
"This is a docstring. On the left."
[_]
"GENERATED ALWAYS AS IDENTITY")