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\n

child-a header

\n<<\noriginal header\n>>\n\n{% endblock %}\n\n
{% block content %}\nSome content\n{% endblock %}
\n\n{% block footer %}\n

footer

\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 %}" + (render "{% script \"/js/site.js\" %}" {}))) + (is + (= "" + (render "{% script \"/js/site.js\" %}" {:selmer/context "/myapp"}))) + (is + (= "" + (render "{% script path %}" {:selmer/context "/myapp" :path "/js/site.js"}))) + (is + (= "" + (render "{% script path|upper %}" {:selmer/context "/myapp" :path "/js/site.js"}))) + (is + (= "" + (render "{% style \"/css/screen.css\" %}" {:selmer/context "/myapp"}))) + (is + (= "" + (render "{% style path %}" {:selmer/context "/myapp" :path "/css/screen.css"}))) + (is + (= "" + (render "{% style path|upper %}" {:selmer/context "/myapp" :path "/css/screen.css"})))) + +(deftest script-async + (is + (= "" + (render "{% script \"/js/site.js\" async=\"true\" %}" {}))) + (is + (= "" + (render "{% script \"/js/site.js\" async=1 %}" {}))) + (is + (= "" + (render "{% with var = 1 %}{% script \"/js/site.js\" async=var %}{% endwith %}" {}))) + (is + (= "" + (render "{% script \"/js/site.js\" async=nil %}" {})))) + +(deftest cycle-test + (is + (= "\"foo\"1\"bar\"2\"baz\"1\"foo\"2\"bar\"1" + (render "{% for i in range %}{% cycle \"foo\" \"bar\" \"baz\" %}{% cycle 1 2 %}{% endfor %}" + {:range (range 5)})))) + +(deftest render-test + (is (= "" + (render-template (parse parse-input (java.io.StringReader. "")) + {:items (range 5)})))) + +(deftest nested-forloop-first + (is (= (render (str "{% for x in list1 %}" + "{% for y in list2 %}" + "{{x}}-{{y}}" + "{% if forloop.first %}'{% endif %} " + "{% endfor %}{% endfor %}") + {:list1 '[a b c] + :list2 '[1 2 3]}) + "a-1' a-2 a-3 b-1' b-2 b-3 c-1' c-2 c-3 "))) + +(deftest forloop-with-one-element + (is (= (render (str "{% for x in list %}" + "-{{x}}" + "{% endfor %}") + {:list '[a]}) + "-a"))) + +(deftest forloop-with-no-elements + (is (= (render (str "before{% for x in list %}" + "-{{x}}" + "{% endfor %}after") + {:list '[]}) + "beforeafter"))) + +(deftest tag-sum-test + (is + (= "3" + (render "{% sum foo %}" {:foo 3})) "sum of Foo solely should be 3") + (is + (= "5" + (render "{% sum foo bar %}" {:foo 2 :bar 3})) "sum of Foo and bar should be 5") + (is + (= "6" + (render "{% sum foo foo %}" {:foo 3})) "sum of Foo twice should be 6") + (is + (= "6" + (render "{% sum foo bar baz %}" {:foo 3 :bar 2 :baz 1}))) + (is + (= "6" + (render "{% sum foo bar.baz %}" {:foo 3 :bar {:baz 3}})))) + + +;; (deftest tag-info-test +;; (is +;; (= {:args ["i" "in" "nums"], :tag-name :for, :tag-type :expr} +;; (read-tag-info (java.io.StringReader. "% for i in nums %}")))) +;; (is +;; (= {:tag-value "nums", :tag-type :filter} +;; (read-tag-info (java.io.StringReader. "{ nums }}"))))) + +(deftest if-tag-test + (is + (= (fix-line-sep "\n\n\n\n

NOT BAR!

\n\n\n\n\"bar\"\n\n\n\n\t\n\tinner\n\t\n") + (render-template (parse parse-input (str path "if.html")) {:nested "x" :inner "y"}))) + (is + (= (fix-line-sep "\n\n\n\n

NOT BAR!

\n\n\n\n\"foo\"\n\n\n") + (render-template (parse parse-input (str path "if.html")) {:user-id "bob"}))) + (is + (= (fix-line-sep "\n\n\n\n

NOT BAR!

\n\n\n\n\"bar\"\n\n\n") + (render-template (parse parse-input (str path "if.html")) {:foo false}))) + (is + (= (fix-line-sep "\n

FOO!

\n\n\n\n\n

NOT BAR!

\n\n\n\n\"bar\"\n\n\n") + (render-template (parse parse-input (str path "if.html")) {:foo true}))) + (is + (= (fix-line-sep "\n

FOO!

\n\n\n\n\n

BAR!

\n\n\n\n\"bar\"\n\n\n") + (render-template (parse parse-input (str path "if.html")) {:foo true :bar "test"}))) + (is + (= "" + (render "{% if x > 2 %}bigger{% endif %}" {:v 3}))) + (is + (= "ok" + (render "{% if x = 2.0 %}ok{% endif %}" {:x 2}))) + (is + (= "doublenil" + (render "{% if x = y %}doublenil{% endif %}" {}))) + (is + (= "ok" + (render "{% if x|length = 5 %}ok{% endif %}" {:x (range 5)}))) + (is + (= "bigger" + (render "{% if v > 2 %}bigger{% endif %}" {:v 3}))) + (is + (= "" + (render "{% if v > 2 %}bigger{% endif %}" {:v 0}))) + (is + (= "not bigger" + (render "{% if not v > 2 %}not bigger{% endif %}" {:v 0}))) + (is + (= "smaller" + (render "{% if not v > 2 %}bigger{% else %}smaller{% endif %}" {:v 5}))) + (is + (= "equal" + (render "{% if 5 = v %}equal{% endif %}" {:v 5}))) + (is + (= "" + (render "{% if not 5 = v %}equal{% endif %}" {:v 5}))) + (is + (= "greater equal" + (render "{% if 5 <= v %}greater equal{% endif %}" {:v 5}))) + (is + (= "less equal" + (render "{% if 5 >= v %}less equal{% endif %}" {:v 5}))) + (is + (= "less equal" + (render "{% if v1 >= v2 %}less equal{% endif %}" {:v1 5 :v2 3}))) + (is + (= " no value " + (render "{% if user-id %} has value {% else %} no value {% endif %}" {}))) + (is (= (render "{% if foo %}foo is true{% endif %}" {:foo true}) + "foo is true")) + (is (= (render "{% if foo %}foo is true{% endif %}" {:foo false}) + "")) + (is (= (render "{% if foo %}foo is true{% else %}foo is false{% endif %}" + {:foo true}) + "foo is true")) + (is (= (render "{% if foo %}foo is true{% else %}foo is false{% endif %}" + {:foo false}) + "foo is false")) + (is (= (render "{% if fruit = \"banana\"%}for monkey{% else %}not banana{% endif %}" + {:fruit "banana"}) + "for monkey")) + + (let [template + (parse parse-input + (java.io.StringReader. + "{% if foo %} + foo is true + {% if bar %}bar is also true{% endif %} + {% else %} foo is false + {% if baz %}but baz is true {% else %}baz is also false{% endif %} + {% endif %}"))] + (is (= (render-template template {:foo true :bar true :baz false}) + "\n foo is true\n bar is also true\n ")) + (is (= (render-template template {:foo false :bar true :baz false}) + " foo is false\n baz is also false\n ")) + (is (= (render-template template {:foo false :bar true :baz true}) + " foo is false\n but baz is true \n "))) + (is (thrown? Exception (render "foo {% else %} bar" {})))) + +(deftest elif + (is (= "bar!" + (str/trim (render "{% if foo %} foo! + {% elif bar %} bar! + {% elif baz %} baz! + {% else %} else! + {% endif %}" + {:foo false + :bar true + :baz true})))) + (is (= "baz!" + (str/trim (render "{% if foo %} foo! + {% elif bar %} bar! + {% elif baz %} baz! + {% else %} else! + {% endif %}" + {:foo false + :bar false + :baz true})))) + (is (= "else!" + (str/trim (render "{% if foo %} foo! + {% elif bar %} bar! + {% elif baz %} baz! + {% else %} else! + {% endif %}" + {:foo false + :bar false + :baz false})))) + (is (= "bar!" + (str/trim (render-file "templates/elif.html" {:bar true})))) + (is (= "" + (str/trim (render "{% if foo %} foo! + {% elif bar %} bar! + {% elif baz %} baz! + {% endif %}" + {:foo false + :bar false + :baz false})))) + (is (= "bar!" + (str/trim (render "{% if foo > 3 %} foo! + {% elif not bar = 3 %} bar! + {% elif baz %} baz! + {% endif %}" + {:foo 2 + :bar 4 + :baz false})))) + (is (= "potato" + (str/trim (render "{% if foo > 3 %} foo! + {% elif any bar baz %} potato + {% endif %}" + {:foo 2 + :bar false + :baz true}))))) + +#_(deftest for-respects-missing-value-formatter + ;; Using bindings instead of set-missing-value-formatter! to avoid cleanup + (binding [*missing-value-formatter* (fn [tag context-map] + (str "missing: " tag))] + (is (= (render "{% for e in things %}{% endfor %}" {}) + "missing: {:tag-name :for, :args [:things]}")) + (is (= (render "{% for e in things.a %}{% endfor %}" {:things {}}) + "missing: {:tag-name :for, :args [:things :a]}")))) + +(deftest test-if-not + (is (= (render "{% if not foo %}foo is true{% endif %}" {:foo true}) + "")) + (is (= (render "{% if not foo %}foo is true{% endif %}" {:foo false}) + "foo is true"))) + +(deftest test-nested-if + (is (= (render (str "{% if foo %}before bar {% if bar %}" + "foo & bar are true" + "{% endif %} after bar{% endif %}") + {:foo true + :bar true}) + "before bar foo & bar are true after bar"))) + +(deftest ifequal-tag-test + (is (= (fix-line-sep "\n

equal!

\n\n\n\n\n\n

not equal

\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:foo "bar"}))) + (is (= (fix-line-sep "\n\n\n

equal!

\n\n\n\n

not equal

\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:foo "baz" :bar "baz"}))) + (is (= (fix-line-sep "\n\n\n

equal!

\n\n\n\n

equal!

\n\n") + (render-template (parse parse-input (str path "ifequal.html")) {:baz "test"}))) + (is (= (fix-line-sep "\n\n\n

equal!

\n\n\n\n

not 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 "" :y [1 2]}) + " ")) + (is (= (render-file "templates/safe.html" {:bar true :unsafe ""}) + ""))) + +#_(deftest safe-tag-rendering + ;; .render-node should return an integer as add is defined as a safe filter + (is (= 42 (-> (parse parse-input + (StringReader. "{{seed|safe}}")) + ^selmer.node.INode first + (.render-node {:seed 42}))))) + +#_(deftest filter-tag-test + (is + (= "ok" + ((filter-tag {:tag-value "foo.bar.baz"}) {:foo {:bar {:baz "ok"}}}))) + (is + (= "ok" + ((filter-tag {:tag-value "foo"}) {:foo "ok"})))) + +#_(deftest tag-content-test + (is + (= {:if {:args nil :content ["foo bar "]} + :else {:args nil :content [" baz"]}} + (into {} + (map + (fn [[k v]] + [k (update-in v [:content] #(map (fn [node] (.render-node ^selmer.node.INode node {})) %))]) + (tag-content (java.io.StringReader. "foo bar {%else%} baz{% endif %}") :if :else :endif))))) + (is + (= {:for {:args nil, :content ["foo bar baz"]}} + (update-in (tag-content (java.io.StringReader. "foo bar baz{% endfor %}") :for :endfor) + [:for :content 0] #(.render-node ^selmer.node.INode % {}))))) + +(deftest filter-upper + (is (= "FOO" (render "{{f|upper}}" {:f "foo"})))) + +(deftest filter-email + (is (= "foo@bar.baz" + (render "{{e|email}}" {:e "foo@bar.baz"}))) + (is (= "foo@bar" + (render "{{e|email:false}}" {:e "foo@bar"}))) + (is (thrown? Exception (render "{{e|email}}" {:e "foo@bar"})))) + +(deftest filter-01234 + (is (= "01234 567890" + (render "{{p|phone}}" {:p "01234 567890"}))) + (is (= "01234 567890" + (render "{{p|phone:44}}" {:p "01234 567890"}))) + (is (= "01234 567890" + (render "{{p|phone:false}}" {:p "01234 567890"}))) + (is (= "01234 567890" + (render "{{p|phone:44:true}}" {:p "01234 567890"}))) + (is (= "01234 567890" + (render "{{p|phone:44:false}}" {:p "01234 567890"}))) + (is (= "01234 567890" + (render "{{p|phone}}" {:p "01234 567890"}))) + (is (= "abc 01234 56789" + (render "{{p|phone:false}}" {:p "abc 01234 56789"}))) + (is (thrown? Exception (render "{{p|phone}}" {:p "abc 01234 56789"}))) + ;; if an international dialing prefix is supplied which doesn't appear + ;; to be valid (and we're validating), we ought to get an exception. + (is (thrown? Exception (render "{{p|phone:true:abc}}" {:p "01234 56789"})))) + +(deftest filter-subs + (is (= "FOO ..." (render "{{f|subs:0:3:\" ...\"}}" {:f "FOO BAR"})))) + +(deftest filter-abbreviate + (are [expected input] (= expected (render input {:f "this is a text to test"})) + "this is a text t..." "{{f|abbreviate:19:19}}" + "this is a text to test" "{{f|abbreviate:22:22}}" + "this is a text to test" "{{f|abbreviate:22:12}}" + "this is a..." "{{f|abbreviate:21:12}}" + "this is a text to ..." "{{f|abbreviate:21}}" + "this is a text to ..." "{{f|abbr-right|abbreviate:21}}" + "this is a text to ..." "{{f|abbr-left|abbr-right|abbreviate:21}}" + "... is a text to test" "{{f|abbr-left|abbreviate:21}}" + "... is a text to test" "{{f|abbr-right|abbr-left|abbreviate:21}}" + "this is a text to tes" "{{f|abbr-ellipsis:\"\"|abbreviate:21}}" + "this is a...t to test" "{{f|abbr-middle|abbreviate:21}}" + "this is a//xt to test" "{{f|abbr-ellipsis://|abbr-middle|abbreviate:21}}" + "this is a …xt to test" "{{f|abbr-ellipsis:…|abbr-middle|abbreviate:21}}") + + (are [expected input] (= expected (render input {:f "1234567890*0987654321"})) + "123456 [...] 7654321" "{{f|abbr-middle|abbr-ellipsis:\" [...] \"|abbreviate:20}}" + "1234567 [..] 7654321" "{{f|abbr-middle|abbr-ellipsis:\" [..] \"|abbreviate:20}}" + "1234567 [.] 87654321" "{{f|abbr-middle|abbr-ellipsis:\" [.] \"|abbreviate:20}}" + "12345678900987654321" "{{f|abbr-middle|abbr-ellipsis:\"\"|abbreviate:20}}" + "123456 [...] 654321" "{{f|abbr-middle|abbr-ellipsis:\" [...] \"|abbreviate:19}}" + "123456 [..] 7654321" "{{f|abbr-middle|abbr-ellipsis:\" [..] \"|abbreviate:19}}" + "1234567 [.] 7654321" "{{f|abbr-middle|abbr-ellipsis:\" [.] \"|abbreviate:19}}" + "...67890*098765..." "{{f|abbr-left|abbreviate:19|abbreviate:18}}" + "...567890*09876..." "{{f|abbreviate:19|abbr-left|abbreviate:18}}") + + (is (thrown-with-msg? Exception #"15 .* 14" + (render "{{f|abbr-ellipsis:\"a long ellipsis\"|abbreviate:14}}" {:f "short text"}))) + (is (thrown-with-msg? Exception #"14 .* 15" + (render "{{f|abbreviate:14:15}}" {:f "short text"})))) + + + +(deftest filter-take + (is (= "[:dog :cat :bird]" + (render "{{seq-of-some-sort|take:3}}" {:seq-of-some-sort [:dog :cat :bird :bird :bird :is :the :word]})))) + + +(deftest filter-drop + (is (= "[:bird :is :the :word]" + (render "{{seq-of-some-sort|drop:4}}" {:seq-of-some-sort [:dog :cat :bird :bird :bird :is :the :word]})))) + +(deftest filter-drop-formatted + (is (= "bird is the word" + (render "{{seq-of-some-sort|drop:4|join:\" \"}}" {:seq-of-some-sort ["dog" "cat" "bird" "bird" "bird" "is" "the" "word"]})))) + +;; How do we handle nils ? +;; nils should return empty strings at the point of injection in a DTL library. - cma +(deftest filter-no-value + (is (= "" (render "{{f|upper}}" {})))) + +#_(deftest filter-currency-format + (let [amount 123.45 + curr (java.text.NumberFormat/getCurrencyInstance (Locale/getDefault)) + curr-de (java.text.NumberFormat/getCurrencyInstance (java.util.Locale. "de")) + curr-de-DE (java.text.NumberFormat/getCurrencyInstance (java.util.Locale. "de" "DE"))] + (is (= (.format curr amount) + (render "{{f|currency-format}}" {:f amount}))) + (is (= (.format curr-de amount) (render "{{f|currency-format:de}}" {:f amount}))) + (is (= (.format curr-de-DE amount) (render "{{f|currency-format:de:DE}}" {:f amount}))))) + +(deftest filter-number-format + (let [number 123.04455 + numberformat "%.3f" + locale (Locale/getDefault) + locale-de (java.util.Locale. "de")] + (is (= (String/format locale numberformat (into-array Object [number])) + (render (str "{{f|number-format:" numberformat "}}") {:f number}))) + (is (= (String/format locale-de numberformat (into-array Object [number])) + (render (str "{{f|number-format:" numberformat ":de}}") {:f number}))))) + +#_(deftest filter-date + (let [date (java.util.Date.) + firstofmarch (java.util.Date. 114 2 1)] + (is (= "" (render "{{d|date:\"yyyy-MM-dd\"}}" {:d nil}))) + (is (= (.format (java.text.SimpleDateFormat. "yyyy-MM-dd HH:mm:ss") date) + (render "{{f|date:\"yyyy-MM-dd HH:mm:ss\"}}" {:f date}))) + (is (= (.format (java.text.SimpleDateFormat. "MMMM" (java.util.Locale. "fr")) firstofmarch) + (render "{{f|date:\"MMMM\":fr}}" {:f firstofmarch}))) + (is (= "00:00" (render "{{d|date:shortTime:en_US}}" {:d firstofmarch}))) + (is (= "上午12:00" (render "{{d|date:shortTime:zh}}" {:d firstofmarch}))) + (is (= "2014-03-01" (render "{{d|date:shortDate:en_US}}" {:d firstofmarch}))) + (is (= "2014/3/1" (render "{{d|date:shortDate:zh}}" {:d firstofmarch}))) + (is (= "2014-03-01 00:00" (render "{{d|date:shortDateTime:en_US}}" {:d firstofmarch}))) + (is (= "2014/3/1 上午12:00" (render "{{d|date:shortDateTime:zh}}" {:d firstofmarch}))) + (is (= "2014年3月1日" (render "{{d|date:longDate:zh}}" {:d firstofmarch}))) + (is (= "2014 Mar 1" (render "{{d|date:longDate:en_US}}" {:d firstofmarch}))))) + +(deftest filter-hash-md5 + (is (= "acbd18db4cc2f85cedef654fccc4a4d8" + (render "{{f|hash:\"md5\"}}" {:f "foo"})))) + +(deftest filter-hash-sha512 + (is (= (str "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d" + "0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19" + "594a7eb539453e1ed7") + (render "{{f|hash:\"sha512\"}}" {:f "foo"})))) + +(deftest filter-hash-invalid-hash + (is (thrown? Exception (render "{{f|hash:\"foo\"}}" {:f "foo"})))) + +(deftest filter-join + (is (= "1, 2, 3, 4" + (render "{{sequence|join:\", \"}}" {:sequence [1 2 3 4]}))) + (is (= "1234" + (render "{{sequence|join}}" {:sequence [1 2 3 4]})))) + +(deftest filter-add + (is (= "11" (render "{{add_me|add:2:3:4}}" {:add_me 2}))) + (is (= "hello" (render "{{h|add:e:l:l:o}}" {:h "h"}))) + (is (= "0" (render "{{paginate.page|add:-1}}" {:paginate {:page 1}})))) + +(deftest filter-count + (is (= "3" (render "{{f|count}}" {:f "foo"}))) + (is (= "4" (render "{{f|count}}" {:f [1 2 3 4]}))) + (is (= "0" (render "{{f|count}}" {:f []}))) + (is (= "0" (render "{{f|count}}" {})))) + +(deftest emptiness + (is (= "true" (render "{{xs|empty?" {:xs []}))) + (is (= "foo" (render "{% if xs|empty? %}foo{% endif %}" {:xs []}))) + (is (= "" (render "{% if xs|not-empty %}foo{% endif %}" {:xs []}))) + (is (= "foo" (render "{% if xs|not-empty %}foo{% endif %}" {:xs [1 2]})))) + +;; switched commas + doublequotes for colons +;; TODO - maybe remain consistent with django's only 1 argument allowed. +;; I like being able to accept multiple arguments. +;; Alternatively, we could have curried filters and just chain +;; it into a val and apply it Haskell-style. +;; I think that could surprise users. (which is bad) +(deftest filter-pluralize + (is (= "s" (render "{{f|pluralize}}" {:f []}))) + (is (= "" (render "{{f|pluralize}}" {:f [1]}))) + (is (= "s" (render "{{f|pluralize}}" {:f [1 2 3]}))) + + (is (= "ies" (render "{{f|pluralize:\"ies\"}}" {:f []}))) + (is (= "" (render "{{f|pluralize:\"ies\"}}" {:f [1]}))) + (is (= "ies" (render "{{f|pluralize:\"ies\"}}" {:f [1 2 3]}))) + + (is (= "ies" (render "{{f|pluralize:y:ies}}" {:f []}))) + (is (= "y" (render "{{f|pluralize:y:ies}}" {:f [1]}))) + (is (= "ies" (render "{{f|pluralize:y:ies}}" {:f [1 2 3]}))) + + (is (= "s" (render "{{f|pluralize}}" {:f 0}))) + (is (= "" (render "{{f|pluralize}}" {:f 1}))) + (is (= "s" (render "{{f|pluralize}}" {:f 3}))) + + (is (= "ies" (render "{{f|pluralize:\"ies\"}}" {:f 0}))) + (is (= "" (render "{{f|pluralize:\"ies\"}}" {:f 1}))) + (is (= "ies" (render "{{f|pluralize:\"ies\"}}" {:f 3}))) + + (is (= "ies" (render "{{f|pluralize:y:ies}}" {:f 0}))) + (is (= "y" (render "{{f|pluralize:y:ies}}" {:f 1}))) + (is (= "ies" (render "{{f|pluralize:y:ies}}" {:f 3})))) + +;; to-json is simply json here +(deftest filter-to-json + (is (= "1" (render "{{f|json}}" {:f 1}))) + (is (= "[1]" (render "{{f|json}}" {:f [1]}))) + #_(is (= {"dan" "awesome", "foo" 27} + (-> ^String (render "{{f|json}}" {:f {:foo 27 :dan "awesome"}}) + (.replaceAll """ "\"") + parse-string))) + #_(is (= {"dan" "awesome", "foo" 27} + (parse-string (render "{{f|json|safe}}" {:f {:foo 27 :dan "awesome"}})))) + ;; safe only works at the end + #_(is (= "{\"foo\":27,\"dan\":\"awesome\"}" + (render "{{f|safe|json}}" {:f {:foo 27 :dan "awesome"}}))) + ;; Do we really want to nil-pun the empty map? + ;; Is that going to surprise the user? + (is (= "null" (render "{{f|json}}" {})))) + +;; TODO +(deftest filter-chaining + (is (= "ACBD18DB4CC2F85CEDEF654FCCC4A4D8" + (render "{{f|hash:\"md5\"|upper}}" {:f "foo"})))) + +(deftest filter-add-2 + (testing "Adds numbers" + (is (= "40" + (render "{{seed|add:1:2:3}}" {:seed 34}))) + (is (= "37.5" + (render "{{seed|add:1.1:-2:3.9}}" {:seed 34.5})))) + (testing "Concat strings if not a number" + (is (= "foo123" + (render "{{seed|add:1:2:3}}" {:seed "foo"}))))) + +(deftest filter-round + (is (= "3" + (render "{{foo|round}}" {:foo 3.33333})))) + +(deftest filter-drop-last + (is (= "[:dog :cat :bird :bird]" + (render "{{seq-of-some-sort|drop-last:4}}" {:seq-of-some-sort [:dog :cat :bird :bird :bird :is :the :word]})))) + +(deftest filter-replace + (is (= "Float posuere erat a ante venenatis ..." + (render "{{foo|replace:Integer:Float}}" {:foo "Integer posuere erat a ante venenatis ..."}))) + (is (= "bar bar test bar ..." + (render "{{foo|replace:foo:bar}}" {:foo "foo foo test foo ..."})))) + +(deftest filter-add-3 + (is (= "5.1" + (render "{{foo|add:2.1}}" {:foo 3}))) + (is (= "4.66" + (render "{{foo|add:2:0.33:-1}}" {:foo 3.33}))) + (is (= "5" + (render "{{foo|add:2}}" {:foo 3})))) + +(deftest filter-multiply + (is (= "6" + (render "{{foo|multiply:2}}" {:foo 3}))) + (is (= "9.99" + (render "{{foo|multiply:3}}" {:foo 3.33}))) + (is (= "1.5" + (render "{{foo|multiply:0.5}}" {:foo 3}))) + (is (thrown? Exception (render "{{foo|multiply:0.5}}" {:foo "bar"})))) + +(deftest filter-divide + (is (= "1.5" + (render "{{foo|divide:2}}" {:foo 3}))) + (is (= "1.11" + (render "{{foo|divide:3}}" {:foo 3.33}))) + (is (= "5" + (render "{{foo|divide:2}}" {:foo 10}))) + (is (thrown? Exception (render "{{foo|divide:foo}}" {:foo 1}))) + (is (thrown? Exception (render "{{foo|divide:0}}" {:foo 1})))) + +(deftest filter-between + (is (= "true" + (render "{% if foo|between?:2:4 %}true{% else %}false{% endif %}" {:foo 3}))) + (is (= "true" + (render "{% if foo|between?:4:2 %}true{% else %}false{% endif %}" {:foo 3}))) + (is (= "false" + (render "{% if foo|between?:2:4 %}true{% else %}false{% endif %}" {:foo 4.33}))) + (is (= "false" + (render "{% if foo|between?:4:2 %}true{% else %}false{% endif %}" {:foo 4.33}))) + (is (= "true" + (render "{% if foo|between?:@min:@max %}true{% else %}false{% endif %}" {:foo 20.1, :min 5.99, :max 100.5}))) + (is (= "true" + (render "{% if foo|between?:@min:@max %}true{% else %}false{% endif %}" {:foo 5.99, :min 5.99, :max 100.5}))) + (is (= "true" + (render "{% if foo|between?:@min:@max %}true{% else %}false{% endif %}" {:foo 100.5, :min 5.99, :max 100.5}))) + (is (= "false" + (render "{% if foo|between?:@min:@max %}true{% else %}false{% endif %}" {:foo 5.98, :min 5.99, :max 100.5}))) + (is (thrown? Exception (render "{{foo|between?:2:4}}" {:foo "throw me"})))) + +(deftest test-escaping + (is (= "<foo bar="baz">\\>" + (render "{{f}}" {:f "\\>"}))) + ;; Escapes the same chars as django's escape + (is (= "&"'<>" + (render "{{f}}" {:f "&\"'<>"}))) + ;; Escapes content that is supposed to be URL encoded + (is (= "clojure+url" + (render "{{f|urlescape}}" {:f "clojure url"})))) + +;; Safe only works at the end. +;; Don't think it should work anywhere else :-) - cbp (agreed, - cma) +(deftest test-safe-filter + (is (= "<foo>" + (render "{{f}}" {:f ""}))) + (is (= "" + (render "{{f|safe}}" {:f ""}))) + (is (= "" + (render "{{f|upper|safe}}" {:f ""})))) + +;; test @-syntax for dereferencing context map in filter arguments +(deftest test-deref-filter-arg + (is (= " Sean " ;; note center filter expects String for width! + (render "{{name|center:@width}}" {:name "Sean" :width "6"}))) + (is (= "4" ;; ensure we can substitute a data structure + (render "{{name|default:@v|count}}" {:v [1 2 3 4]}))) + (is (= "@" ;; literal @ is not dereferenced + (render "{{name|default:@}}" {:name nil}))) + (is (= "@foo" ;; literal @foo used when no context map match + (render "{{name|default:@foo}}" {:name nil}))) + (is (= "quux" ;; test nested lookup + (render "{{name|default:@foo.bar.baz}}" {:name nil :foo {:bar {:baz "quux"}}})))) + +;; (deftest custom-resource-path-setting +;; (is (nil? *custom-resource-path*)) +;; (do +;; (set-resource-path! "/some/path") +;; (is (= "file:////some/path/" *custom-resource-path*))) +;; (do (set-resource-path! "/any/other/path/") +;; (is (= "file:////any/other/path/" *custom-resource-path*))) +;; (do (set-resource-path! "file:////any/other/path/") +;; (is (= "file:////any/other/path/" *custom-resource-path*))) +;; (set-resource-path! nil) +;; (is (nil? *custom-resource-path*))) + +(deftest custom-resource-path-setting-url + (p/set-resource-path! (clojure.java.io/resource "templates/inheritance")) + #_(is (string? *custom-resource-path*)) + (is (= (fix-line-sep "Hello, World!\n") (render-file "foo.html" {:name "World"}))) + (p/set-resource-path! nil)) + +(deftest safe-filter + (f/add-filter! :foo (fn [^String x] [:safe (.toUpperCase x)])) + (is + (= "
I'M SAFE
" + (render "{{x|foo}}" {:x "
I'm safe
"}))) + (f/add-filter! :bar #(.toUpperCase ^String %)) + (is + (= "<DIV>I'M NOT SAFE</DIV>" + (render "{{x|bar}}" {:x "
I'm not safe
"})))) + +(deftest remove-filter + (testing "we can add and remove a filter" + (f/add-filter! :temp (fn [x] (str "TEMP_" (str/upper-case x)))) + (is (= "TEMP_FOO_BAR" (render "{{x|temp}}" {:x "foo_bar"}))) + (f/remove-filter! :temp) + (is (thrown? Exception (render "{{x|temp}}" {:x "foo_bar"}))))) + +(deftest linebreaks-test + (testing "single newlines become
, double newlines become

" + (is (= "


bar
baz

" + (render "{{foo|linebreaks|safe}}" {:foo "\nbar\nbaz"}))))) + +(deftest linebreaks-br-test + (testing "works like linebreaks, but no

tags" + (is (= "
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 (= "<foo bar="baz">\\>" +;; (render "{{f}}" {:f "\\>"}))))) + +;; (deftest with-escaping-test +;; (testing "with-escaping macro when turn-off-escaping! has been called" +;; (try +;; (turn-off-escaping!) +;; (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"}))) +;; (with-escaping +;; (is (= "<foo bar="baz">\\>" +;; (render "{{f}}" {:f "\\>"})))) +;; (is (= "I <3 ponies" (render "{{name}}" {:name "I <3 ponies"}))) +;; (finally (turn-on-escaping!))))) + +(deftest name-test + (testing "converts keywords to strings" + (is (= "foobar" (render "{{foo|name}}" {:foo :foobar}))) + (is (= "foobar" (render "{{foo/bar}}" {"foo/bar" "foobar"})))) + (testing "leaves strings as they are" + (is (= "foobar" (render "{{foo|name}}" {:foo "foobar"}))))) + +#_(deftest handler-metadata + (testing "puts tag into FunctionNode handlers" + (is (= {:tag {:tag-type :filter, :tag-value "foo"}} + (as-> (parse-input (java.io.StringReader. "{{foo}}")) $ + (first $) + (.handler ^selmer.node.FunctionNode $) + (meta $)))))) + +(deftest testing-boolean-values + (testing "Boolean value" + (is (= "Hello true" (render "Hello {{name}}" {:name true}))) + (is (= "Hello false" (render "Hello {{name}}" {:name false}))) + (is (= "Hello " (render "Hello {{name}}" {:name nil}))))) + +(deftest missing-values + (testing "Missing value - default behaviour" + (is (= "" (render "{{missing}}" {}))) + (is (= "" (render "{{missing.too}}" {} "")))) + + #_(testing "Missing value - with custom missing value handlers" + ;; Using bindings instead of set-missing-value-formatter! to avoid cleanup + (binding [*missing-value-formatter* (constantly "XXX") + *filter-missing-values* false] + (is (= "XXX" (render "{{missing}}" {}))) + (is (= "XXX" (render "{{missing.too}}" {})))) + (binding [*missing-value-formatter* (fn [tag context-map] + (if (= (:tag-type tag) :filter) + (str "") + (str ""))) + *filter-missing-values* false] + (is (= "Hi " (render "Hi {{name}}" {}))) + (is (= "Hi mr. " (render "Hi mr. {{name.lastname}}" {}))) + + (let [custom-tag-handler (tag-handler + (fn [_ context-map] + (when-let [l (:list context-map)] + (clojure.string/join ", " (:list context-map)))) + :bar)] + (is (= "1, 2, 3, 4" + (render-template + (parse parse-input (java.io.StringReader. "{% bar %}") {:custom-tags {:bar custom-tag-handler}}) + {:list [1 2 3 4]}))) + (is (= "" + (render-template + (parse parse-input (java.io.StringReader. "{% bar %}") {:custom-tags {:bar custom-tag-handler}}) + {})))) + + (is (= "" (render "{{name|count}}" {}))))) + + #_(testing "Missing value - custom missing value handler with filtering of missing values turned on" + (binding [*missing-value-formatter* (constantly "XXX") + *filter-missing-values* true] + (is (= "XXX" (render "{{missing}}" {}))) + (is (= "XXX" (render "{{missing.too}}" {}))) + (is (= "0" (render "{{missing|count}}" {})))))) + +(deftest testing-known-variables + (testing "Basic variables" + (is (= #{:name} (known-variables "{{name}}"))) + (is (= #{:name} (known-variables "{{name|capitalize}}"))) + (is (= #{:person} (known-variables "{{person.name|capitalize}}")))) + + (testing "If statements" + (is (= #{:foo :bar :baz} (known-variables "{% if any foo bar baz %}hello{% endif %}"))) + (is (= #{:foo :bar :baz} (known-variables "{% if not any foo bar baz %}hello{% endif %}"))) + (is (= #{:foo :bar} (known-variables "{% if all foo bar %}hello{% endif %}"))) + (is (= #{:x} (known-variables "{% if 6 >= x %}yes!{% endif %}"))) + (is (= #{:x :y} (known-variables "{% if x <= y %}yes!{% endif %}"))) + (is (= #{:x} (known-variables "{% if x > 5 %}yes!{% else %}no!{% endif %}"))) + (is (= #{:vals} (known-variables "{% if vals|length <= 3 %}yes!{% else %}no!{% endif %}")))) + + (testing "ifequal" + (is (= #{:foo :bar} (known-variables "{% ifequal foo bar %}yes!{% endifequal %}"))) + (is (= #{:foo :bar} (known-variables "{% ifequal foo bar %}yes!{% else %}no!{% endifequal %}"))) + (is (= #{:foo} (known-variables "{% ifequal foo \"this also works\" %}yes!{% endifequal %}")))) + + (testing "ifunequal" + (is (= #{:foo :bar} (known-variables "{% ifunequal foo bar %}yes!{% endifunequal %}")))) + + (testing "for" + (is (= #{:some-list} (known-variables "{% for x in some-list %}element: {{x}} first? {{forloop.first}} last? {{forloop.last}}{% endfor %}"))) + (is (= #{:items} (known-variables "{% for item in items %} {{item.name}}{{item.age}} {% 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 "

"}) """))))
+
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 #]
+

[(content)]

+[# endblock content #] diff --git a/test-resources/lib_tests/templates/child.html b/test-resources/lib_tests/templates/child.html new file mode 100644 index 00000000..5692c77d --- /dev/null +++ b/test-resources/lib_tests/templates/child.html @@ -0,0 +1,5 @@ +{% extends "templates/base.html" %} + +{% block content %} +

{{content}}

+{% endblock content %} diff --git a/test-resources/lib_tests/templates/elif.html b/test-resources/lib_tests/templates/elif.html new file mode 100644 index 00000000..e7bcc4f6 --- /dev/null +++ b/test-resources/lib_tests/templates/elif.html @@ -0,0 +1,5 @@ +{% if foo %} foo! +{% elif bar %} bar! +{% elif baz %} baz! +{% else %} else! +{% endif %} diff --git a/test-resources/lib_tests/templates/if.html b/test-resources/lib_tests/templates/if.html new file mode 100644 index 00000000..363ba3ad --- /dev/null +++ b/test-resources/lib_tests/templates/if.html @@ -0,0 +1,22 @@ +{% if foo %} +

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 %} +

not equal

+{% endifequal %} diff --git a/test-resources/lib_tests/templates/include.html b/test-resources/lib_tests/templates/include.html new file mode 100644 index 00000000..72742e3c --- /dev/null +++ b/test-resources/lib_tests/templates/include.html @@ -0,0 +1 @@ +{% include "templates/snippet.html" with url="/page?name=foo" gridid="abc" %} \ No newline at end of file diff --git a/test-resources/lib_tests/templates/inheritance/another-parent.html b/test-resources/lib_tests/templates/inheritance/another-parent.html new file mode 100644 index 00000000..b0d70574 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/another-parent.html @@ -0,0 +1 @@ +
{% block content %}{% endblock %}
diff --git a/test-resources/lib_tests/templates/inheritance/base.html b/test-resources/lib_tests/templates/inheritance/base.html new file mode 100644 index 00000000..a9a9ba03 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/base.html @@ -0,0 +1,10 @@ + +{% block header %} +original header +{% endblock %} + +
{% block content %}{% endblock %}
+ +{% block footer %} +{% endblock %} + \ No newline at end of file diff --git a/test-resources/lib_tests/templates/inheritance/blocks.html b/test-resources/lib_tests/templates/inheritance/blocks.html new file mode 100644 index 00000000..008a6bb0 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/blocks.html @@ -0,0 +1,2 @@ +{% block foo %}Here's the default text of foo {{foo}}{% endblock %} +{% block bar %}Here's the default text of bar {{bar}}{% endblock %} diff --git a/test-resources/lib_tests/templates/inheritance/child-a.html b/test-resources/lib_tests/templates/inheritance/child-a.html new file mode 100644 index 00000000..8da55e51 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/child-a.html @@ -0,0 +1,9 @@ +{% extends "templates/inheritance/base.html" %} +{% block header %} +

child-a header

+<<{{block.super}}>> +{% endblock %} + +{% block footer %} +

footer

+{% endblock %} \ No newline at end of file diff --git a/test-resources/lib_tests/templates/inheritance/child-b.html b/test-resources/lib_tests/templates/inheritance/child-b.html new file mode 100644 index 00000000..c946f806 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/child-b.html @@ -0,0 +1,9 @@ +{% extends "templates/inheritance/child-a.html" %} +{% block header %} +B header +{{block.super}} +{% endblock %} + +{% block content %} +Some content +{% endblock %} \ No newline at end of file diff --git a/test-resources/lib_tests/templates/inheritance/child-c.html b/test-resources/lib_tests/templates/inheritance/child-c.html new file mode 100644 index 00000000..c976b059 --- /dev/null +++ b/test-resources/lib_tests/templates/inheritance/child-c.html @@ -0,0 +1,2 @@ +{% extends "templates/inheritance/include-head.html" %} +{% block my-script %}