From 52203a49aa544b2c11c96445d8732893160c436b Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Tue, 19 Apr 2022 17:50:19 +0300 Subject: WIP #16 Pretty much done. Needs more testing. And new documentation. --- src/clarktown/core.clj | 151 ++++++++++++++++- src/clarktown/correctors.clj | 17 ++ src/clarktown/correctors/atx_heading_block.clj | 19 +++ src/clarktown/correctors/code_block.clj | 27 +++ src/clarktown/matchers/code_block.clj | 10 ++ src/clarktown/matchers/empty_block.clj | 11 ++ src/clarktown/matchers/heading_block.clj | 29 ++++ src/clarktown/matchers/horizontal_line_block.clj | 10 ++ src/clarktown/matchers/list_block.clj | 10 ++ src/clarktown/matchers/quote_block.clj | 11 ++ src/clarktown/parser.clj | 195 ---------------------- src/clarktown/parsers.clj | 136 ++++++++++----- src/clarktown/parsers/bold.clj | 18 -- src/clarktown/parsers/code_block.clj | 30 ---- src/clarktown/parsers/empty_block.clj | 17 -- src/clarktown/parsers/heading_block.clj | 72 -------- src/clarktown/parsers/horizontal_line_block.clj | 16 -- src/clarktown/parsers/inline_code.clj | 21 --- src/clarktown/parsers/italic.clj | 18 -- src/clarktown/parsers/link_and_image.clj | 27 --- src/clarktown/parsers/list_block.clj | 127 -------------- src/clarktown/parsers/paragraph_block.clj | 9 - src/clarktown/parsers/quote_block.clj | 24 --- src/clarktown/parsers/strikethrough.clj | 18 -- src/clarktown/renderers/bold.clj | 18 ++ src/clarktown/renderers/code_block.clj | 23 +++ src/clarktown/renderers/empty_block.clj | 7 + src/clarktown/renderers/heading_block.clj | 44 +++++ src/clarktown/renderers/horizontal_line_block.clj | 7 + src/clarktown/renderers/inline_code.clj | 21 +++ src/clarktown/renderers/italic.clj | 18 ++ src/clarktown/renderers/link_and_image.clj | 27 +++ src/clarktown/renderers/list_block.clj | 121 ++++++++++++++ src/clarktown/renderers/paragraph_block.clj | 9 + src/clarktown/renderers/quote_block.clj | 16 ++ src/clarktown/renderers/strikethrough.clj | 18 ++ 36 files changed, 710 insertions(+), 642 deletions(-) create mode 100644 src/clarktown/correctors.clj create mode 100644 src/clarktown/correctors/atx_heading_block.clj create mode 100644 src/clarktown/correctors/code_block.clj create mode 100644 src/clarktown/matchers/code_block.clj create mode 100644 src/clarktown/matchers/empty_block.clj create mode 100644 src/clarktown/matchers/heading_block.clj create mode 100644 src/clarktown/matchers/horizontal_line_block.clj create mode 100644 src/clarktown/matchers/list_block.clj create mode 100644 src/clarktown/matchers/quote_block.clj delete mode 100644 src/clarktown/parser.clj delete mode 100644 src/clarktown/parsers/bold.clj delete mode 100644 src/clarktown/parsers/code_block.clj delete mode 100644 src/clarktown/parsers/empty_block.clj delete mode 100644 src/clarktown/parsers/heading_block.clj delete mode 100644 src/clarktown/parsers/horizontal_line_block.clj delete mode 100644 src/clarktown/parsers/inline_code.clj delete mode 100644 src/clarktown/parsers/italic.clj delete mode 100644 src/clarktown/parsers/link_and_image.clj delete mode 100644 src/clarktown/parsers/list_block.clj delete mode 100644 src/clarktown/parsers/paragraph_block.clj delete mode 100644 src/clarktown/parsers/quote_block.clj delete mode 100644 src/clarktown/parsers/strikethrough.clj create mode 100644 src/clarktown/renderers/bold.clj create mode 100644 src/clarktown/renderers/code_block.clj create mode 100644 src/clarktown/renderers/empty_block.clj create mode 100644 src/clarktown/renderers/heading_block.clj create mode 100644 src/clarktown/renderers/horizontal_line_block.clj create mode 100644 src/clarktown/renderers/inline_code.clj create mode 100644 src/clarktown/renderers/italic.clj create mode 100644 src/clarktown/renderers/link_and_image.clj create mode 100644 src/clarktown/renderers/list_block.clj create mode 100644 src/clarktown/renderers/paragraph_block.clj create mode 100644 src/clarktown/renderers/quote_block.clj create mode 100644 src/clarktown/renderers/strikethrough.clj (limited to 'src') diff --git a/src/clarktown/core.clj b/src/clarktown/core.clj index a6ed42c..cabd6e6 100644 --- a/src/clarktown/core.clj +++ b/src/clarktown/core.clj @@ -1,7 +1,145 @@ (ns clarktown.core (:require - [clarktown.parser :as parser] - [clarktown.parsers :as parsers])) + [clojure.string :as string] + [clarktown.parsers :as parsers] + [clarktown.correctors :as correctors])) + + +(defn- stitch-code-blocks + "Since code blocks can span multiple blocks (a block is separated by + two line breaks from another block) , we need to stitch them together + into one block in order for a block parser to be able to do anything + with it." + [blocks] + (loop [stitched-blocks [] + code-block-started? false + blocks blocks] + (if (empty? blocks) + stitched-blocks + (let [block (first blocks)] + (if (and (string/starts-with? (string/trim block) "```") + (not (string/ends-with? (string/trim block) "```"))) + (recur (conj stitched-blocks block) + true + (drop 1 blocks)) + (if code-block-started? + (let [last-block (last stitched-blocks) + last-block-index (- (count stitched-blocks) 1)] + (if (string/ends-with? (string/trim block) "```") + (recur (assoc stitched-blocks last-block-index (str last-block "\n\n" block)) + false + (drop 1 blocks)) + (recur (assoc stitched-blocks last-block-index (str last-block "\n\n" block)) + true + (drop 1 blocks)))) + (recur (conj stitched-blocks block) + false + (drop 1 blocks)))))))) + + +(defn- correct-block-separations + "Corrects block separations and adds newlines above or + below a block where needed." + [correctors lines] + (->> lines + (map-indexed + (fn [index line] + (let [add-line-above? (some #(true? (% lines line index)) (:empty-line-above? correctors)) + add-line-below? (some #(true? (% lines line index)) (:empty-line-below? correctors))] + (cond + ; If code block starts but there is no empty newline + ; above, let's fix that + (and add-line-above? + (not add-line-below?)) + (str \newline line) + + ; If the code block ends, but there is no empty newline + ; below, let's fix that. + (and add-line-below? + (not add-line-above?)) + (str line \newline) + + ; If the code block needs a newline both above and below, + ; let's fix that. + (and add-line-above? + add-line-below?) + (str \newline line \newline) + + ; otherwise is what it is + :else line)))))) + + +(defn- correct-markdown + "Corrects invalid Markdown for the parser." + [markdown given-correctors] + (let [lines (string/split-lines markdown)] + (->> lines + (correct-block-separations (:block-separations given-correctors)) + (string/join \newline)))) + + +(defn- find-parser-by-block + "Find a parser from `parsers` that matches the given `block`." + [parsers block] + (->> parsers + (filter + (fn [{:keys [matcher]}] + (when matcher + (matcher block)))) + first)) + + +(defn- parse-block-with-known-parser + "Parses a given `block` with a known `parser`." + [parser parsers block] + (loop [block block + renderers (:renderers parser)] + (if (empty? renderers) + block + (let [renderer (first renderers)] + (recur (renderer block parsers) + (drop 1 renderers)))))) + + +(defn- parse-block-with-unknown-parsers + "Parses the given `block` with all the parsers that do not have + a matcher function, useful for any fallback parsing one might want + to do." + [parsers block] + (loop [block block + parsers (filter #(= nil (:matcher %)) parsers)] + (if (empty? parsers) + block + (recur (loop [block block + renderers (:renderers (first parsers))] + (if (empty? renderers) + block + (let [renderer (first renderers)] + (recur (renderer block parsers) + (drop 1 renderers))))) + (drop 1 parsers))))) + + +(defn- parse-blocks + "Parses each individual Markdown block, given as `blocks`, with + the list of `parsers`." + [blocks parsers] + (for [block blocks] + (if-let [parser (find-parser-by-block parsers block)] + (->> (string/trim block) + (parse-block-with-known-parser parser parsers)) + (->> (string/trim block) + (parse-block-with-unknown-parsers parsers))))) + + +(defn parse + "Parses given `markdown` with `parsers`." + [markdown given-parsers given-correctors] + (let [blocks (-> (correct-markdown markdown given-correctors) + (string/split #"\n\n") + stitch-code-blocks) + parsed-blocks (parse-blocks blocks given-parsers)] + (string/join "\n\n" parsed-blocks))) (defn render @@ -22,9 +160,8 @@ :renderers [(fn [block] ...) (fn [block] ...)]} ```" ([markdown] - (render markdown parsers/parsers)) + (render markdown parsers/default-parsers)) ([markdown given-parsers] - (parser/parse markdown given-parsers))) - -(comment - (render (slurp "./test.md"))) \ No newline at end of file + (render markdown given-parsers correctors/default-correctors)) + ([markdown given-parsers given-correctors] + (parse markdown given-parsers given-correctors))) \ No newline at end of file diff --git a/src/clarktown/correctors.clj b/src/clarktown/correctors.clj new file mode 100644 index 0000000..ba49879 --- /dev/null +++ b/src/clarktown/correctors.clj @@ -0,0 +1,17 @@ +(ns clarktown.correctors + (:require + [clarktown.correctors.code-block :as code-block] + [clarktown.correctors.atx-heading-block :as atx-heading-block])) + + +(def block-separation-correctors + {:empty-line-above? + [code-block/empty-line-above? + atx-heading-block/empty-line-above?] + :empty-line-below? + [code-block/empty-line-below? + atx-heading-block/empty-line-below?]}) + + +(def default-correctors + {:block-separations block-separation-correctors}) \ No newline at end of file diff --git a/src/clarktown/correctors/atx_heading_block.clj b/src/clarktown/correctors/atx_heading_block.clj new file mode 100644 index 0000000..a792572 --- /dev/null +++ b/src/clarktown/correctors/atx_heading_block.clj @@ -0,0 +1,19 @@ +(ns clarktown.correctors.atx-heading-block + (:require + [clojure.string :as string])) + + +(defn empty-line-above? + [lines line index] + (and (string/starts-with? (string/trim line) "#") + (> index 0) + (not (= (-> (nth lines (- index 1)) + string/trim) "")))) + + +(defn empty-line-below? + [lines line index] + (and (string/starts-with? (string/trim line) "#") + (< index (- (count lines) 1)) + (not (= (-> (nth lines (+ index 1)) + string/trim) "")))) \ No newline at end of file diff --git a/src/clarktown/correctors/code_block.clj b/src/clarktown/correctors/code_block.clj new file mode 100644 index 0000000..e767390 --- /dev/null +++ b/src/clarktown/correctors/code_block.clj @@ -0,0 +1,27 @@ +(ns clarktown.correctors.code-block + (:require + [clojure.string :as string])) + + +(defn empty-line-above? + [lines line index] + (and (= (string/trim line) "```") + (> index 0) + (->> (take index lines) + (filter #(= (string/trim %) "```")) + count + odd?) + (not (= (-> (nth lines (- index 1)) + string/trim) "")))) + + +(defn empty-line-below? + [lines line index] + (and (= (string/trim line) "```") + (< index (- (count lines) 1)) + (->> (take index lines) + (filter #(= (string/trim %) "```")) + count + even?) + (not (= (-> (nth lines (+ index 1)) + string/trim) "")))) \ No newline at end of file diff --git a/src/clarktown/matchers/code_block.clj b/src/clarktown/matchers/code_block.clj new file mode 100644 index 0000000..655c951 --- /dev/null +++ b/src/clarktown/matchers/code_block.clj @@ -0,0 +1,10 @@ +(ns clarktown.matchers.code-block + (:require + [clojure.string :as string])) + + +(defn match? + "Determines whether we're dealing with a code block." + [block] + (and (string/starts-with? block "```") + (string/ends-with? block "```"))) \ No newline at end of file diff --git a/src/clarktown/matchers/empty_block.clj b/src/clarktown/matchers/empty_block.clj new file mode 100644 index 0000000..cc7b7f4 --- /dev/null +++ b/src/clarktown/matchers/empty_block.clj @@ -0,0 +1,11 @@ +(ns clarktown.matchers.empty-block + (:require + [clojure.string :as string])) + + +(defn match? + "Determines if the current block is an empty block or not." + [block] + (-> (string/replace block #"\n" "") + string/trim + string/blank?)) \ No newline at end of file diff --git a/src/clarktown/matchers/heading_block.clj b/src/clarktown/matchers/heading_block.clj new file mode 100644 index 0000000..2295f26 --- /dev/null +++ b/src/clarktown/matchers/heading_block.clj @@ -0,0 +1,29 @@ +(ns clarktown.matchers.heading-block + (:require + [clojure.string :as string])) + + +(defn is-atx-heading? + "Determines whether the given block is a atx heading." + [block] + (-> (string/replace block #"\n" "") + string/trim + (string/starts-with? "#"))) + + +(defn is-settext-heading? + "Determines whether the given block is a settext heading." + [block] + (let [lines (-> (string/split-lines block)) + chars (-> (last lines) + string/trim + (string/split #""))] + (and (> (count lines) 1) + (every? #{"-" "="} chars)))) + + +(defn match? + "Determines whether the given block is a heading block." + [block] + (or (is-atx-heading? block) + (is-settext-heading? block))) \ No newline at end of file diff --git a/src/clarktown/matchers/horizontal_line_block.clj b/src/clarktown/matchers/horizontal_line_block.clj new file mode 100644 index 0000000..a2dde6e --- /dev/null +++ b/src/clarktown/matchers/horizontal_line_block.clj @@ -0,0 +1,10 @@ +(ns clarktown.matchers.horizontal-line-block + (:require + [clojure.string :as string])) + + +(defn match? + "Determines whether the given block is a horizontal line block." + [block] + (or (= "***" (string/trim block)) + (= "---" (string/trim block)))) \ No newline at end of file diff --git a/src/clarktown/matchers/list_block.clj b/src/clarktown/matchers/list_block.clj new file mode 100644 index 0000000..a37b06e --- /dev/null +++ b/src/clarktown/matchers/list_block.clj @@ -0,0 +1,10 @@ +(ns clarktown.matchers.list-block + (:require + [clojure.string :as string])) + + +(defn match? + "Determines whether we're dealing with a list block or not." + [block] + (->> (string/trim block) + (re-matches #"(?s)^(\d\.\s|\*{1}\s|\-{1}\s).*$"))) \ No newline at end of file diff --git a/src/clarktown/matchers/quote_block.clj b/src/clarktown/matchers/quote_block.clj new file mode 100644 index 0000000..230b561 --- /dev/null +++ b/src/clarktown/matchers/quote_block.clj @@ -0,0 +1,11 @@ +(ns clarktown.matchers.quote-block + (:require + [clojure.string :as string])) + + +(defn match? + "Determines whether the given block is a quote block." + [block] + (-> (string/replace block #"\n" "") + string/trim + (string/starts-with? ">"))) \ No newline at end of file diff --git a/src/clarktown/parser.clj b/src/clarktown/parser.clj deleted file mode 100644 index ecee37d..0000000 --- a/src/clarktown/parser.clj +++ /dev/null @@ -1,195 +0,0 @@ -(ns clarktown.parser - (:require - [clojure.string :as string])) - - -(defn- stitch-code-blocks - "Since code blocks can span multiple blocks (a block is separated by - two line breaks from another block) , we need to stitch them together - into one block in order for a block parser to be able to do anything - with it." - [blocks] - (loop [stitched-blocks [] - code-block-started? false - blocks blocks] - (if (empty? blocks) - stitched-blocks - (let [block (first blocks)] - (if (and (string/starts-with? (string/trim block) "```") - (not (string/ends-with? (string/trim block) "```"))) - (recur (conj stitched-blocks block) - true - (drop 1 blocks)) - (if code-block-started? - (let [last-block (last stitched-blocks) - last-block-index (- (count stitched-blocks) 1)] - (if (string/ends-with? (string/trim block) "```") - (recur (assoc stitched-blocks last-block-index (str last-block "\n\n" block)) - false - (drop 1 blocks)) - (recur (assoc stitched-blocks last-block-index (str last-block "\n\n" block)) - true - (drop 1 blocks)))) - (recur (conj stitched-blocks block) - false - (drop 1 blocks)))))))) - - -(defn- needs-empty-line-above? - "Determines whether the current line needs an empty line correction - above." - [lines line index] - (cond - ; code block - (and (= (string/trim line) "```") - (> index 0) - (->> (take index lines) - (filter #(= (string/trim %) "```")) - count - odd?) - (not (= (-> (nth lines (- index 1)) - string/trim) ""))) - true - - ; ATX heading block - (and (string/starts-with? (string/trim line) "#") - (> index 0) - (not (= (-> (nth lines (- index 1)) - string/trim) ""))) - true - - - ; everything else stays normal - :else false)) - - -(defn- needs-empty-line-below? - "Determines whether the current line needs an empty line correction - below." - [lines line index] - (cond - ; code block - (and (= (string/trim line) "```") - (< index (- (count lines) 1)) - (->> (take index lines) - (filter #(= (string/trim %) "```")) - count - even?) - (not (= (-> (nth lines (+ index 1)) - string/trim) ""))) - true - - ; ATX heading block - (and (string/starts-with? (string/trim line) "#") - (< index (- (count lines) 1)) - (not (= (-> (nth lines (+ index 1)) - string/trim) ""))) - true - - ; everything else stays normal - :else false)) - - -(defn- correct-block-separations - "Corrects block separations and adds newlines above or - below a block where needed." - [lines] - (->> lines - (map-indexed - (fn [index line] - (let [add-line-above? (needs-empty-line-above? lines line index) - add-line-below? (needs-empty-line-below? lines line index)] - (cond - ; If code block starts but there is no empty newline - ; above, let's fix that - (and add-line-above? - (not add-line-below?)) - (str \newline line) - - ; If the code block ends, but there is no empty newline - ; below, let's fix that. - (and add-line-below? - (not add-line-above?)) - (str line \newline) - - ; If the code block needs a newline both above and below, - ; let's fix that. - (and add-line-above? - add-line-below?) - (str \newline line \newline) - - ; otherwise is what it is - :else line)))))) - - -(defn- correct-markdown - "Corrects invalid Markdown for the parser." - [markdown] - (let [lines (string/split-lines markdown)] - (->> lines - correct-block-separations - (string/join \newline)))) - - -(defn- find-parser-by-block - "Find a parser from `parsers` that matches the given `block`." - [parsers block] - (->> parsers - (filter - (fn [{:keys [matcher]}] - (when matcher - (matcher block)))) - first)) - - -(defn- parse-block-with-known-parser - "Parses a given `block` with a known `parser`." - [parser parsers block] - (loop [block block - renderers (:renderers parser)] - (if (empty? renderers) - block - (let [renderer (first renderers)] - (recur (renderer block parsers) - (drop 1 renderers)))))) - - -(defn- parse-block-with-unknown-parsers - "Parses the given `block` with all the parsers that do not have - a matcher function, useful for any fallback parsing one might want - to do." - [parsers block] - (loop [block block - parsers (filter #(= nil (:matcher %)) parsers)] - (if (empty? parsers) - block - (recur (loop [block block - renderers (:renderers (first parsers))] - (if (empty? renderers) - block - (let [renderer (first renderers)] - (recur (renderer block parsers) - (drop 1 renderers))))) - (drop 1 parsers))))) - - -(defn- parse-blocks - "Parses each individual Markdown block, given as `blocks`, with - the list of `parsers`." - [blocks parsers] - (for [block blocks] - (if-let [parser (find-parser-by-block parsers block)] - (->> (string/trim block) - (parse-block-with-known-parser parser parsers)) - (->> (string/trim block) - (parse-block-with-unknown-parsers parsers))))) - - -(defn parse - "Parses given `markdown` with `parsers`." - [markdown parsers] - (let [blocks (-> (correct-markdown markdown) - (string/split #"\n\n") - stitch-code-blocks) - parsed-blocks (parse-blocks blocks parsers)] - (string/join "\n\n" parsed-blocks))) diff --git a/src/clarktown/parsers.clj b/src/clarktown/parsers.clj index cd909b3..77c9794 100644 --- a/src/clarktown/parsers.clj +++ b/src/clarktown/parsers.clj @@ -1,45 +1,95 @@ (ns clarktown.parsers (:require - [clarktown.parsers.bold :as bold] - [clarktown.parsers.italic :as italic] - [clarktown.parsers.inline-code :as inline-code] - [clarktown.parsers.strikethrough :as strikethrough] - [clarktown.parsers.link-and-image :as link-and-image] - [clarktown.parsers.empty-block :as empty-block] - [clarktown.parsers.horizontal-line-block :as horizontal-line-block] - [clarktown.parsers.quote-block :as quote-block] - [clarktown.parsers.heading-block :as heading-block] - [clarktown.parsers.code-block :as code-block] - [clarktown.parsers.list-block :as list-block] - [clarktown.parsers.paragraph-block :as paragraph-block])) - - -(def parsers - [{:matcher empty-block/is? - :renderers [empty-block/render]} - {:matcher horizontal-line-block/is? - :renderers [horizontal-line-block/render]} - {:matcher heading-block/is? - :renderers [link-and-image/render - bold/render - italic/render - inline-code/render - strikethrough/render - heading-block/render]} - {:matcher quote-block/is? - :renderers [quote-block/render]} - {:matcher code-block/is? - :renderers [code-block/render]} - {:matcher list-block/is? - :renderers [link-and-image/render - bold/render - italic/render - inline-code/render - strikethrough/render - list-block/render]} - {:renderers [link-and-image/render - bold/render - italic/render - inline-code/render - strikethrough/render - paragraph-block/render]}]) + [clarktown.matchers.empty-block] + [clarktown.renderers.empty-block] + [clarktown.matchers.horizontal-line-block] + [clarktown.renderers.horizontal-line-block] + [clarktown.matchers.heading-block] + [clarktown.renderers.heading-block] + [clarktown.matchers.quote-block] + [clarktown.renderers.quote-block] + [clarktown.matchers.code-block] + [clarktown.renderers.code-block] + [clarktown.matchers.list-block] + [clarktown.renderers.list-block] + [clarktown.renderers.paragraph-block] + [clarktown.renderers.link-and-image] + [clarktown.renderers.bold] + [clarktown.renderers.italic] + [clarktown.renderers.inline-code] + [clarktown.renderers.strikethrough])) + + +(def + ^{:doc "Detects, parses and renders a empty block."} + empty-block-parser + {:matcher clarktown.matchers.empty-block/match? + :renderers [clarktown.renderers.empty-block/render]}) + + +(def + ^{:doc "Detects, parses and renders a horizontal line block."} + horizontal-line-block-parser + {:matcher clarktown.matchers.horizontal-line-block/match? + :renderers [clarktown.renderers.horizontal-line-block/render]}) + + +(def + ^{:doc "Detects, parses and renders a heading block."} + heading-block-parser + {:matcher clarktown.matchers.heading-block/match? + :renderers [clarktown.renderers.link-and-image/render + clarktown.renderers.bold/render + clarktown.renderers.italic/render + clarktown.renderers.inline-code/render + clarktown.renderers.strikethrough/render + clarktown.renderers.heading-block/render]}) + + +(def + ^{:doc "Detects, parses and renders a quote block."} + quote-block-parser + {:matcher clarktown.matchers.quote-block/match? + :renderers [clarktown.renderers.quote-block/render]}) + + +(def + ^{:doc "Detects, parses and renders a code block."} + code-block-parser + {:matcher clarktown.matchers.code-block/match? + :renderers [clarktown.renderers.code-block/render]}) + + +(def + ^{:doc "Detects, parses and renders a list block."} + list-block-parser + {:matcher clarktown.matchers.list-block/match? + :renderers [clarktown.renderers.link-and-image/render + clarktown.renderers.bold/render + clarktown.renderers.italic/render + clarktown.renderers.inline-code/render + clarktown.renderers.strikethrough/render + clarktown.renderers.list-block/render]}) + + +(def + ^{:doc "Parses and renders a quote block."} + paragraph-block-parser + {:renderers [clarktown.renderers.link-and-image/render + clarktown.renderers.bold/render + clarktown.renderers.italic/render + clarktown.renderers.inline-code/render + clarktown.renderers.strikethrough/render + clarktown.renderers.paragraph-block/render]}) + + +(def + ^{:doc "A set of default parsers."} + default-parsers + [empty-block-parser + horizontal-line-block-parser + heading-block-parser + quote-block-parser + code-block-parser + list-block-parser + paragraph-block-parser]) diff --git a/src/clarktown/parsers/bold.clj b/src/clarktown/parsers/bold.clj deleted file mode 100644 index 79579bf..0000000 --- a/src/clarktown/parsers/bold.clj +++ /dev/null @@ -1,18 +0,0 @@ -(ns clarktown.parsers.bold - (:require - [clojure.string :as string])) - - -(defn render - "Renders all occurring bold text as bold." - [block _] - (loop [block block - matches (-> (re-seq #"(\*{2}|\_{2})[^\*|\_](.*?)[^\*|\_](\*{2}|\_{2})" block) - distinct)] - (if (empty? matches) - block - (let [match (ffirst matches) - value (subs match 2 (- (count match) 2)) - replacement (str "" value "")] - (recur (string/replace block match replacement) - (drop 1 matches)))))) diff --git a/src/clarktown/parsers/code_block.clj b/src/clarktown/parsers/code_block.clj deleted file mode 100644 index c6ecfea..0000000 --- a/src/clarktown/parsers/code_block.clj +++ /dev/null @@ -1,30 +0,0 @@ -(ns clarktown.parsers.code-block - (:require - [clojure.string :as string])) - - -(defn is? - "Determines whether we're dealing with a code block." - [block] - (and (string/starts-with? block "```") - (string/ends-with? block "```"))) - - -(defn render - "Renders the code block." - [block _] - (let [language (->> block - (re-find #"\`\`\`(\w+)") - second) - lines (string/split-lines block) - block* (->> (next lines) - (take (- (count lines) 2)) - (string/join \newline)) - code (-> block* - (string/replace #"&" "&") - (string/replace #"<" "<") - (string/replace #">" ">") - string/trim)] - (if language - (str "
" code "
") - (str "
" code "
")))) diff --git a/src/clarktown/parsers/empty_block.clj b/src/clarktown/parsers/empty_block.clj deleted file mode 100644 index 0ed5a08..0000000 --- a/src/clarktown/parsers/empty_block.clj +++ /dev/null @@ -1,17 +0,0 @@ -(ns clarktown.parsers.empty-block - (:require - [clojure.string :as string])) - - -(defn is? - "Determines if the current block is an empty block or not." - [block] - (-> (string/replace block #"\n" "") - string/trim - string/blank?)) - - -(defn render - "Renders an empty block." - [_ _] - "") diff --git a/src/clarktown/parsers/heading_block.clj b/src/clarktown/parsers/heading_block.clj deleted file mode 100644 index def2394..0000000 --- a/src/clarktown/parsers/heading_block.clj +++ /dev/null @@ -1,72 +0,0 @@ -(ns clarktown.parsers.heading-block - (:require - [clojure.string :as string])) - - -(defn is-hashbang-heading? - "Determines whether the given block is a hashbang heading." - [block] - (-> (string/replace block #"\n" "") - string/trim - (string/starts-with? "#"))) - - -(defn is-settext-heading? - "Determines whether the given block is a settext heading." - [block] - (let [lines (-> (string/split-lines block)) - chars (-> (last lines) - string/trim - (string/split #""))] - (and (> (count lines) 1) - (every? #{"-" "="} chars)))) - - -(defn is? - "Determines whether the given block is a heading block." - [block] - (or (is-hashbang-heading? block) - (is-settext-heading? block))) - - -(defn render-hashbang-heading - "Renders the hashbang heading block." - [block] - (let [single-line-block (-> (string/replace block #"\n" "") - string/trim) - size (-> (string/split single-line-block #" ") - first - string/trim - count) - value (->> (string/split single-line-block #" ") - next - (string/join " ") - string/trim)] - (str "" value ""))) - - -(defn render-settext-heading - "Renders the settext heading block." - [block] - (let [lines (string/split-lines block) - value (->> (split-at (- (count lines) 1) lines) - first - (string/join "\n")) - h1? (= "=" (-> (last lines) - string/trim - (string/split #"") - first))] - (if h1? - (str "

" value "

") - (str "

" value "

")))) - - -(render-settext-heading "Hello world\nAnd you too\n===") - - -(defn render - "Renders the heading block." - [block _] - (if (is-hashbang-heading? block) - (render-hashbang-heading block) - (render-settext-heading block))) diff --git a/src/clarktown/parsers/horizontal_line_block.clj b/src/clarktown/parsers/horizontal_line_block.clj deleted file mode 100644 index b1d1e05..0000000 --- a/src/clarktown/parsers/horizontal_line_block.clj +++ /dev/null @@ -1,16 +0,0 @@ -(ns clarktown.parsers.horizontal-line-block - (:require - [clojure.string :as string])) - - -(defn is? - "Determines whether the given block is a horizontal line block." - [block] - (or (= "***" (string/trim block)) - (= "---" (string/trim block)))) - - -(defn render - "Renders the horizontal line block." - [_ _] - "
") diff --git a/src/clarktown/parsers/inline_code.clj b/src/clarktown/parsers/inline_code.clj deleted file mode 100644 index 1de73bf..0000000 --- a/src/clarktown/parsers/inline_code.clj +++ /dev/null @@ -1,21 +0,0 @@ -(ns clarktown.parsers.inline-code - (:require - [clojure.string :as string])) - - -(defn render - "Renders all occurring inline code." - [block _] - (loop [block block - matches (-> (re-seq #"\`.*?\`" block) - distinct)] - (if (empty? matches) - block - (let [match (first matches) - value (-> (subs match 1 (- (count match) 1)) - (string/replace #"&" "&") - (string/replace #"<" "<") - (string/replace #">" ">")) - replacement (str "" value "")] - (recur (string/replace block match replacement) - (drop 1 matches)))))) diff --git a/src/clarktown/parsers/italic.clj b/src/clarktown/parsers/italic.clj deleted file mode 100644 index 915a017..0000000 --- a/src/clarktown/parsers/italic.clj +++ /dev/null @@ -1,18 +0,0 @@ -(ns clarktown.parsers.italic - (:require - [clojure.string :as string])) - - -(defn render - "Renders all occurring italic text as italic." - [block _] - (loop [block block - matches (-> (re-seq #"(\*{1,}?|\_{1,}?)(.*?)(\*{1,}?|\_{1,}?)" block) - distinct)] - (if (empty? matches) - block - (let [match (ffirst matches) - value (subs match 1 (- (count match) 1)) - replacement (str "" value "")] - (recur (string/replace block match replacement) - (drop 1 matches)))))) diff --git a/src/clarktown/parsers/link_and_image.clj b/src/clarktown/parsers/link_and_image.clj deleted file mode 100644 index 3be2efe..0000000 --- a/src/clarktown/parsers/link_and_image.clj +++ /dev/null @@ -1,27 +0,0 @@ -(ns clarktown.parsers.link-and-image - (:require - [clojure.string :as string])) - - -(defn encode-href - [href] - (-> href - (string/replace "_" "_"))) - - -(defn render - "Renders all occurring links and images." - [block _] - (loop [block block - matches (-> (re-seq #"\!?\[([a-zA-Z0-9\-\.\,]+( [a-zA-Z0-9\-\.\,]+)*)\]\((.*?)\)" block) - distinct)] - (if (empty? matches) - block - (let [[whole-match label _ href] (first matches) - image? (string/starts-with? whole-match "!") - image (str "\""") - link (str "" label "")] - (recur (if image? - (string/replace block whole-match image) - (string/replace block whole-match link)) - (drop 1 matches)))))) \ No newline at end of file diff --git a/src/clarktown/parsers/list_block.clj b/src/clarktown/parsers/list_block.clj deleted file mode 100644 index 52f955f..0000000 --- a/src/clarktown/parsers/list_block.clj +++ /dev/null @@ -1,127 +0,0 @@ -(ns clarktown.parsers.list-block - (:require - [clojure.string :as string])) - - -(defn is? - "Determines whether we're dealing with a list block or not." - [block] - (->> (string/trim block) - (re-matches #"(?s)^(\d\.\s|\*{1}\s|\-{1}\s).*$"))) - - -(defn string->indent-n - "Returns the indentation count from left of `str`, which must be - in spaces and not tabs." - [str] - (count (take-while #{\space} str))) - - -(defn compose-items-with-indent-guides - "Composes a vector of maps from given `block` that adds a unique - ID to each line as well as its `indent-n` which is used later - on to determine hierarchies. " - [block] - (->> (string/split-lines block) - (mapv - (fn [line] - {:id (random-uuid) - :indent-n (string->indent-n line) - :value (-> line - string/trim)})))) - - -(defn find-parent-id - "Assuming a 1-level `items`, will attempt to find the parent `id` - of the item at given `index`. Will return `nil` otherwise." - [items index] - (let [indent-n-at-index (:indent-n (nth items index))] - (-> (->> (split-at index items) - first - reverse - (remove #(or (> (:indent-n %) indent-n-at-index) - (= (:indent-n %) indent-n-at-index))) - first - :id)))) - - -(defn compose-items-with-parents - "Composes a 1-level list of items from `block` and adds parent - information to each if they belong to another item. The result - of this is used to build the final data tree." - [block] - (let [items (compose-items-with-indent-guides block)] - (->> items - (map-indexed - (fn [index line] - (merge line {:parent (find-parent-id items index)})))))) - - -(defn add-to-parent - "Recursively scans `items`, which can be multiple levels deep, - and tries to find a home for `item` according to its parent ID." - [items item] - (->> items - (mapv - (fn [i] - (if (= (:id i) (:parent item)) - (if (:items i) - (assoc i :items (concat (:items i) [item])) - (assoc i :items [item])) - (if (:items i) - (assoc i :items (add-to-parent (:items i) item)) - i)))))) - - -(defn compose-item-tree - "Given a `block`, composes a data representation of it based on - the indentation of each line." - [block] - (loop [result [] - items (compose-items-with-parents block)] - (if (empty? items) - result - (let [item (first items) - parent (:parent item) - new-item {:id (:id item) - :value (:value item)}] - (recur (if parent - (add-to-parent result item) - (concat result [new-item])) - (drop 1 items)))))) - - -(defn render-items - "Renders an ordered/un-ordered list hierarchy from given `items`." - [items] - (loop [result "" - inner-items items] - (if (empty? inner-items) - (if (or (string/starts-with? (:value (first items)) "*") - (string/starts-with? (:value (first items)) "-")) - (str "") - (str "
    " result "
")) - (let [inner-item (first inner-items) - value (cond - ; * unordered list - (string/starts-with? (:value inner-item) "*") - (-> (string/replace-first (:value inner-item) "*" "") - string/trim) - ; - unordered list - (string/starts-with? (:value inner-item) "-") - (-> (string/replace-first (:value inner-item) "-" "") - string/trim) - :else - (-> (string/replace-first (:value inner-item) #"\d\." "") - string/trim))] - (recur (if (:items inner-item) - (str result "
  • " value (render-items (:items inner-item)) "
  • ") - (str result "
  • " value "
  • ")) - (drop 1 inner-items)))))) - - -(defn render - "Renders the list block" - [block _] - (-> (compose-item-tree block) - (render-items))) \ No newline at end of file diff --git a/src/clarktown/parsers/paragraph_block.clj b/src/clarktown/parsers/paragraph_block.clj deleted file mode 100644 index 3a7c633..0000000 --- a/src/clarktown/parsers/paragraph_block.clj +++ /dev/null @@ -1,9 +0,0 @@ -(ns clarktown.parsers.paragraph-block - (:require - [clojure.string :as string])) - - -(defn render - "Renders the paragraph block." - [block _] - (str "

    " (string/trim block) "

    ")) diff --git a/src/clarktown/parsers/quote_block.clj b/src/clarktown/parsers/quote_block.clj deleted file mode 100644 index ff2dda8..0000000 --- a/src/clarktown/parsers/quote_block.clj +++ /dev/null @@ -1,24 +0,0 @@ -(ns clarktown.parsers.quote-block - (:require - [clojure.string :as string] - [clarktown.parser :as parser])) - - -(defn is? - "Determines whether the given block is a quote block." - [block] - (-> (string/replace block #"\n" "") - string/trim - (string/starts-with? ">"))) - - -(defn render - "Renders a quote block." - [block parsers] - (let [matches (re-seq #">.*" block) - blocks (->> (for [match matches] - (-> (subs match 1) - string/trim)) - (string/join "\n")) - block (parser/parse blocks parsers)] - (str "
    " block "
    "))) diff --git a/src/clarktown/parsers/strikethrough.clj b/src/clarktown/parsers/strikethrough.clj deleted file mode 100644 index 6f03152..0000000 --- a/src/clarktown/parsers/strikethrough.clj +++ /dev/null @@ -1,18 +0,0 @@ -(ns clarktown.parsers.strikethrough - (:require - [clojure.string :as string])) - - -(defn render - "Renders all occurring strikethrough text." - [block _] - (loop [block block - matches (-> (re-seq #"~~.*?~~" block) - distinct)] - (if (empty? matches) - block - (let [match (first matches) - value (subs match 2 (- (count match) 2)) - replacement (str "" value "")] - (recur (string/replace block match replacement) - (drop 1 matches)))))) diff --git a/src/clarktown/renderers/bold.clj b/src/clarktown/renderers/bold.clj new file mode 100644 index 0000000..1ce7f84 --- /dev/null +++ b/src/clarktown/renderers/bold.clj @@ -0,0 +1,18 @@ +(ns clarktown.renderers.bold + (:require + [clojure.string :as string])) + + +(defn render + "Renders all occurring bold text as bold." + [block _] + (loop [block block + matches (-> (re-seq #"(\*{2}|\_{2})[^\*|\_](.*?)[^\*|\_](\*{2}|\_{2})" block) + distinct)] + (if (empty? matches) + block + (let [match (ffirst matches) + value (subs match 2 (- (count match) 2)) + replacement (str "" value "")] + (recur (string/replace block match replacement) + (drop 1 matches)))))) diff --git a/src/clarktown/renderers/code_block.clj b/src/clarktown/renderers/code_block.clj new file mode 100644 index 0000000..2bed4e6 --- /dev/null +++ b/src/clarktown/renderers/code_block.clj @@ -0,0 +1,23 @@ +(ns clarktown.renderers.code-block + (:require + [clojure.string :as string])) + + +(defn render + "Renders the code block." + [block _] + (let [language (->> block + (re-find #"\`\`\`(\w+)") + second) + lines (string/split-lines block) + block* (->> (next lines) + (take (- (count lines) 2)) + (string/join \newline)) + code (-> block* + (string/replace #"&" "&") + (string/replace #"<" "<") + (string/replace #">" ">") + string/trim)] + (if language + (str "
    " code "
    ") + (str "
    " code "
    ")))) diff --git a/src/clarktown/renderers/empty_block.clj b/src/clarktown/renderers/empty_block.clj new file mode 100644 index 0000000..66e819e --- /dev/null +++ b/src/clarktown/renderers/empty_block.clj @@ -0,0 +1,7 @@ +(ns clarktown.renderers.empty-block) + + +(defn render + "Renders an empty block." + [_ _] + "") diff --git a/src/clarktown/renderers/heading_block.clj b/src/clarktown/renderers/heading_block.clj new file mode 100644 index 0000000..f953d0a --- /dev/null +++ b/src/clarktown/renderers/heading_block.clj @@ -0,0 +1,44 @@ +(ns clarktown.renderers.heading-block + (:require + [clojure.string :as string] + [clarktown.matchers.heading-block :as matcher])) + + +(defn render-atx-heading + "Renders the hashbang heading block." + [block] + (let [single-line-block (-> (string/replace block #"\n" "") + string/trim) + size (-> (string/split single-line-block #" ") + first + string/trim + count) + value (->> (string/split single-line-block #" ") + next + (string/join " ") + string/trim)] + (str "" value ""))) + + +(defn render-settext-heading + "Renders the settext heading block." + [block] + (let [lines (string/split-lines block) + value (->> (split-at (- (count lines) 1) lines) + first + (string/join "\n")) + h1? (= "=" (-> (last lines) + string/trim + (string/split #"") + first))] + (if h1? + (str "

    " value "

    ") + (str "

    " value "

    ")))) + + +(defn render + "Renders the heading block." + [block _] + (if (matcher/is-atx-heading? block) + (render-atx-heading block) + (render-settext-heading block))) diff --git a/src/clarktown/renderers/horizontal_line_block.clj b/src/clarktown/renderers/horizontal_line_block.clj new file mode 100644 index 0000000..f141e5f --- /dev/null +++ b/src/clarktown/renderers/horizontal_line_block.clj @@ -0,0 +1,7 @@ +(ns clarktown.renderers.horizontal-line-block) + + +(defn render + "Renders the horizontal line block." + [_ _] + "
    ") diff --git a/src/clarktown/renderers/inline_code.clj b/src/clarktown/renderers/inline_code.clj new file mode 100644 index 0000000..29593a8 --- /dev/null +++ b/src/clarktown/renderers/inline_code.clj @@ -0,0 +1,21 @@ +(ns clarktown.renderers.inline-code + (:require + [clojure.string :as string])) + + +(defn render + "Renders all occurring inline code." + [block _] + (loop [block block + matches (-> (re-seq #"\`.*?\`" block) + distinct)] + (if (empty? matches) + block + (let [match (first matches) + value (-> (subs match 1 (- (count match) 1)) + (string/replace #"&" "&") + (string/replace #"<" "<") + (string/replace #">" ">")) + replacement (str "" value "")] + (recur (string/replace block match replacement) + (drop 1 matches)))))) diff --git a/src/clarktown/renderers/italic.clj b/src/clarktown/renderers/italic.clj new file mode 100644 index 0000000..a1568f6 --- /dev/null +++ b/src/clarktown/renderers/italic.clj @@ -0,0 +1,18 @@ +(ns clarktown.renderers.italic + (:require + [clojure.string :as string])) + + +(defn render + "Renders all occurring italic text as italic." + [block _] + (loop [block block + matches (-> (re-seq #"(\*{1,}?|\_{1,}?)(.*?)(\*{1,}?|\_{1,}?)" block) + distinct)] + (if (empty? matches) + block + (let [match (ffirst matches) + value (subs match 1 (- (count match) 1)) + replacement (str "" value "")] + (recur (string/replace block match replacement) + (drop 1 matches)))))) diff --git a/src/clarktown/renderers/link_and_image.clj b/src/clarktown/renderers/link_and_image.clj new file mode 100644 index 0000000..ea4a006 --- /dev/null +++ b/src/clarktown/renderers/link_and_image.clj @@ -0,0 +1,27 @@ +(ns clarktown.renderers.link-and-image + (:require + [clojure.string :as string])) + + +(defn encode-href + [href] + (-> href + (string/replace "_" "_"))) + + +(defn render + "Renders all occurring links and images." + [block _] + (loop [block block + matches (-> (re-seq #"\!?\[([a-zA-Z0-9\-\.\,]+( [a-zA-Z0-9\-\.\,]+)*)\]\((.*?)\)" block) + distinct)] + (if (empty? matches) + block + (let [[whole-match label _ href] (first matches) + image? (string/starts-with? whole-match "!") + image (str "\""") + link (str "" label "")] + (recur (if image? + (string/replace block whole-match image) + (string/replace block whole-match link)) + (drop 1 matches)))))) \ No newline at end of file diff --git a/src/clarktown/renderers/list_block.clj b/src/clarktown/renderers/list_block.clj new file mode 100644 index 0000000..2a40b06 --- /dev/null +++ b/src/clarktown/renderers/list_block.clj @@ -0,0 +1,121 @@ +(ns clarktown.renderers.list-block + (:require + [clojure.string :as string])) + + +(defn string->indent-n + "Returns the indentation count from left of `str`, which must be + in spaces and not tabs." + [str] + (count (take-while #{\space} str))) + + +(defn compose-items-with-indent-guides + "Composes a vector of maps from given `block` that adds a unique + ID to each line as well as its `indent-n` which is used later + on to determine hierarchies. " + [block] + (->> (string/split-lines block) + (mapv + (fn [line] + {:id (random-uuid) + :indent-n (string->indent-n line) + :value (-> line + string/trim)})))) + + +(defn find-parent-id + "Assuming a 1-level `items`, will attempt to find the parent `id` + of the item at given `index`. Will return `nil` otherwise." + [items index] + (let [indent-n-at-index (:indent-n (nth items index))] + (-> (->> (split-at index items) + first + reverse + (remove #(or (> (:indent-n %) indent-n-at-index) + (= (:indent-n %) indent-n-at-index))) + first + :id)))) + + +(defn compose-items-with-parents + "Composes a 1-level list of items from `block` and adds parent + information to each if they belong to another item. The result + of this is used to build the final data tree." + [block] + (let [items (compose-items-with-indent-guides block)] + (->> items + (map-indexed + (fn [index line] + (merge line {:parent (find-parent-id items index)})))))) + + +(defn add-to-parent + "Recursively scans `items`, which can be multiple levels deep, + and tries to find a home for `item` according to its parent ID." + [items item] + (->> items + (mapv + (fn [i] + (if (= (:id i) (:parent item)) + (if (:items i) + (assoc i :items (concat (:items i) [item])) + (assoc i :items [item])) + (if (:items i) + (assoc i :items (add-to-parent (:items i) item)) + i)))))) + + +(defn compose-item-tree + "Given a `block`, composes a data representation of it based on + the indentation of each line." + [block] + (loop [result [] + items (compose-items-with-parents block)] + (if (empty? items) + result + (let [item (first items) + parent (:parent item) + new-item {:id (:id item) + :value (:value item)}] + (recur (if parent + (add-to-parent result item) + (concat result [new-item])) + (drop 1 items)))))) + + +(defn render-items + "Renders an ordered/un-ordered list hierarchy from given `items`." + [items] + (loop [result "" + inner-items items] + (if (empty? inner-items) + (if (or (string/starts-with? (:value (first items)) "*") + (string/starts-with? (:value (first items)) "-")) + (str "") + (str "
      " result "
    ")) + (let [inner-item (first inner-items) + value (cond + ; * unordered list + (string/starts-with? (:value inner-item) "*") + (-> (string/replace-first (:value inner-item) "*" "") + string/trim) + ; - unordered list + (string/starts-with? (:value inner-item) "-") + (-> (string/replace-first (:value inner-item) "-" "") + string/trim) + ; ordered list + :else + (-> (string/replace-first (:value inner-item) #"\d\." "") + string/trim))] + (recur (if (:items inner-item) + (str result "
  • " value (render-items (:items inner-item)) "
  • ") + (str result "
  • " value "
  • ")) + (drop 1 inner-items)))))) + + +(defn render + "Renders the list block" + [block _] + (-> (compose-item-tree block) + (render-items))) \ No newline at end of file diff --git a/src/clarktown/renderers/paragraph_block.clj b/src/clarktown/renderers/paragraph_block.clj new file mode 100644 index 0000000..c7bec22 --- /dev/null +++ b/src/clarktown/renderers/paragraph_block.clj @@ -0,0 +1,9 @@ +(ns clarktown.renderers.paragraph-block + (:require + [clojure.string :as string])) + + +(defn render + "Renders the paragraph block." + [block _] + (str "

    " (string/trim block) "

    ")) diff --git a/src/clarktown/renderers/quote_block.clj b/src/clarktown/renderers/quote_block.clj new file mode 100644 index 0000000..ee30635 --- /dev/null +++ b/src/clarktown/renderers/quote_block.clj @@ -0,0 +1,16 @@ +(ns clarktown.renderers.quote-block + (:require + [clojure.string :as string] + [clarktown.parser :as parser])) + + +(defn render + "Renders a quote block." + [block parsers] + (let [matches (re-seq #">.*" block) + blocks (->> (for [match matches] + (-> (subs match 1) + string/trim)) + (string/join "\n")) + block (parser/parse blocks parsers)] + (str "
    " block "
    "))) diff --git a/src/clarktown/renderers/strikethrough.clj b/src/clarktown/renderers/strikethrough.clj new file mode 100644 index 0000000..8e124a0 --- /dev/null +++ b/src/clarktown/renderers/strikethrough.clj @@ -0,0 +1,18 @@ +(ns clarktown.renderers.strikethrough + (:require + [clojure.string :as string])) + + +(defn render + "Renders all occurring strikethrough text." + [block _] + (loop [block block + matches (-> (re-seq #"~~.*?~~" block) + distinct)] + (if (empty? matches) + block + (let [match (first matches) + value (subs match 2 (- (count match) 2)) + replacement (str "" value "")] + (recur (string/replace block match replacement) + (drop 1 matches)))))) -- cgit v1.2.3