1 Reference
These are the namespaces that matter most when extending code.doc for code.
2 Parsing
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"}])
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"]]
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"}
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
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]}]
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]}]
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 "."})
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]
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
v 3.0
(defmulti nav-element
:type)
link
(nav-element {:type :chapter :tag "chk" :number 1 :title "Introduction"}) => [:h4 [:a {:href "#chk"} "1 Introduction"]]
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}]
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 Introduction"]]]
render a navigation section
(winterfell/nav-element {:type :section :tag "sec1" :number "1.1" :title "First Steps"})
=> [:h5 " "
[:i [:a {:href "#sec1"} "1.1 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 Introduction"]]
[:a {:class "section", :data-scroll "", :href "#sec1"} [:h5 [:i "1.1 First Steps"]]]]