diff --git a/clojure/src/firn/file.clj b/clojure/src/firn/file.clj
index eb58952e..d9dec307 100644
--- a/clojure/src/firn/file.clj
+++ b/clojure/src/firn/file.clj
@@ -73,19 +73,6 @@
[f m]
(merge f m))
-(defn get-keywords
- "Returns a list of org-keywords from a file. All files must have keywords."
- [f]
- (let [expected-keywords (get-in f [:as-edn :children 0 :children])]
- (if (= "keyword" (:type (first expected-keywords)))
- expected-keywords
- (u/print-err! :error "The org file <<" (f :name) ">> does not have 'front-matter' Please set at least the #+TITLE keyword for your file."))))
-
-(defn get-keyword
- "Fetches a(n org) #+keyword from a file, if it exists."
- [f keywrd]
- (->> f get-keywords (u/find-first #(= keywrd (:key %))) :value))
-
(defn keywords->map
"Converts an org-file's keywords into a map.
[{:type keyword, :key TITLE, :value Firn, :post_blank 0}
@@ -93,7 +80,7 @@
Becomes
{:title Firn, :date-created <2020-03-01--09-53>, :status active, :firn-layout project}"
[f]
- (let [kw (get-keywords f)
+ (let [kw (org/get-keywords f)
lower-case-it #(when % (s/lower-case %))
dash-it #(when % (s/replace % #"_" "-"))
key->keyword (fn [k] (-> k :key lower-case-it dash-it keyword))]
@@ -103,7 +90,7 @@
"Returns true if a file meets the conditions of being 'private'
Assumes the files has been read into memory and parsed to edn."
[config f]
- (let [is-private? (get-keyword f "FIRN_PRIVATE")
+ (let [is-private? (org/get-keyword f "FIRN_PRIVATE")
file-path (-> f :path (s/split #"/"))
in-priv-folder? (some (set file-path) (config :ignored-dirs))]
(or
@@ -140,8 +127,8 @@
items to keep track of headline values that precede a logbook.
This is easier and more performant than searching an entire edn-tree of
headings to see if they have a logbook to associate with. ┬──┬◡ノ(° -°ノ)"
- [tree-seq]
- (loop [tree-items tree-seq
+ [tree-data]
+ (loop [tree-items tree-data
output []
last-headline nil]
(if (empty? tree-items)
@@ -157,6 +144,59 @@
output)]
(recur remaining-items new-output headline-val)))))
+(defn extract-metadata-helper
+ "There are lots of things we want to extract when iterating over the AST.
+ Rather than filter/loop/map over it several times, it all happens here.
+ Collects:
+ - Logbooks
+ - Links
+ - Headings for TOC.
+ - eventually... a plugin for custom file collection?"
+ [tree-data file-metadata]
+ (loop [tree-data tree-data
+ out-logs []
+ out-links []
+ out-toc []
+ last-headline nil] ; the most recent headline we've encountered.
+ (if (empty? tree-data)
+ ;; All done! return the collected stuff.
+ {:logbook out-logs
+ :toc out-toc
+ :links out-links}
+ ;; Do the work.
+ (let [x (first tree-data)
+ xs (rest tree-data)]
+ (case (:type x)
+ "headline" ; if headline, collect data, push into toc, and set as "last-headline"
+ (let [toc-item {:level (x :level)
+ :text (org/get-headline-helper x)
+ :anchor (org/make-headline-anchor x)}
+ new-toc (conj out-toc toc-item)]
+ (recur xs out-logs out-links new-toc x))
+
+ "clock" ; if clock, merge headline-data into it, and push/recurse new-logs.
+ (let [headline-meta {:from-headline (-> last-headline :children first :raw)}
+ log-augmented (merge headline-meta file-metadata x)
+ new-logs (conj out-logs log-augmented)]
+ (recur xs new-logs out-links out-toc last-headline))
+
+ "link" ; if link, also merge file metadata and push into new-links and recurse.
+ (let [link-item (merge x file-metadata)
+ new-links (conj out-links link-item)]
+ (recur xs out-logs new-links out-toc last-headline))
+
+ ;; default case, recur.
+ (recur xs out-logs out-links out-toc last-headline))))))
+
+(defn htmlify
+ "Renders files according to their `layout` keyword."
+ [config f]
+ (let [layout (keyword (org/get-keyword f "FIRN_LAYOUT"))
+ as-html (when-not (is-private? config f)
+ (layout/apply-layout config f layout))]
+ ;; as-html
+ (change f {:as-html as-html})))
+
(defn extract-metadata
"Iterates over a tree, and returns metadata for site-wide usage such as
links (for graphing between documents, tbd) and logbook entries."
@@ -164,45 +204,38 @@
(let [org-tree (file :as-edn)
tree-data (tree-seq map? :children org-tree)
file-metadata {:from-file (file :name) :from-file-path (file :path-web)}
- links (filter #(= "link" (:type %)) tree-data)
- logbook (extract-metadata-logbook-helper tree-data)
- logbook-aug (map #(merge % file-metadata) logbook)
- logbook-sorted (sort-logbook logbook-aug file)
- links-aug (map #(merge % file-metadata) links)
- date-updated (get-keyword file "DATE_UPDATED")
- date-created (get-keyword file "DATE_CREATED")]
-
- {:links links-aug
+ date-updated (org/get-keyword file "DATE_UPDATED")
+ date-created (org/get-keyword file "DATE_CREATED")
+ metadata (extract-metadata-helper tree-data file-metadata)
+ logbook-sorted (sort-logbook (metadata :logbook) file)]
+
+ {:links (metadata :links)
:logbook logbook-sorted
:logbook-total (sum-logbook logbook-sorted)
- :keywords (get-keywords file)
- :title (get-keyword file "TITLE")
- :firn-under (get-keyword file "FIRN_UNDER")
+ :keywords (org/get-keywords file)
+ :title (org/get-keyword file "TITLE")
+ :firn-under (org/get-keyword file "FIRN_UNDER")
+ :toc (metadata :toc)
:date-updated (when date-updated (u/strip-org-date date-updated))
:date-created (when date-created (u/strip-org-date date-created))
:date-updated-ts (when date-updated (u/org-date->ts date-updated))
:date-created-ts (when date-created (u/org-date->ts date-created))}))
-(defn htmlify
- "Renders files according to their `layout` keyword."
- [config f]
- (let [layout (keyword (get-keyword f "FIRN_LAYOUT"))
- as-html (when-not (is-private? config f)
- (layout/apply-layout config f layout))]
- ;; as-html
- (change f {:as-html as-html})))
-
(defn process-one
"Munge the 'file' datastructure; slowly filling it up, using let-shadowing.
Essentially, converts `org-mode file string` -> json, edn, logbook, keywords"
[config f]
+
(let [new-file (make config f) ; make an empty "file" map.
as-json (->> f slurp org/parse!) ; slurp the contents of a file and parse it to json.
as-edn (-> as-json (json/parse-string true)) ; convert the json to edn.
new-file (change new-file {:as-json as-json :as-edn as-edn}) ; shadow the new-file to add the json and edn.
file-metadata (extract-metadata new-file) ; collect the file-metadata from the edn tree.
- new-file (change new-file {:meta file-metadata}) ; shadow hte file and add the metadata
- final-file (htmlify config new-file)] ; parses the edn tree -> html.
+ new-file (change new-file {:meta file-metadata}) ; shadow the file and add the metadata
+ ;; TODO PERF: htmlify happening as well in `process-all`.
+ ;; this is due to the dev server. There should be a conditional
+ ;; that checks if we are running in server.
+ final-file (htmlify config new-file)] ; parses the edn tree -> html.
final-file))
@@ -225,6 +258,7 @@
:site-map @site-map
:site-links @site-links
:site-logs @site-logs)
+ ;; FIXME: I think we are rendering html twice here, should prob only happen here?
with-html (into {} (for [[k pf] output] [k (htmlify config-with-data pf)]))
final (assoc config-with-data :processed-files with-html)]
final)
@@ -242,9 +276,11 @@
(dissoc (processed-file :meta) :logbook :links :keywords)
{:path (str "/" (processed-file :path-web))})]
-
+
;; add to sitemap when file is not private.
+
+
(when-not is-private
(swap! site-map conj new-site-map-item)
(swap! site-links concat (-> processed-file :meta :links))
@@ -265,13 +301,13 @@
:description (str (f :as-html))))]
(io/make-parents feed-file)
(->> processed-files
- (filter (fn [[_ f]] (-> f :meta :date-created)))
- (map make-rss)
- (sort-by :pubDate)
- (reverse)
- (u/prepend-vec first-entry) ; first entry must be about the site
- (apply rss/channel-xml)
- (spit feed-file)))
+ (filter (fn [[_ f]] (-> f :meta :date-created)))
+ (map make-rss)
+ (sort-by :pubDate)
+ (reverse)
+ (u/prepend-vec first-entry) ; first entry must be about the site
+ (apply rss/channel-xml)
+ (spit feed-file)))
config)
(defn reload-requested-file
diff --git a/clojure/src/firn/layout.clj b/clojure/src/firn/layout.clj
index a3c9ccff..35b93f64 100644
--- a/clojure/src/firn/layout.clj
+++ b/clojure/src/firn/layout.clj
@@ -6,7 +6,8 @@
(:require [firn.markup :as markup]
[firn.org :as org]
- [hiccup.core :as h]))
+ [hiccup.core :as h]
+ [sci.core :as sci]))
(defn- internal-default-layout
"The default template if no `layout` key is specified.
@@ -51,10 +52,7 @@
(= action :file)
(markup/to-html (file :as-edn))
-
;; render a headline title.
-
-
(and is-headline? (= opts :title))
(let [hl (org/get-headline org-tree action)]
(-> hl :children first markup/to-html))
@@ -77,11 +75,22 @@
(= action :logbook-polyline)
(org/poly-line (-> file :meta :logbook) opts)
- :else
- (str "DEBUG: Incorrect use of `render` function in template:
-
action: => " action " << is this a valid value?
-
opts: => " opts " << is this a valid value?
"
- "
")))))
+ ;; render a table of contents
+ (= action :toc)
+ (let [toc (-> file :meta :toc) ; get the toc for hte file.
+ firn_toc (sci/eval-string (org/get-keyword file "FIRN_TOC")) ; read in keyword for overrides
+ opts (or firn_toc opts {})] ; apply most pertinent options.
+ (when (seq toc)
+ (markup/make-toc toc opts)))
+
+ :else ; error message to indicate incorrect use of render.
+ (str "
"
+ "
Render Error.
"
+ "
Incorrect use of `render` function in template:
+
action: => " action " << is this a valid value?
+
opts: => " opts " << is this a valid value?
"
+ "
"
+ "
")))))
(defn prepare
"Prepare functions and data to be available in layout functions.
@@ -111,3 +120,4 @@
[config file layout]
(let [selected-layout (get-layout config file layout)]
(h/html (selected-layout (prepare config file)))))
+
diff --git a/clojure/src/firn/markup.clj b/clojure/src/firn/markup.clj
index a04dc91d..e182f0b4 100644
--- a/clojure/src/firn/markup.clj
+++ b/clojure/src/firn/markup.clj
@@ -6,7 +6,99 @@
(declare to-html)
-;; Renderers
+;; Feature: Table of Contents --------------------------------------------------
+
+(defn make-toc-helper-reduce
+ "(ಥ﹏ಥ) Yeah. So. See the docstring for make-toc.
+ Basically, this is a bit of a nightmare. This turns a flat list into a tree
+ So that we can property create nested table of contents."
+ [{:keys [out prev min-level] :as acc} curr]
+ (cond
+ ;; top level / root headings.
+ (or (empty? out) (= min-level (curr :level)))
+ (let [with-meta (assoc curr :next-sibling [:out (count out)])
+ with-meta (assoc with-meta :next-child [:out (count out) :children])]
+ (-> acc
+ (update :out conj with-meta)
+ (assoc :prev with-meta)))
+
+ ;; if the new items level >= prev item, go to the last item in out
+ ;; iterate through children, and try and find `prev`, when you do, collect "path to prev"
+ ;; if/when you do update the child list.
+ (> (curr :level) (prev :level))
+ (let [parent-path (count (get-in acc (prev :next-child)))
+ with-meta (assoc curr :next-sibling (prev :next-child))
+ with-meta (assoc with-meta :next-child (conj (prev :next-child) parent-path :children))]
+ (-> acc
+ (update-in (prev :next-child) conj with-meta)
+ (assoc :prev with-meta)))
+
+ (= (curr :level) (prev :level))
+ (let [parent-path (count (get-in acc (prev :next-sibling)))
+ with-meta (assoc curr :next-sibling (prev :next-sibling)) ;; if more, add children, if equal, conj onto children.
+ with-meta (assoc with-meta :next-child (conj (prev :next-sibling) parent-path :children))] ;; if more, add children, if equal, conj onto children.
+ (-> acc
+ (update-in (prev :next-sibling) conj with-meta)
+ (assoc :prev with-meta)))
+
+ (< (curr :level) (prev :level))
+ (let [difference (- (prev :level) (curr :level)) ; if we are on level 5, and the next is level 3...
+ diff-to-take (* difference 2) ; we need to take (5 - 3 ) * 2 = 4 items off the last :next-sibling
+ ;; HACK: we can use the prev-elements :next-sibling path and chop N
+ ;; elements off the ending based on our heading's leve; which gives us
+ ;; the path to conj onto.
+ path (vec (drop-last diff-to-take (prev :next-sibling)))
+ parent-path (count (get-in acc path))
+ with-meta (assoc curr :next-sibling path) ;; if more, add children, if equal, conj onto children.
+ with-meta (assoc with-meta :next-child (conj path parent-path :children))]
+ (-> acc
+ (update-in path conj with-meta)
+ (assoc :prev with-meta)))
+
+ :else
+ (do (println "Something has gone wrong. ") acc)))
+
+(defn toc->html
+ [toc kind]
+ (->> toc
+ (map (fn [x]
+ (if (empty? (x :children))
+ [:li
+ [:a {:href (x :anchor)} (x :text)]]
+ [:li
+ [:a {:href (x :anchor)} (x :text)]
+ [kind (toc->html (x :children) kind)]])))))
+
+(defn make-toc
+ "toc: a flattened list of headlines with a :level value of 1-> N:
+ [{:level 1, :text 'Process', :anchor '#process'} {:level 2, :text 'Relevance', :anchor '#relevance'}]
+
+ We conditonally thread the heading, passing in configured values, such as
+ where to start the table of contents (at a specific headline?)
+ or unto what depth we want the headings to render."
+ ([toc]
+ (make-toc toc {}))
+ ([toc {:keys [headline depth list-type exclusive?]
+ :or {depth nil list-type :ol}
+ :as opts}]
+ (let [s-h (u/find-first #(= (% :text) headline) toc) ; if user specified a heading to start at, go find it.
+ toc (cond->> toc ; apply some filtering to the toc, if params are passed in.
+ depth (filter #(<= (% :level) depth)) ; if depth; keep everything under that depth.
+ headline (drop-while #(not= s-h %)) ; drop everything up till the selected heading we want.
+ headline (u/take-while-after-first ; remove everything that's not a child of the selected heading.
+ #(> (% :level) (s-h :level)))
+ exclusive? (drop 1)) ; don't include selected heading; just it's children.)
+
+ min-level (if (seq toc) (:level (apply min-key :level toc)) 1) ; get the min level for calibrating the reduce.
+ toc-cleaned (->> toc
+ (map #(assoc % :children [])) ; create a "children" key on every item.)
+ (reduce make-toc-helper-reduce {:out [] :prev nil :min-level min-level})
+ :out)]
+
+ (if (empty? toc-cleaned) nil
+ (into [list-type] (toc->html toc-cleaned list-type))))))
+
+;; General Renderers -----------------------------------------------------------
(defn date->html
[v]
@@ -33,26 +125,13 @@
[:span.firn-img-caption desc]]
[:img {:src path}]))
-(defn clean-anchor
- "converts `::*My Heading` => #my-heading
- NOTE: This could be a future problem area; ex: forwars slashes have to be
- replaces, otherwise they break the html rendering, thus
- 'my heading / example -> my-heading--example
- Future chars to watch out for: `>` `<` `&` `!`"
- [anchor]
- (str "#" (-> anchor
- (s/replace #"::\*" "")
- (s/replace #"\/" "")
- (s/replace #" " "-")
- (s/lower-case))))
-
(defn internal-link-handler
"Takes an org link and converts it into an html path."
[org-link]
(let [regex #"(file:)(.*)\.(org)(\:\:\*.+)?"
res (re-matches regex org-link)
anchor-link (last res)
- anchor-link (when anchor-link (-> res last clean-anchor))]
+ anchor-link (when anchor-link (-> res last u/clean-anchor))]
(if anchor-link
(str "./" (nth res 2) anchor-link)
(str "./" (nth res 2)))))
@@ -109,10 +188,10 @@
keywrd (v :keyword)
priority (v :priority)
value (v :value)
- parent {:type "headline" :level level :children [v]}
+ parent {:type "headline" :level level :children [v]} ; reconstruct the parent so we can pull out the content.
heading-priority (u/str->keywrd "span.firn-headline-priority.firn-headline-priority__" priority)
heading-keyword (u/str->keywrd "span.firn-headline-keyword.firn-headline-keyword__" keywrd)
- heading-anchor (-> parent org/get-headline-helper clean-anchor)
+ heading-anchor (org/make-headline-anchor parent)
heading-id+class #(u/str->keywrd "h" % heading-anchor ".firn-headline.firn-headline-" %)
h-level (case level
1 (heading-id+class 1)
diff --git a/clojure/src/firn/org.clj b/clojure/src/firn/org.clj
index c186d605..3587502e 100644
--- a/clojure/src/firn/org.clj
+++ b/clojure/src/firn/org.clj
@@ -4,7 +4,8 @@
Which are created by the rust binary."
(:require [clojure.java.shell :as sh]
[clojure.string :as s]
- [firn.util :as u])
+ [firn.util :as u]
+ [firn.org :as org])
(:import iceshelf.clojure.rust.ClojureRust)
(:import (java.time LocalDate)))
@@ -46,9 +47,10 @@
(for [child title-children]
(s/trim
(case (:type child)
- "text" (get-trimmed-val child :value)
- "link" (get-trimmed-val child :desc)
- "code" (get-trimmed-val child :value)
+ "text" (get-trimmed-val child :value)
+ "link" (get-trimmed-val child :desc)
+ "code" (get-trimmed-val child :value)
+ "verbatim" (get-trimmed-val child :value)
"")))))))
(defn get-headline
@@ -65,6 +67,12 @@
(let [headline (get-headline tree name)]
(update headline :children (fn [d] (filter #(not= (:type %) "title") d)))))
+(defn make-headline-anchor
+ "Takes a headline data structure and returns the id 'anchored' for slugifying
+ TODO: test me."
+ [node]
+ (-> node get-headline-helper u/clean-anchor))
+
(defn parsed-org-date->unix-time
"Converts the parsed org date (ex: [2020-04-27 Mon 15:39] -> 1588003740000)
and turns it into a unix timestamp."
@@ -78,8 +86,25 @@
(u/print-err! :warning (str "Failed to parse the logbook for file:" "<<" name ">>" "\nThe logbook may be incorrectly formatted.\nError value:" e))
"???"))))
-;; -- stats --
+;; NOTE: These should be in File.clj, but they are not due to a circular dependency.
+;; --
+
+(defn get-keywords
+ "Returns a list of org-keywords from a file. All files must have keywords."
+ [f]
+ (let [expected-keywords (get-in f [:as-edn :children 0 :children])]
+ (if (= "keyword" (:type (first expected-keywords)))
+ expected-keywords
+ (u/print-err! :error "The org file <<" (f :name) ">> does not have 'front-matter' Please set at least the #+TITLE keyword for your file."))))
+(defn get-keyword
+ "Fetches a(n org) #+keyword from a file, if it exists."
+ [f keywrd]
+ (->> f get-keywords (u/find-first #(= keywrd (:key %))) :value))
+
+;; --
+
+;; -- stats --
(defn- find-day-to-update
[calendar-year log-entry]
@@ -90,7 +115,7 @@
(defn- update-logbook-day
"Updates a day in a calander from build-year with logbook data."
[{:keys [duration] :as log-entry}]
- (fn [{:keys [log-count logs-raw log-sum day] :as cal-day}]
+ (fn [{:keys [log-count logs-raw log-sum] :as cal-day}]
(let [log-count (inc log-count)
logs-raw (conj logs-raw log-entry)
log-sum (u/timestr->add-time log-sum duration)]
@@ -123,17 +148,17 @@
;; Rendered charts:
+;; TODO This should be in markup, as it's spitting out html
(defn poly-line
"Takes a logbook, formats it so that it can be plotted along a polyline."
([logbook]
(poly-line logbook {}))
([logbook
{:keys [width height stroke stroke-width]
- :or {width 365 height 100 stroke "#0074d9" stroke-width 1}
- :as opts}]
+ :or {width 365 height 100 stroke "#0074d9" stroke-width 1}}]
[:div
(for [[year year-of-logs] (logbook-year-stats logbook)
- :let [max-log (apply max-key :hour-sum year-of-logs) ;; Don't need this yet.
+ :let [; max-log (apply max-key :hour-sum year-of-logs) ;; Don't need this yet.
;; This should be measured against the height and whatever the max-log is.
g-multiplier (/ height 8) ;; 8 - max hours we expect someone to log in a day
fmt-points #(str %1 "," (* g-multiplier (%2 :hour-sum)))
diff --git a/clojure/src/firn/util.clj b/clojure/src/firn/util.clj
index bb032907..37fa0f33 100644
--- a/clojure/src/firn/util.clj
+++ b/clojure/src/firn/util.clj
@@ -155,6 +155,12 @@
[f coll]
(first (filter f coll)))
+(defn take-while-after-first
+ [pred lst]
+ (let [head (first lst)
+ tail (take-while pred (rest lst))]
+ (concat [head] tail)))
+
;; For interception thread macros and enabling printing the passed in value.
(def spy #(do (println "DEBUG:" %) %))
@@ -166,10 +172,10 @@
"<2020-05-14 19:11> -> 2020-05-14 19:11"
[org-date]
(-> org-date
- (s/replace #"\]" "")
- (s/replace #"\[" "")
- (s/replace #"<" "")
- (s/replace #">" "")))
+ (s/replace #"\]" "")
+ (s/replace #"\[" "")
+ (s/replace #"<" "")
+ (s/replace #">" "")))
(defn org-date->java-date
"Converts <2020-02-25 05:51> -> java..."
@@ -183,9 +189,9 @@
(defn org-date->ts
[org-date]
(-> org-date
- strip-org-date
- (org-date->java-date)
- java-date->unix-ts))
+ strip-org-date
+ (org-date->java-date)
+ java-date->unix-ts))
(defn native-image?
"Check if we are in the native-image or REPL."
@@ -198,10 +204,26 @@
[& args]
(keyword (apply str args)))
+(defn clean-anchor
+ "converts `::*My Heading` => #my-heading
+ NOTE: This could be a future problem area; ex: forwards slashes have to be
+ replaced, otherwise they break the html rendering, thus
+ 'my heading / example -> my-heading--example
+ Future chars to watch out for: `>` `<` `&` `!`"
+ [anchor]
+ (str "#" (-> anchor
+ (s/replace #"::\*" "")
+ (s/replace #"\/" "")
+ (s/replace #"\." "")
+ (s/replace #" " "-")
+ (s/lower-case))))
+
+
;; Time ------------------------------------------------------------------------
;; NOTE: timestr->hours-min + timevec->time-str could use better input testing?
;; At the very least, `Integer.` is an opportunity for errors when parsing.
+
(defn parse-int [number-string]
(try (Integer/parseInt number-string)
(catch Exception e nil)))
diff --git a/clojure/test/firn/file_test.clj b/clojure/test/firn/file_test.clj
index d48e347e..c6747743 100644
--- a/clojure/test/firn/file_test.clj
+++ b/clojure/test/firn/file_test.clj
@@ -45,19 +45,6 @@
(t/is (= (new-file :path) (.getPath ^java.io.File test-file)))
(t/is (= (new-file :path-web) "file1")))))
-(t/deftest get-keywords
- (t/testing "A file with keywords returns a vector where each item is a map with a key of :type 'keyword'"
- (let [file-1 (stub/gtf :tf-1 :processed)
- res (sut/get-keywords file-1)]
- (doseq [keywrd res]
- (t/is (= "keyword" (:type keywrd)))))))
-
-(t/deftest get-keyword
- (t/testing "It returns a keyword"
- (let [file-1 (stub/gtf :tf-1 :processed)
- res (sut/get-keyword file-1 "FIRN_LAYOUT")]
- (t/is (= "default" res)))))
-
(t/deftest keywords->map
(t/testing "A list of keywords gets converted into a map. "
(let [file-1 (stub/gtf :tf-1 :processed)
diff --git a/clojure/test/firn/layout_test.clj b/clojure/test/firn/layout_test.clj
index 4d02ef46..ee45a2d4 100644
--- a/clojure/test/firn/layout_test.clj
+++ b/clojure/test/firn/layout_test.clj
@@ -2,7 +2,7 @@
(:require [firn.layout :as sut]
[firn.stubs :as stub]
[clojure.test :as t]
- [firn.file :as file]
+ [firn.org :as org]
[firn.build :as build]))
(t/deftest prepare
@@ -15,6 +15,6 @@
(t/testing "The tf-layout file returns a sci function.")
(let [test-file (stub/gtf :tf-layout :processed)
sample-config (build/setup (stub/sample-config))
- layout (keyword (file/get-keyword test-file "FIRN_LAYOUT"))
+ layout (keyword (org/get-keyword test-file "FIRN_LAYOUT"))
res (sut/get-layout sample-config test-file layout)]
(t/is (= sci.impl.vars.SciVar (type res)))))
diff --git a/clojure/test/firn/markup_test.clj b/clojure/test/firn/markup_test.clj
index 1b3c9851..8433f8b8 100644
--- a/clojure/test/firn/markup_test.clj
+++ b/clojure/test/firn/markup_test.clj
@@ -50,13 +50,51 @@
(t/is (= res1 "./foo"))
(t/is (= res2 "./foo#my-headline-link")))))
-(t/deftest clean-anchor
- (t/testing "Expected results."
- (let [res1 (sut/clean-anchor "foo bar")
- res2 (sut/clean-anchor "foo bar baz")
- res3 (sut/clean-anchor "foo / bar")
- res4 (sut/clean-anchor "foo bar")]
- (t/is (= res1 "#foo-bar"))
- (t/is (= res2 "#foo-bar-baz"))
- (t/is (= res3 "#foo--bar"))
- (t/is (= res4 "#foo--------bar")))))
+(t/deftest make-toc
+ (let [ex1 [{:level 1, :text "Process" :anchor "#process"}
+ {:level 2, :text "Relevance" :anchor "#relevance"}]
+ ex1-res [:ol [:li
+ [:a {:href "#process"} "Process"]
+ [:ol [[:li [:a {:href "#relevance"} "Relevance"]]]]]]
+
+ ex2 [{:level 1, :text "Process" :anchor "#process"}
+ {:level 2, :text "Relevance" :anchor "#relevance"}
+ {:level 3, :text "Level3" :anchor "#level3"}
+ {:level 2, :text "Level2-again" :anchor "#level2-again"}
+ {:level 4, :text "Foo" :anchor "#foo"}]
+
+ ex2-res [:ol
+ [:li
+ [:a {:href "#process"} "Process"]
+ [:ol
+ '([:li
+ [:a {:href "#relevance"} "Relevance"]
+ [:ol ([:li [:a {:href "#level3"} "Level3"]])]]
+ [:li
+ [:a {:href "#level2-again"} "Level2-again"]
+ [:ol ([:li [:a {:href "#foo"} "Foo"]])]])]]]]
+
+ (t/testing "expected results, no options."
+ (let [res (sut/make-toc ex1)
+ res2 (sut/make-toc ex2)]
+ (t/is (= res ex1-res))
+ (t/is (= res2 ex2-res))))
+ (t/testing "Depth limits work."
+ (let [res-d1 (sut/make-toc ex2 {:depth 1})
+ res-d2 (sut/make-toc ex2 {:depth 2})
+ expected-d1 [:ol [:li [:a {:href "#process"} "Process"]]]
+ expected-d2 [:ol
+ [:li [:a {:href "#process"} "Process"]
+ [:ol
+ '([:li [:a {:href "#relevance"} "Relevance"]]
+ [:li [:a {:href "#level2-again"} "Level2-again"]])]]]]
+ (t/is (= res-d1 expected-d1))
+ (t/is (= res-d2 expected-d2))))
+
+ (t/testing "headline select works"
+ (let [res-1 (sut/make-toc ex2 {:headline "Relevance"})
+ expected [:ol
+ [:li
+ [:a {:href "#relevance"} "Relevance"]
+ [:ol '([:li [:a {:href "#level3"} "Level3"]])]]]]
+ (t/is (= res-1 expected))))))
diff --git a/clojure/test/firn/org_test.clj b/clojure/test/firn/org_test.clj
index 10d995c2..0ffdfa6b 100644
--- a/clojure/test/firn/org_test.clj
+++ b/clojure/test/firn/org_test.clj
@@ -71,3 +71,16 @@
(t/is (= (second-of-2020 :log-count) 1))
(t/is (= (second-of-2020 :hour-sum) 0.18))
(t/is (= (-> second-of-2020 :logs-raw count) 1))))
+
+(t/deftest get-keywords
+ (t/testing "A file with keywords returns a vector where each item is a map with a key of :type 'keyword'"
+ (let [file-1 (stub/gtf :tf-1 :processed)
+ res (sut/get-keywords file-1)]
+ (doseq [keywrd res]
+ (t/is (= "keyword" (:type keywrd)))))))
+
+(t/deftest get-keyword
+ (t/testing "It returns a keyword"
+ (let [file-1 (stub/gtf :tf-1 :processed)
+ res (sut/get-keyword file-1 "FIRN_LAYOUT")]
+ (t/is (= "default" res)))))
diff --git a/clojure/test/firn/util_test.clj b/clojure/test/firn/util_test.clj
index f635cbee..a091aefe 100644
--- a/clojure/test/firn/util_test.clj
+++ b/clojure/test/firn/util_test.clj
@@ -83,6 +83,17 @@
(let [res (sut/prepend-vec 1 [2 3])]
(t/is (= res [1 2 3])))))
+(t/deftest clean-anchor
+ (t/testing "Expected results."
+ (let [res1 (sut/clean-anchor "foo bar")
+ res2 (sut/clean-anchor "foo bar baz")
+ res3 (sut/clean-anchor "foo / bar")
+ res4 (sut/clean-anchor "foo bar")]
+ (t/is (= res1 "#foo-bar"))
+ (t/is (= res2 "#foo-bar-baz"))
+ (t/is (= res3 "#foo--bar"))
+ (t/is (= res4 "#foo--------bar")))))
+
;; -- Time / Date Tests --------------------------------------------------------
(t/deftest timestr->hours-min
diff --git a/docs/layout.org b/docs/layout.org
index 89e56009..07110b8e 100644
--- a/docs/layout.org
+++ b/docs/layout.org
@@ -1,6 +1,6 @@
#+TITLE: Layout
#+DATE_CREATED: <2020-03-24 Tue>
-#+DATE_UPDATED: <2020-05-25 21:05>
+#+DATE_UPDATED: <2020-06-14 09:29>
#+FILE_UNDER: docs
#+FIRN_LAYOUT: docs
@@ -136,9 +136,9 @@ file-specific meta-data. Example meta-data:
| logbook | A list of logbooks entries per file. | list |
| partials | a list of invokable partials in your =partials= folder | list |
| render | Enables rendering parts or entirety of an org file. | function |
-| site-links | A list of all links across all documents | list |
-| site-logs | A list of ALL logbook entries. | list |
-| site-map | A list of all files on the wiki | list |
+| site-links | A list of all links across all documents | vector |
+| site-logs | A list of ALL logbook entries. | vector |
+| site-map | A list of all files on the wiki | vector |
| title | The org mode file. | string |
| meta | A map of metadata about the file (logbook, links, etc) | map |
| title | The #+TITLE value of the file. | string |
@@ -146,6 +146,7 @@ file-specific meta-data. Example meta-data:
| date-updated | The #+DATE_UPDATED value of the file | string |
| date-created | The #+DATE_CREATED value of the file | string |
| logbook-total | The sum of all the logbook entries | string |
+| toc | Table of contents | vector |
This may seem like a lot of information to make available to a layout template. And that's because it is. But thanks to destructuring in Clojure, you can make your templates only ask for what they need:
@@ -239,6 +240,42 @@ Graphs all logbook entries for the current file in a polyline, generating a char
[:div (render :logbook-polyline {:width 365})]])))
#+END_SRC
+* Table of Contents
+When a file is processed, Firn collects *all* of it's headlines, whether you're
+choosing to render the entire file, or just one headline.
+
+There are several ways you can create table of contents in your files, from simple to more complex use cases.
+
+1. Render a table of contents for an entire file.
+
+#+BEGIN_SRC clojure
+(defn default
+ [{:keys [render partials]}]
+ (let [{:keys [head]} partials]
+
+ (head
+ [:body
+ [:div (render :toc)]
+ [:div (render :file)]])))
+#+END_SRC
+
+2. Render a table of contents for everything within a specific headline.
+
+#+BEGIN_SRC clojure
+(defn default
+ [{:keys [render partials]}]
+ (let [{:keys [head]} partials]
+ (head
+ [:body
+ ;; only renders a table of contents for a single headline's children.
+ ;; `:eclusive?` means we don't render "Notes"; just headlines that fall under it.
+ [:div (render :toc {:headline "Notes"
+ :depth 4
+ :exclusive? true})]
+ [:div (render "Notes")]])))
+#+END_SRC
+
+
* Styling Layouts
You can write css as you normally would by placing css files in the =_firn/static/css=