2014-12-05 15:56:40 +00:00
|
|
|
(ns cryogen-core.toc
|
2015-11-01 05:59:54 +00:00
|
|
|
(:require [clojure.zip :as z]
|
2017-01-16 21:16:11 +00:00
|
|
|
[net.cgrand.enlive-html :as enlive]
|
2015-07-08 20:34:39 +00:00
|
|
|
[hiccup.core :as hiccup]))
|
2014-12-04 16:38:48 +00:00
|
|
|
|
2014-12-14 14:07:25 +00:00
|
|
|
(def _h [:h1 :h2 :h3 :h4 :h5 :h6])
|
2015-10-31 18:50:51 +00:00
|
|
|
|
2015-11-01 05:59:54 +00:00
|
|
|
(defn- compare-index [i1 i2] (- (.indexOf ^clojure.lang.APersistentVector _h i2) (.indexOf ^clojure.lang.APersistentVector _h i1)))
|
2014-12-14 14:07:25 +00:00
|
|
|
|
2015-01-09 12:13:35 +00:00
|
|
|
(defn- get-headings
|
|
|
|
"Turn a body of html content into a vector of elements whose tags are
|
|
|
|
headings."
|
|
|
|
[content]
|
2014-12-04 16:38:48 +00:00
|
|
|
(reduce
|
|
|
|
(fn [headings {:keys [tag attrs content] :as elm}]
|
2014-12-14 14:07:25 +00:00
|
|
|
(if (some #{tag} _h)
|
2014-12-04 16:38:48 +00:00
|
|
|
(conj headings elm)
|
|
|
|
(if-let [more-headings (get-headings content)]
|
|
|
|
(into headings more-headings)
|
|
|
|
headings)))
|
|
|
|
[] content))
|
|
|
|
|
2015-11-01 05:59:54 +00:00
|
|
|
(defn- zip-toc-tree-to-insertion-point
|
|
|
|
"Given a toc-tree zipper and a header level, navigate
|
|
|
|
the zipper to the appropriate parent of the level for that header
|
|
|
|
to be inserted and return the zipper."
|
|
|
|
[toctree h-tag]
|
|
|
|
(if-let [current-tag (-> toctree first :value :tag)]
|
|
|
|
(let [direction (compare-index h-tag current-tag)]
|
|
|
|
(cond (zero? direction) (z/up toctree) ; Tag belongs at current level
|
|
|
|
(neg? direction) toctree ; Tag belongs below this level
|
|
|
|
(pos? direction) (recur (z/up toctree) h-tag))) ; Keep looking up
|
|
|
|
; This level is the root list, return it
|
|
|
|
toctree))
|
|
|
|
|
|
|
|
(defn- insert-toc-tree-entry
|
|
|
|
"Inserts a toc-tree (zipper) entry for the given entry at the appropriate place.
|
|
|
|
Obeys the invariant that the toc-tree (zipper) is always moved to the inserted loc."
|
|
|
|
[tree entry]
|
|
|
|
(let [{htag :tag} entry
|
|
|
|
tree (zip-toc-tree-to-insertion-point tree htag)]
|
|
|
|
(-> tree (z/append-child {:children [] :value entry}) z/down z/rightmost)))
|
|
|
|
|
|
|
|
(defn- build-toc-tree
|
|
|
|
"Given a sequence of header nodes, build a toc tree using zippers
|
|
|
|
and return it."
|
|
|
|
[headings]
|
|
|
|
(loop [zp (z/zipper
|
|
|
|
map?
|
|
|
|
:children
|
|
|
|
(fn [node children] (assoc node :children (apply vector children)))
|
|
|
|
{:value :root :children []})
|
|
|
|
items headings]
|
2015-01-09 12:16:22 +00:00
|
|
|
(if-let [{tag :tag {id :id} :attrs [{{name :name} :attrs} title :as htext] :content} (first items)]
|
|
|
|
(let [anchor (or id name)]
|
2015-07-08 20:34:39 +00:00
|
|
|
(if (nil? anchor)
|
2015-11-01 05:59:54 +00:00
|
|
|
(recur zp (rest items))
|
2016-10-23 03:04:29 +00:00
|
|
|
(recur (insert-toc-tree-entry zp
|
|
|
|
{:tag tag
|
|
|
|
:anchor anchor
|
|
|
|
:text (or
|
|
|
|
(if (string? title) title (-> title :content first))
|
|
|
|
(first htext))})
|
2015-11-01 05:59:54 +00:00
|
|
|
(rest items))))
|
|
|
|
(z/root zp))))
|
|
|
|
|
|
|
|
|
|
|
|
(defn- make-toc-entry
|
|
|
|
"Given an anchor link and some text, construct a toc entry
|
|
|
|
consisting of link to the anchor using the given text, wrapped
|
|
|
|
in an <li> tag."
|
|
|
|
[anchor text]
|
|
|
|
(when (and anchor text)
|
|
|
|
[:li [:a {:href (str "#" anchor)} text]]))
|
|
|
|
|
2015-10-25 23:14:43 +00:00
|
|
|
|
2015-11-01 05:59:54 +00:00
|
|
|
(defn- build-toc
|
|
|
|
"Given the root of a toc tree and either :ol or :ul,
|
|
|
|
generate the table of contents and return it as a hiccup tree."
|
|
|
|
[toc-tree list-open & {:keys [outer-list?] :or {outer-list? true}}]
|
|
|
|
(let [{:keys [children], {:keys [anchor text]} :value} toc-tree
|
|
|
|
li (make-toc-entry anchor text)
|
|
|
|
first-list-open (if outer-list?
|
|
|
|
(keyword (str (name list-open) ".content"))
|
|
|
|
list-open)]
|
|
|
|
; Create hiccup sequence of :ol/:ul tag and sequence of :li tags
|
|
|
|
(if (seq children)
|
|
|
|
(let [sublist [first-list-open (map build-toc children
|
|
|
|
(repeat list-open)
|
|
|
|
(repeat :outer-list?)
|
|
|
|
(repeat false))]]
|
|
|
|
(if-let [li li] ; The root element has nothing so ignore it
|
|
|
|
(seq [li sublist]) ; Use seq to lazily concat the li with the sublists
|
|
|
|
sublist))
|
|
|
|
li))) ; Or just return the naked :li tag
|
2015-10-31 18:50:51 +00:00
|
|
|
|
2015-11-01 05:59:54 +00:00
|
|
|
(defn generate-toc
|
2015-10-25 23:14:43 +00:00
|
|
|
"Reads an HTML string and parses it for headers, then returns a list of links
|
|
|
|
to them.
|
2014-12-04 16:38:48 +00:00
|
|
|
|
2015-10-25 23:14:43 +00:00
|
|
|
Optionally, a map of :list-type can be provided with value :ul, :ol, or true.
|
|
|
|
:ol and true will result in an ordered list being generated for the table of
|
|
|
|
contents, while :ul will result in an unordered list. The default is an
|
|
|
|
ordered list."
|
2017-01-16 21:16:11 +00:00
|
|
|
[html & {:keys [list-type] :or {list-type :ol}}]
|
2015-11-09 01:29:37 +00:00
|
|
|
(let [list-type (if (true? list-type) :ol list-type)]
|
|
|
|
(-> html
|
2017-01-16 21:16:11 +00:00
|
|
|
(enlive/html-snippet)
|
2015-11-01 05:59:54 +00:00
|
|
|
(get-headings)
|
|
|
|
(build-toc-tree)
|
|
|
|
(build-toc list-type)
|
2015-11-09 01:29:37 +00:00
|
|
|
(hiccup/html))))
|