diff --git a/deps.edn b/deps.edn index dcf461a2..aa8153ac 100644 --- a/deps.edn +++ b/deps.edn @@ -8,6 +8,7 @@ "feature-test-check" "feature-spec-alpha" "feature-rewrite-clj" + "feature-selmer" "pods/src" "babashka.nrepl/src" "depstar/src" "process/src" @@ -35,7 +36,8 @@ babashka/clojure-lanterna {:mvn/version "0.9.8-SNAPSHOT"} org.clojure/core.match {:mvn/version "1.0.0"} hiccup/hiccup {:mvn/version "2.0.0-alpha2"} - rewrite-clj/rewrite-clj {:mvn/version "1.0.605-alpha"}} + rewrite-clj/rewrite-clj {:mvn/version "1.0.605-alpha"} + org.clojars.borkdude/selmer {:mvn/version "1.12.35-pre1"}} :aliases {:babashka/dev {:main-opts ["-m" "babashka.main"]} :profile diff --git a/feature-selmer/babashka/impl/selmer.clj b/feature-selmer/babashka/impl/selmer.clj new file mode 100644 index 00000000..8b2f2abb --- /dev/null +++ b/feature-selmer/babashka/impl/selmer.clj @@ -0,0 +1,58 @@ +(ns babashka.impl.selmer + {:no-doc true} + (:require [babashka.impl.classpath :refer [resource]] + [sci.core :as sci] + [selmer.filters :as filters] + [selmer.parser] + [selmer.tags :as tags] + [selmer.util :refer [*resource-fn*]])) + +(def spns (sci/create-ns 'selmer.parser nil)) + +(defn make-ns [ns sci-ns] + (reduce (fn [ns-map [var-name var]] + (let [m (meta var) + no-doc (:no-doc m) + doc (:doc m) + arglists (:arglists m)] + (if no-doc ns-map + (assoc ns-map var-name + (sci/new-var (symbol var-name) @var + (cond-> {:ns sci-ns + :name (:name m)} + (:macro m) (assoc :macro true) + doc (assoc :doc doc) + arglists (assoc :arglists arglists))))))) + {} + (ns-publics ns))) + +(def selmer-parser-ns (make-ns 'selmer.parser spns)) + +(defn render-file + "Parses files if there isn't a memoized post-parse vector ready to go, + renders post-parse vector with passed context-map regardless. Double-checks + last-modified on files. Uses classpath for filename-or-url path " + [& args] + (binding [*resource-fn* resource] + (apply selmer.parser/render-file args))) + +(def selmer-parser-namespace + (assoc selmer-parser-ns 'render-file (sci/copy-var render-file spns))) + +(def stns (sci/create-ns 'selmer.tags nil)) + +(def selmer-tags-ns (sci/create-ns 'selmer.tags stns)) + +(def selmer-tags-namespace + {;; needed by selmer.parser/add-tag! + 'expr-tags (sci/copy-var tags/expr-tags stns) + ;; needed by selmer.parser/add-tag! + 'tag-handler (sci/copy-var tags/tag-handler stns)}) + +(def sfns (sci/create-ns 'selmer.filters nil)) + +(def selmer-filters-ns (sci/create-ns 'selmer.filters sfns)) + +(def selmer-filters-namespace + {'add-filter! (sci/copy-var filters/add-filter! sfns) + 'remove-filter! (sci/copy-var filters/remove-filter! sfns)}) diff --git a/project.clj b/project.clj index 71bd0356..382e2dd5 100644 --- a/project.clj +++ b/project.clj @@ -54,6 +54,8 @@ :feature/spec-alpha {:source-paths ["feature-spec-alpha"]} :feature/rewrite-clj {:source-paths ["feature-rewrite-clj"] :dependencies [[rewrite-clj/rewrite-clj "1.0.605-alpha"]]} + :feature/selmer {:source-paths ["feature-selmer"] + :dependencies [[org.clojars.borkdude/selmer "1.12.35-pre1"]]} :test [:feature/xml :feature/lanterna :feature/yaml @@ -70,6 +72,7 @@ :feature/test-check :feature/spec-alpha :feature/rewrite-clj + :feature/selmer {:dependencies [[com.clojure-goes-fast/clj-async-profiler "0.4.1"] [com.opentable.components/otj-pg-embedded "0.13.3"]]}] :uberjar {:global-vars {*assert* false} diff --git a/script/compile b/script/compile index 95291f3f..7bce6b1d 100755 --- a/script/compile +++ b/script/compile @@ -85,6 +85,12 @@ if [ "$BABASHKA_FEATURE_HSQLDB" = "true" ]; then args+=("-H:IncludeResources=org/hsqldb/.*\.properties" "-H:IncludeResources=org/hsqldb/.*\.sql") fi +BABASHKA_FEATURE_SELMER=${BABASHKA_FEATURE_SELMER:-} + +if [ "$BABASHKA_FEATURE_SELMER" = "true" ]; then + args+=("-H:IncludeResources=json.human.css") +fi + BABASHKA_LEAN=${BABASHKA_LEAN:-} if [ "$BABASHKA_LEAN" = "true" ] @@ -104,6 +110,7 @@ then export BABASHKA_FEATURE_TEST_CHECK="${BABASHKA_FEATURE_TEST_CHECK:-false}" export BABASHKA_FEATURE_SPEC_ALPHA="${BABASHKA_FEATURE_SPEC_ALPHA:-false}" export BABASHKA_FEATURE_REWRITE_CLJ="${BABASHKA_FEATURE_REWRITE_CLJ:-false}" + export BABASHKA_FEATURE_SELMER="${BABASHKA_FEATURE_SELMER:-false}" fi "$GRAALVM_HOME/bin/native-image" "${args[@]}" diff --git a/script/uberjar b/script/uberjar index cc699bab..8a2bb9a6 100755 --- a/script/uberjar +++ b/script/uberjar @@ -153,6 +153,14 @@ else BABASHKA_LEIN_PROFILES+=",-feature/rewrite-clj" fi +if [ "$BABASHKA_FEATURE_SELMER" != "false" ] +then + BABASHKA_LEIN_PROFILES+=",+feature/selmer" +else + BABASHKA_LEIN_PROFILES+=",-feature/selmer" +fi + + if [ -z "$BABASHKA_JAR" ]; then lein with-profiles "$BABASHKA_LEIN_PROFILES,+reflection,-uberjar" do run lein with-profiles "$BABASHKA_LEIN_PROFILES" do clean, uberjar diff --git a/script/uberjar.bat b/script/uberjar.bat index fba9e27c..845852f6 100755 --- a/script/uberjar.bat +++ b/script/uberjar.bat @@ -118,6 +118,11 @@ set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/rewrite-clj set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/rewrite-clj ) +if not "%BABASHKA_FEATURE_REWRITE_SELMER%"=="false" ( +set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,+feature/selmer +) else ( +set BABASHKA_LEIN_PROFILES=%BABASHKA_LEIN_PROFILES%,-feature/selmer +) call lein with-profiles %BABASHKA_LEIN_PROFILES% bb "(+ 1 2 3)" diff --git a/src/babashka/impl/classpath.clj b/src/babashka/impl/classpath.clj index 82428526..cd223e1f 100644 --- a/src/babashka/impl/classpath.clj +++ b/src/babashka/impl/classpath.clj @@ -15,7 +15,7 @@ (deftype DirectoryResolver [path] IResourceResolver - (getResource [_ resource-paths {:keys [:url?]}] + (getResource [_ resource-paths url?] (some (fn [resource-path] (let [f (io/file path resource-path)] @@ -28,23 +28,22 @@ resource-paths))) (defn path-from-jar - [^java.io.File jar-file resource-paths opts] - (let [url? (:url? opts)] - (with-open [jar (JarFile. jar-file)] - (some (fn [path] - (when-let [entry (.getEntry jar path)] - (if url? - ;; manual conversion, faster than going through .toURI - (java.net.URL. "jar" nil - (str "file:" (.getAbsolutePath jar-file) "!/" path)) - {:file path - :source (slurp (.getInputStream jar entry))}))) - resource-paths)))) + [^java.io.File jar-file resource-paths url?] + (with-open [jar (JarFile. jar-file)] + (some (fn [path] + (when-let [entry (.getEntry jar path)] + (if url? + ;; manual conversion, faster than going through .toURI + (java.net.URL. "jar" nil + (str "file:" (.getAbsolutePath jar-file) "!/" path)) + {:file path + :source (slurp (.getInputStream jar entry))}))) + resource-paths))) (deftype JarFileResolver [jar-file] IResourceResolver - (getResource [_ resource-paths opts] - (path-from-jar jar-file resource-paths opts))) + (getResource [_ resource-paths url?] + (path-from-jar jar-file resource-paths url?))) (defn part->entry [part] (when-not (str/blank? part) @@ -107,6 +106,12 @@ [] (:cp @cp-state)) +(defn resource [path] + (when-let [st @cp-state] + (let [loader (:loader st)] + (if (str/starts-with? path "/") nil ;; non-relative paths always return nil + (getResource loader [path] true))))) + (def cns (sci/create-ns 'babashka.classpath nil)) (def classpath-namespace diff --git a/src/babashka/impl/clojure/java/io.clj b/src/babashka/impl/clojure/java/io.clj index bf6d8863..2e1ba3d0 100644 --- a/src/babashka/impl/clojure/java/io.clj +++ b/src/babashka/impl/clojure/java/io.clj @@ -1,6 +1,7 @@ (ns babashka.impl.clojure.java.io {:no-doc true} - (:require [clojure.java.io :as io] + (:require [babashka.impl.classpath :as cp] + [clojure.java.io :as io] [sci.core :as sci :refer [copy-var]] [sci.impl.types :as types]) (:import [java.io File ])) @@ -64,4 +65,5 @@ 'make-parents (copy-var io/make-parents io-ns) 'output-stream (copy-var io/output-stream io-ns) 'reader (copy-var io/reader io-ns) - 'writer (copy-var io/writer io-ns)}) + 'writer (copy-var io/writer io-ns) + 'resource (sci/copy-var cp/resource io-ns)}) diff --git a/src/babashka/impl/features.clj b/src/babashka/impl/features.clj index acca92fa..68970dbd 100644 --- a/src/babashka/impl/features.clj +++ b/src/babashka/impl/features.clj @@ -15,6 +15,7 @@ (def hiccup? (not= "false" (System/getenv "BABASHKA_FEATURE_HICCUP"))) (def test-check? (not= "false" (System/getenv "BABASHKA_FEATURE_TEST_CHECK"))) (def rewrite-clj? (not= "false" (System/getenv "BABASHKA_FEATURE_REWRITE_CLJ"))) +(def selmer? (not= "false" (System/getenv "BABASHKA_FEATURE_SELMER"))) ;; excluded by default (def jdbc? (= "true" (System/getenv "BABASHKA_FEATURE_JDBC"))) @@ -70,3 +71,6 @@ (when rewrite-clj? (require '[babashka.impl.rewrite-clj])) + +(when selmer? + (require '[babashka.impl.selmer])) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index b86c2f3f..83f9dfcb 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -165,9 +165,9 @@ When no eval opts or subcommand is provided, the implicit subcommand is repl.") (let [arg (first command-line-args) tasks (:tasks @common/bb-edn)] (if (or (when-let [s (tasks/doc-from-task - ctx - tasks - (get tasks (symbol arg)))] + ctx + tasks + (get tasks (symbol arg)))] [(do (println "-------------------------") (println arg) (println "Task") @@ -207,7 +207,8 @@ When no eval opts or subcommand is provided, the implicit subcommand is repl.") :feature/hiccup %s :feature/test-check %s :feature/spec-alpha %s - :feature/rewrite-clj %s}") + :feature/rewrite-clj %s + :feature/selmer %s}") version features/core-async? features/csv? @@ -225,7 +226,8 @@ When no eval opts or subcommand is provided, the implicit subcommand is repl.") features/hiccup? features/test-check? features/spec-alpha? - features/rewrite-clj?))) + features/rewrite-clj? + features/selmer?))) (defn read-file [file] (let [f (io/file file)] @@ -365,7 +367,13 @@ When no eval opts or subcommand is provided, the implicit subcommand is repl.") 'rewrite-clj.zip @(resolve 'babashka.impl.rewrite-clj/zip-namespace) 'rewrite-clj.zip.subedit - @(resolve 'babashka.impl.rewrite-clj/subedit-namespace)))) + @(resolve 'babashka.impl.rewrite-clj/subedit-namespace)) + features/selmer? (assoc 'selmer.parser + @(resolve 'babashka.impl.selmer/selmer-parser-namespace) + 'selmer.tags + @(resolve 'babashka.impl.selmer/selmer-tags-namespace) + 'selmer.filters + @(resolve 'babashka.impl.selmer/selmer-filters-namespace)))) (def imports '{ArithmeticException java.lang.ArithmeticException @@ -683,11 +691,6 @@ When no eval opts or subcommand is provided, the implicit subcommand is repl.") (assoc 'clojure.core (assoc core-extras 'load-file load-file*)) - (assoc-in ['clojure.java.io 'resource] - (fn [path] - (when-let [{:keys [:loader]} @cp/cp-state] - (if (str/starts-with? path "/") nil ;; non-relative paths always return nil - (cp/getResource loader [path] {:url? true}))))) (assoc-in ['user (with-meta '*input* (when-not stream? {:sci.impl/deref! true}))] input-var)) diff --git a/test-resources/lib_tests/babashka/run_all_libtests.clj b/test-resources/lib_tests/babashka/run_all_libtests.clj index d720b436..f021a764 100644 --- a/test-resources/lib_tests/babashka/run_all_libtests.clj +++ b/test-resources/lib_tests/babashka/run_all_libtests.clj @@ -198,6 +198,8 @@ (test-namespaces 'helins.binf.test) +(test-namespaces 'selmer.core-test) + ;;;; final exit code (let [{:keys [:test :fail :error] :as m} @status] diff --git a/test-resources/lib_tests/selmer/core_test.clj b/test-resources/lib_tests/selmer/core_test.clj new file mode 100644 index 00000000..ddf65652 --- /dev/null +++ b/test-resources/lib_tests/selmer/core_test.clj @@ -0,0 +1,1260 @@ +(ns selmer.core-test + (:require #_[selmer.template-parser :refer :all] + #_[selmer.util :refer :all] + [cheshire.core :as cheshire] + [clojure.java.io :as io] + [clojure.string :as str] + [clojure.test :refer [deftest are is testing]] + [selmer.filters :as f] + [selmer.parser :as p :refer [render render-file render-template + parse parse-input known-variables]] + [selmer.tags :as tags]) + (:import (java.io StringReader ByteArrayInputStream) + java.io.File + java.util.Locale)) + +(def path (str "test-resources/lib_tests/templates" File/separator)) + +(defn fix-line-sep [s] (clojure.string/replace s "\n" (System/lineSeparator))) + +(deftest dev-error-handling + (is (= "No filter defined with the name 'woot'" + (try (render "{{blah|safe|woot" {:blah "woot"}) + (catch Exception ex (.getMessage ex)))))) + +(deftest custom-handler-test + (let [handler (tags/tag-handler + (fn [args context-map content] + (get-in content [:foo :content])) + :foo :endfoo)] + (is + (= "some bar content" + (render-template (parse parse-input (java.io.StringReader. "{% foo %}some {{bar}} content{% endfoo %}") + {:custom-tags {:foo handler}}) {:bar "bar"})))) + + (let [handler (tags/tag-handler + (fn [args context-map] (clojure.string/join "," args)) + :bar)] + (is (= "arg1,arg2" + (render-template (parse parse-input (java.io.StringReader. "{% bar arg1 arg2 %}") + {:custom-tags {:bar handler}}) {})))) + + (p/add-tag! :bar (fn [args context-map] (clojure.string/join "," args))) + (render-template (parse parse-input (java.io.StringReader. "{% bar arg1 arg2 %}")) {})) + +(deftest remove-tag + (p/add-tag! :temp (fn [args _] (str "TEMP_" (clojure.string/join "_" (map (comp clojure.string/upper-case str) args))))) + (is (= "TEMP_ARG1_ARG2" (render "{% temp arg1 arg2 %}" {}))) + (p/remove-tag! :temp) + (is (thrown? Exception (render "{% temp arg1 arg2 %}" {})))) + +(deftest custom-filter-test + (is (= "BAR" + (p/render-template (p/parse p/parse-input (java.io.StringReader. "{{bar|embiginate}}") + {:custom-filters + {:embiginate (fn [^String s] (.toUpperCase s))}}) {:bar "bar"})))) + +(deftest boolean-filter-test + (is (= "0" + (p/render-template (p/parse p/parse-input (java.io.StringReader. "{{bar|bit}}") + {:custom-filters + {:bit (fn [^Boolean b] (if (true? b) 1 0))}}) {:bar false})))) + +(deftest passthrough + (let [s "a b c d"] + (is (= s (render s {})))) + (let [s "{{blah}} a b c d"] + (is (= " a b c d" (render s {})))) + (let [s "{{blah}} a b c d"] + (is (= "blah a b c d" (render s {:blah "blah"})))) + ;; Invalid tags are now ignored ;) + (let [s "{a b c} \nd"] + (is (= s (render s {}))))) + +#_(deftest inheritance + (binding + [*tag-second-pattern* (pattern *tag-second*) + *filter-open-pattern* (pattern "\\" *tag-open* "\\" *filter-open* "\\s*") + *filter-close-pattern* (pattern "\\s*\\" *filter-close* "\\" *tag-close*) + *filter-pattern* (pattern "\\" *tag-open* "\\" *filter-open* "\\s*.*\\s*\\" *filter-close* "\\" *tag-close*) + *tag-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*.*\\s*\\" *tag-second* "\\" *tag-close*) + *tag-open-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*") + *tag-close-pattern* (pattern "\\s*\\" *tag-second* "\\" *tag-close*) + *include-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*include.*") + *extends-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*extends.*") + *block-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*block.*") + *block-super-pattern* (pattern "\\" *tag-open* "\\" *filter-open* "\\s*block.super\\s*\\" *filter-close* "\\" *tag-close*) + *endblock-pattern* (pattern "\\" *tag-open* "\\" *tag-second* "\\s*endblock.*")] + (is + (= (fix-line-sep "\n
{% block header %}\nB header\n\nfooter
\n{% endblock %}\n") + (preprocess-template "templates/inheritance/child-b.html"))) + (is + (= "{%ifequal greeting|default:\"Hello!\" name|default:\"Jane Doe\"%} {{greeting|default:\"Hello!\"}} {{name|default:\"Jane Doe\"}} {%endifequal%}" + (preprocess-template "templates/inheritance/parent.html"))) + (is + (= (fix-line-sep "\n \n \n {% block hello %}\n\n Hello \n World\n{% endblock %}\n \n") + (preprocess-template "templates/inheritance/super-b.html"))) + (is + (= (fix-line-sep "\n \n \n {% block hello %}\n\n\n Hello \n World\nCruel World\n{% endblock %}\n \n") + (preprocess-template "templates/inheritance/super-c.html"))) + (is + (= (fix-line-sep "start a\n{% block a %}{% endblock %}\nstop a\n\n{% block content %}{% endblock %}\n\nHello, {{name}}!\n") + (preprocess-template "templates/inheritance/inherit-a.html"))) + (is + (= (fix-line-sep "start a\n{% block a %}\nstart b\n{% block b %}{% endblock %}\nstop b\n{% endblock %}\nstop a\n\n{% block content %}content{% endblock %}\n\nHello, {{name}}!\n") + (preprocess-template "templates/inheritance/inherit-b.html"))) + (is + (= (fix-line-sep "start a\n{% block a %}\nstart b\n{% block b %}\nstart c\nstop c\n{% endblock %}\nstop b\n{% endblock %}\nstop a\n\n{% block content %}content{% endblock %}\n\nHello, {{name}}!\n") + (preprocess-template "templates/inheritance/inherit-c.html"))) + (is + (= (fix-line-sep "{% block my-script %}{% endblock %}\n\nmy-body\n") + (preprocess-template "templates/inheritance/child-c.html"))) + (is + (= (fix-line-sep "{% block my-script %}{% endblock %}\n\nmy-body\n") + (preprocess-template "templates/inheritance/child-d.html"))) + (is + (= (fix-line-sep "\n\nmy-body\n") + (render-file "templates/inheritance/child-c.html" {}))) + (is + (= (fix-line-sep "\n\nmy-body\n") + (render-file "templates/inheritance/child-d.html" {}))) + (is + (= (fix-line-sep "blah
\n\n\n") + (render-file "templates/child.html" {:content "blah"}))) + (is + (= "hello" + (render "{% if any foo bar baz %}hello{% endif %}" {:bar "foo"}))) + (is + (= "hello" + (render "{% if not any foo bar baz %}hello{% endif %}" {}))) + (is + (= "hello" + (render "{% if all foo bar %}hello{% endif %}" {:foo "foo" :bar "bar"}))) + (is + (= "" + (render "{% if all foo bar %}hello{% endif %}" {:foo "foo"}))) + (is + (= "hello" + (render "{% if not all foo bar baz %}hello{% endif %}" {:foo "foo"}))) + (is + (= "" + (render "{% if not all foo bar %}hello{% endif %}" {:foo "foo" :bar "bar"}))) + (is + (= "/page?name=foo - abc" (render-file "templates/include.html" {}))) + (is + (= "/page?name=foo - xyz" (render-file "templates/include.html" {:gridid "xyz"}))))) + +(deftest include-in-path-name + (is + (= "main template foo body" (p/render-file "templates/my-include.html" {:foo "foo"})))) + +(deftest include-with-form + (testing "bindings made using `include` special form `with` should use default values if none are provided." + (is + (= "foo baz default-value another-default-value" (p/render-file "templates/inheritance/include/another-parent.html" {}))) + (is + (= "foo baz some-value another-default-value" (p/render-file "templates/inheritance/include/another-parent.html" {:my-variable "some-value"}))) + (is + (= "foo baz some-value some-other-value" (p/render-file "templates/inheritance/include/another-parent.html" {:my-variable "some-value" + :my-other-variable "some-other-value"}))))) + +(deftest nested-includes + (testing "bindings made using built-in tag `with` should propagate down nested includes" + (is + (= "foo bar baz some-value some-other-value" (p/render-file "templates/inheritance/include/grandparent.html" {})))) + (testing "bindings made using `include` special default `with` should propagate down nested includes" + (is + (= "foo bar baz default-value other-default-value" (p/render-file "templates/inheritance/include/another-grandparent.html" {}))) + (is + (= "foo bar baz some-value other-default-value" (p/render-file "templates/inheritance/include/another-grandparent.html" {:my-variable "some-value"}))) + (is + (= "foo bar baz some-value some-other-value" (p/render-file "templates/inheritance/include/another-grandparent.html" {:my-variable "some-value" + :my-other-variable "some-other-value"}))))) + + +(deftest render-file-accepts-resource-URL + (is + (= "main template foo body" (p/render-file (io/resource "templates/my-include.html") {:foo "foo"})))) + +(deftest render-file-accepts-custom-resource-path-without-protocol + (is + (= "barfoo" + (p/render-file "my-include-child.html" + {:foo "bar" :bar "foo"} + {:custom-resource-path (-> (io/resource "templates/") + io/as-file + .getAbsoluteFile + .toURI + .toURL)})))) + +#_(deftest render-file-accepts-url-stream-handler + (is + (= + "main template zip body" + (render-file "templates/my-include.html" + {:zip "zip"} + {:custom-resource-path "https://example.com/" + :url-stream-handler + (proxy [java.net.URLStreamHandler] [] + (openConnection [url] + (proxy [java.net.URLConnection] [url] + (getInputStream [] + (case (str url) + "https://example.com/templates/my-include.html" + (ByteArrayInputStream. + (.getBytes "main template {% include \"templates/my-include-child.html\" %} body")) + "https://example.com/templates/my-include-child.html" + (ByteArrayInputStream. (.getBytes "{{ zip }}")))))))})))) + +(deftest custom-tags + (is + (= "<<1>><<2>><<3>>" + (render "[% for ele in foo %]<<[{ele}]>>[%endfor%]" + {:foo [1 2 3]} + {:tag-open \[ + :tag-close \]}))) + (is + (= (fix-line-sep "Base template.\n\n\t\n\n\n\n") + (p/render-file "templates/child-custom.html" + {} + {:tag-open \[ + :tag-close \] + :filter-open \( + :filter-close \) + :tag-second \# + :short-comment-second \%})))) + +(deftest no-tag + (is (= "{" (render-file "templates/no_tag.html" {})))) + +(deftest tags-validation + (is + (= "5" (render-file "templates/tags-test.html" {:business {:employees (range 5)}})))) + +#_(deftest test-now + (let [date-format "dd MM yyyy" + formatted-date (.format (java.text.SimpleDateFormat. date-format) (java.util.Date.))] + (is (= (str "\"" formatted-date "\"") (render (str "{% now \"" date-format "\"%}") {}))))) + +(deftest test-comment + (is + (= "foo bar blah" + (render "foo bar {% comment %} baz test {{x}} {% endcomment %} blah" {}))) + (is + (= "foo bar blah" + (render "foo bar {% comment %} baz{% if x %}nonono{%endif%} test {{x}} {% endcomment %} blah" {}))) + (is + (= "foo if blah" + (render "foo {% if x %}if{# nonono #}{%endif%} blah" {:x true}))) + (is + (= "foo bar blah" + (render "foo bar {# baz test {{x}} #} blah" {})))) + + +(deftest test-firstof + (is (= "x" (render "{% firstof var1 var2 var3 %}" {:var2 "x" :var3 "not me"})))) + + +(deftest test-verbatim + (is (= "{{if dying}}Still alive.{{/if}}" + (render "{% verbatim %}{{if dying}}Still alive.{{/if}}{% endverbatim %}" {}))) + (is (= (fix-line-sep "\n{%=file.name%}
\n\n") + (render-file "templates/verbatim.html" {})))) + +(deftest test-with + (is + (= "5 employees" + (render "{% with total=business.employees|count %}{{ total }} employee{{ business.employees|pluralize }}{% endwith %}" + {:business {:employees (range 5)}}))) + (is + (= "total:5 employees" + (render "{% with label=label total = business.employees|count %}{{label}}{{ total }} employee{{ business.employees|pluralize }}{% endwith %}" + {:label "total:" :business {:employees (range 5)}}))) + (is + (= "foocorp" + (render "{% with name=business.name %}{{name}}{% endwith %}" + {:business {:name "foocorp"}}))) + (is + (= "1+1=2" + (render "{% with math=\"1+1=2\" %}{{ math }}{% endwith %}" {}))) + (is + (= "1+1=2" + (render "{% with math.math=\"1+1=2\" %}{{ math.math }}{% endwith %}" {})))) + +(deftest test-for + (is + (= " s a " + (render "{%for x in foo.0%} {{x.id}} {%endfor%}" {:foo [[{:id "s"} {:id "a"}]]}))) + (is + (= "not equal
\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:foo "bar"}))) + (is (= (fix-line-sep "\n\n\nnot equal
\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:foo "baz" :bar "baz"}))) + (is (= (fix-line-sep "\n\n\nnot equal
\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:baz "fail"}))) + + (is (= (render "{% ifequal foo|upper \"FOO\" %}yez{% endifequal %}" {:foo "foo"}) + "yez")) + + (is (= (render "{% ifequal foo \"foo\" %}yez{% endifequal %}" {:foo "foo"}) + "yez")) + (is (= (render "{% ifequal foo \"foo\" bar %}yez{% endifequal %}" + {:foo "foo" + :bar "foo"}) + "yez")) + (is (= (render "{% ifequal foo \"foo\" bar %}yez{% endifequal %}" + {:foo "foo" + :bar "bar"}) + "")) + (is (= (render "{% ifequal foo \"foo\" %}foo{% else %}no foo{% endifequal %}" + {:foo "foo"}) + "foo")) + (is (= (render "{% ifequal foo \"foo\" %}foo{% else %}no foo{% endifequal %}" + {:foo false}) + "no foo")) + (is (= (render "{% ifequal foo :foo %}foo{% endifequal %}" + {:foo :foo}) + "foo"))) + +(deftest ifunequal-tag-test + (is (= (render "{% ifunequal foo \"bar\" %}yez{% endifunequal %}" {:foo "foo"}) + "yez")) + (is (= (render "{% ifunequal foo \"foo\" %}yez{% endifunequal %}" {:foo "foo"}) + "")) + (is (= (render "{% ifunequal foo|upper \"foo\" %}yez{% endifunequal %}" {:foo "foo"}) + "yez")) + (is (= (render "{% ifunequal foo :bar %}foo{% endifunequal %}" + {:foo :foo}) + "foo"))) + +(deftest safe-tag + (is (= (render "{% safe %} {% if bar %}{% for i in y %} {{foo|upper}} {% endfor %}{%endif%} {% endsafe %}" + {:bar true :foo "" + (is (= "
bar
baz
tags"
+ (is (= " [(content)] {{content}} not equal footer {%=file.name%}
bar
baz"
+ (render "{{foo|linebreaks-br|safe}}" {:foo "\nbar\nbaz"})))))
+
+(deftest linenumbers-test
+ (testing "displays text with line numbers"
+ (is (= "1. foo\n2. bar\n3. baz"
+ (render "{{foo|linenumbers}}" {:foo "foo\nbar\nbaz"})))))
+
+(deftest lower-test
+ (testing "converts words to lower case"
+ (is (= "foobar" (render "{{foo|lower}}" {:foo "FOOBaR"})))
+ (is (= "foobar" (render "{{foo|lower}}" {:foo "foobar"})))))
+
+(deftest literals-test
+ (testing "converts words to lower case"
+ (is (= "foobar" (render "{{\"FOObar\"|lower}}" {})))))
+
+#_(deftest number-format-test
+ (testing "formats the number with default locale"
+ (let [locale-number (String/format (Locale/getDefault) "%.3f"
+ (into-array Object [123.045]))]
+ (is (= locale-number (render "{{amount|number-format:%.3f}}" {:amount 123.04455})))
+ (is (= locale-number (render "{{amount|number-format:%.3f}}" {:amount 123.045})))))
+ (testing "formats the number with specified locale"
+ (is (= "123,045" (render "{{amount|number-format:%.3f:de}}" {:amount 123.04455})))))
+
+(deftest default-if-empty-test
+ (testing "default when empty behavior"
+ (is (= "yogthos" (render "{{name|default-if-empty:\"I <3 ponies\"}}" {:name "yogthos"})))
+ (is (= "I <3 ponies" (render "{{name|default-if-empty:\"I <3 ponies\"}}" {:name nil})))
+ (is (= "I <3 ponies" (render "{{name|default-if-empty:\"I <3 ponies\"}}" {:name []})))
+ (is (= "I <3 ponies" (render "{{name|default-if-empty:\"I <3 ponies\"}}" {})))))
+
+;; (deftest turn-off-escaping-test
+;; (testing "with escaping turned off"
+;; (try
+;; (turn-off-escaping!)
+;; (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"})))
+;; (is (= "I <3 ponies" (render "{{name|default-if-empty:\"I <3 ponies\"}}" {})))
+;; (is (= "I <3 ponies" (render "{{name|default-if-empty:\"I <3 ponies\"|safe}}" {})))
+;; (finally (turn-on-escaping!)))))
+
+;; (deftest without-escaping-test
+;; (testing "without-escaping macro"
+;; (without-escaping
+;; (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"}))))
+;; ;; ensure escaping is on after the macro.
+;; (is (= " {% endfor %}")))
+ (is (= #{:items} (known-variables "{% for x,y in items %}{{x}},{{y}}{% endfor %}"))))
+
+ (testing "sum"
+ (is (= #{:foo :bar :baz} (known-variables "{% sum foo bar baz %}"))))
+
+ (testing "now"
+ (is (= #{} (known-variables "{% now \"dd MM yyyy\" %}"))))
+
+ (testing "firstof"
+ (is (= #{:var1 :var2 :var3} (known-variables "{% firstof var1 var2 var3 %}"))))
+
+ (testing "verbatim"
+ (is (= #{} (known-variables "{% verbatim %}{{if dying}}Still alive.{{/if}}{% endverbatim %}"))))
+
+
+ (testing "nesting"
+ (is (= #{:x :y :z} (known-variables "{% if x <= y %}{% if z = 2 %}yes!{% else %}not!{% endif %}{% endif %}")))
+ (is (= #{:items :foo} (known-variables "{% for item,idx in items|sort %}
+ {{item.name}} {{item.age}}
+ {% ifequal item.middeName foo %}
+ BOOM
+ {% endifequal %}
+ {% endfor %}")))))
+
+#_(deftest debug-test
+ (is (str/includes? (render "{% debug %}" {:debug-value 1})
+ "debug-value"))
+ (testing "basic rendering escapes HTML"
+ (is (str/includes? (basic-edn->html {:a "{{item.name}}
+ {{item.age}} "}) """))))
+
diff --git a/test-resources/lib_tests/templates/any.html b/test-resources/lib_tests/templates/any.html
new file mode 100644
index 00000000..604b7233
--- /dev/null
+++ b/test-resources/lib_tests/templates/any.html
@@ -0,0 +1,3 @@
+{% for p in products %}
+ {% if any p.a p.b p.c p.d p.e p.f p.g %}{% endif %}
+{% endfor %}
diff --git a/test-resources/lib_tests/templates/base-custom.html b/test-resources/lib_tests/templates/base-custom.html
new file mode 100644
index 00000000..36283a06
--- /dev/null
+++ b/test-resources/lib_tests/templates/base-custom.html
@@ -0,0 +1,4 @@
+Base template.
+[# block body #]
+ [# block content #][# endblock #]
+[# endblock #]
diff --git a/test-resources/lib_tests/templates/base.html b/test-resources/lib_tests/templates/base.html
new file mode 100644
index 00000000..82e452d4
--- /dev/null
+++ b/test-resources/lib_tests/templates/base.html
@@ -0,0 +1,4 @@
+Base template.
+{% block body %}
+ {% block content %}{% endblock %}
+{% endblock %}
diff --git a/test-resources/lib_tests/templates/child-custom.html b/test-resources/lib_tests/templates/child-custom.html
new file mode 100644
index 00000000..19af95d5
--- /dev/null
+++ b/test-resources/lib_tests/templates/child-custom.html
@@ -0,0 +1,5 @@
+[# extends "templates/base-custom.html" #]
+
+[# block content #]
+
+{% endblock %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/super-a.html b/test-resources/lib_tests/templates/inheritance/super-a.html
new file mode 100644
index 00000000..00faa164
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/super-a.html
@@ -0,0 +1,8 @@
+
+
+
+ {% block hello %}
+ Hello
+ {% endblock %}
+
+
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/super-b.html b/test-resources/lib_tests/templates/inheritance/super-b.html
new file mode 100644
index 00000000..863a704b
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/super-b.html
@@ -0,0 +1,4 @@
+{% extends "templates/inheritance/super-a.html" %}
+{% block hello %}
+{{ block.super }} World
+{% endblock %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/super-c.html b/test-resources/lib_tests/templates/inheritance/super-c.html
new file mode 100644
index 00000000..75647828
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/super-c.html
@@ -0,0 +1,4 @@
+{% extends "templates/inheritance/super-b.html" %}
+{% block hello %}
+{{ block.super }}Cruel World
+{% endblock %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/my-include-child.html b/test-resources/lib_tests/templates/my-include-child.html
new file mode 100644
index 00000000..9f5a8235
--- /dev/null
+++ b/test-resources/lib_tests/templates/my-include-child.html
@@ -0,0 +1 @@
+{{ foo }}{{ bar }}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/my-include.html b/test-resources/lib_tests/templates/my-include.html
new file mode 100644
index 00000000..b4a6572c
--- /dev/null
+++ b/test-resources/lib_tests/templates/my-include.html
@@ -0,0 +1 @@
+main template {% include "templates/my-include-child.html" %} body
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/nested-for.html b/test-resources/lib_tests/templates/nested-for.html
new file mode 100644
index 00000000..8197f94c
--- /dev/null
+++ b/test-resources/lib_tests/templates/nested-for.html
@@ -0,0 +1,13 @@
+
+
+FOO!
+{% endif %}
+
+
+{% if bar %}
+BAR!
+{% else %}
+NOT BAR!
+{% endif %}
+
+{% if user-id %}
+"foo"
+{% else %}
+"bar"
+{% endif %}
+
+{% if nested %}
+ {% if inner %}
+ inner
+ {% endif %}
+{% endif %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/ifequal.html b/test-resources/lib_tests/templates/ifequal.html
new file mode 100644
index 00000000..29bd3ff2
--- /dev/null
+++ b/test-resources/lib_tests/templates/ifequal.html
@@ -0,0 +1,13 @@
+{% ifequal foo "bar" %}
+equal!
+{% endifequal %}
+
+{% ifequal foo bar %}
+equal!
+{% endifequal %}
+
+{% ifequal baz "test" %}
+equal!
+{% else %}
+child-a header
+<<{{block.super}}>>
+{% endblock %}
+
+{% block footer %}
+Hello {{user}}
+{% endblock %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include-head.html b/test-resources/lib_tests/templates/inheritance/include-head.html
new file mode 100644
index 00000000..761bdef6
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include-head.html
@@ -0,0 +1,2 @@
+{% include "templates/inheritance/include/head.html" %}
+my-body
diff --git a/test-resources/lib_tests/templates/inheritance/include-in-block.html b/test-resources/lib_tests/templates/inheritance/include-in-block.html
new file mode 100644
index 00000000..95206853
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include-in-block.html
@@ -0,0 +1,5 @@
+{% extends "templates/inheritance/another-parent.html" %}
+
+{% block content %}
+{% include "templates/inheritance/include/snippet.html" %}
+{% endblock %}
diff --git a/test-resources/lib_tests/templates/inheritance/include-snippet.html b/test-resources/lib_tests/templates/inheritance/include-snippet.html
new file mode 100644
index 00000000..f39a1185
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include-snippet.html
@@ -0,0 +1 @@
+base tempate {% include "templates/inheritance/include/snippet.html" %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/another-grandparent.html b/test-resources/lib_tests/templates/inheritance/include/another-grandparent.html
new file mode 100644
index 00000000..8b4254c2
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/another-grandparent.html
@@ -0,0 +1 @@
+foo {% include "templates/inheritance/include/parent.html" with my-variable="default-value" my-other-variable="other-default-value" %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/another-parent.html b/test-resources/lib_tests/templates/inheritance/include/another-parent.html
new file mode 100644
index 00000000..22d5fcbc
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/another-parent.html
@@ -0,0 +1 @@
+foo {% include "templates/inheritance/include/child.html" with my-variable="default-value" my-other-variable="another-default-value"%}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/child.html b/test-resources/lib_tests/templates/inheritance/include/child.html
new file mode 100644
index 00000000..e7a659fb
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/child.html
@@ -0,0 +1 @@
+baz {{ my-variable }} {{ my-other-variable }}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/grandparent.html b/test-resources/lib_tests/templates/inheritance/include/grandparent.html
new file mode 100644
index 00000000..e0df5ffd
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/grandparent.html
@@ -0,0 +1 @@
+foo {% with my-variable="some-value" my-other-variable="some-other-value"%}{% include "templates/inheritance/include/parent.html" %}{% endwith %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/head.html b/test-resources/lib_tests/templates/inheritance/include/head.html
new file mode 100644
index 00000000..ce3db9a5
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/head.html
@@ -0,0 +1 @@
+{% block my-script %}{% endblock %}
diff --git a/test-resources/lib_tests/templates/inheritance/include/parent.html b/test-resources/lib_tests/templates/inheritance/include/parent.html
new file mode 100644
index 00000000..d8bdaba1
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/parent.html
@@ -0,0 +1 @@
+bar {% include "templates/inheritance/include/child.html" %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/include/snippet.html b/test-resources/lib_tests/templates/inheritance/include/snippet.html
new file mode 100644
index 00000000..b6fc4c62
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/include/snippet.html
@@ -0,0 +1 @@
+hello
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/inherit-a.html b/test-resources/lib_tests/templates/inheritance/inherit-a.html
new file mode 100644
index 00000000..c8f224ec
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/inherit-a.html
@@ -0,0 +1,7 @@
+start a
+{% block a %}{% endblock %}
+stop a
+
+{% block content %}{% endblock %}
+
+{% include "templates/inheritance/foo.html" %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/inherit-b.html b/test-resources/lib_tests/templates/inheritance/inherit-b.html
new file mode 100644
index 00000000..c71ac5fa
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/inherit-b.html
@@ -0,0 +1,8 @@
+{% extends "templates/inheritance/inherit-a.html" %}
+{% block a %}
+start b
+{% block b %}{% endblock %}
+stop b
+{% endblock %}
+
+{% block content %}content{% endblock %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/inherit-c.html b/test-resources/lib_tests/templates/inheritance/inherit-c.html
new file mode 100644
index 00000000..b96eb319
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/inherit-c.html
@@ -0,0 +1,6 @@
+{% extends "templates/inheritance/inherit-b.html" %}
+
+{% block b %}
+start c
+stop c
+{% endblock %}
diff --git a/test-resources/lib_tests/templates/inheritance/parent.html b/test-resources/lib_tests/templates/inheritance/parent.html
new file mode 100644
index 00000000..1f137ff9
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/parent.html
@@ -0,0 +1 @@
+{% include "templates/inheritance/child.html" with name = "Jane Doe" greeting="Hello!" %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/inheritance/register.html b/test-resources/lib_tests/templates/inheritance/register.html
new file mode 100644
index 00000000..9afe4583
--- /dev/null
+++ b/test-resources/lib_tests/templates/inheritance/register.html
@@ -0,0 +1,8 @@
+{% block register %}
+
+{% for user in users %}
+
+
+
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/no_tag.html b/test-resources/lib_tests/templates/no_tag.html
new file mode 100644
index 00000000..81750b96
--- /dev/null
+++ b/test-resources/lib_tests/templates/no_tag.html
@@ -0,0 +1 @@
+{
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/numerics.html b/test-resources/lib_tests/templates/numerics.html
new file mode 100644
index 00000000..8f83cad9
--- /dev/null
+++ b/test-resources/lib_tests/templates/numerics.html
@@ -0,0 +1,4 @@
+{% for p in ps %}
+ {% if p.a > p.b %}
+ {% endif %}
+{% endfor %}
diff --git a/test-resources/lib_tests/templates/raw.html b/test-resources/lib_tests/templates/raw.html
new file mode 100644
index 00000000..e69de29b
diff --git a/test-resources/lib_tests/templates/safe.html b/test-resources/lib_tests/templates/safe.html
new file mode 100644
index 00000000..3b802505
--- /dev/null
+++ b/test-resources/lib_tests/templates/safe.html
@@ -0,0 +1 @@
+{% safe %}{% block foo %}{% if bar %}{{unsafe}}{% endif %}{% endblock %}{% endsafe %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/snippet.html b/test-resources/lib_tests/templates/snippet.html
new file mode 100644
index 00000000..37dbcef1
--- /dev/null
+++ b/test-resources/lib_tests/templates/snippet.html
@@ -0,0 +1 @@
+{{url}} - {{gridid}}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/tags-test.html b/test-resources/lib_tests/templates/tags-test.html
new file mode 100644
index 00000000..40f75fd9
--- /dev/null
+++ b/test-resources/lib_tests/templates/tags-test.html
@@ -0,0 +1 @@
+{% with total=business.employees|count %}{{ total }}{% endwith %}
\ No newline at end of file
diff --git a/test-resources/lib_tests/templates/validation-test.html b/test-resources/lib_tests/templates/validation-test.html
new file mode 100644
index 00000000..1b492235
--- /dev/null
+++ b/test-resources/lib_tests/templates/validation-test.html
@@ -0,0 +1,14 @@
+{% extends "app/views/templates/base.html" %}
+{% block body %}
+