Skip to content

Commit

Permalink
refactor: Refactor implementations
Browse files Browse the repository at this point in the history
  • Loading branch information
liquidz committed Dec 31, 2023
1 parent 2ac9556 commit 4992079
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 107 deletions.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A proof of concept for simple template engine using threading macros.

== License

Copyright © 2022-2023 https://scrapbox.io/uochan/uochan[Masashi Iizuka]
Copyright © 2022-2024 https://scrapbox.io/uochan/uochan[Masashi Iizuka]

This program and the accompanying materials are made available under the
terms of the Eclipse Public License 2.0 which is available at
Expand Down
11 changes: 9 additions & 2 deletions bb_test_runner.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
(ns bb-test-runner
(:require
[clojure.test :as t]
[mint.core-test]))
[mint.core-test]
[mint.evaluator-test]
[mint.evaluator.condition-test]
[mint.evaluator.threading-test]))

(t/run-tests 'mint.core-test)
(t/run-tests
'mint.core-test
'mint.evaluator-test
'mint.evaluator.condition-test
'mint.evaluator.threading-test)
4 changes: 4 additions & 0 deletions src/mint/constant.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(ns mint.constant)

(def start-delm "{{")
(def end-delm "}}")
94 changes: 8 additions & 86 deletions src/mint/core.cljc
Original file line number Diff line number Diff line change
@@ -1,90 +1,12 @@
(ns mint.core
(:require
[clojure.string :as str]))

(def ^:private start-delm "{{->")
(def ^:private end-delm "}}")
(def ^:private end-delm-count (count end-delm))

(defmulti eval*
(fn [form _data]
(when (sequential? form)
(first form))))

(defmethod eval* :default
[form data]
(let [seq-form? (sequential? form)
sym-form? (symbol? form)]
(cond
(and (not seq-form?)
(not sym-form?))
form

(not seq-form?)
(get data (keyword form))

:else
(when-let [f (get data (keyword (first form)))]
(->> (rest form)
(seq)
(map #(eval* % data))
(apply f))))))

(defmethod eval* '->
[[_ v & forms] data]
(when-let [x (eval* v data)]
(reduce (fn [accm form]
(let [[head & args] (if (sequential? form) form [form])
form' (concat (list head accm) args)]
(eval* form' data)))
x forms)))

(defmethod eval* '->>
[[_ v & forms] data]
(when-let [x (eval* v data)]
(reduce (fn [accm form]
(let [form' (concat (if (sequential? form) form [form])
[accm])]
(eval* form' data)))
x forms)))

(defn- parse-pre
[s]
(if-let [idx (str/index-of s start-delm)]
[(subs s 0 idx) (subs s idx)]
[s nil]))

(defn- parse-post
[s]
(if-let [idx (str/index-of s end-delm)]
[(subs s 0 (+ idx end-delm-count)) (subs s (+ idx end-delm-count))]
[nil s]))

(defn- parse
[s]
(let [[pre tmp] (parse-pre s)
[body post] (when tmp (parse-post tmp))]
[pre body post]))

(defn- eval-body
[body data]
(let [form-str (-> body
(str/replace start-delm "(->")
(str/replace end-delm ")"))]
(try
(-> form-str
(read-string)
(eval* data)
(str))
(catch #?(:clj Exception :cljs js/Error) ex
(throw (ex-info "Invalid form" {:form form-str
:message (ex-message ex)}))))))
[mint.evaluator :as evaluator]
[mint.evaluator.condition]
[mint.evaluator.threading]
[mint.parser :as parser]))

(defn render
[s data]
(loop [content s
res []]
(let [[pre body post] (parse content)]
(if (and body post)
(recur post (conj res pre (eval-body body data)))
(str/join "" (conj res pre))))))
[template-str data]
(-> template-str
(parser/parse)
(evaluator/evaluate data)))
46 changes: 46 additions & 0 deletions src/mint/evaluator.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(ns mint.evaluator
(:require
[clojure.string :as str]
[mint.constant :as const]))

(defmulti eval*
(fn [sexp _data]
(if (sequential? sexp)
(first sexp)
sexp)))

(defmethod eval* :default
[form data]
(let [[first-item :as form] (if (sequential? form) form [form])
form-count (count form)]
(cond
(and (= 1 form-count)
(not (symbol? first-item)))
first-item

(= 1 form-count)
(get data (keyword first-item))

:else
(let [f (get data (keyword first-item))]
(when (fn? f)
(some->> (rest form)
(seq)
(map #(eval* % data))
(apply f)))))))

(defn- evaluate*
[expression-str data]
(-> expression-str
(str/replace const/start-delm "(")
(str/replace const/end-delm ")")
(read-string)
(eval* data)))

(defn evaluate
[parsed data]
(->> parsed
(map #(if (= :expression (first %))
(evaluate* (second %) data)
(second %)))
(str/join "")))
8 changes: 8 additions & 0 deletions src/mint/evaluator/condition.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
(ns mint.evaluator.condition
(:require
[mint.evaluator :as evaluator]))

(defmethod evaluator/eval* 'when
[[_ v & forms] data]
(when (evaluator/eval* v data)
(evaluator/eval* (last forms) data)))
23 changes: 23 additions & 0 deletions src/mint/evaluator/threading.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns mint.evaluator.threading
(:require
[mint.evaluator :as evaluator]))

(defmethod evaluator/eval* '->
[[_ v & forms] data]
(reduce
(fn [accm form]
(let [[head & rest-form] (if (sequential? form) form [form])
form' (concat (list head accm) rest-form)]
(evaluator/eval* form' data)))
(evaluator/eval* v data)
forms))

(defmethod evaluator/eval* '->>
[[_ v & forms] data]
(reduce
(fn [accm form]
(let [form' (concat (if (sequential? form) form [form])
[accm])]
(evaluator/eval* form' data)))
(evaluator/eval* v data)
forms))
29 changes: 29 additions & 0 deletions src/mint/parser.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(ns mint.parser
(:require
[clojure.string :as str]
[mint.constant :as const]))

(defn- parse*
[template-str]
(let [start-idx (str/index-of template-str const/start-delm)
end-idx (and start-idx
(str/index-of template-str const/end-delm (inc start-idx)))]
(if (and start-idx end-idx)
(let [end-idx' (+ end-idx (count const/end-delm))
pre (subs template-str 0 start-idx)
body (subs template-str start-idx end-idx')
post (subs template-str end-idx')]
[[:string pre]
[:expression body]
[:string post]])
[[:string template-str]])))

(defn parse
[template-str]
(->> (loop [result []
template-str template-str]
(let [[pre body post :as parsed] (parse* template-str)]
(if (seq post)
(recur (concat result [pre body]) (second post))
(concat result parsed))))
(remove #(= % [:string ""]))))
18 changes: 0 additions & 18 deletions test/mint/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,6 @@
:clj (t/deftest README-test
(t/is (testdoc (slurp (io/file "README.adoc"))))))

(t/deftest eval*-test
(t/is (= 10 (sut/eval* 'foo {:foo 10})))
(t/is (= 10 (sut/eval* '(inc 9) {:inc inc})))
(t/is (= 10 (sut/eval* '(inc (inc 8)) {:inc inc})))

(t/testing "thread first"
(t/is (= 10 (sut/eval* '(-> 9 inc) {:inc inc})))
(t/is (= 10 (sut/eval* '(-> foo (- 1)) {:foo 11, :- -})))
(t/is (= 10 (sut/eval* '(-> (inc foo) (- 1)) {:foo 10, :- -, :inc inc}))))

(t/testing "thread last"
(t/is (= 10 (sut/eval* '(->> 9 inc) {:inc inc})))
(t/is (= 10 (sut/eval* '(->> foo (- 11)) {:foo 1, :- -})))
(t/is (= 10 (sut/eval* '(->> (inc foo) (- 11)) {:foo 0, :- -, :inc inc}))))

(t/testing "qualified keyword"
(t/is (= 10 (sut/eval* '(->> foo/bar) {:foo/bar 10})))))

(t/deftest render-test
(t/is (= "" (sut/render "" {})))
(t/is (= "foo" (sut/render "foo" {})))
Expand Down
14 changes: 14 additions & 0 deletions test/mint/evaluator/condition_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
(ns mint.evaluator.condition-test
(:require
[clojure.test :as t]
[mint.evaluator :as sut]
[mint.evaluator.condition]))

(t/deftest eval*-condition-test
(t/testing "when"
(t/is (= "hello" (sut/eval* '(when foo "hello") {:foo 10})))
(t/is (= "10hello" (sut/eval* '(when foo (str foo "hello")) {:foo 10
:str str})))
(t/is (nil? (sut/eval* '(when foo "hello") {:foo nil})))
(t/is (nil? (sut/eval* '(when foo "hello") {:foo false})))
(t/is (nil? (sut/eval* '(when foo "hello") {})))))
16 changes: 16 additions & 0 deletions test/mint/evaluator/threading_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(ns mint.evaluator.threading-test
(:require
[clojure.test :as t]
[mint.evaluator :as sut]
[mint.evaluator.threading]))

(t/deftest eval*-threading-test
(t/testing "thread first"
(t/is (= 10 (sut/eval* '(-> 9 inc) {:inc inc})))
(t/is (= 10 (sut/eval* '(-> foo (- 1)) {:foo 11, :- -})))
(t/is (= 10 (sut/eval* '(-> (inc foo) (- 1)) {:foo 10, :- -, :inc inc}))))

(t/testing "thread last"
(t/is (= 10 (sut/eval* '(->> 9 inc) {:inc inc})))
(t/is (= 10 (sut/eval* '(->> foo (- 11)) {:foo 1, :- -})))
(t/is (= 10 (sut/eval* '(->> (inc foo) (- 11)) {:foo 0, :- -, :inc inc})))))
44 changes: 44 additions & 0 deletions test/mint/evaluator_test.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
(ns mint.evaluator-test
(:require
[clojure.test :as t]
[mint.evaluator :as sut]))

(t/deftest eval*-default-test
(t/testing "replace value"
(t/is (= 10 (sut/eval* 'foo {:foo 10}))))

(t/testing "eval function"
(t/is (= 10 (sut/eval* '(inc 9) {:inc inc})))
(t/is (= 10 (sut/eval* '(inc (inc 8)) {:inc inc}))))

(t/testing "qualified keyword"
(t/is (= 10 (sut/eval* '(->> foo/bar) {:foo/bar 10}))))

(t/testing "unknown value"
(t/is (nil? (sut/eval* 'unknown {}))))

(t/testing "unknown function"
(t/is (nil? (sut/eval* '(unknown 1) {})))))

(t/deftest evaluate-test
(t/is (= "" (sut/evaluate [] {})))

(t/testing "string"
(t/is (= "foo"
(sut/evaluate [[:string "foo"]] {}))))

(t/testing "expression"
(t/is (= "bar"
(sut/evaluate [[:expression "{{foo}}"]]
{:foo "bar"})))
(t/is (= "bar"
(sut/evaluate [[:expression "{{f}}"]
[:expression "{{oo}}"]]
{:f "b" :oo "ar"}))))

(t/testing "string and expression"
(t/is (= "hello world!"
(sut/evaluate [[:string "hello "]
[:expression "{{foo}}"]
[:string "!"]]
{:foo "world"})))))

0 comments on commit 4992079

Please sign in to comment.