diff --git a/README.adoc b/README.adoc index 309489a..a469e17 100644 --- a/README.adoc +++ b/README.adoc @@ -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 diff --git a/bb_test_runner.clj b/bb_test_runner.clj index 11223df..f4156e1 100644 --- a/bb_test_runner.clj +++ b/bb_test_runner.clj @@ -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) diff --git a/src/mint/constant.cljc b/src/mint/constant.cljc new file mode 100644 index 0000000..be334ed --- /dev/null +++ b/src/mint/constant.cljc @@ -0,0 +1,4 @@ +(ns mint.constant) + +(def start-delm "{{") +(def end-delm "}}") diff --git a/src/mint/core.cljc b/src/mint/core.cljc index 4024217..05ea1f9 100644 --- a/src/mint/core.cljc +++ b/src/mint/core.cljc @@ -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))) diff --git a/src/mint/evaluator.cljc b/src/mint/evaluator.cljc new file mode 100644 index 0000000..6fbe46d --- /dev/null +++ b/src/mint/evaluator.cljc @@ -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 ""))) diff --git a/src/mint/evaluator/condition.cljc b/src/mint/evaluator/condition.cljc new file mode 100644 index 0000000..550f8f5 --- /dev/null +++ b/src/mint/evaluator/condition.cljc @@ -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))) diff --git a/src/mint/evaluator/threading.cljc b/src/mint/evaluator/threading.cljc new file mode 100644 index 0000000..562e610 --- /dev/null +++ b/src/mint/evaluator/threading.cljc @@ -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)) diff --git a/src/mint/parser.cljc b/src/mint/parser.cljc new file mode 100644 index 0000000..3ef38c7 --- /dev/null +++ b/src/mint/parser.cljc @@ -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 ""])))) diff --git a/test/mint/core_test.cljc b/test/mint/core_test.cljc index 95a8889..52f6e18 100644 --- a/test/mint/core_test.cljc +++ b/test/mint/core_test.cljc @@ -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" {}))) diff --git a/test/mint/evaluator/condition_test.cljc b/test/mint/evaluator/condition_test.cljc new file mode 100644 index 0000000..6541ec0 --- /dev/null +++ b/test/mint/evaluator/condition_test.cljc @@ -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") {}))))) diff --git a/test/mint/evaluator/threading_test.cljc b/test/mint/evaluator/threading_test.cljc new file mode 100644 index 0000000..d723d70 --- /dev/null +++ b/test/mint/evaluator/threading_test.cljc @@ -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}))))) diff --git a/test/mint/evaluator_test.cljc b/test/mint/evaluator_test.cljc new file mode 100644 index 0000000..7d8b0b9 --- /dev/null +++ b/test/mint/evaluator_test.cljc @@ -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"})))))