diff --git a/src/io/perun.clj b/src/io/perun.clj index 0311681f..20c022c1 100644 --- a/src/io/perun.clj +++ b/src/io/perun.clj @@ -9,7 +9,8 @@ [clojure.string :as string] [clojure.edn :as edn] [io.perun.core :as perun] - [io.perun.meta :as pm])) + [io.perun.meta :as pm] + [clojure.string :as str])) (def ^:private ^:deps global-deps '[]) @@ -458,18 +459,57 @@ :meta meta :cmd-opts cmd-opts)))) -(def ^:private ^:deps asciidoctor-deps - '[[org.clojure/tools.namespace "0.3.0-alpha3"] - [org.asciidoctor/asciidoctorj "1.5.4"]]) +(defn extensions->regex + "Helper function to convert a sequence of extensions to a regex" + [extensions] + {:pre [map (fn [ext] (assert (.startsWith ext ".")) extensions)]} + (->> extensions + (map (fn [ext] (str/escape ext {\. "\\."}))) + (str/join "|") + (#(str ".*(?:" % ")$")) + re-pattern)) + +(def ^:private +collect-images-defaults+ + {:extensions [".png" ".svg" ".jpg"]}) + +(deftask collect-images + "Collect images from a directory" + [d img-dir IMGDIR str "the image directory" + e extensions EXTENSIONS [str] "extensions of files to include"] + (let [{:keys [img-dir extensions] :as options} (merge +collect-images-defaults+ *opts*) + include-regex (extensions->regex extensions)] + (boot/with-pre-wrap fileset + (-> fileset + (boot/add-resource (clojure.java.io/file img-dir) :include #{include-regex}) + (boot/commit!))))) + +(defn asciidoctor-deps [diagram?] + (let [adoc-deps '[[clj-time "0.14.0"] + [org.clojure/tools.namespace "0.3.0-alpha3"] + [org.asciidoctor/asciidoctorj "1.5.4"]] + diagram-deps '[[org.asciidoctor/asciidoctorj-diagram "1.5.0"]]] + (-> (if diagram? + (into adoc-deps diagram-deps) + adoc-deps) + (with-meta {:private true :deps true})))) (def ^:private +asciidoctor-defaults+ {:out-dir "public" :out-ext ".html" :filterer identity + :images false :extensions [".ad" ".asc" ".adoc" ".asciidoc"] :meta {:original true :include-rss true - :include-atom true}}) + :include-atom true} + :safe 1}) + +(defn strip-nil-vals + "Helper function for removing nil values, to ensure proper merge of maps" + [m] + (->> m + (remove #(nil? (second %))) + (into {}))) (deftask asciidoctor* "Parse asciidoc files using Asciidoctor @@ -479,17 +519,23 @@ [d out-dir OUTDIR str "the output directory" _ filterer FILTER code "predicate to use for selecting entries (default: `identity`)" e extensions EXTENSIONS [str] "extensions of files to process" - m meta META edn "metadata to set on each entry"] - (let [pod (create-pod asciidoctor-deps) - options (merge +asciidoctor-defaults+ *opts*)] - (content-task - {:render-form-fn (fn [data] `(io.perun.asciidoctor/process-asciidoctor ~data)) - :paths-fn #(content-paths % options) - :passthru-fn content-passthru - :task-name "asciidoctor" - :tracer :io.perun/asciidoctor - :rm-originals true - :pod pod}))) + _ diagram bool "if `true`, generate images from inline text using asciidoctor-diagram" + m meta META edn "metadata to set on each entry" + s safe SAFE int "security level (default: 1)"] + (let [{:keys [diagram safe out-dir] :as options} (merge +asciidoctor-defaults+ (strip-nil-vals *opts*)) + pod (create-pod (asciidoctor-deps diagram)) + img-dir (.getPath (boot/tmp-dir!))] + (comp (content-task + {:render-form-fn (fn [data] `(io.perun.asciidoctor/process-asciidoctor ~out-dir ~img-dir ~diagram ~safe ~data)) + :paths-fn #(content-paths % options) + :passthru-fn content-passthru + :task-name "asciidoctor" + :tracer :io.perun/asciidoctor + :rm-originals true + :pod pod}) + (if diagram + (collect-images :img-dir img-dir) + identity)))) (deftask asciidoctor "Parse asciidoc files with yaml front matter using Asciidoctor @@ -499,13 +545,17 @@ [d out-dir OUTDIR str "the output directory" _ filterer FILTER code "predicate to use for selecting entries (default: `identity`)" e extensions EXTENSIONS [str] "extensions of files to process" - m meta META edn "metadata to set on each entry"] + _ diagram bool "if `true`, generate images from inline text using asciidoctor-diagram" + m meta META edn "metadata to set on each entry" + s safe SAFE int "security level (default: 1)"] (let [{:keys [out-dir filterer extensions meta]} (merge +asciidoctor-defaults+ *opts*)] (comp (yaml-metadata :filterer filterer :extensions extensions) (asciidoctor* :out-dir out-dir :filterer filterer + :diagram diagram :extensions extensions - :meta meta)))) + :meta meta + :safe safe)))) (deftask global-metadata "Read global metadata from `perun.base.edn` or configured file. diff --git a/src/io/perun/asciidoctor.clj b/src/io/perun/asciidoctor.clj index 02c52529..8b1a1d94 100644 --- a/src/io/perun/asciidoctor.clj +++ b/src/io/perun/asciidoctor.clj @@ -1,16 +1,101 @@ (ns io.perun.asciidoctor (:require [io.perun.core :as perun] - [clojure.java.io :as io]) + [clj-time.coerce :as tc] + [clj-time.format :as tf] + [clojure.java.io :as io] + [clojure.string :as str]) (:import [org.asciidoctor Asciidoctor Asciidoctor$Factory])) -(def container - (Asciidoctor$Factory/create "")) +(defn keywords->names + "Converts a map with keywords to a map with named keys. Only handles the top + level of any nested structure." + [m] + (reduce-kv #(assoc %1 (name %2) %3) {} m)) -(defn asciidoctor-to-html [file-content] - (.convert container file-content {})) +(defn names->keywords + "Converts a map with named keys to a map with keywords. Only handles the top + level of any nested structure." + [m] + (reduce-kv #(assoc %1 (keyword %2) %3) {} m)) -(defn process-asciidoctor [{:keys [entry]}] +(defn container + "Creates a new Asciidoctor container, with or without the + `asciidoctor-diagram` library." + [diagram] + (doto (Asciidoctor$Factory/create "") + (.requireLibraries (if diagram + '("asciidoctor-diagram") + '())))) + +(defn meta->attributes + "Takes the Perun meta and converts it to a collection of attributes, which can + be handed to the AsciidoctorJ process." + [meta] + (-> meta + keywords->names + (java.util.HashMap.))) + +(defn parse-date + "Tries to parse a date string into a DateTime object" + [date] + (when date + (if-let [parsed (tc/to-date date)] + parsed + (perun/report-info "asciidoctor" "failed to parse date %s" date)))) + +(defn attributes->meta + "Add duplicate entries for the metadata keys gathered from the AsciidoctorJ + parsing using keys that adhere to the Perun specification of keys. The native + AsciidoctorJ keys are still available." + [attributes] + (let [meta (names->keywords (into {} attributes))] + (merge meta + {:author-email (:email meta) + :title (:doctitle meta) + :date-published (parse-date (:revdate meta))}))) + +(defn protect-meta + "Strip keywords from metadata that are being used by Perun to properly + function." + [meta] + (dissoc meta + :canonical-url :content :extension :filename :full-path :parent-path + :path :permalink :short-filename :slug)) + +(defn options + "Create an options object" + [safe attributes outdir] + {:pre [(number? safe)]} + {"attributes" (if attributes + attributes + (java.util.HashMap.)) + "safe" (int safe) + "base_dir" outdir}) + +(defn parse-file-metadata + "Processes the asciidoctor content and extracts all the attributes." + [container adoc-content options] + (->> (.readDocumentStructure container adoc-content options) + (.getHeader) + (.getAttributes) + attributes->meta + protect-meta)) + +(defn asciidoctor-to-html [container file-content options] + (.convert container file-content options)) + +(defn strip-trailing-slash + [path] + (str/replace path "/^" "")) + +(defn process-asciidoctor [out-dir img-dir diagram safe {:keys [entry]}] (perun/report-debug "asciidoctor" "processing asciidoctor" (:filename entry)) - (let [file-content (-> entry :full-path io/file slurp) - html (asciidoctor-to-html file-content)] - (assoc entry :rendered html))) + (let [outdir (str/replace (str/join "/" [img-dir out-dir (:parent-path entry)]) "/^" "") + _ (.mkdirs (clojure.java.io/file outdir)) + file-content (-> entry :full-path io/file slurp) + attributes (meta->attributes (assoc entry :outdir outdir)) + opts (options safe attributes outdir) + cont (container diagram) + html (asciidoctor-to-html cont file-content opts) + meta (parse-file-metadata cont file-content opts)] + (merge (assoc entry :rendered html) meta))) diff --git a/src/io/perun/yaml.clj b/src/io/perun/yaml.clj index 3ce30b72..0d562959 100644 --- a/src/io/perun/yaml.clj +++ b/src/io/perun/yaml.clj @@ -7,7 +7,7 @@ (:import [flatland.ordered.map OrderedMap] [flatland.ordered.set OrderedSet])) -(def ^:dynamic *yaml-head* #"---\r?\n") +(def ^:dynamic *yaml-head* #"(?<=^|\n)---\r?\n") (defn substr-between "Find string that is nested in between two strings. Return first match.