(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 << resolve-var-from-kw env-map resolve-arg]] [selmer.tags :as tags] [clojure.set :as set]) (: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 (= "
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}} "}) """))))
(deftest allow-whitespace-in-filter-test
(is (= "bar" (render "{{ foo | default:bar }}" {:dude 1}))))
;; String interopolation
;; setup namespaces, vars + alias for << tests
(def one "one")
(def y 1)
(require '[selmer.benchmark :as sb])
(deftest string-interpolation-test
(is (= "one plus one is two."
(<< "{{one}} plus {{one}} is two.")))
(let [one 1]
(is (= "1 + 1 = 2"
(<< "{{one}} + {{one}} = 2"))))
(let [one 1
one 11]
(is (= "11 + 11 = 2"
(<< "{{one}} + {{one}} = 2"))))
(is (= "selmer.benchmark/user has 10 items."
(<< "selmer.benchmark/user has {{selmer..benchmark/user|count}} items.")))
(is (= "sb/user has 10 items."
(<< "sb/user has {{sb/user|count}} items.")))
(is (= "" (let [y nil] (<< "{{y}}")))
"<< picks up local values even if they are nil")
(is (= "false" (let [y false] (<< "{{y}}")))
"<< picks up local values even if they are false"))
(deftest resolve-arg-test
(is (= "John"
(resolve-arg "{{variable}}" {:variable "John"}))
"When arg is a variable, returns it substituted by its value.")
(is (= "Hello John!"
(resolve-arg "Hello {{variable}}!" {:variable "John"}))
"When arg contains a variable, return it with the variable substituted by its value.")
(is (= "JOHN"
(resolve-arg "{{variable|upper}}" {:variable "John"}))
"When arg is a filter, returns it where the filter was applied to its value.")
(is (= "Hello JOHN!"
(resolve-arg "Hello {{variable|upper}}!" {:variable "John"}))
"When arg contains a filter, returns it where the filter was applied to its value.")
(is (= "Mr John"
(resolve-arg "{% if variable = \"John\" %}Mr {{variable}}{% endif %}" {:variable "John"}))
"When arg is a tag, returns it where the tag was rendered to its value.")
(is (= "Hello Mr John!"
(resolve-arg "Hello {% if variable = \"John\" %}Mr {{variable}}{% endif %}!" {:variable "John"}))
"When arg contains a tag, returns it where the tag was rendered to its value.")
(is (= "Hello John!"
(resolve-arg "\"Hello John!\"" {}))
"When arg is a double quoted literal string, returns it without double quoting.")
(is (= "Hello John!"
(resolve-arg "Hello John!" {}))
"When arg is a literal string, returns it as is.")
(is (= "29.99"
(resolve-arg "29.99" {}))
"When arg is a literal number, returns it as is."))