marginalia

0.9.2


lightweight literate programming for clojure -- inspired by docco

dependencies

org.clojure/clojure
1.11.1
org.clojure/clojurescript
1.11.132
org.clojure/tools.namespace
1.4.5
org.clojure/tools.cli
1.0.219
org.markdownj/markdownj-core
0.4
de.ubercode.clostache/clostache
1.4.0



(this space intentionally left almost blank)
 
(ns leiningen.marg)

Support eval-in-project in both Leiningen 1.x and 2.x.

(defn eval-in-project
  [project form init]
  (let [[eip two?] (or (try (require 'leiningen.core.eval)
                            [(resolve 'leiningen.core.eval/eval-in-project)
                             true]
                            (catch java.io.FileNotFoundException _))
                       (try (require 'leiningen.compile)
                            [(resolve 'leiningen.compile/eval-in-project)]
                            (catch java.io.FileNotFoundException _)))]
    (if two?
      (eip project form init)
      (eip project form nil nil init))))
(def dep ['marginalia "0.9.2"])
(defn- add-marg-dep [project]
  ;; Leiningen 2 is a bit smarter about only conjing it in if it
  ;; doesn't already exist and warning the user.
  (if-let [conj-dependency (resolve 'leiningen.core.project/conj-dependency)]
    (conj-dependency project dep)
    (update-in project [:dependencies] conj dep)))

Note:

The docstring for the marg function is used by Leiningen when a user types lein help marg. Because of this, and because, for instance, an escaped asterisk in a docstring will cause errors when you attempt to run lein marg, some extra care (read: hacking) is required to get this docstring to look reasonable both in the lein help output and in a marginalia uberdoc.

Run Marginalia against your project source files.

Usage:

lein marg <options> <files>

Marginalia accepts options as described below:

-d --dir Directory into which the documentation will be written (default docs)

-f --file File into which the documentation will be written (default uberdoc.html)

-n --name Project name (Taken from project.clj by default.)

-v --version Project version (Taken from project.clj by default.)

-D --desc Project description (Taken from project.clj by default.)

-a --deps Project dependencies in the form <group1>:<artifact1>:<version1>;<group2>... (Taken from project.clj by default.)

-c --css Additional css resources <resource1>;<resource2>;... (Taken from project.clj by default.)

-j --js Additional javascript resources <jsfile1>;<jsfile2>;... (Taken from project.clj by default.)

-m --multi Generate each namespace documentation as a separate file

(defn marg
  [project & args]
  (eval-in-project (add-marg-dep project)
                   `(binding [marginalia.html/*resources* ""]
                      (marginalia.core/run-marginalia (list ~@args)))
                   '(require 'marginalia.core)))
 

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 you’re 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 you’re 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 crystallize 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
  (:require
   [clojure.java.io :as io]
   [clojure.string  :as str]
   [clojure.tools.cli :refer [cli]]
   [marginalia.html :refer [uberdoc-html index-html single-page-html]]
   [marginalia.parser :refer [parse-file parse-ns *lift-inline-comments* *delete-lifted-comments*]])
  (:import
   (java.io File FileReader)))
(set! *warn-on-reflection* true)

File System Utilities

Performs roughly the same task as the UNIX ls. That is, returns a seq of the filenames at a given directory. If a path to a file is supplied, then the seq contains only the original path given.

(defn ls
  [path]
  (let [file (io/file path)]
    (if (.isDirectory file)
      (seq (.list file))
      (when (.exists file)
        [path]))))
(defn mkdir [path]
  (.mkdirs (io/file path)))

Ensure that the directory specified by path exists. If not then make it so. Here is a snowman ☃

(defn ensure-directory!
  [path]
  (when-not (ls path)
    (mkdir path)))

Many Marginalia fns use dir? to recursively search a filepath.

(defn dir?
  [path]
  (.isDirectory (io/file path)))

Returns a string containing the files extension.

(defn find-file-extension
  [^File file]
  (second (re-find #"\.([^.]+)$" (.getName file))))

Predicate. Returns true for "normal" files with a file extension which passes the provided predicate.

(defn processable-file?
  [pred ^File file]
  (when (.isFile file)
    (-> file find-file-extension pred)))

Returns a seq of processable file paths (strings) in alphabetical order by namespace.

(defn find-processable-file-paths
  [dir pred]
  (->> (io/file dir)
       (file-seq)
       (filter (partial processable-file? pred))
       (sort-by parse-ns)
       (map #(.getCanonicalPath ^File %))))

Project Info Parsing

Marginalia will parse info out of your project.clj to display in the generated html file's header.

Parses a project.clj file and returns a map in the following form

 {:name
  :version
  :dependencies
  :dev-dependencies
  etc...}

by merging into the name and version information the rest of the defproject forms (:dependencies, etc)

(defn parse-project-form
  [[_ project-name version-number & attributes]]
  (merge {:name    (str project-name)
	  :version version-number}
	 (apply hash-map attributes)))

Parses a project file -- './project.clj' by default -- and returns a map assembled according to the logic in parse-project-form.

(defn parse-project-file
  ([] (parse-project-file "./project.clj"))
  ([path]
      (try
        (let [rdr (clojure.lang.LineNumberingPushbackReader.
                    (FileReader.
                     (io/file path)))]
          (loop [line (read rdr)]
            (let [found-project? (= 'defproject (first line))]
              (if found-project?
                (parse-project-form line)
                (recur (read rdr))))))
	(catch Exception e
          (throw (Exception.
                  (str
                   "There was a problem reading the project definition from "
                   path)))))))

Source File Analysis

TODO: why are these args unused?

(defn end-of-block? [_cur-group _groups lines]
  (let [line (first lines)
        next-line (second lines)
        next-line-code (get next-line :code-text )]
    (when (or (and (:code-text line)
                   (:docs-text next-line))
              (re-find #"^\(def" (str/trim next-line-code)))
      true)))
(defn merge-line [line m]
  (cond
   (:docstring-text line) (assoc m
                            :docs
                            (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]
  (loop [cur-group {}
         groups []
         lines doc-lines]
    (cond
     (empty? lines) (conj groups cur-group)
     (end-of-block? cur-group groups lines)
     (recur (merge-line (first lines) {}) (conj groups cur-group) (rest lines))
     :else (recur (merge-line (first lines) cur-group) groups (rest lines)))))
(defn path-to-doc [filename]
  {:ns     (parse-ns (io/file filename))
   :groups (parse-file filename)})

Output Generation

(defn filename-contents
  [props output-dir all-files parsed-file]
  {:name     (io/file output-dir (str (:ns parsed-file) ".html"))
   :contents (single-page-html props parsed-file all-files)})
(defn multidoc!
  [output-dir files-to-analyze props]
  (let [parsed-files (map path-to-doc files-to-analyze)
        index (index-html props parsed-files)
        pages (map #(filename-contents props output-dir parsed-files %) parsed-files)]
    (doseq [f (conj pages {:name     (io/file output-dir "toc.html")
                           :contents index})]
           (spit (:name f) (:contents f)))))

Generates an uberdoc html file from 3 pieces of information:

  1. The path to spit the result (output-file-name)
  2. Results from processing source files (path-to-doc)
  3. Project metadata as a map, containing at a minimum the following:
    • :name
    • :version
(defn uberdoc!
  [output-file-name files-to-analyze props]
  (let [source (uberdoc-html
                props
                (map path-to-doc files-to-analyze))]
    (spit output-file-name source)))

External Interface (command-line, lein, cake, etc)

These functions support Marginalia's use by client software or command-line users.

(def ^:private file-extensions #{"clj" "cljs" "cljx" "cljc"})

Given a collection of filepaths, returns a lazy sequence of filepaths to all .clj, .cljs, .cljx, and .cljc files on those paths: directory paths will be searched recursively for files.

(defn format-sources
  [sources]
  (if (nil? sources)
    (find-processable-file-paths "./src" file-extensions)
    (->> sources
         (mapcat #(if (dir? %)
                    (find-processable-file-paths % file-extensions)
                    [(.getCanonicalPath (io/file %))])))))
(defn split-deps [deps]
  (when deps
    (for [d (str/split deps #";")
          :let [[group artifact version] (str/split d #":")]]
      [(if (= group artifact) artifact (str group "/" artifact))
       version])))

Check if a source file is excluded from the generated documentation

(defn source-excluded?
  [source opts]
  (if-not (empty?
           (filter #(if (re-find (re-pattern %) source)
                      true
                      false)
                   (-> opts :marginalia :exclude)))
    true
    false))

Default generation: given a collection of filepaths in a project, find the .clj files at these paths and, if Clojure source files are found:

  1. Print out a message to std out letting a user know which files are to be processed;
  2. Create the docs directory inside the project folder if it doesn't already exist;
  3. Call the uberdoc! function to generate the output file at 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.

(defn run-marginalia
  [args & [project]]
  (let [[{:keys [dir file name version desc deps css js multi
                 leiningen exclude
                 lift-inline-comments exclude-lifted-comments]} files help]
        (cli args
             ["-d" "--dir"
              "Directory into which the documentation will be written" :default "./docs"]
             ["-f" "--file"
              "File into which the documentation will be written" :default "uberdoc.html"]
             ["-n" "--name"
              "Project name - if not given will be taken from project.clj"]
             ["-v" "--version"
              "Project version - if not given will be taken from project.clj"]
             ["-D" "--desc"
              "Project description - if not given will be taken from project.clj"]
             ["-a" "--deps"
              "Project dependencies in the form <group1>:<artifact1>:<version1>;<group2>...
                 If not given will be taken from project.clj"]
             ["-c" "--css"
              "Additional css resources <resource1>;<resource2>;...
                 If not given will be taken from project.clj."]
             ["-j" "--js"
              "Additional javascript resources <resource1>;<resource2>;...
                 If not given will be taken from project.clj"]
             ["-m" "--multi"
              "Generate each namespace documentation as a separate file" :flag true]
             ["-l" "--leiningen"
              "Generate the documentation for a Leiningen project file."]
             ["-e" "--exclude"
              "Exclude source file(s) from the document generation process <file1>;<file2>;...
                 If not given will be taken from project.clj"]
             ["-L" "--lift-inline-comments"
              "Lift ;; inline comments to the top of the enclosing form.
                 They will be treated as if they preceded the enclosing form." :flag true]
             ["-X" "--exclude-lifted-comments"
              "If ;; inline comments are being lifted into documentation
                 then also exclude them from the source code display." :flag true])
        sources (distinct (format-sources (seq files)))
        sources (if leiningen (cons leiningen sources) sources)]
    (if-not sources
      (do
        (println "Wrong number of arguments passed to Marginalia.")
        (println help))
      (binding [*lift-inline-comments*   lift-inline-comments
                *delete-lifted-comments* exclude-lifted-comments]
        (let [project-clj (or project
                              (when (.exists (io/file "project.clj"))
                                (parse-project-file)))
              choose #(or %1 %2)
              marg-opts (merge-with choose
                                    {:css        (when css (str/split css #";"))
                                     :javascript (when js (str/split js #";"))
                                     :exclude    (when exclude (str/split exclude #";"))
                                     :leiningen  leiningen}
                                    (:marginalia project-clj))
              opts (merge-with choose
                               {:name         name
                                :version      version
                                :description  desc
                                :dependencies (split-deps deps)
                                :multi        multi
                                :marginalia   marg-opts}
                               project-clj)
              sources (->> sources
                           (filter #(not (source-excluded? % opts)))
                           (into []))]
          (println "Generating Marginalia documentation for the following source files:")
          (doseq [s sources]
            (println "  " s))
          (println)
          (ensure-directory! dir)
          (if multi
            (multidoc! dir sources opts)
            (uberdoc! (str dir "/" file) sources opts))
          (println "Done generating your documentation in" dir)
          (println ""))))))
 

Utilities for converting parse results into html.

(ns marginalia.html
  (:use [marginalia.hiccup :only (html escape-html)])
  (:require [clojure.string :as str])
  (:import [com.petebevin.markdown MarkdownProcessor]))
(def ^{:dynamic true} *resources* "./vendor/")
(defn css-rule [rule]
  (let [sels (reverse (rest (reverse rule)))
        props (last rule)]
    (str (apply str (interpose " " (map name sels)))
         "{" (apply str (map #(str (name (key %)) ":" (val %) ";") props)) "}")))

Quick and dirty dsl for inline css rules, similar to hiccup.

ex. (css [:h1 {:color "blue"}] [:div.content p {:text-indent "1em"}])

-> h1 {color: blue;} div.content p {text-indent: 1em;}

(defn css
  [& rules]
  (html [:style {:type "text/css"}
         (apply str (map css-rule rules))]))

Stolen from leiningen

(defn slurp-resource
  [resource-name]
  (try
    (-> (.getContextClassLoader (Thread/currentThread))
        (.getResourceAsStream resource-name)
        (java.io.InputStreamReader.)
        (slurp))
    (catch java.lang.NullPointerException npe
      (println (str "Could not locate resources at " resource-name))
      (println "    ... attempting to fix.")
      (let [resource-name (str *resources* resource-name)]
        (try
          (-> (.getContextClassLoader (Thread/currentThread))
              (.getResourceAsStream resource-name)
              (java.io.InputStreamReader.)
              (slurp))
          (catch java.lang.NullPointerException npe
            (println (str "    STILL could not locate resources at " resource-name ". Giving up!"))))))))
(defn inline-js [resource]
  (let [src (slurp-resource resource)]
    (html [:script {:type "text/javascript"}
            src])))
(defn inline-css [resource]
  (let [src (slurp-resource resource)]
    (html [:style {:type "text/css"}
           (slurp-resource resource)])))

The following functions handle preparation of doc text (both comment and docstring based) for display through html & css.

Markdown processor.

(def mdp (com.petebevin.markdown.MarkdownProcessor.))

Markdown string to html converter. Translates strings like:

"# header!" -> "<h1>header!</h1>"

"## header!" -> "<h2>header!</h2>"

...

(defn md
  [s]
  (.markdown mdp s))

As a result of docifying then grouping, you'll end up with a seq like this one:

[...
{:docs [{:docs-text "Some doc text"}]
 :codes [{:code-text "(def something \"hi\")"}]}
...]

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.

Converts a docs section to html by threading each doc line through the forms outlined above.

ex. (docs-to-html [{:doc-text "# hello world!"} {:docstring-text "I'm a docstring!}])

-> "<h1>hello world!</h1><br />"

(defn docs-to-html
  [docs]
  (-> docs
      str
      (md)))
(defn codes-to-html [code-block]
  (html [:pre {:class "brush: clojure"}
         (escape-html code-block)]))
(defn section-to-html [section]
  (html [:tr
         [:td {:class "docs"} (docs-to-html
                               (if (= (:type section) :comment)
                                 (:raw section)
                                 (:docstring section)))]
         [:td {:class "codes"} (if (= (:type section) :code)
                                  (codes-to-html (:raw section)))]]))
(defn dependencies-html [deps & header-name]
  (when-let [deps (seq deps)]
    (let [header-name (or header-name "dependencies")]
      (html [:div {:class "dependencies"}
             [:h3 header-name]
             [:table
              (map #(html [:tr
                           [:td {:class "dep-name"} (str (first %))]
                           [:td {:class "dotted"} [:hr]]
                           [:td {:class "dep-version"} (second %)]])
                   deps)]]))))

Generate Optional Metadata

Add metadata to your documentation.

To add to the head of the docs, specify a hash map for the :meta key :marginalia in project.clj:

:marginalia {:meta {:robots "noindex"}}

Generate meta tags from project info.

(defn metadata-html
  [project-info]
  (let [options (:marginalia project-info)
        meta (:meta options)]
    (html (when meta
            (map #(vector :meta {:name (name (key %)) :contents (val %)}) meta)))))

Load Optional Resources

Use external Javascript and CSS in your documentation. For example:

To format Latex math equations, download the MathJax Javascript library to the docs directory and then add

:marginalia {:javascript ["mathjax/MathJax.js"]}

to project.clj. :javascript and :css accept a vector of paths or URLs

Below is a simple example of both inline and block formatted equations.

Optionally, you can put the MathJax CDN URL directly as a value of :javascript like this:

:marginalia {
  :javascript
    ["http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"]}

That way you won't have to download and carry around the MathJax library.

When \(a \ne 0\), there are two solutions to \(ax^2 + bx + c = 0\) and they are $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$

Generate script and link tags for optional external javascript and css.

(defn opt-resources-html
  [project-info]
  (let [options (:marginalia project-info)
        javascript (:javascript options)
        css (:css options)]
    (html (concat
           (when javascript
             (map #(vector :script {:type "text/javascript" :src %}) javascript))
           (when css
             (map #(vector :link {:tyle "text/css" :rel "stylesheet" :href %}) css))))))

Is <h1/> overloaded? Maybe we should consider redistributing header numbers instead of adding classes to all the h1 tags.

(defn header-html [project-info]
  (html
   [:tr
    [:td {:class "docs"}
     [:div {:class "header"}
      [:h1 {:class "project-name"} (if (seq (:url project-info))
                                     [:a {:href (:url project-info)} (:name project-info)]
                                     (:name project-info))]
      [:h2 {:class "project-version"} (:version project-info)]
      [:br]
      (md (:description project-info))]
     (dependencies-html (:dependencies project-info))
     (dependencies-html (:dev-dependencies project-info) "dev dependencies")]
    [:td {:class "codes"
          :style "text-align: center; vertical-align: middle;color: #666;padding-right:20px"}
     [:br]
     [:br]
     [:br]
     "(this space intentionally left almost blank)"]]))

Creates an 'a' tag pointing to the namespace-name, either as an anchor (if anchor? is true) or as a link to a separate $namespace-name.html file. If attrs aren't empty, they are added to the resulting tag.

(defn link-to-namespace
  [namespace-name anchor? & attrs]
  [:a (into {:href (if anchor?
                   (str "#" namespace-name)
                   (str namespace-name ".html"))}
            attrs)
   namespace-name])

This is a hack, as in the case when anchor? is false, the link will contain a reference to toc.html which might not even exist.

(defn link-to-toc
  [anchor?]
  (link-to-namespace "toc" anchor? {:class "toc-link"}))
(defn toc-html [props docs]
  (html
   [:tr
    [:td {:class "docs"}
     [:div {:class "toc"}
      [:a {:name "toc"} [:h3 "namespaces"]]
      [:ul
       (map #(vector :li (link-to-namespace (:ns %) (:uberdoc? props)))
            docs)]]]
    [:td {:class "codes"} "&nbsp;"]]))
(defn floating-toc-html [docs]
  [:div {:id "floating-toc"}
   [:ul
    (map #(vector :li {:class "floating-toc-li"
                       :id (str "floating-toc_" (:ns %))}
                  (:ns %))
         docs)]])
(defn groups-html [props doc]
  (html
   [:tr
    [:td {:class "docs"}
     [:div {:class "docs-header"}
      [:a {:class "anchor" :name (:ns doc) :href (str "#" (:ns doc))}
       [:h1 {:class "project-name"}
        (:ns doc)]
       (link-to-toc (:uberdoc? props))]]]
    [:td {:class "codes"}]]
   (map section-to-html (:groups doc))
   [:tr
    [:td {:class "spacer docs"} "&nbsp;"]
    [:td {:class "codes"}]]))
(def reset-css
  (css [:html {:margin 0 :padding 0}]
       [:h1 {:margin 0 :padding 0}]
       [:h2 {:margin 0 :padding 0}]
       [:h3 {:margin 0 :padding 0}]
       [:h4 {:margin 0 :padding 0}]
       [:a {:color "#261A3B"}]
       [:a:visited {:color "#261A3B"}]))
(def header-css
  (css [:.header {:margin-top "30px"}]
       [:h1.project-name {:font-size "34px"
                          :display "inline"}]
       [:h2.project-version {:font-size "18px"
                             :margin-top 0
                             :display "inline"
                             :margin-left "10px"}]
       [:.toc-link {:font-size "12px"
                    :margin-left "10px"
                    :color "#252519"
                    :text-decoration "none"}]
       [:.toc-link:hover {:color "#5050A6"}]
       [:.toc :h1 {:font-size "34px"
                   :margin 0}]
       [:.docs-header {:border-bottom "dotted #aaa 1px"
                       :padding-bottom "10px"
                       :margin-bottom "25px"}]
       [:.toc :h1 {:font-size "24px"}]
       [:.toc {:border-bottom "solid #bbb 1px"
               :margin-bottom "40px"}]
       [:.toc :ul {:margin-left "20px"
                   :padding-left "0px"
                   :padding-top 0
                   :margin-top 0}]
       [:.toc :li {:list-style-type "none"
                   :padding-left 0}]
       [:.dependencies {}]
       [:.dependencies :table {:font-size "16px"
                               :width "99.99%"
                               :border "none"
                               :margin-left "20px"}]
       [:.dependencies :td {:padding-right "20px;"
                            :white-space "nowrap"}]
       [:.dependencies :.dotted {:width "99%"}]
       [:.dependencies :.dotted :hr {:height 0
                                     :noshade "noshade"
                                     :color "transparent"
                                     :background-color "transparent"
                                     :border-bottom "dotted #bbb 1px"
                                     :border-top "none"
                                     :border-left "none"
                                     :border-right "none"
                                     :margin-bottom "-6px"}]
       [:.dependencies :.dep-version {:text-align "right"}]
       [:.plugins :ul {:margin-left "20px"
                       :padding-left "0px"
                       :padding-top 0
                       :margin-top 0}]
       [:.plugins :li {:list-style-type "none"
                       :padding-left 0}]
       [:.header :p {:margin-left "20px"}]))
(def floating-toc-css
  (css [:#floating-toc {:position "fixed"
                        :top "10px"
                        :right "20px"
                        :height "20px"
                        :overflow "hidden"
                        :text-align "right"}]
       [:#floating-toc :li {:list-style-type "none"
                            :margin 0
                            :padding 0}]))
(def general-css
  (css
   [:body {:margin 0
           :padding 0
           :font-family "'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;"
           :font-size "16px"
           :color "#252519"
           :background-color "#F5F5FF"}]
   [:h1 {:font-size "20px"
         :margin-top 0}]
   [:h2 {:font-size "18px"}]
   [:h3 {:font-size "16px"}]
   [:a.anchor {:text-decoration "none"
              :color "#252519"}]
   [:a.anchor:hover {:color "#5050A6"}]
   [:table {:border-spacing 0
            :border-bottom "solid #ddd 1px;"
            :margin-bottom "10px"}]
   [:code {:display "inline"}]
   [:p {:margin-top "8px"}]
   [:tr {:margin "0px"
         :padding "0px"}]
   [:td.docs {:width "410px"
              :max-width "410px"
              :vertical-align "top"
              :margin "0px"
              :padding-left "55px"
              :padding-right "20px"
              :border "none"
              :background-color "#FFF"}]
   [:td.docs :pre {:font-size "12px"
                   :overflow "hidden"}]
   [:td.codes {:width "55%"
               :background-color "#F5F5FF"
               :vertical-align "top"
               :margin "0px"
               :padding-left "20px"
               :border "none"
               :overflow "hidden"
               :font-size "10pt"
               :border-left "solid #E5E5EE 1px"}]
   [:td.spacer {:padding-bottom "40px"}]
   [:pre :code {:display "block"
                :padding "4px"}]
   [:code {:background-color "ghostWhite"
           :border "solid #DEDEDE 1px"
           :padding-left "3px"
           :padding-right "3px"
           :font-size "14px"}]
   [:.syntaxhighlighter :code {:font-size "13px"}]
   [:.footer {:text-align "center"}]))

Notice that we're inlining the css & javascript for SyntaxHighlighter (inline-js & inline-css) to be able to package the output as a single file (uberdoc if you will). It goes without saying that all this is WIP and will probably change in the future.

(defn page-template
  [project-metadata opt-resources header toc content floating-toc]
  (html
   "<!DOCTYPE html>\n"
   [:html
    [:head
     [:meta {:http-equiv "Content-Type" :content "text/html" :charset "utf-8"}]
     [:meta {:name "description" :content (:description project-metadata)}]
     (metadata-html project-metadata)
     (inline-css (str *resources* "shCore.css"))
     (css
      [:.syntaxhighlighter {:overflow "hidden !important"}])
     (inline-css (str *resources* "shThemeMarginalia.css"))
     reset-css
     header-css
     floating-toc-css
     general-css
     (inline-js (str *resources* "jquery-1.7.1.min.js"))
     (inline-js (str *resources* "xregexp-min.js"))
     (inline-js (str *resources* "shCore.js"))
     (inline-js (str *resources* "shBrushClojure.js"))
     opt-resources
     [:title (:name project-metadata) " -- Marginalia"]]
    [:body
     [:table
      header
      toc
      content]
     [:div {:class "footer"}
      "Generated by "
      [:a {:href "https://github.com/clj-commons/marginalia"} "Marginalia"]
      ".&nbsp;&nbsp;"
      "Syntax highlighting provided by Alex Gorbatchev's "
      [:a {:href "http://alexgorbatchev.com/SyntaxHighlighter/"}
       "SyntaxHighlighter"]
      floating-toc]
     (inline-js (str *resources* "app.js"))]]))

Syntax highlighting is done a bit differently than docco. Instead of embedding the highlighting metadata on the parse / html gen phase, we use SyntaxHighlighter to do it in javascript.

This generates a stand alone html file (think lein uberjar). It's probably the only var consumers will use.

(defn uberdoc-html
  [project-metadata docs]
  (page-template
   project-metadata
   (opt-resources-html project-metadata)
   (header-html project-metadata)
   (toc-html {:uberdoc? true} docs)
   (map #(groups-html {:uberdoc? true} %) docs)
   (floating-toc-html docs)))
(defn index-html
  [project-metadata docs]
  (page-template
   project-metadata
   (opt-resources-html project-metadata)
   (header-html project-metadata)
   (toc-html {:uberdoc? false} docs)
      ;; no contents)) ;; no floating toc

no floating toc

(defn single-page-html
  [project-metadata doc all-docs]
  (page-template
   project-metadata
   (opt-resources-html project-metadata)
    ;; no header
    ;; no toc
   (groups-html {:uberdoc? false} doc)
    ;; no floating toc))
 

A place to examine poor parser behavior. These should go in tests when they get written.

(ns problem-cases.general)
[::foo]
{:foo 43}
{::foo 42}

private docstring

(defn ^:private private-fn  [])

docstring

(defn public-fn  []
  (let [x (private-fn)]
        (count x)))

Should have only this comment in the left margin. See https://github.com/clj-commons/marginalia/issues/4

(defn parse-bool [v] (condp = (.trim (str v))
                         "0" false
                         "1" true
                         "throw exception here"))

Here is a docstring. It should be to the left.

(defn a-function 
  [x]
  (* x x))

Here is a docstring. It should be to the left.

(defn b-function
  [x]
  "Here is just a string.  It should be to the right."
  (* x x))

Defines a relation... duh!

(defprotocol Relation
  (select     [this predicate]
    "Confines the query to rows for which the predicate is true
     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)))"))

This is a defmulti docstring, it should also be on the left

(defmulti bazfoo
  class)
(defmethod bazfoo String [s]
  "This is a defmethod docstring.  It should be on the left."
  (vec (seq s)))
(bazfoo "abc")

This is a protocol docstring. It should be on the left.

(defprotocol Foo
  (lookup  [cache e])
  (has?    [cache e] )
  (hit     [cache e])
  (miss    [cache e ret]))

This is also a docstring via metadata. It should be on the left.

(def 
  a 42)

This is also a docstring via metadata. It should be on the left.

(def 
  b 42)

This is also a docstring via metadata. It should be on the left.

(def 
  c
  "This is just a value.  It should be on the right.")

From fnparse

Padded on the front with optional whitespace.

(comment
  (do-template [rule-name token]
               (h/defrule rule-name
                 (h/lit token))
               <escape-char-start> \\
               <str-delimiter>   \"
               <value-separator> \,
               <name-separator>  \:
               <array-start>     \[
               <array-end>       \]
               <object-start>    \{
               <object-end>      \}))

Issue #26: Angle-bracket in Function Name Breaks Layout

(defn <test [] nil)

(defn test-html-entities-in-doc
  []
  nil)
(defmulti kompile identity)
(defmethod kompile [:standard]
  [_]
  "GENERATED ALWAYS AS IDENTITY")

strict-eval-op-fn is used to define functions of the above pattern for functions such as +, *, etc. Cljs special forms defined this way are applyable, such as (apply + [1 2 3]).

Resulting expressions are wrapped in an anonymous function and, down the line, called, like so:

 (+ 1 2 3) -> (function(){...}.call(this, 1 2 3)
(defn strict-eval-op-fn
  [op inc-ind-str ind-str op nl]
  (ind-str
   "(function() {" nl
   (inc-ind-str
    "var _out = arguments[0];" nl
    "for(var _i=1; _i<arguments.length; _i++) {" nl
    (inc-ind-str
     "_out = _out " op " arguments[_i];")
    nl
    "}" nl
    "return _out;")
   nl
   "})"))
'(defn special-forms []
  {'def     handle-def
   'fn      handle-fn
   'fn*     handle-fn
   'set!    handle-set
   'let     handle-let
   'defn    handle-defn
   'aget    handle-aget
   'aset    handle-aset
   'if      handle-if
   'while   handle-while
   'when    handle-when
   'doto    handle-doto
   '->      handle-->
   '->>     handle-->>
   'not     handle-not
   'do      handle-do
   'cond    handle-cond
   '=       (make-lazy-op '==)
   '>       (make-lazy-op '>)
   '<       (make-lazy-op '<)
   '>=      (make-lazy-op '>=)
   '<=      (make-lazy-op '<=)
   'or      (make-lazy-op '||)
   'and     (make-lazy-op '&&)
   'doseq   handle-doseq
   'instanceof handle-instanceof
   'gensym handle-gensym
   'gensym-str handle-gensym-str})
'(defn greater [a b]
  (>= a b))
'(fact
  (greater 2 1) => truthy)
'(file->tickets commits)
(defmulti ns-kw-mm identity)
(defmethod ns-kw-mm ::foo [_] :problem-cases.general/foo)
(defmethod ns-kw-mm :user/foo [_] :user/foo)
(defmethod ns-kw-mm :foo [_] :foo)