diff --git a/project.clj b/project.clj index 23effc4..9361e10 100644 --- a/project.clj +++ b/project.clj @@ -1,19 +1,19 @@ -(defproject org.domaindrivenarchitecture/cryogen-core "0.1.48-SNAPSHOT" +(defproject org.domaindrivenarchitecture/cryogen-core "0.1.53-SNAPSHOT" :description "Cryogen's compiler" :url "https://github.com/cryogen-project/cryogen-core" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.8.0"] + [camel-snake-kebab "0.4.0"] + [cheshire "5.7.0"] [clj-rss "0.2.3"] - [me.raynes/fs "1.4.6"] - [crouton "0.1.2"] - [cheshire "5.6.3"] [clj-text-decoration "0.0.3"] - [io.aviso/pretty "0.1.33"] - [hiccup "1.0.5"] - [selmer "1.10.3"] - [pandect "0.6.1"] + [enlive "1.1.6"] [hawk "0.2.11"] - [clj-tagsoup "0.3.0" :exclusions [org.clojure/clojure]]] + [hiccup "1.0.5"] + [io.aviso/pretty "0.1.33"] + [me.raynes/fs "1.4.6"] + [pandect "0.6.1"] + [selmer "1.10.6"]] :deploy-repositories [["snapshots" :clojars] ["releases" :clojars]]) diff --git a/src/cryogen_core/compiler.clj b/src/cryogen_core/compiler.clj index 4854b76..72f0721 100644 --- a/src/cryogen_core/compiler.clj +++ b/src/cryogen_core/compiler.clj @@ -1,21 +1,20 @@ (ns cryogen-core.compiler - (:require [selmer.parser :refer [cache-off! render-file]] - [selmer.util :refer [set-custom-resource-path!]] - [io.aviso.exception :refer [write-exception]] - [clojure.java.io :refer [copy file reader writer]] + (:require [clojure.java.io :as io] + [clojure.pprint :refer [pprint]] [clojure.string :as s] + [io.aviso.exception :refer [write-exception]] + [net.cgrand.enlive-html :as enlive] + [selmer.parser :refer [cache-off! render-file]] + [selmer.util :refer [set-custom-resource-path!]] [text-decoration.core :refer :all] - [pl.danieljanus.tagsoup :as tagsoup] - [hiccup.core :as hiccup] - [cryogen-core.toc :refer [generate-toc]] - [cryogen-core.sass :as sass] + [cryogen-core.io :as cryogen-io] + [cryogen-core.klipse :as klipse] [cryogen-core.markup :as m] - [cryogen-core.io :refer - [get-resource find-assets create-folder create-file-recursive create-file wipe-public-folder - copy-resources copy-resources-from-theme path]] - [cryogen-core.sitemap :as sitemap] [cryogen-core.rss :as rss] - [clojure.inspector :as inspector]) + [cryogen-core.sass :as sass] + [cryogen-core.sitemap :as sitemap] + [clojure.inspector :as inspector] + [cryogen-core.toc :as toc]) (:import java.util.Locale)) (cache-off!) @@ -38,14 +37,16 @@ looking under the implemented protocol's subdirectory, but fallsback to look at the templates directory." [root mu ignored-files] - (let [assets (find-assets (path "templates" (m/dir mu) root) - (m/ext mu) - ignored-files)] + (let [assets (cryogen-io/find-assets + (cryogen-io/path "templates" (m/dir mu) root) + (m/ext mu) + ignored-files)] (if (seq assets) assets - (find-assets (path "templates" root) - (m/ext mu) - ignored-files)))) + (cryogen-io/find-assets + (cryogen-io/path "templates" root) + (m/ext mu) + ignored-files)))) (defn find-posts "Returns a list of markdown files representing posts under the post root." @@ -68,9 +69,9 @@ ([file-name params] (page-uri file-name nil params)) ([file-name uri-type {:keys [blog-prefix clean-urls?] :as params}] - (let [page-uri (params uri-type) - uri-end (if clean-urls? (s/replace file-name #"(index)?\.html" "/") file-name)] - (path "/" blog-prefix page-uri uri-end)))) + (let [page-uri (get params uri-type) + uri-end (if clean-urls? (s/replace file-name #"(index)?\.html" "/") file-name)] + (cryogen-io/path "/" blog-prefix page-uri uri-end)))) (defn read-page-meta "Returns the clojure map from the top of a markdown page/post" @@ -84,13 +85,13 @@ "Returns a map with the given page's file-name, metadata and content parsed from the file with the given markup." [^java.io.File page config markup] - (with-open [rdr (java.io.PushbackReader. (reader page))] - (let [re-root (re-pattern (str "^.*?(" (:page-root config) "|" (:post-root config) ")/")) - page-fwd (s/replace (str page) "\\" "/") ;; make it work on Windows + (with-open [rdr (java.io.PushbackReader. (io/reader page))] + (let [re-root (re-pattern (str "^.*?(" (:page-root config) "|" (:post-root config) ")/")) + page-fwd (s/replace (str page) "\\" "/") ;; make it work on Windows page-name (s/replace page-fwd re-root "") file-name (s/replace page-name (re-pattern-from-ext (m/ext markup)) ".html") page-meta (read-page-meta page-name rdr) - content ((m/render-fn markup) rdr config)] + content ((m/render-fn markup) rdr config)] {:file-name file-name :page-meta page-meta :content content}))) @@ -103,7 +104,7 @@ {:file-name file-name :content content :toc (if-let [toc (:toc page-meta)] - (generate-toc content :list-type toc))})) + (toc/generate-toc content :list-type toc))})) (defn parse-page "Parses a page/post and returns a map of the content, uri, date etc." @@ -112,7 +113,8 @@ (merge (merge-meta-and-content file-name page-meta content) {:uri (page-uri file-name :page-root-uri config) - :page-index (:page-index page-meta)}))) + :page-index (:page-index page-meta) + :klipse (klipse/merge-configs (:klipse config) (:klipse page-meta))}))) (defn parse-post "Return a map with the given post's information." @@ -120,28 +122,29 @@ (let [{:keys [file-name page-meta content]} (page-content page config markup)] (merge (merge-meta-and-content file-name page-meta content) - (let [date (if (:date page-meta) - (.parse (java.text.SimpleDateFormat. (:post-date-format config)) (:date page-meta)) - (parse-post-date file-name (:post-date-format config))) - archive-fmt (java.text.SimpleDateFormat. (get config :archive-group-format "yyyy MMMM") (Locale/getDefault)) + (let [date (if (:date page-meta) + (.parse (java.text.SimpleDateFormat. (:post-date-format config)) (:date page-meta)) + (parse-post-date file-name (:post-date-format config))) + archive-fmt (java.text.SimpleDateFormat. (:archive-group-format config "yyyy MMMM") (Locale/getDefault)) formatted-group (.format archive-fmt date)] {:date date :formatted-archive-group formatted-group :parsed-archive-group (.parse archive-fmt formatted-group) :uri (page-uri file-name :post-root-uri config) - :tags (set (:tags page-meta))})))) + :tags (set (:tags page-meta)) + :klipse (klipse/merge-configs (:klipse config) (:klipse page-meta))})))) (defn read-posts "Returns a sequence of maps representing the data from markdown files of posts. Sorts the sequence by post date." [config] - (->> (mapcat + (->> (m/markups) + (mapcat (fn [mu] (->> (find-posts config mu) (pmap #(parse-post % config mu)) - (remove #(= (:draft? %) true)))) - (m/markups)) + (remove #(= (:draft? %) true))))) (sort-by :date) reverse (drop-while #(and (:hide-future-posts? config) (.after (:date %) (java.util.Date.)))))) @@ -150,12 +153,12 @@ "Returns a sequence of maps representing the data from markdown files of pages. Sorts the sequence by post date." [config] - (->> (mapcat + (->> (m/markups) + (mapcat (fn [mu] (->> (find-pages config mu) - (map #(parse-page % config mu)))) - (m/markups)) + (map #(parse-page % config mu))))) (sort-by :page-index))) (defn tag-post @@ -163,7 +166,8 @@ [tags post] (reduce (fn [tags tag] (update-in tags [tag] (fnil conj []) (select-keys post [:uri :title :content :date :enclosure]))) - tags (:tags post))) + tags + (:tags post))) (defn group-by-tags "Maps all the tags with a list of posts that contain each tag" @@ -201,13 +205,13 @@ :uri (page-uri (str (name tag) ".html") :tag-root-uri config)}) (defn add-prev-next - "Adds a :prev and :next key to the page/post data containing the title and uri of the prev/next + "Adds a :prev and :next key to the page/post data containing the metadata of the prev/next post/page if it exists" [pages] (map (fn [[prev target next]] (assoc target - :prev (if prev (select-keys prev [:title :uri]) nil) - :next (if next (select-keys next [:title :uri]) nil))) + :prev (if prev (dissoc prev :content) nil) + :next (if next (dissoc next :content) nil))) (partition 3 1 (flatten [nil pages nil])))) (defn group-pages @@ -252,26 +256,30 @@ "When `clean-urls?` is set, appends `/index.html` before spit; otherwise just spits." [file-uri {:keys [clean-urls?]} data] (if clean-urls? - (create-file-recursive (path file-uri "index.html") data) - (create-file file-uri data))) + (cryogen-io/create-file-recursive (cryogen-io/path file-uri "index.html") data) + (cryogen-io/create-file file-uri data))) + +(defn- print-debug-info [data] + (println "DEBUG:") + (pprint data)) (defn compile-pages "Compiles all the pages into html and spits them out into the public folder" [{:keys [blog-prefix page-root-uri debug?] :as params} pages] (when-not (empty? pages) (println (blue "compiling pages")) - (create-folder (path "/" blog-prefix page-root-uri)) + (cryogen-io/create-folder (cryogen-io/path "/" blog-prefix page-root-uri)) (doseq [{:keys [uri] :as page} pages] - (println "\t-->" (cyan uri)) + (println "-->" (cyan uri)) (when debug? - (println "\t-->" (cyan page))) + (print-debug-info page)) (write-html uri params (render-file (str "/html/" (:layout page)) (merge params {:active-page "pages" :home false - :servlet-context (path "/" blog-prefix "/") + :servlet-context (cryogen-io/path "/" blog-prefix "/") :page page :uri uri})))))) @@ -280,74 +288,75 @@ [{:keys [blog-prefix post-root-uri disqus-shortname debug?] :as params} posts] (when-not (empty? posts) (println (blue "compiling posts")) - (create-folder (path "/" blog-prefix post-root-uri)) - (doseq [post posts] - (println "\t-->" (cyan (:uri post))) - (println "\t-->" (cyan debug?)) + (cryogen-io/create-folder (cryogen-io/path "/" blog-prefix post-root-uri)) + (doseq [{:keys [uri] :as post} posts] + (println "-->" (cyan uri)) (when debug? - (println "\t-->" (cyan post))) - (write-html (:uri post) + (print-debug-info post)) + (write-html uri params (render-file (str "/html/" (:layout post)) (merge params {:active-page "posts" - :servlet-context (path "/" blog-prefix "/") + :servlet-context (cryogen-io/path "/" blog-prefix "/") :post post :disqus-shortname disqus-shortname - :uri (:uri post)})))))) + :uri uri})))))) (defn compile-tags "Compiles all the tag pages into html and spits them out into the public folder" [{:keys [blog-prefix tag-root-uri] :as params} posts-by-tag] (when-not (empty? posts-by-tag) (println (blue "compiling tags")) - (create-folder (path "/" blog-prefix tag-root-uri)) + (cryogen-io/create-folder (cryogen-io/path "/" blog-prefix tag-root-uri)) (doseq [[tag posts] posts-by-tag] (let [{:keys [name uri]} (tag-info params tag)] - (println "\t-->" (cyan uri)) + (println "-->" (cyan uri)) (write-html uri params (render-file "/html/tag.html" (merge params {:active-page "tags" - :servlet-context (path "/" blog-prefix "/") + :servlet-context (cryogen-io/path "/" blog-prefix "/") :name name :posts posts :uri uri}))))))) (defn compile-tags-page [{:keys [blog-prefix] :as params}] + "Compiles a page with links to each tag page. Spits the page into the public folder" (println (blue "compiling tags page")) (let [uri (page-uri "tags.html" params)] (write-html uri params (render-file "/html/tags.html" (merge params - {:active-page "tags" - :servlet-context (path "/" blog-prefix "/") - :uri uri}))))) + {:active-page "tags" + :servlet-context (cryogen-io/path "/" blog-prefix "/") + :uri uri}))))) (defn content-until-more-marker - [^String content] - (let [index (.indexOf content "")] - (if (pos? index) - (let [s (subs content 0 index)] - (->> ((tagsoup/parse-string s) 2) - (drop 2) - hiccup/html))))) + "Returns the content until the special comment, + closing any unclosed tags. Returns nil if there's no such comment." + [content] + (when-let [index (s/index-of content "")] + (->> (subs content 0 index) + enlive/html-snippet + enlive/emit* + (apply str)))) (defn create-preview "Creates a single post preview" [blocks-per-preview post] - (merge post - {:content (or (content-until-more-marker (:content post)) - (->> ((tagsoup/parse-string (:content post)) 2) - (drop 2) - (take blocks-per-preview) - hiccup/html))})) + (update post :content + #(or (content-until-more-marker %) + (->> (enlive/html-snippet %) + (take blocks-per-preview) + enlive/emit* + (apply str))))) (defn create-previews "Returns a sequence of vectors, each containing a set of post previews" - [posts-per-page blocks-per-preview posts] + [posts posts-per-page blocks-per-preview] (->> posts (map #(create-preview blocks-per-preview %)) (partition-all posts-per-page) @@ -359,51 +368,53 @@ [previews params] (mapv (fn [[prev target next]] (merge target - {:prev (if prev (page-uri (path "p" (str (:index prev) ".html")) params) nil) - :next (if next (page-uri (path "p" (str (:index next) ".html")) params) nil)})) + {:prev (if prev (page-uri (cryogen-io/path "p" (str (:index prev) ".html")) params) nil) + :next (if next (page-uri (cryogen-io/path "p" (str (:index next) ".html")) params) nil)})) (partition 3 1 (flatten [nil previews nil])))) (defn compile-preview-pages "Compiles a series of pages containing 'previews' from each post" [{:keys [blog-prefix posts-per-page blocks-per-preview] :as params} posts] (when-not (empty? posts) - (let [previews (-> (create-previews posts-per-page blocks-per-preview posts) + (let [previews (-> posts + (create-previews posts-per-page blocks-per-preview) (create-preview-links params)) - previews (if (> (count previews) 1) (assoc-in previews [1 :prev] (page-uri "index.html" params)) previews)] - (create-folder (path "/" blog-prefix "p")) + previews (if (> (count previews) 1) + (assoc-in previews [1 :prev] (page-uri "index.html" params)) + previews)] + (cryogen-io/create-folder (cryogen-io/path "/" blog-prefix "p")) (doseq [{:keys [index posts prev next]} previews :let [index-page? (= 1 index)]] - (write-html (if index-page? (page-uri "index.html" params) (page-uri (path "p" (str index ".html")) params)) - params - (render-file "/html/previews.html" - (merge params - {:active-page "preview" - :home (when index-page? true) - :servlet-context (path "/" blog-prefix "/") - :posts posts - :prev-uri prev - :next-uri next}))))))) + (write-html + (if index-page? (page-uri "index.html" params) + (page-uri (cryogen-io/path "p" (str index ".html")) params)) + params + (render-file "/html/previews.html" + (merge params + {:active-page "preview" + :home (when index-page? true) + :servlet-context (cryogen-io/path "/" blog-prefix "/") + :posts posts + :prev-uri prev + :next-uri next}))))))) (defn compile-index "Compiles the index page into html and spits it out into the public folder" - [{:keys [disqus?] :as params}] + [{:keys [disqus? debug? home-page] :as params}] (println (blue "compiling index")) - (let [uri (page-uri "index.html" params) - debug? (-> params :debug?) - home-page (-> params :home-page) - meta {:active-page "home" - :home true - :disqus? disqus? - :uri uri - :post home-page - :page home-page}] + (let [uri (page-uri "index.html" params)] (when debug? - (println "\t-->" (cyan meta))) + (print-debug-info meta)) (write-html uri params (render-file (str "/html/" (:layout home-page)) (merge params - meta))))) + {:active-page "home" + :home true + :disqus? disqus? + :uri uri + :post home-page + :page home-page}))))) (defn compile-archives "Compiles the archives page into html and spits it out into the public folder" @@ -414,28 +425,28 @@ params (render-file "/html/archives.html" (merge params - {:active-page "archives" - :archives true - :groups (group-for-archive posts) - :servlet-context (path "/" blog-prefix "/") - :uri uri}))))) + {:active-page "archives" + :archives true + :groups (group-for-archive posts) + :servlet-context (cryogen-io/path "/" blog-prefix "/") + :uri uri}))))) (defn compile-authors "For each author, creates a page with filtered posts." [{:keys [blog-prefix author-root-uri author] :as params} posts] (println (blue "compiling authors")) - (create-folder (path "/" blog-prefix author-root-uri)) + (cryogen-io/create-folder (cryogen-io/path "/" blog-prefix author-root-uri)) ;; if the post author is empty defaults to config's :author (doseq [{:keys [author posts]} (group-for-author posts author)] (let [uri (page-uri (str author ".html") :author-root-uri params)] - (println "\t-->" (cyan uri)) + (println "-->" (cyan uri)) (write-html uri params (render-file "/html/author.html" (merge params {:author author :groups (group-for-archive posts) - :servlet-context (path "/" blog-prefix "/") + :servlet-context (cryogen-io/path "/" blog-prefix "/") :uri uri})))))) (defn tag-posts @@ -446,11 +457,11 @@ (defn- template-dir? "Checks that the dir exists in the templates directory." [dir] - (.isDirectory (file (str "resources/templates/" dir)))) + (.isDirectory (io/file (str "resources/templates/" dir)))) (defn- markup-entries [post-root page-root] (let [entries (for [mu (m/markups) - t (distinct [post-root page-root])] + t (distinct [post-root page-root])] [(str (m/dir mu) "/" t) t])] (apply concat entries))) @@ -459,7 +470,7 @@ [{:keys [post-root page-root] :as config}] (let [folders (->> (markup-entries post-root page-root) (filter template-dir?))] - (copy-resources + (cryogen-io/copy-resources (merge config {:resources folders :ignored-files (map #(re-pattern-from-ext (m/ext %)) (m/markups))})))) @@ -469,7 +480,7 @@ [] (try (let [config (-> "templates/config.edn" - get-resource + cryogen-io/get-resource slurp read-string (update-in [:blog-prefix] (fnil str "")) @@ -492,22 +503,35 @@ (catch Exception _ (throw (IllegalArgumentException. "Failed to parse config.edn"))))) +(defn klipsify + "Add the klipse html under the :klipse key and adds nohighlight + classes to any code blocks that are to be klipsified. Expects + configuration to be under :klipse, if there's none it does nothing." + [{:keys [klipse content] :as post-or-page}] + (-> post-or-page + (update :klipse klipse/emit content) + (update :content klipse/tag-nohighlight (:settings klipse)))) + (defn compile-assets "Generates all the html and copies over resources specified in the config" [] (println (green "compiling assets...")) - (let [{:keys [^String site-url blog-prefix rss-name recent-posts sass-src sass-dest - sass-path compass-path keep-files ignored-files previews? clean-urls? - debug? author-root-uri] :as config} (read-config) - posts (add-prev-next (read-posts config)) - pages (add-prev-next (read-pages config)) - home-pages (filter #(boolean (:home? %)) pages) - pages-without-home (filter #(boolean (not (:home? %))) pages) - [navbar-pages sidebar-pages] (group-pages pages-without-home) - navmap-pages (build-nav-map pages-without-home) + (let [{:keys [^String site-url blog-prefix rss-name recent-posts sass-dest keep-files ignored-files previews? author-root-uri theme] + :as config} (read-config) + posts (map klipsify (add-prev-next (read-posts config))) posts-by-tag (group-by-tags posts) - posts (tag-posts posts config) + posts (tag-posts posts config) latest-posts (->> posts (take recent-posts) vec) + pages (map klipsify (read-pages config)) + home-page (->> pages + (filter #(boolean (:home? %))) + (first)) + other-pages (->> pages + (remove #{home-page}) + (add-prev-next)) + [navbar-pages + sidebar-pages] (group-pages other-pages) + navmap-pages (build-nav-map other-pages) params (merge config {:today (java.util.Date.) :title (:site-title config) @@ -517,55 +541,51 @@ :navbar-pages navbar-pages :navmap-pages navmap-pages :sidebar-pages sidebar-pages - :home-page (if (not-empty home-pages) - (first home-pages) - (merge (first latest-posts) - {:layout "home.html"})) + :home-page (if home-page + home-page + (assoc (first latest-posts) :layout "home.html")) :archives-uri (page-uri "archives.html" config) :index-uri (page-uri "index.html" config) :tags-uri (page-uri "tags.html" config) - :rss-uri (path "/" blog-prefix rss-name) - :site-url (if (.endsWith site-url "/") (.substring site-url 0 (dec (count site-url))) site-url) - :theme-path (str "file:resources/templates/themes/" (:theme config))})] + :rss-uri (cryogen-io/path "/" blog-prefix rss-name) + :site-url (if (.endsWith site-url "/") (.substring site-url 0 (dec (count site-url))) site-url)})] (when debug? (println (blue "debug: navbar-pages:")) (println "\t-->" (cyan navbar-pages)) (println (blue "debug: navmap-pages:")) (println "\t-->" (cyan navmap-pages)) ) - (set-custom-resource-path! (:theme-path params)) - (wipe-public-folder keep-files) + (set-custom-resource-path! (str "file:resources/templates/themes/" theme)) + (cryogen-io/wipe-public-folder keep-files) (println (blue "copying theme resources")) - (copy-resources-from-theme config) + (cryogen-io/copy-resources-from-theme config) (println (blue "copying resources")) - (copy-resources config) + (cryogen-io/copy-resources config) (copy-resources-from-markup-folders config) - (compile-pages params pages-without-home) + (compile-pages params other-pages) (compile-posts params posts) (compile-tags params posts-by-tag) (compile-tags-page params) - (when previews? - (compile-preview-pages params posts)) - (when (or (not-empty home-pages) (not previews?)) + (if previews? + (compile-preview-pages params posts) (compile-index params)) (compile-archives params posts) (when author-root-uri (println (blue "generating authors views")) (compile-authors params posts)) (println (blue "generating site map")) - (create-file (path "/" blog-prefix "sitemap.xml") (sitemap/generate site-url ignored-files)) + (->> (sitemap/generate site-url ignored-files) + (cryogen-io/create-file (cryogen-io/path "/" blog-prefix "sitemap.xml"))) (println (blue "generating main rss")) - (create-file (path "/" blog-prefix rss-name) (rss/make-channel config posts)) + (->> (rss/make-channel config posts) + (cryogen-io/create-file (cryogen-io/path "/" blog-prefix rss-name))) (println (blue "generating filtered rss")) (rss/make-filtered-channels config posts-by-tag) (println (blue "compiling sass")) (sass/compile-sass->css! - {:path-sass sass-path - :path-compass compass-path - :src-sass sass-src - :dest-sass (path ".." "public" blog-prefix sass-dest) - :ignored-files ignored-files - :base-dir "resources/templates/"}))) + (merge (select-keys config [:sass-path :compass-path :sass-src :ignored-files]) + {:sass-dest (cryogen-io/path ".." "public" blog-prefix sass-dest) + :base-dir "resources/templates/"})))) (defn compile-assets-timed [] (time diff --git a/src/cryogen_core/github.clj b/src/cryogen_core/github.clj deleted file mode 100644 index 44c51ca..0000000 --- a/src/cryogen_core/github.clj +++ /dev/null @@ -1,37 +0,0 @@ -(ns cryogen-core.github - (:require [cheshire.core :as json]) - (:import (org.apache.commons.codec.binary Base64 StringUtils))) - -(defn get-gist [gist-uri] - (let [gist-id (last (clojure.string/split gist-uri #"/+")) ;;just need id for git api - gist-resp (try (slurp (str "https://api.github.com/gists/" gist-id)) - (catch Exception e {:error (.getMessage e)}))] - - (when-not (:error gist-resp) - (if-let [gist (-> (json/parse-string gist-resp) - (get "files") - first ;;todo: optionally get all gist files? - val)] - - {:content (get gist "content") - :language (get gist "language") - :name (get gist "filename") - :id gist-id})))) - -(defn get-src [git-file] - (let [git-re (re-find #"github.com/(.*)/blob/(.+?)/(.+)" git-file) ;;want second and last now (user/repo,file) for git api - git-res (str "https://api.github.com/repos/" (second git-re) "/contents/" (last git-re)) - git-resp (try (slurp git-res) - (catch Exception e {:error (.getMessage e)}))] - (when-not (:error git-resp) - (if-let [git-src (json/parse-string git-resp)] - {:content (String. ^bytes (Base64/decodeBase64 ^String (get git-src "content")) "UTF-8") - :name (get git-src "name") - :uri (get (get git-src "_links") "html")})))) - - -(defn get-gits-ex [] - [(get-gist "https://gist.github.com/viperscape/cec68f0791687f5959f1") - (get-src "https://github.com/viperscape/kuroshio/blob/master/examples/pubsub.clj")]) - -;(prn (get-gits-ex)) diff --git a/src/cryogen_core/klipse.clj b/src/cryogen_core/klipse.clj new file mode 100644 index 0000000..e3be3ee --- /dev/null +++ b/src/cryogen_core/klipse.clj @@ -0,0 +1,149 @@ +(ns cryogen-core.klipse + (:require [clojure.string :as str] + [camel-snake-kebab.core :refer [->snake_case_string ->camelCaseString]] + [cheshire.core :as json] + [net.cgrand.enlive-html :as enlive])) + +;;;;;;;;;;; +;; utils + +(defn map-keys + "Applies f to each key in m" + [f m] + (zipmap (map f (keys m)) (vals m))) + +(defn update-existing + "Like clojure.core/update, but returns m untouched if it doesn't contain k" + [m k f & args] + (if (contains? m k) (apply update m k f args) m)) + +(def map-or-nil? (some-fn map? nil?)) + +(defn deep-merge + "Like clojure.core/merge, but also merges nested maps under the same key." + [& ms] + (apply merge-with + (fn [v1 v2] + (if (and (map-or-nil? v1) (map-or-nil? v2)) + (deep-merge v1 v2) + v2)) + ms)) + +(defn filter-html-elems + "Recursively walks a sequence of enlive-style html elements depth first + and returns a flat sequence of the elements where (pred elem)" + [pred html-elems] + (reduce (fn [acc {:keys [content] :as elem}] + (into (if (pred elem) (conj acc elem) acc) + (filter-html-elems pred content))) + [] html-elems)) + +(defn code-block-classes + "Takes a string of html and returns a sequence of + all the classes on all code blocks." + [html] + (->> html + enlive/html-snippet + (filter-html-elems (comp #{:code} :tag)) + (keep (comp :class :attrs)) + (mapcat #(str/split % #" ")))) + +;;;;;;;;;;;; +;; klipse + +(defn eval-classes + "Takes the :settings map and returns all values that are css class selectors." + [settings] + (filter #(str/starts-with? % ".") (vals settings))) + +(defn tag-nohighlight + "Takes html as a string and a coll of class-selectors and adds + nohighlight to all code blocks that includes one of them." + [html settings] + (letfn [(tag [h clas] + (enlive/sniptest h + [(keyword (str "code" clas))] + (fn [x] + (update-in x [:attrs :class] #(str % " nohighlight")))))] + (reduce tag html (eval-classes settings)))) + +(def defaults + {:js-src + {:min "https://storage.googleapis.com/app.klipse.tech/plugin_prod/js/klipse_plugin.min.js" + :non-min "https://storage.googleapis.com/app.klipse.tech/plugin/js/klipse_plugin.js"} + + :css-base "https://storage.googleapis.com/app.klipse.tech/css/codemirror.css"}) + +;; This needs to be updated whenever a new clojure selector is introduced. +;; It should only be necessary for react wrappers and the like, so not very often. +;; When (if?) self hosted cljs becomes compatible with advanced builds +;; this can be removed and we can just always use minified js. +(def clojure-selectors + "A set of selectors that imply clojure evaluation." + #{"selector" "selector_reagent"}) + +(defn clojure-eval-classes + "Takes settings and returns a set of the html classes that imply clojure eval." + [normalized-settings] + (reduce (fn [classes selector] + (if-let [klass (get normalized-settings selector)] + (conj classes (->> klass rest (apply str))) ;; Strip the leading . + classes)) + #{} clojure-selectors)) + +(defn clojure-eval? + "Takes settings and html and returns whether there is any clojure eval." + [normalized-settings html] + (boolean (some (clojure-eval-classes normalized-settings) (code-block-classes html)))) + +(defn normalize-settings + "Transform the keys to the correct snake-case or camelCase strings." + [settings] + (-> (map-keys ->snake_case_string settings) + (update-existing "codemirror_options_in" (partial map-keys ->camelCaseString)) + (update-existing "codemirror_options_out" (partial map-keys ->camelCaseString)))) + +(defn merge-configs + "Merges the defaults, global config and post config, + transforms lisp-case keywords into snake_case/camelCase strings + Returns nil if there's no post-config. + A post-config with the value true counts as an empty map." + [global-config post-config] + (when post-config + (let [post-config (if (true? post-config) {} post-config)] + (deep-merge defaults + (update-existing global-config :settings normalize-settings) + (update-existing post-config :settings normalize-settings))))) + +(defn infer-clojure-eval + "Infers whether there's clojure eval and returns the config with the + appropriate value assoc'd to :js. + Returns the config untouched if :js is already specified." + [config html] + (if (:js config) + config + (assoc config :js + (if (clojure-eval? (:settings config) html) :non-min :min)))) + +(defn include-css [href] + (str "")) + +(defn include-js [src] + (str "")) + +(defn emit + "Takes the :klipse config from config.edn and the :klipse config from the + current post, and returns the html to include on the bottom of the page." + [config html] + (when-let [{:keys [settings js-src js css-base css-theme]} + (infer-clojure-eval config html)] + + (assert (#{:min :non-min} js) + (str ":js needs to be one of :min or :non-min but was: " js)) + + (str (include-css css-base) "\n" + (when css-theme (str (include-css css-theme) "\n")) + "\n" + (include-js (js js-src))))) diff --git a/src/cryogen_core/markup.clj b/src/cryogen_core/markup.clj index d9c7143..eee6977 100644 --- a/src/cryogen_core/markup.clj +++ b/src/cryogen_core/markup.clj @@ -1,6 +1,5 @@ (ns cryogen-core.markup - (:require [clojure.string :as s]) - (:import java.util.Collections)) + (:require [clojure.string :as s])) (defonce markup-registry (atom [])) @@ -18,7 +17,7 @@ [blog-prefix text] (if (s/blank? blog-prefix) text - (clojure.string/replace text #"href=.?/|src=.?/" #(str (subs % 0 (dec (count %))) blog-prefix "/")))) + (s/replace text #"href=.?/|src=.?/" #(str (subs % 0 (dec (count %))) blog-prefix "/")))) (defn markups "Return a vector of Markup implementations. This is the primary entry point diff --git a/src/cryogen_core/plugins.clj b/src/cryogen_core/plugins.clj index e907745..a85892a 100644 --- a/src/cryogen_core/plugins.clj +++ b/src/cryogen_core/plugins.clj @@ -1,6 +1,5 @@ (ns cryogen-core.plugins - (:require [cryogen-core.compiler :refer [compile-assets-timed]] - [clojure.edn :as edn] + (:require [clojure.edn :as edn] [clojure.string :as s] [text-decoration.core :refer :all])) diff --git a/src/cryogen_core/rss.clj b/src/cryogen_core/rss.clj index f2ffb75..ef28bff 100644 --- a/src/cryogen_core/rss.clj +++ b/src/cryogen_core/rss.clj @@ -1,7 +1,7 @@ (ns cryogen-core.rss (:require [clj-rss.core :as rss] [text-decoration.core :refer :all] - [cryogen-core.io :refer [create-file path]]) + [cryogen-core.io :as cryogen-io]) (:import java.util.Date)) @@ -31,6 +31,6 @@ (defn make-filtered-channels [{:keys [rss-filters blog-prefix] :as config} posts-by-tag] (doseq [filter rss-filters] - (let [uri (path "/" blog-prefix (str (name filter) ".xml"))] + (let [uri (cryogen-io/path "/" blog-prefix (str (name filter) ".xml"))] (println "\t-->" (cyan uri)) - (create-file uri (make-channel config (get posts-by-tag filter)))))) + (cryogen-io/create-file uri (make-channel config (get posts-by-tag filter)))))) diff --git a/src/cryogen_core/sass.clj b/src/cryogen_core/sass.clj index f85bf43..b164081 100644 --- a/src/cryogen_core/sass.clj +++ b/src/cryogen_core/sass.clj @@ -1,8 +1,8 @@ (ns cryogen-core.sass - (:require [clojure.java.shell :as shell] - [clojure.java.io :as io] + (:require [clojure.java.io :as io] + [clojure.java.shell :as shell] [text-decoration.core :refer :all] - [cryogen-core.io :refer [ignore match-re-filter]])) + [cryogen-core.io :as cryogen-io])) (defmacro sh [& args] @@ -11,64 +11,51 @@ (defn sass-installed? "Checks for the installation of Sass." - [path-sass] - (= 0 (:exit (sh path-sass "--version")))) + [sass-path] + (zero? (:exit (sh sass-path "--version")))) (defn compass-installed? "Checks for the installation of Compass." - [path-compass] + [compass-path] (try - (= 0 (:exit (sh path-compass "--version"))) + (zero? (:exit (sh compass-path "--version"))) (catch java.io.IOException _ false))) (defn find-sass-files "Given a Diretory, gets files, Filtered to those having scss or sass - extention. Ignores files matching any ignored regexps." + extention. Ignores files matching any ignored regexps." [base-dir dir ignored-files] - (let [^java.io.FilenameFilter filename-filter (match-re-filter #"(?i:s[ca]ss$)")] + (let [^java.io.FilenameFilter filename-filter (cryogen-io/match-re-filter #"(?i:s[ca]ss$)")] (->> (.listFiles (io/file base-dir dir) filename-filter) (filter #(not (.isDirectory ^java.io.File %))) - (filter (ignore ignored-files)) + (filter (cryogen-io/ignore ignored-files)) (map #(.getName ^java.io.File %))))) (defn compile-sass-file! - "Given a sass file which might be in src-sass directory, - output the resulting css in dest-sass. All error handling is - done by sh / launching the sass command." - [{:keys [src-sass - dest-sass - path-sass - path-compass - base-dir]}] + "Given a sass file which might be in sass-src directory, + output the resulting css in sass-dest. All error handling is + done by sh / launching the sass command." + [{:keys [sass-src sass-dest sass-path compass-path base-dir]}] (shell/with-sh-dir base-dir - (if (compass-installed? path-compass) - (sh path-sass "--compass" "--update" (str src-sass ":" dest-sass)) - (sh path-sass "--update" (str src-sass ":" dest-sass))))) + (if (compass-installed? compass-path) + (sh sass-path "--compass" "--update" (str sass-src ":" sass-dest)) + (sh sass-path "--update" (str sass-src ":" sass-dest))))) (defn compile-sass->css! - "Given a directory src-sass, looks for all sass files and compiles them into -dest-sass. Prompts you to install sass if he finds sass files and can't find -the command. Shows you any problems it comes across when compiling. " - [{:keys [src-sass - dest-sass - path-sass - ignored-files - base-dir] :as opts}] - (when-let [sass-files (seq (find-sass-files base-dir src-sass ignored-files))] - (if (sass-installed? path-sass) - ;; I found sass files, - ;; If sass is installed + "Given a directory sass-src, looks for all sass files and compiles them into + sass-dest. Prompts you to install sass if he finds sass files and can't find + the command. Shows you any problems it comes across when compiling. " + [{:keys [sass-src sass-dest sass-path ignored-files base-dir] :as opts}] + (when (seq (find-sass-files base-dir sass-src ignored-files)) + (if (sass-installed? sass-path) (do - (println "\t" (cyan src-sass) "-->" (cyan dest-sass)) + (println "\t" (cyan sass-src) "-->" (cyan sass-dest)) (let [result (compile-sass-file! opts)] (if (zero? (:exit result)) - ;; no problems in sass compilation (println "Successfully compiled sass files") - ;; else I show the error (println (red (:err result)) (red (:out result)))))) - ;; Else I prompt to install Sass (println "Sass seems not to be installed, but you have scss / sass files in " - src-sass + sass-src " - You might want to install it here: sass-lang.com")))) diff --git a/src/cryogen_core/sitemap.clj b/src/cryogen_core/sitemap.clj index 618828a..edaaf0e 100644 --- a/src/cryogen_core/sitemap.clj +++ b/src/cryogen_core/sitemap.clj @@ -1,6 +1,6 @@ (ns cryogen-core.sitemap (:require [clojure.xml :refer [emit]] - [cryogen-core.io :refer [get-resource find-assets]]) + [cryogen-core.io :as cryogen-io]) (:import java.util.Date)) ;;generate sitemaps using the sitemap spec @@ -19,7 +19,7 @@ {:tag :urlset :attrs {:xmlns "http://www.sitemaps.org/schemas/sitemap/0.9"} :content - (for [^java.io.File f (find-assets "public" ".html" ignored-files)] + (for [^java.io.File f (cryogen-io/find-assets "public" ".html" ignored-files)] {:tag :url :content [{:tag :loc diff --git a/src/cryogen_core/toc.clj b/src/cryogen_core/toc.clj index 0324c67..3544ba3 100644 --- a/src/cryogen_core/toc.clj +++ b/src/cryogen_core/toc.clj @@ -1,6 +1,6 @@ (ns cryogen-core.toc (:require [clojure.zip :as z] - [crouton.html :as html] + [net.cgrand.enlive-html :as enlive] [hiccup.core :as hiccup])) (def _h [:h1 :h2 :h3 :h4 :h5 :h6]) @@ -102,13 +102,10 @@ :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." - [^String html & {:keys [list-type] :or {list-type :ol}}] + [html & {:keys [list-type] :or {list-type :ol}}] (let [list-type (if (true? list-type) :ol list-type)] (-> html - (.getBytes "UTF-8") - (java.io.ByteArrayInputStream.) - (html/parse) - :content + (enlive/html-snippet) (get-headings) (build-toc-tree) (build-toc list-type) diff --git a/src/cryogen_core/watcher.clj b/src/cryogen_core/watcher.clj index 215d98a..5f0a5ae 100644 --- a/src/cryogen_core/watcher.clj +++ b/src/cryogen_core/watcher.clj @@ -1,20 +1,20 @@ (ns cryogen-core.watcher - (:require [clojure.java.io :refer [file]] - [cryogen-core.io :refer [ignore]] - [pandect.algo.md5 :refer [md5]] + (:require [clojure.java.io :as io] + [clojure.set :as set] [hawk.core :as hawk] - [clojure.set :as set])) + [pandect.algo.md5 :as md5] + [cryogen-core.io :as cryogen-io])) (defn get-assets [path ignored-files] (->> path - file + io/file file-seq (filter #(not (.isDirectory ^java.io.File %))) - (filter (ignore ignored-files)))) + (filter (cryogen-io/ignore ignored-files)))) (defn checksums [path ignored-files] (let [files (get-assets path ignored-files)] - (zipmap (map md5 files) files))) + (zipmap (map md5/md5 files) files))) (defn find-changes [old-sums new-sums] (let [old-sum-set (-> old-sums keys set) diff --git a/test/cryogen_core/compiler_test.clj b/test/cryogen_core/compiler_test.clj index b27b5aa..58af165 100644 --- a/test/cryogen_core/compiler_test.clj +++ b/test/cryogen_core/compiler_test.clj @@ -1,9 +1,8 @@ (ns cryogen-core.compiler-test (:require [clojure.test :refer :all] + [me.raynes.fs :as fs] [cryogen-core.compiler :refer :all] - [cryogen-core.io :refer [path]] - [cryogen-core.markup :as m] - [me.raynes.fs :as fs]) + [cryogen-core.markup :as m]) (:import [java.io File])) ; Test that the content-until-more-marker return nil or correct html text. @@ -22,7 +21,8 @@ and more content. ") - "
+ "
+
this post has more marker
"))) diff --git a/test/cryogen_core/klipse_test.clj b/test/cryogen_core/klipse_test.clj new file mode 100644 index 0000000..33b15f6 --- /dev/null +++ b/test/cryogen_core/klipse_test.clj @@ -0,0 +1,75 @@ +(ns cryogen-core.klipse-test + (:require [cryogen-core.klipse :refer :all] + [clojure.test :refer [deftest testing is are]])) + +(deftest map-keys-test + (is (= {"a" 1 "b" 2} (map-keys name {:a 1 :b 2})))) + +(deftest update-existing-test + (is (= {:a 1 :b 2} (update-existing {:a 1 :b 1} :b inc))) + (is (= {:a 1} (update-existing {:a 1} :b (constantly 2))))) + +(deftest deep-merge-test + (is (= {:a {:b 1 :c 2}} (deep-merge {:a {:b 1}} {:a {:c 2}}))) + (is (= {:a {:b 1}} (deep-merge {:a {:b 1}} {:a nil}))) + (is (= {:a {:b 1 :c 3}} (deep-merge {:a {:b 1 :c 2}} {:a {:c 3}})))) + +;; For testing convenience. +(defn elt + "Returns an enlive style html element." + ([tag] (elt tag nil)) + ([tag attrs & content] + {:tag tag, :attrs attrs, :content content})) + +(deftest filter-html-elems-test + (is (= [(elt :div {:class "x"} :content [(elt :div {:class "x"} "foo")]) + (elt :div {:class "x"} "foo")]) + (filter-html-elems (comp #{"x"} :class :attrs) + [(elt :h1 {:class "y"} "things!") + (elt :div {:class "x"} (elt :div {:class "x"} "foo"))]))) + +(deftest code-block-classes-test + (is (= ["clojure" "ruby"] + (code-block-classes + "

stuff

+
(def x 42)
+
123
"))))
+
+(deftest clojure-eval-classes-test
+  (is (= #{"eval-cljs" "eval-reagent"}
+         (clojure-eval-classes {"selector" ".eval-cljs"
+                                "selector_reagent" ".eval-reagent"
+                                "selector_eval_ruby" ".eval-ruby"}))))
+
+(deftest clojure-eval?-test
+  (is (clojure-eval? {"selector" ".eval-cljs"}
+                     "

stuff

+
(def x 42)
+
123
"))
+
+  (is (not (clojure-eval? {"selector" ".eval-cljs"
+                           "selector_eval_ruby" ".eval-ruby"}
+                          "

stuff

+
123
"))))
+
+(deftest normalize-settings-test
+  (is (= {"selector_reagent" ".reagent"
+          "codemirror_options_in" {"lineNumbers" true}}
+         (normalize-settings
+          {:selector-reagent ".reagent"
+           :codemirror-options-in {:line-numbers true}}))))
+
+(deftest merge-configs-test
+  (testing "Things are merged correctly"
+    (is (= (merge defaults
+                  {:settings {"selector" ".clojure-eval"
+                              "codemirror_options_in" {"lineNumbers" true}}})
+           (merge-configs {:settings {:codemirror-options-in {:line-numbers true}}}
+                          {:settings {:selector ".clojure-eval"}}))))
+
+  (testing "If it's all set up in config.edn, in the post it can be just :klipse true"
+    (is (= (merge defaults {:settings {"selector_js" ".javascript"}})
+           (merge-configs {:settings {:selector-js ".javascript"}} true))))
+
+  (testing "Returns nil if there's nothing in the blog post"
+    (is (nil? (merge-configs {:settings {:selector ".clojure-eval"}} nil)))))
diff --git a/test/cryogen_core/toc_test.clj b/test/cryogen_core/toc_test.clj
index 4a608e9..83bb390 100644
--- a/test/cryogen_core/toc_test.clj
+++ b/test/cryogen_core/toc_test.clj
@@ -1,38 +1,35 @@
 (ns cryogen-core.toc-test
   (:require [clojure.test :refer :all]
-            [clojure.string :refer [join]]
-            [clojure.zip :as z]
-            [crouton.html :as html]
+            [clojure.string :as s]
+            [net.cgrand.enlive-html :as enlive]
             [hiccup.core :as hiccup]
             [cryogen-core.toc :refer :all]))
 
 ; Reimport private functions
 (def get-headings #'cryogen-core.toc/get-headings)
 (def make-toc-entry #'cryogen-core.toc/make-toc-entry)
-(def zip-toc-tree-to-insertion-point #'cryogen-core.toc/zip-toc-tree-to-insertion-point)
-(def insert-toc-tree-entry #'cryogen-core.toc/insert-toc-tree-entry)
 (def build-toc-tree #'cryogen-core.toc/build-toc-tree)
 (def build-toc #'cryogen-core.toc/build-toc)
 
 (defn parse-to-headings
   [hiccup-seq]
-  (-> hiccup-seq hiccup/html html/parse-string :content get-headings))
+  (-> hiccup-seq hiccup/html enlive/html-snippet get-headings))
 
 ; Test that the get-headings function properly filters non-headers
 (deftest test-get-headings
   (let [noisy-headers [:div [:h1 "First H1"]
-                            [:p "Ignore..."]
-                            [:h2 "First H2"]]]
+                       [:p "Ignore..."]
+                       [:h2 "First H2"]]]
     (is (= (parse-to-headings noisy-headers)
-         [{:tag :h1 :attrs nil :content ["First H1"]}
-          {:tag :h2 :attrs nil :content ["First H2"]}]))))
+           [{:tag :h1 :attrs nil :content ["First H1"]}
+            {:tag :h2 :attrs nil :content ["First H2"]}]))))
 
 ; Test that the make-toc-entry ignores invalid input
 (deftest test-make-toc-entry
   (is (nil?
-         (make-toc-entry nil "Text")))
+        (make-toc-entry nil "Text")))
   (is (nil?
-         (make-toc-entry "anchor" nil)))
+        (make-toc-entry "anchor" nil)))
   (is (= [:li [:a {:href "#anchor"} "Text"]]
          (make-toc-entry "anchor" "Text"))))
 
@@ -45,65 +42,62 @@
 ; * h1
 (deftest test-build-toc
   (let [simplest-header [:div [:h2 [:a {:name "test"}] "Test"]]
-        no-headers [:div [:p "This is not a header"]]
+        no-headers      [:div [:p "This is not a header"]]
+
         closing-header-larger-than-opening-1
-        [:div [:h2 [:a {:name "starting_low"}]
-                    "Starting Low"]
-              [:h1 [:a {:name "finishing_high"}]
-                    "Finishing High"]]
+                        [:div [:h2 [:a {:name "starting_low"}]
+                               "Starting Low"]
+                         [:h1 [:a {:name "finishing_high"}]
+                          "Finishing High"]]
+
         closing-header-larger-than-opening-2
-        [:div [:h2 [:a {:name "starting_low"}]
-                    "Starting Low"]
-              [:h4 [:a {:name "jumping_in"}]
-                    "Jumping Right In"]
-              [:h3 [:a {:name "pulling_back"}]
-                    "But then pull back"]
-              [:h2 [:a {:name "to_the_top"}]
-                    "To the top"]]]
+                        [:div [:h2 [:a {:name "starting_low"}]
+                               "Starting Low"]
+                         [:h4 [:a {:name "jumping_in"}]
+                          "Jumping Right In"]
+                         [:h3 [:a {:name "pulling_back"}]
+                          "But then pull back"]
+                         [:h2 [:a {:name "to_the_top"}]
+                          "To the top"]]]
     (is (= [:ol.content (seq [[:li [:a {:href "#test"} "Test"]]])]
-           (-> simplest-header parse-to-headings build-toc-tree
+           (-> simplest-header
+               (parse-to-headings)
+               (build-toc-tree)
                (build-toc :ol))))
-    (is (nil?
-        (-> no-headers parse-to-headings build-toc-tree
-            (build-toc :ol))))
+    (is (-> no-headers
+            (parse-to-headings)
+            (build-toc-tree)
+            (build-toc :ol)
+            (nil?)))
     (is (= [:ol.content (seq [[:li [:a {:href "#starting_low"} "Starting Low"]]
                               [:li [:a {:href "#finishing_high"} "Finishing High"]]])]
-         (-> closing-header-larger-than-opening-1
-             parse-to-headings 
-             build-toc-tree 
-             (build-toc :ol)))
+           (-> closing-header-larger-than-opening-1
+               (parse-to-headings)
+               (build-toc-tree)
+               (build-toc :ol)))
         "No outer header should be less indented than the first header tag.")
-    (is (= [:ul.content 
-            (seq [
-                (seq [
-                  [:li [:a {:href "#starting_low"} "Starting Low"]]
-                  [:ul 
-                   (seq [
-                         [:li [:a {:href "#jumping_in"} "Jumping Right In"]]
-                         [:li [:a {:href "#pulling_back"} "But then pull back"]]
-                         ])
-                   ] ])
-                  [:li [:a {:href "#to_the_top"} "To the top"]]
-                  ])]
+    (is (= [:ul.content
+            (seq [(seq [[:li [:a {:href "#starting_low"} "Starting Low"]]
+                        [:ul
+                         (seq [[:li [:a {:href "#jumping_in"} "Jumping Right In"]]
+                               [:li [:a {:href "#pulling_back"} "But then pull back"]]])]])
+                  [:li [:a {:href "#to_the_top"} "To the top"]]])]
            (-> closing-header-larger-than-opening-2
-               parse-to-headings
-               build-toc-tree
+               (parse-to-headings)
+               (build-toc-tree)
                (build-toc :ul)))
-        (join "" ["Inner headers can be more indented, "
-                  "but outer headers cannot be less indented "
-                  "than the original header."]))
-    ))
+        (s/join "" ["Inner headers can be more indented, "
+                    "but outer headers cannot be less indented "
+                    "than the original header."]))))
 
 
 (deftest test-generate-toc
   (let [htmlString "

Test

"] (is (= "
  1. Test
" - (generate-toc htmlString))) + (generate-toc htmlString))) (is (= "
  1. Test
" - (generate-toc htmlString :list-type true))) + (generate-toc htmlString :list-type true))) (is (= "
  1. Test
" - (generate-toc htmlString :list-type :ol))) + (generate-toc htmlString :list-type :ol))) (is (= "" - (generate-toc htmlString :list-type :ul))))) - - + (generate-toc htmlString :list-type :ul)))))