1    Reference

These are the namespaces that matter most when extending code.doc for code.

2    Parsing



parse-file ^

[file {:keys [root], :as opts}]
Added 3.0

parses the entire file

v 3.0
(defn parse-file
  ([file {:keys [root] :as opts}]
   (let [path (if (:root opts)
                (str (:root opts) "/" file)
                file)]
     (if (str/ends-with? file ".md")
       (parse-markdown (slurp path))
       (with-open [input (io/input-stream path)]
         (parse-loop (-> (nav/parse-root (slurp path))
                         (nav/down))
                     opts))))))
link
(with-redefs [slurp (constantly "(+ 1 1)") io/input-stream (fn [_] (java.io.ByteArrayInputStream. (.getBytes "")))] (parse-file "foo.clj" {})) => (contains [{:type :code :indentation 0 :code ["(+ 1 1)"] :line {:row 1 :col 1 :end-row 1 :end-col 8}}]) (with-redefs [slurp (constantly "# Chapternncontentnn## Section")] (parse-file "foo.md" {})) => (contains [{:type :chapter :title "Chapter"} {:type :paragraph :text "ncontentn"} {:type :section :title "Section"}])

parse-frontmatter ^

[lines]
Added 4.1

parses edn frontmatter

v 4.1
(defn parse-frontmatter
  [lines]
  (if (= "---" (some-> lines first str/trim))
    (let [[frontmatter remaining] (split-with #(not= "---" (str/trim %))
                                              (rest lines))]
      (if (and (seq remaining) (seq frontmatter))
        [{:type :article}
         (-> (str/join "n" frontmatter)
             edn/read-string
             (assoc :type :article))
         (rest remaining)]
        [nil nil lines]))
    [nil nil lines]))
link
(parse-frontmatter ["---" "{:title "Hello"}" "---" "# Heading"]) => [{:type :article} {:type :article :title "Hello"} ["# Heading"]]

parse-header ^

[line]
Added 4.1

parses heading ids

v 4.1
(defn parse-header
  [line]
  (if (>= (count-indentation line) 4)
    nil
    (let [trimmed (str/trim line)
          [_ marker title tag] (re-matches #"^(#{1,4})s+(.*?)(?:s+{#([^}]+)})?$" trimmed)]
      (cond
        marker
        (cond-> {:type (case (count marker)
                         1 :chapter
                         2 :section
                         3 :subsection
                         4 :subsubsection)
                 :title title}
          tag (assoc :tag tag))

        :else nil))))
link
(parse-header "## Section {#section-id}") => {:type :section :title "Section" :tag "section-id"}

parse-markdown ^

[content]
Added 4.1

parses frontmatter, directives, and fenced code blocks

v 4.1
(defn parse-markdown
  [content]
  (let [lines (str/split-lines content)
        [frontmatter article-meta lines] (parse-frontmatter lines)]
    (loop [remaining lines
           current-block []
           output (cond-> []
                    frontmatter (conj article-meta))
           code-state nil]
      (cond
        (empty? remaining)
        (cond-> output
          code-state (conj (markdown-code-block (:lang code-state)
                                                (:lines code-state)))
          (seq current-block) (conj (markdown-paragraph current-block)))

        code-state
        (let [line (first remaining)
              trimmed (str/trim line)]
          (if (str/starts-with? trimmed (:fence code-state))
            (recur (rest remaining)
                   current-block
                   (conj output (markdown-code-block (:lang code-state)
                                                     (:lines code-state)))
                   nil)
            (recur (rest remaining)
                   current-block
                   output
                   (update code-state :lines conj line))))

        :else
        (let [line    (first remaining)
              trimmed (str/trim line)
              header  (parse-header line)
              fence   (when-let [[_ marker lang] (re-matches #"^(```|~~~)s*([^`s~]*)s*$" trimmed)]
                        {:fence marker :lang lang})]
          (cond
            fence
            (recur (rest remaining)
                   []
                   (cond-> output
                     (seq current-block) (conj (markdown-paragraph current-block)))
                   (assoc fence :lines []))

            header
            (recur (rest remaining)
                   []
                   (cond-> output
                     (seq current-block) (conj (markdown-paragraph current-block))
                     true (conj header))
                   nil)

            (directive-line? line)
            (recur (rest remaining)
                   []
                   (cond-> output
                     (seq current-block) (conj (markdown-paragraph current-block))
                     true (conj (parse-markdown-directive line)))
                   nil)

            :else
            (recur (rest remaining)
                   (conj current-block line)
                   output
                   nil)))))))
link
(parse-markdown (str "---n{:title "Hello"}n---n" "# Heading {#heading}n" "[[:callout {:tone :success :title "Nice" :content "Yep"}]]n" "```clojuren(+ 1 1)n```")) => [{:type :article :title "Hello"} {:type :chapter :title "Heading" :tag "heading"} {:type :callout :tone :success :title "Nice" :content "Yep"} {:type :block :indentation 0 :lang "clojure" :code "(+ 1 1)"}]

3    Publishing



all-pages ^

[{:keys [publish]}]
Added 3.0

finds and creates entries for all documents

v 3.0
(defn all-pages
  ([{:keys [publish]}]
   (let [sites (:sites publish)]
     (with-meta (reduce-kv (fn [out k v]
                             (let [settings (dissoc v :pages)
                                   files (:pages v)
                                   k (name k)]
                               (create-ns (symbol k))
                               (reduce-kv (fn [out fk fmeta]
                                            (let [fk (name fk)
                                                  id (symbol k fk)
                                                  fv (-> fmeta
                                                         (merge settings)
                                                         (assoc :ns k)
                                                         (assoc :name fk)
                                                         (assoc :id id))]
                                              (intern (symbol k) (symbol fk) fv)
                                              (assoc out id fv)))
                                          out
                                          files)))
                           {}
                           sites)
       sites))))
link
(-> (all-pages {:publish (config/load "config/publish.edn")}) keys sort vec) ;;[hara/hara-code hara/hara-code-query ... spirit/spirit-net-mail] => sequential?

deploy-template ^

[site params lookup {:keys [root]}]
Added 3.0

copies all assets in the template folder to output

v 3.0
(defn deploy-template
  ([site params lookup {:keys [root]}]
   (let [{:keys [write print]} (task/single-function-print params)
         {:keys [theme]} (lookup site)
         {:keys [theme output] :as out} (lookup site)
         output-path (fs/path root output)
         template-path (fs/path root *template-path* (coerce/to-string site))]
     (cond (fs/exists? template-path)
           (let [{:keys [copy]} (read-string (slurp (fs/path template-path *default-deploy*)))]
             (mapcat (fn [entry]
                       (let [entry-path   (fs/path template-path entry)
                             entry-files  (filter fs/file? (fs/select entry-path))
                             output-files (map (fn [entry-file]
                                                 (fs/path output-path
                                                          (str (fs/relativize entry-path
                                                                              entry-file))))
                                               entry-files)]
                         (mapv (fn [i in out]
                                 (if write (fs/create-directory (fs/parent out)))
                                 (if (:function print)
                                   (print/println
                                    (ansi/blue (format "%4s" (inc i)))
                                    (ansi/bold (str (fs/relativize root out)))
                                    '<=
                                    (str (fs/relativize root in))))
                                 (fs/copy-single in out {:options [:replace-existing :copy-attributes]
                                                         :simulate (not write)})
                                 [(str in) (str out)])
                               (range (count entry-files))
                               entry-files
                               output-files)))
                     copy))

           :else
           (throw (ex-info (str "Template for " site " not initialiseed") {:path (str template-path)}))))))
link
(deploy-template "hara" {:print {:function true}} (all-sites {:root "."}) {:root "."})

init-template ^

[site params lookup {:keys [root]}]
Added 3.0

initialises template for customisation and deployment

v 3.0
(defn init-template
  ([site params lookup {:keys [root]}]
   (let [{:keys [write print]} (task/single-function-print params)
         {:keys [theme]} (lookup site)
         {:keys [resource manifest]} (load-theme theme)
         template-path (fs/path root *template-path* (coerce/to-string site))]
     (cond (fs/exists? template-path)
           (throw (ex-info (str "Template for " site " already exists") {:path (str template-path)}))

           :else
           (let [move-list   (mapv (juxt (fn [path]
                                           (-> (str resource "/" path)
                                               (io/resource)))
                                         (fn [path]
                                           (fs/path template-path path)))
                                   manifest)]
             (mapv (fn [i [resource out]]
                     (try
                       (when write
                         (fs/create-directory (fs/parent out))
                         (with-open [input (.openStream ^java.net.URL resource)]
                           (fs/write-into out input {:options [:replace-existing]})))
                       (if (:function print)
                         (print/println
                          (ansi/blue (format "%4s" (inc i)))
                          (ansi/bold (str (fs/relativize root out)))
                          '<=
                          (str resource)))
                       (catch Exception e
                         (print/println "Cannot initialise filename:" out)))
                     out)
                   (range (count move-list))
                   move-list))))))
link
(init-template "spirit" {:write true} (all-sites {:root "."}) {:root "."})

load-theme ^

[theme]
Added 3.0

loads a theme to provide the renderer

v 3.0
(defn load-theme
  ([theme]
   (let [ns (cond (string? theme)
                  (symbol (str "code.doc.theme." theme))

                  (symbol? theme) theme

                  :else (throw (ex-info "Cannot load theme" {:theme theme})))
         theme (do (require ns)
                   (load-var ns "settings"))]
     (update-in theme [:render]
                (fn [m]
                  (collection/map-vals (fn [v] [:fn (load-var ns v)]) m))))))
link
(load-theme "bolton") => (contains {:engine "winterfell", :resource "assets/code.doc/theme/bolton", :copy ["assets"], :render map?, :manifest sequential?})

render ^

[key {:keys [write full skip], :as params} lookup project]
Added 3.0

renders .clj file to html

v 3.0
(defn render
  ([key {:keys [write full skip] :as params} lookup project]
   (let [params  (task/single-function-print params)
         interim (prepare/prepare key params lookup project)
         {:keys [ns theme name] :as entry} (lookup key)
         theme   (load-theme theme)
         root    (:root project)
         start   (System/currentTimeMillis)
         current (let [now (java.util.Date. start)]
                   {:date (-> (java.text.SimpleDateFormat. "dd MMMM yyyy")
                              (.format now))
                    :time (-> (java.text.SimpleDateFormat. "HH mm")
                              (.format now))})
         include-path  (fs/path root *template-path* ns *default-include*)
         template-path (fs/path root *template-path* ns (or (:base entry)
                                                            (:base theme)
                                                            *default-base*))
         output-path   (fs/path root (:output entry) (str name ".html"))
         include   (-> (read-string (slurp include-path))
                       (merge (:template interim)
                              (:render theme)
                              current
                              (dissoc entry :template)
                              (get-in project [:publish :template])
                              (select-keys project [:url :version])))
         output    (slurp template-path)
         output    (reduce-kv (fn [^String html k v]
                                (let [value (cond (string? v) v

                                                  (vector? v)
                                                  (case (first v)
                                                    :fn  ((second v) key interim lookup)))]
                                  (.replaceAll html
                                               (str "<@=" (clojure.core/name k) ">")
                                               (prose/escape-dollars (str value)))))
                              output
                              include)
         original  (if (fs/exists? output-path) (slurp output-path) "")
         deltas    (text.diff/diff original output)
         _         (if (-> params :print :function)
                     (print/print (text.diff/->string deltas)))
         updated   (when (and write (seq deltas))
                     (spit output-path output)
                     true)
         end       (System/currentTimeMillis)]
     (cond-> (text.diff/summary deltas)
       full  (assoc :deltas deltas)
       :them (assoc :path (str (fs/relativize (:root project) output-path))
                    :updated (boolean updated)
                    :time (- end start))))))
link
(let [project (publish/make-project) lookup (all-pages project)] (render 'hara/index {:write false} lookup project)) => (contains {:path string? :updated boolean? :time number?})

4    Rendering



nav-element ^

Added 3.0

seed function for rendering a navigation element

v 3.0
(defmulti nav-element
  :type)
link
(nav-element {:type :chapter :tag "chk" :number 1 :title "Introduction"}) => [:h4 [:a {:href "#chk"} "1   Introduction"]]

page-element ^

Added 3.0

seed function for rendering a page element

v 3.0
(defmulti page-element
  :type)
link
(page-element {:type :html :src "
"}) => "
" (page-element {:type :chapter :tag "chk" :number 1 :title "Introduction"}) => [:div [:span {:id "chk"}] [:h2 [:b "1    Introduction"]]] (page-element {:type :hero :title "foundation-code"}) => (contains [:section {:class "hero"}]) (let [element (page-element {:type :widget/js :src "js/widgets/sample.js" :class "sample" :props {:value ""}})] [(first element) (second element) (-> element (nth 2) (nth 2) (clojure.string/includes? "\u003c/script\u003e"))]) => [:div [:div {:class "widget-js sample" :data-widget-state "pending" :aria-live "polite"} "Loading widget…"] true]

render-chapter ^

[{:keys [tag title number elements link table only exclude], :as elem}]
Added 3.0

seed function for rendering a chapter element

v 3.0
(defn render-chapter
  ([{:keys [tag title number elements
            link table only exclude] :as elem}]
   (let [sections (mapv (fn [{:keys [tag title number] :as elem}]
                          [:a {:class "section"
                               :data-scroll ""
                               :href (str "#" tag)}
                           [:h5 [:i (str number "   " title)]]])
                        elements)
         api-entries (when (and (empty? sections) link table)
                       (let [entries (api/select-entries elem)]
                         (mapv (fn [entry]
                                 [:a {:class "section"
                                      :data-scroll ""
                                      :href (str "#" (api/entry-tag link entry))}
                                  [:h5 [:i (str entry)]]])
                               entries)))]
     (apply vector
            :li
            [:a {:class "chapter"
                 :data-scroll ""
                 :href (str "#" tag)}
             [:h4 (str number "   " title)]]
            (concat sections api-entries)))))
link
(render-chapter {:tag "chk" :number 1 :title "Introduction" :elements [{:tag "sec1" :number "1.1" :title "Section 1"}]}) => [:li [:a {:class "chapter", :data-scroll "", :href "#chk"} [:h4 "1   Introduction"]] [:a {:class "section", :data-scroll "", :href "#sec1"} [:h5 [:i "1.1   Section 1"]]]]

5    Walkthrough

This walkthrough follows a single page through the code.doc pipeline: parsing a Clojure or Markdown source file, looking up publishing resources, and rendering the resulting elements as Hiccup HTML.

5.1    Parsing directives and facts

code.doc.parse turns raw source into a sequence of elements. Directives such as [[:chapter ...]] become element maps, and fact forms become test blocks that can be executed during publication.

parse a chapter directive

(-> (nav/parse-string "[[:chapter {:title \"Hello\"}]]")
    (parse/parse-directive))
=> {:type :chapter :title "Hello"}

parse a fact form into a test element

(-> (nav/parse-string "(fact \"basic addition\" (+ 1 1) \n => 2)")
    (parse/parse-fact-form))
=> {:type :test :indentation 2 :caption "basic addition" :code "(+ 1 1) \n => 2"}

parse a comment block into a code block element

(-> (nav/parse-string "(comment (+ 1 1) \n => 2)")
    (parse/parse-comment-form))
=> {:type :block :indentation 2 :code "(+ 1 1) \n => 2"}

5.2    Parsing Markdown

Markdown files are first-class documentation sources. The parser extracts front matter, headings, embedded directives, and fenced code blocks.

parse a markdown heading with an id

(parse/parse-header "## Section {#section-id}")
=> {:type :section :title "Section" :tag "section-id"}

parse a complete markdown snippet

(parse/parse-markdown (str "---\n{:title \"Hello\"}\n---\n"
                           "# Heading {#heading}\n"
                           "[[:callout {:tone :success :title \"Nice\" :content \"Yep\"}]]\n"
                           "```clojure\n(+ 1 1)\n```"))
=> [{:type :article :title "Hello"}
    {:type :chapter :title "Heading" :tag "heading"}
    {:type :callout :tone :success :title "Nice" :content "Yep"}
    {:type :block :indentation 0 :lang "clojure" :code "(+ 1 1)"}]

5.3    Loading themes and vars

Before rendering, the executive resolves external resources. load-var pulls in a function from any namespace, and load-theme loads a theme definition so its render functions can be used during page generation.

load a var by namespace and name

(exec/load-var "clojure.core" "apply")
=> fn?

load the bolton theme

(exec/load-theme "bolton")
=> (contains {:engine "winterfell"
              :resource string?
              :render map?})

5.4    Rendering elements

code.doc.engine.winterfell converts parsed elements into Hiccup data structures. Chapter and section tags become anchors so the table of contents can link directly to them.

render a chapter heading

(winterfell/page-element {:type :chapter :tag "intro" :number 1 :title "Introduction"})
=> [:div [:span {:id "intro"}] [:h2 [:b "1 &nbsp;&nbsp; Introduction"]]]

render a navigation section

(winterfell/nav-element {:type :section :tag "sec1" :number "1.1" :title "First Steps"})
=> [:h5 "&nbsp;&nbsp;"
    [:i [:a {:href "#sec1"} "1.1 &nbsp; First Steps"]]]

render a chapter entry for the table of contents

(winterfell/render-chapter {:tag "intro" :number 1 :title "Introduction"
                            :elements [{:tag "sec1" :number "1.1" :title "First Steps"}]})
=> [:li
    [:a {:class "chapter", :data-scroll "", :href "#intro"} [:h4 "1 &nbsp; Introduction"]]
    [:a {:class "section", :data-scroll "", :href "#sec1"} [:h5 [:i "1.1 &nbsp; First Steps"]]]]