Compare commits

..

No commits in common. "jtwig-3.0" and "master" have entirely different histories.

15 changed files with 429 additions and 478 deletions

View file

@ -1,12 +1,10 @@
(defproject clj-jtwig "0.5.1" (defproject clj-jtwig "0.5.1"
:description "Clojure wrapper for Jtwig" :description "Clojure wrapper for JTwig"
:url "https://github.com/gered/clj-jtwig" :url "https://github.com/gered/clj-jtwig"
:license {:name "Apache License, Version 2.0" :license {:name "Apache License, Version 2.0"
:url "http://www.apache.org/licenses/LICENSE-2.0"} :url "http://www.apache.org/licenses/LICENSE-2.0"}
:repositories [["sonatype" {:url "http://oss.sonatype.org/content/repositories/releases" :repositories [["sonatype" {:url "http://oss.sonatype.org/content/repositories/releases"
:snapshots false}]] :snapshots false}]]
:dependencies [[org.clojure/clojure "1.6.0"] :dependencies [[org.clojure/clojure "1.6.0"]
[com.lyncode/jtwig-core "3.0.0-SNAPSHOT"] [com.lyncode/jtwig-core "2.1.7"]
[org.apache.commons/commons-lang3 "3.1"]] [org.apache.commons/commons-lang3 "3.1"]])
:source-paths ["src/clojure"]
:java-source-paths ["src/java"])

View file

@ -1,12 +1,8 @@
(ns clj-jtwig.core (ns clj-jtwig.core
"wrapper functions for working with Jtwig from clojure" "wrapper functions for working with JTwig from clojure"
(:import (com.lyncode.jtwig JtwigTemplate JtwigContext JtwigModelMap) (:import (com.lyncode.jtwig JtwigTemplate JtwigContext JtwigModelMap)
(com.lyncode.jtwig.content.api Renderable) (com.lyncode.jtwig.resource ClasspathJtwigResource)
(com.lyncode.jtwig.configuration JtwigConfiguration) (com.lyncode.jtwig.tree.api Content)
(com.lyncode.jtwig.parser.config TagSymbols)
(com.lyncode.jtwig.render RenderContext)
(com.lyncode.jtwig.render.config RenderConfiguration)
(com.lyncode.jtwig.resource ClasspathJtwigResource StringJtwigResource FileJtwigResource)
(java.io File FileNotFoundException ByteArrayOutputStream) (java.io File FileNotFoundException ByteArrayOutputStream)
(java.net URL)) (java.net URL))
(:require [clojure.walk :refer [stringify-keys]]) (:require [clojure.walk :refer [stringify-keys]])
@ -14,8 +10,6 @@
[clj-jtwig.utils] [clj-jtwig.utils]
[clj-jtwig.options])) [clj-jtwig.options]))
(defonce configuration (JtwigConfiguration.))
(declare flush-template-cache!) (declare flush-template-cache!)
(defn set-options! (defn set-options!
@ -25,45 +19,31 @@
see clj-jtwig.options for the option keys you can specify here." see clj-jtwig.options for the option keys you can specify here."
[& opts] [& opts]
(doseq [[k v] (apply hash-map opts)] (doseq [[k v] (apply hash-map opts)]
(cond (if (= :cache-compiled-templates k)
(= k :cache-compiled-templates)
; always clear the cache when toggling. this will help ensure that any possiblity of weird behaviour from ; always clear the cache when toggling. this will help ensure that any possiblity of weird behaviour from
; leftover stuff being stuck in the cache pre-toggle-on/off won't happen ; leftover stuff being stuck in the cache pre-toggle-on/off won't happen
(flush-template-cache!) (flush-template-cache!))
(= k :strict-mode)
(-> configuration .render (.strictMode v))
(= k :tag-symbols)
(-> configuration
.parse
(.withSymbols (condp = v
:default TagSymbols/DEFAULT
:js TagSymbols/JAVASCRIPT))))
(swap! options assoc k v))) (swap! options assoc k v)))
; cache of compiled templates. key is the file path. value is a map with :last-modified which is the source file's ; cache of compiled templates. key is the file path. value is a map with :last-modified which is the source file's
; last modification timestamp and :template which is a com.lyncode.jtwig.content.api.Renderable object which has been ; last modification timestamp and :template which is a com.lyncode.jtwig.tree.api.Content object which has been
; compiled already and can be rendered by calling it's 'render' method ; compiled already and can be rendered by calling it's 'render' method
(defonce compiled-templates (atom {})) (defonce compiled-templates (atom {}))
(defn- compile-template-string [^String contents] (defn- compile-template-string [^String contents]
(-> contents (->> contents
(StringJtwigResource.) (new JtwigTemplate)
(JtwigTemplate. configuration)
(.compile))) (.compile)))
(defn- compile-template-file [^File file] (defn- compile-template-file [^File file]
(if (inside-jar? file) (if (inside-jar? file)
(-> (.getPath file) (->> (.getPath file)
(get-jar-resource-filename) (get-jar-resource-filename)
(ClasspathJtwigResource.) (new ClasspathJtwigResource)
(JtwigTemplate. configuration) (new JtwigTemplate)
(.compile)) (.compile))
(-> file (->> file
(FileJtwigResource.) (new JtwigTemplate)
(JtwigTemplate. configuration)
(.compile)))) (.compile))))
(defn- newer? [^File file other-timestamp] (defn- newer? [^File file other-timestamp]
@ -139,32 +119,33 @@
(new JtwigContext model-map-obj @functions))) (new JtwigContext model-map-obj @functions)))
(defn- render-compiled-template (defn- render-compiled-template
[^Renderable renderable model-map] [^Content compiled-template model-map]
(let [context (make-context model-map)]
; technically we don't have to use with-open with a ByteArrayOutputStream but if we later
; decide to use another OutputStream implementation, this is already all set up :)
(with-open [stream (new ByteArrayOutputStream)] (with-open [stream (new ByteArrayOutputStream)]
(let [context (make-context model-map) (.render compiled-template stream context)
render-context (RenderContext/create (.render configuration) context stream)]
(.render renderable render-context)
(.toString stream)))) (.toString stream))))
(defn render (defn render
"renders a template contained in the provided string, using the values in model-map "renders a template contained in the provided string, using the values in model-map
as the model for the template. templates rendered using this function are always as the model for the template. templates rendered using this function are always
parsed, compiled and rendered. the compiled results are never cached." parsed, compiled and rendered. the compiled results are never cached."
[^String s & [model-map]] [s model-map]
(let [renderable (compile-template-string s)] (let [compiled-template (compile-template-string s)]
(render-compiled-template renderable model-map))) (render-compiled-template compiled-template model-map)))
(defn render-file (defn render-file
"renders a template from a file, using the values in model-map as the model for the template" "renders a template from a file, using the values in model-map as the model for the template"
[^String filename & [model-map]] [^String filename model-map]
(let [file (new File filename) (let [file (new File filename)
renderable (compile-template! file)] compiled-template (compile-template! file)]
(render-compiled-template renderable model-map))) (render-compiled-template compiled-template model-map)))
(defn render-resource (defn render-resource
"renders a template from a resource file, using the values in the model-map as the model for "renders a template from a resource file, using the values in the model-map as the model for
the template." the template."
[^String filename & [model-map]] [^String filename model-map]
(if-let [resource-filename (get-resource-path filename)] (if-let [resource-filename (get-resource-path filename)]
(render-file (.getPath resource-filename) model-map) (render-file (.getPath resource-filename) model-map)
(throw (new FileNotFoundException (str "Template file \"" filename "\" not found."))))) (throw (new FileNotFoundException (str "Template file \"" filename "\" not found.")))))

View file

@ -1,27 +1,37 @@
(ns clj-jtwig.functions (ns clj-jtwig.functions
"custom template function/filter support functions." "custom template function/filter support functions."
(:import (com.lyncode.jtwig.functions.repository FunctionResolver) (:import (com.lyncode.jtwig.functions JtwigFunction)
(com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException) (com.lyncode.jtwig.functions.repository DefaultFunctionRepository)
(com.lyncode.jtwig.functions.annotations JtwigFunction) (com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException))
(com.lyncode.jtwig.functions.parameters GivenParameters) (:require [clj-jtwig.convert :refer [java->clojure clojure->java]])
(clj_jtwig TemplateFunction))
(:require [clj-jtwig.convert :refer [java->clojure clojure->java]]
[clj-jtwig.function-utils :refer [make-function-handler]])
(:use [clj-jtwig.standard-functions] (:use [clj-jtwig.standard-functions]
[clj-jtwig.web.web-functions])) [clj-jtwig.web.web-functions]))
(def ^:private object-array-type (Class/forName "[Ljava.lang.Object;")) (defn- make-function-handler [f]
(reify JtwigFunction
(execute [_ arguments]
(try
(clojure->java (apply f (map java->clojure arguments)))
(catch Exception ex
(throw (new FunctionException ex)))))))
(def ^:private function-parameters (doto (GivenParameters.) (defn- make-aliased-array [aliases]
(.add (to-array [object-array-type])))) (let [n (count aliases)
array (make-array String n)]
(doseq [index (range n)]
(aset array index (nth aliases index)))
array))
(defn- add-function-library! [repository functions] (defn- add-function-library! [repository functions]
(doseq [fn-obj functions] (doseq [[name {:keys [aliases fn]}] functions]
(.store repository fn-obj)) (.add repository
(make-function-handler fn)
name
(make-aliased-array aliases)))
repository) repository)
(defn- create-function-repository [] (defn- create-function-repository []
(doto (new FunctionResolver) (doto (new DefaultFunctionRepository (make-array JtwigFunction 0))
(add-function-library! standard-functions) (add-function-library! standard-functions)
(add-function-library! web-functions))) (add-function-library! web-functions)))
@ -34,34 +44,36 @@
[] []
(reset! functions (create-function-repository))) (reset! functions (create-function-repository)))
; intended for internal-use only. mainly exists for use in unit tests
(defn get-function [^String name]
(try
(.get @functions name function-parameters)
(catch FunctionNotFoundException ex)))
; intended for internal-use only. mainly exists for use in unit tests
(defn function-exists? [^String name] (defn function-exists? [^String name]
(not (nil? (get-function name)))) (try
(.retrieve @functions name)
true
(catch FunctionNotFoundException ex
false)))
(defn add-function!
"adds a new template function under the name specified. templates can call the function by the
name specified (or one of the aliases specified) and passing in the same number of arguments
accepted by f. the return value of f is returned to the template."
([^String name f]
(add-function! name nil f))
([^String name aliases f]
(let [handler (make-function-handler f)]
(.add @functions handler name (make-aliased-array aliases))
(.retrieve @functions name))))
(defmacro deftwigfn (defmacro deftwigfn
"defines a new template function. templates can call it by by the name specified and passing in the "defines a new template function. templates can call it by by the name specified and passing in the
same number of arguments as in args. the return value of the last form in body is returned to the same number of arguments as in args. the return value of the last form in body is returned to the
template. functions defined this way have no aliases and can only be called by the name given." template. functions defined this way have no aliases and can only be called by the name given."
[fn-name args & body] [fn-name args & body]
`(let [f# (fn ~args ~@body)] `(do
(.store (add-function! ~fn-name (fn ~args ~@body))))
@functions
(make-function-handler ~fn-name [] f#))
(get-function ~fn-name)))
(defmacro defaliasedtwigfn (defmacro defaliasedtwigfn
"defines a new template function. templates can call it by by the name specified (or one of the "defines a new template function. templates can call it by by the name specified (or one of the
aliases specified) and passing in the same number of arguments as in args. the return value of aliases specified) and passing in the same number of arguments as in args. the return value of
the last form in body is returned to the template." the last form in body is returned to the template."
[fn-name args aliases & body] [fn-name args aliases & body]
`(let [f# (fn ~args ~@body)] `(do
(.store (add-function! ~fn-name ~aliases (fn ~args ~@body))))
@functions
(make-function-handler ~fn-name ~aliases f#))
(get-function ~fn-name)))

View file

@ -28,7 +28,7 @@
; part of the filename, and if so use that file instead. ; part of the filename, and if so use that file instead.
; note that enabling this option does obviously incur a slight file I/O performance penalty ; note that enabling this option does obviously incur a slight file I/O performance penalty
; whenever these functions are used ; whenever these functions are used
:check-for-minified-web-resources false :check-for-minified-web-resources true
; automatically convert keyword keys in maps to/from strings as necessary when being passed ; automatically convert keyword keys in maps to/from strings as necessary when being passed
; in model-maps, when passed to Jtwig functions and when returned as values from Jtwig ; in model-maps, when passed to Jtwig functions and when returned as values from Jtwig
@ -36,18 +36,4 @@
; having to do any manual conversions yourself and to keep your Clojure code as idiomatic ; having to do any manual conversions yourself and to keep your Clojure code as idiomatic
; as possible. Jtwig model-maps at the very least do require all the keys to be strings ; as possible. Jtwig model-maps at the very least do require all the keys to be strings
; (not keywords) to ensure that model-map value resolution works as expected. ; (not keywords) to ensure that model-map value resolution works as expected.
:auto-convert-map-keywords true :auto-convert-map-keywords true}))
; the root path (relative to the classpath) where web resources such as js, css, images are
; located. typically in your project structure this path will be located under the
; "resources" directory.
:web-resource-path-root "public"
; when enabled, exceptions will be thrown when attempting to use variables that do not exist
:strict-mode false
; allows for changing the tag symbols used from normal Twig-style to a style more friendly
; with certain Javascript template engines (e.g. Angular).
; :default => tag: {% %}, output: {{ }}, comment: {# #}
; :js => tag: <# #>, output: <@ @>, comment: <$ $>
:tag-symbols :default}))

View file

@ -0,0 +1,203 @@
(ns clj-jtwig.standard-functions
"standard function definitions. these are functions that are not yet included in JTwig's standard function
library and are just here to fill in the gaps for now."
(:import (org.apache.commons.lang3.text WordUtils)
(org.apache.commons.lang3 StringUtils))
(:use [clojure.pprint]
[clj-jtwig.options]))
(defn- possible-keyword-string [x]
(if (and (:auto-convert-map-keywords @options)
(string? x))
(keyword x)
x))
; we are using a separate map to hold the standard functions instead of using deftwigfn, etc. because doing it this
; way makes it easy to re-add all these functions when/if the JTwig function repository object needs to be
; recreated (e.g. during unit tests).
; the keys are function names. each value is a map containing :fn which is the actual function, and an optional
; :aliases, which should be a vector of strings containing one or more possible aliases for this function.
(defonce standard-functions
{"blank_if_null"
{:fn (fn [x]
(if (nil? x) "" x))
:aliases ["nonull"]}
"butlast"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals butlast)
(butlast sequence)))}
"center"
{:fn (fn [s size & [padding-string]]
(StringUtils/center s size (or padding-string " ")))}
"contains"
{:fn (fn [coll value]
(cond
(map? coll) (contains? coll (possible-keyword-string value))
(string? coll) (.contains coll value)
; explicit use of '=' to allow testing for falsey values
(coll? coll) (not (nil? (some #(= value %) coll)))
:else (throw (new Exception (str "'contains' passed invalid collection type: " (type coll))))))}
"dump"
{:fn (fn [x]
(with-out-str
(clojure.pprint/pprint x)))}
"dump_table"
{:fn (fn [x]
(with-out-str
(clojure.pprint/print-table x)))}
"index_of"
{:fn (fn [coll value]
(cond
(instance? java.util.List coll) (.indexOf coll value)
(string? coll) (.indexOf coll (if (char? value) (int value) value))
:else (throw (new Exception (str "'index_of' passed invalid collection type: " (type coll))))))}
"last_index_of"
{:fn (fn [coll value]
(cond
(instance? java.util.List coll) (.lastIndexOf coll value)
(string? coll) (.lastIndexOf coll (if (char? value) (int value) value))
:else (throw (new Exception (str "'last_index_of' passed invalid collection type: " (type coll))))))}
"max"
{:fn (fn [& numbers]
(if (coll? (first numbers))
(apply max (first numbers))
(apply max numbers)))}
"min"
{:fn (fn [& numbers]
(if (coll? (first numbers))
(apply min (first numbers))
(apply min numbers)))}
"normalize_space"
{:fn (fn [s]
(StringUtils/normalizeSpace s))}
"nth"
{:fn (fn [sequence index & optional-not-found]
(let [values (if (map? sequence) ; map instance check to match behaviour of jtwig's first/last implementation
(-> sequence vals)
sequence)]
(if optional-not-found
(nth values index (first optional-not-found))
(nth values index))))}
"pad_left"
{:fn (fn [s size & [padding-string]]
(StringUtils/leftPad s size (or padding-string " ")))}
"pad_right"
{:fn (fn [s size & [padding-string]]
(StringUtils/rightPad s size (or padding-string " ")))}
"random"
{:fn (fn [& values]
(let [first-value (first values)]
(cond
(and (= (count values) 1)
(coll? first-value))
(rand-nth first-value)
(> (count values) 1)
(rand-nth values)
(string? first-value)
(rand-nth (seq first-value))
(number? first-value)
(rand-int first-value)
:else
(rand))))}
"range"
{:fn (fn [low high & [step]]
(range low high (or step 1)))}
"repeat"
{:fn (fn [s n]
(StringUtils/repeat s n))}
"rest"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals rest)
(rest sequence)))}
"second"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals second)
(second sequence)))}
"sort"
{:fn (fn [sequence]
(sort < sequence))}
"sort_descending"
{:fn (fn [sequence]
(sort > sequence))
:aliases ["sort_desc"]}
"sort_by"
{:fn (fn [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) coll)))}
"sort_descending_by"
{:fn (fn [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) #(compare %2 %1) coll)))
:aliases ["sort_desc_by"]}
"to_double"
{:fn (fn [x]
(Double/parseDouble x))}
"to_float"
{:fn (fn [x]
(Float/parseFloat x))}
"to_int"
{:fn (fn [x]
(Integer/parseInt x))}
"to_keyword"
{:fn (fn [x]
(keyword x))}
"to_long"
{:fn (fn [x]
(Long/parseLong x))}
"to_string"
{:fn (fn [x]
(cond
(keyword? x) (name x)
(instance? clojure.lang.LazySeq x) (str (seq x))
(coll? x) (str x)
:else (.toString x)))}
"wrap"
{:fn (fn [s length & [wrap-long-words? new-line-string]]
(WordUtils/wrap
s
length
new-line-string
(if (nil? wrap-long-words?)
false
wrap-long-words?)))}})

View file

@ -5,10 +5,13 @@
(:import (java.net URI)) (:import (java.net URI))
(:require [clj-jtwig.web.middleware :refer [*servlet-context-path*]] (:require [clj-jtwig.web.middleware :refer [*servlet-context-path*]]
[clojure.string :as str]) [clojure.string :as str])
(:use [clj-jtwig.function-utils] (:use [clj-jtwig.utils]
[clj-jtwig.utils]
[clj-jtwig.options])) [clj-jtwig.options]))
;; TODO: while 'public' is the default with Compojure, applications can override with something else ...
;; should make this customizable (some option added to clj-jtwig.options likely ...)
(def root-resource-path "public")
(defn- get-context-url [url] (defn- get-context-url [url]
(str *servlet-context-path* url)) (str *servlet-context-path* url))
@ -19,7 +22,8 @@
(defn- get-resource-modification-timestamp [^String resource-url] (defn- get-resource-modification-timestamp [^String resource-url]
(if (relative-url? resource-url) (if (relative-url? resource-url)
(->> (str (:web-resource-path-root @options) resource-url)
(->> (str root-resource-path resource-url)
(get-context-url) (get-context-url)
(get-resource-modification-date)))) (get-resource-modification-date))))
@ -47,22 +51,27 @@
(minified-url? url)) (minified-url? url))
url url
(let [minified-url (make-minified-url url)] (let [minified-url (make-minified-url url)]
(if (get-resource-path (str (:web-resource-path-root @options) minified-url)) (if (get-resource-path (str root-resource-path minified-url))
minified-url minified-url
url)))) url))))
(deflibrary web-functions ; defined using the same type of map structure as in clj-jtwig.standard-functions
(library-function "path" [url]
(get-context-url url))
(library-function "stylesheet" [url & [media]] (defonce web-functions
{"path"
{:fn (fn [url]
(get-context-url url))}
"stylesheet"
{:fn (fn [url & [media]]
(let [fmt (if media (let [fmt (if media
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" media=\"%s\" />" "<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" media=\"%s\" />"
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" />") "<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" />")
resource-path (get-minified-resource-url url)] resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path) media))) (format fmt (get-url-string resource-path) media)))}
(library-function "javascript" [url] "javascript"
{:fn (fn [url]
(let [fmt "<script type=\"text/javascript\" src=\"%s\"></script>" (let [fmt "<script type=\"text/javascript\" src=\"%s\"></script>"
resource-path (get-minified-resource-url url)] resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path))))) (format fmt (get-url-string resource-path))))}})

View file

@ -1,34 +0,0 @@
(ns clj-jtwig.function-utils
"utility macros for creating Jtwig function handlers. intended for internal clj-jtwig use only."
(:import (com.lyncode.jtwig.functions.exceptions FunctionException)
(com.lyncode.jtwig.functions.annotations JtwigFunction Parameter)
(clj_jtwig TemplateFunction))
(:require [clj-jtwig.convert :refer [java->clojure clojure->java]]))
(defmacro make-function-handler [name aliases f]
(let [arguments (with-meta
(gensym "arguments#")
`{Parameter {}})]
`(reify TemplateFunction
(~(with-meta
'execute
`{JtwigFunction {:name ~name :aliases ~aliases}})
[_ ~arguments]
(try
(clojure->java (apply ~f (map java->clojure ~arguments)))
(catch Exception ex#
(throw (new FunctionException ex#))))))))
(defmacro deflibrary [name & function-handlers]
`(def ~(symbol name)
[~@function-handlers]))
(defmacro library-function
[fn-name args & body]
`(let [f# (fn ~args ~@body)]
(make-function-handler ~fn-name [] f#)))
(defmacro library-aliased-function
[fn-name aliases args & body]
`(let [f# (fn ~args ~@body)]
(make-function-handler ~fn-name ~aliases f#)))

View file

@ -1,164 +0,0 @@
(ns clj-jtwig.standard-functions
"standard function definitions. these are functions that are not yet included in Jtwig's standard function
library and are just here to fill in the gaps for now."
(:import (org.apache.commons.lang3.text WordUtils)
(org.apache.commons.lang3 StringUtils))
(:use [clojure.pprint]
[clj-jtwig.function-utils]
[clj-jtwig.options]))
(defn- possible-keyword-string [x]
(if (and (:auto-convert-map-keywords @options)
(string? x))
(keyword x)
x))
(deflibrary standard-functions
(library-aliased-function "blank_if_null" ["nonull"] [x]
(if (nil? x) "" x))
(library-function "butlast" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals butlast)
(butlast sequence)))
(library-function "center" [s size & [padding-string]]
(StringUtils/center s size (or padding-string " ")))
(library-function "contains" [coll value]
(cond
(map? coll) (contains? coll (possible-keyword-string value))
(string? coll) (.contains coll value)
; explicit use of '=' to allow testing for falsey values
(coll? coll) (not (nil? (some #(= value %) coll)))
:else (throw (new Exception (str "'contains' passed invalid collection type: " (type coll))))))
(library-function "dump" [x]
(with-out-str
(clojure.pprint/pprint x)))
(library-function "dump_table" [x]
(with-out-str
(clojure.pprint/print-table x)))
(library-function "index_of" [coll value]
(cond
(instance? java.util.List coll) (.indexOf coll value)
(string? coll) (.indexOf coll (if (char? value) (int value) value))
:else (throw (new Exception (str "'index_of' passed invalid collection type: " (type coll))))))
(library-function "last_index_of" [coll value]
(cond
(instance? java.util.List coll) (.lastIndexOf coll value)
(string? coll) (.lastIndexOf coll (if (char? value) (int value) value))
:else (throw (new Exception (str "'last_index_of' passed invalid collection type: " (type coll))))))
(library-function "max" [& numbers]
(if (coll? (first numbers))
(apply max (first numbers))
(apply max numbers)))
(library-function "min" [& numbers]
(if (coll? (first numbers))
(apply min (first numbers))
(apply min numbers)))
(library-function "normalize_space" [s]
(StringUtils/normalizeSpace s))
(library-function "nth" [sequence index & [optional-not-found]]
(let [values (if (map? sequence) ; map instance check to match behaviour of jtwig's first/last implementation
(-> sequence vals)
sequence)]
(if optional-not-found
(nth values index optional-not-found)
(nth values index))))
(library-function "pad_left" [s size & [padding-string]]
(StringUtils/leftPad s size (or padding-string " ")))
(library-function "pad_right" [s size & [padding-string]]
(StringUtils/rightPad s size (or padding-string " ")))
(library-function "random" [& values]
(let [first-value (first values)]
(cond
(and (= (count values) 1)
(coll? first-value))
(rand-nth first-value)
(> (count values) 1)
(rand-nth values)
(string? first-value)
(rand-nth (seq first-value))
(number? first-value)
(rand-int first-value)
:else
(rand))))
(library-function "range" [low high & [step]]
(range low high (or step 1)))
(library-function "repeat" [s n]
(StringUtils/repeat s n))
(library-function "rest" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals rest)
(rest sequence)))
(library-function "second" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals second)
(second sequence)))
(library-function "sort" [sequence]
(sort < sequence))
(library-aliased-function "sort_descending" ["sort_desc"] [sequence]
(sort > sequence))
(library-function "sort_by" [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) coll)))
(library-aliased-function "sort_descending_by" ["sort_desc_by"] [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) #(compare %2 %1) coll)))
(library-function "to_double" [x]
(Double/parseDouble x))
(library-function "to_float" [x]
(Float/parseFloat x))
(library-function "to_int" [x]
(Integer/parseInt x))
(library-function "to_keyword" [x]
(keyword x))
(library-function "to_long" [x]
(Long/parseLong x))
(library-function "to_string" [x]
(cond
(keyword? x) (name x)
(instance? clojure.lang.LazySeq x) (str (seq x))
(coll? x) (str x)
:else (.toString x)))
(library-function "wrap" [s length & [wrap-long-words? new-line-string]]
(WordUtils/wrap
s
length
new-line-string
(if (nil? wrap-long-words?)
false
wrap-long-words?))))

View file

@ -1,10 +0,0 @@
package clj_jtwig;
import com.lyncode.jtwig.functions.exceptions.FunctionException;
// this is defined in Java only because Clojure defprotocol/definterface don't allow
// including method definitions with vararg parameters
public interface TemplateFunction {
public abstract Object execute (Object... arguments) throws FunctionException;
}

View file

@ -167,15 +167,15 @@
(set-options! :auto-convert-map-keywords true) (set-options! :auto-convert-map-keywords true)
(is (= (render "{{keys_are_all_keywords(x)}}" {:x {:a "foo" :b "bar" :c "baz"}}) (is (= (render "{{keys_are_all_keywords(x)}}" {:x {:a "foo" :b "bar" :c "baz"}})
"1")) "true"))
(is (= (render "{{keys_are_all_keywords(x)}}" {:x {"a" "foo" "b" "bar" "c" "baz"}}) (is (= (render "{{keys_are_all_keywords(x)}}" {:x {"a" "foo" "b" "bar" "c" "baz"}})
"1")) "true"))
(set-options! :auto-convert-map-keywords false) (set-options! :auto-convert-map-keywords false)
(is (= (render "{{keys_are_all_keywords(x)}}" {"x" {:a "foo" :b "bar" :c "baz"}}) (is (= (render "{{keys_are_all_keywords(x)}}" {"x" {:a "foo" :b "bar" :c "baz"}})
"1")) "true"))
(is (= (render "{{keys_are_all_strings(x)}}" {"x" {"a" "foo" "b" "bar" "c" "baz"}}) (is (= (render "{{keys_are_all_strings(x)}}" {"x" {"a" "foo" "b" "bar" "c" "baz"}})
"1")) "true"))
(set-options! :auto-convert-map-keywords true) (set-options! :auto-convert-map-keywords true)
(reset-functions!)))) (reset-functions!))))
@ -195,16 +195,16 @@
(every? string? (keys x))) (every? string? (keys x)))
(set-options! :auto-convert-map-keywords true) (set-options! :auto-convert-map-keywords true)
(is (= (render "{{keys_are_all_keywords(get_map_with_keywords(null))}}") (is (= (render "{{keys_are_all_keywords(get_map_with_keywords(null))}}" {})
"1")) "true"))
(is (= (render "{{keys_are_all_keywords(get_map_with_strings(null))}}") (is (= (render "{{keys_are_all_keywords(get_map_with_strings(null))}}" {})
"1")) "true"))
(set-options! :auto-convert-map-keywords false) (set-options! :auto-convert-map-keywords false)
(is (= (render "{{keys_are_all_keywords(get_map_with_keywords(null))}}") (is (= (render "{{keys_are_all_keywords(get_map_with_keywords(null))}}" {})
"1")) "true"))
(is (= (render "{{keys_are_all_strings(get_map_with_strings(null))}}") (is (= (render "{{keys_are_all_strings(get_map_with_strings(null))}}" {})
"1")) "true"))
(set-options! :auto-convert-map-keywords true) (set-options! :auto-convert-map-keywords true)
(reset-functions!)))) (reset-functions!))))

View file

@ -1,14 +1,13 @@
(ns clj-jtwig.core-test (ns clj-jtwig.core-test
(:import (java.io FileNotFoundException) (:import (java.io FileNotFoundException))
(com.lyncode.jtwig.exception CalculateException RenderException))
(:require [clojure.test :refer :all] (:require [clojure.test :refer :all]
[clj-jtwig.core :refer :all] [clj-jtwig.core :refer :all]
[clj-jtwig.functions :refer :all])) [clj-jtwig.functions :refer :all]))
; The purpose of these tests is to establish that our wrapper around Jtwig works. That is, ; The purpose of these tests is to establish that our wrapper around JTwig works. That is,
; we will be focusing on stuff like making sure that passing Clojure data structures ; we will be focusing on stuff like making sure that passing Clojure data structures
; (e.g. maps, vectors, lists) over to Jtwig works fine. ; (e.g. maps, vectors, lists) over to JTwig works fine.
; Jtwig includes its own test suite which tests actual template parsing and evaluation ; JTwig includes its own test suite which tests actual template parsing and evaluation
; functionality, so there's no point in duplicating that kind of testing here once we ; functionality, so there's no point in duplicating that kind of testing here once we
; establish that the above mentioned stuff works fine. ; establish that the above mentioned stuff works fine.
; ;
@ -21,8 +20,9 @@
{:name "Bob"}) {:name "Bob"})
"Hello Bob!") "Hello Bob!")
"passing a model-map") "passing a model-map")
(is (= (render "Hello {{ name }}!") (is (= (render "Hello {{ name }}!"
"Hello !") nil)
"Hello null!")
"not passing a model-map") "not passing a model-map")
(is (= (render "Hello {{ name }}!" (is (= (render "Hello {{ name }}!"
{"name" "Bob"}) {"name" "Bob"})
@ -49,7 +49,7 @@
(set-options! :auto-convert-map-keywords true)))) (set-options! :auto-convert-map-keywords true))))
(deftest passing-model-map-data (deftest passing-model-map-data
(testing "Passing Clojure data structures to JtwigContext's" (testing "Passing Clojure data structures to JTwigContext's"
(is (= (render "float {{ x }}" (is (= (render "float {{ x }}"
{:x 3.14}) {:x 3.14})
"float 3.14") "float 3.14")
@ -60,7 +60,7 @@
"passing an integer") "passing an integer")
(is (= (render "null {{ x }}" (is (= (render "null {{ x }}"
{:x nil}) {:x nil})
"null ") "null null")
"passing a nil value") "passing a nil value")
(is (= (render "char {{ x }}" (is (= (render "char {{ x }}"
{:x \a}) {:x \a})
@ -108,8 +108,9 @@
(render-file invalid-filename (render-file invalid-filename
{:name "Bob"})) {:name "Bob"}))
"trying to render a file that doesn't exist") "trying to render a file that doesn't exist")
(is (= (render-file test-filename) (is (= (render-file test-filename
"Hello from a file!") nil)
"Hello null from a file!")
"not passing a model-map") "not passing a model-map")
(is (= (render-file test-filename (is (= (render-file test-filename
{"name" "Bob"}) {"name" "Bob"})
@ -134,39 +135,3 @@
"passing a model-map where the keys are keywords and try skipping auto stringifying the keys") "passing a model-map where the keys are keywords and try skipping auto stringifying the keys")
(set-options! :auto-convert-map-keywords true))))) (set-options! :auto-convert-map-keywords true)))))
(deftest options
(testing "clj-jtwig options specific tests"
(do
(set-options! :strict-mode true)
(is (thrown-with-msg?
RenderException
#"com.lyncode.jtwig.exception.CalculateException"
(render "{{ foo }}"))
"trying to output a non-existant variable under strict-mode")
(is (= (render "{{ foo }}" {:foo "bar"})
"bar")
"trying to output an existing variable under strict-mode")
(set-options! :strict-mode false)
(is (= (render "{{ foo }}")
"")
"trying to output a non-existant variable under non-strict-mode")
(is (= (render "{{ foo }}" {:foo "bar"})
"bar")
"trying to output an existing variable under non-strict-mode"))
(do
(set-options! :tag-symbols :js)
(is (= (render "@> 1 <@" {:foo "bar"})
"1")
"js-style output symbols")
(is (= (render "<# if (foo) #>bar<# endif #>" {:foo true})
"bar")
"js-style tag symbols")
(is (= (render "<$ this is a comment $>")
"")
"js-style comment symbols")
(set-options! :tag-symbols :default))))

View file

@ -1,12 +1,15 @@
(ns clj-jtwig.functions-test (ns clj-jtwig.functions-test
(:import (com.lyncode.jtwig.functions.repository CallableFunction))
(:require [clojure.test :refer :all] (:require [clojure.test :refer :all]
[clj-jtwig.core :refer :all] [clj-jtwig.core :refer :all]
[clj-jtwig.functions :refer :all])) [clj-jtwig.functions :refer :all]))
; TODO: is there a better way to test that something is an instance of some object generated by reify?
(defn valid-function-handler? [x] (defn valid-function-handler? [x]
(and (not (nil? x)) (and (not (nil? x))
(instance? CallableFunction x))) (-> x
(class)
(.getName)
(.startsWith "clj_jtwig.functions$make_function_handler"))))
(deftest template-functions (deftest template-functions
(testing "Adding custom template functions" (testing "Adding custom template functions"
@ -24,7 +27,7 @@
(deftwigfn "add" [a b] (deftwigfn "add" [a b]
(+ a b)))) (+ a b))))
(is (= (render "{{add(1, 2)}}") (is (= (render "{{add(1, 2)}}" nil)
"3") "3")
"calling a custom function") "calling a custom function")
(is (= (render "{{add(a, b)}}" {:a 1 :b 2}) (is (= (render "{{add(a, b)}}" {:a 1 :b 2})
@ -49,13 +52,13 @@
(is (true? (function-exists? "plus"))) (is (true? (function-exists? "plus")))
(is (true? (function-exists? "myAddFn"))) (is (true? (function-exists? "myAddFn")))
(is (= (render "{{add(1, 2)}}") (is (= (render "{{add(1, 2)}}" nil)
"3") "3")
"calling a custom function by name") "calling a custom function by name")
(is (= (render "{{plus(1, 2)}}") (is (= (render "{{plus(1, 2)}}" nil)
"3") "3")
"calling a custom function by alias") "calling a custom function by alias")
(is (= (render "{{myAddFn(1, 2)}}") (is (= (render "{{myAddFn(1, 2)}}" nil)
"3") "3")
"calling a custom function by another alias") "calling a custom function by another alias")
@ -74,17 +77,18 @@
(apply + numbers)))) (apply + numbers))))
(is (true? (function-exists? "addAll"))) (is (true? (function-exists? "addAll")))
(is (= (render "{{add2(1, 2)}}") (is (= (render "{{add2(1, 2)}}" nil)
"3") "3")
"fixed number of arguments (correct amount)") "fixed number of arguments (correct amount)")
(is (thrown? (is (thrown-with-msg?
Exception Exception
(render "{{add2(1)}}"))) #"clojure\.lang\.ArityException: Wrong number of args"
(is (= (render "{{addAll(1, 2, 3, 4, 5)}}") (render "{{add2(1)}}" nil)))
(is (= (render "{{addAll(1, 2, 3, 4, 5)}}" nil)
"15") "15")
"variable number of arguments (non-zero)") "variable number of arguments (non-zero)")
(is (= (render "{{addAll}}") (is (= (render "{{addAll}}" nil)
"") "null")
"variable number of arguments (zero)") "variable number of arguments (zero)")
(reset-functions!))) (reset-functions!)))
@ -133,28 +137,28 @@
; verify that the clojure function recognizes the correct types when the variable is passed via a constant ; verify that the clojure function recognizes the correct types when the variable is passed via a constant
; value embedded in the template ; value embedded in the template
(is (= (render "{{typename(42)}}") (is (= (render "{{typename(42)}}" nil)
"java.lang.Integer") "java.lang.Integer")
"integer typename via constant value embedded in the template") "integer typename via constant value embedded in the template")
(is (= (render "{{typename(3.14)}}") (is (= (render "{{typename(3.14)}}" nil)
"java.lang.Double") "java.lang.Double")
"float typename via constant value embedded in the template") "float typename via constant value embedded in the template")
(is (= (render "{{typename('foobar')}}") (is (= (render "{{typename('foobar')}}" nil)
"java.lang.String") "java.lang.String")
"string typename via constant value embedded in the template") "string typename via constant value embedded in the template")
(is (= (render "{{typename('a')}}") (is (= (render "{{typename('a')}}" nil)
"java.lang.Character") "java.lang.Character")
"char typename via constant value embedded in the template") "char typename via constant value embedded in the template")
(is (= (render "{{typename(true)}}") (is (= (render "{{typename(true)}}" nil)
"java.lang.Boolean") "java.lang.Boolean")
"boolean typename via constant value embedded in the template") "boolean typename via constant value embedded in the template")
(is (= (render "{{typename([1, 2, 3, 4, 5])}}") (is (= (render "{{typename([1, 2, 3, 4, 5])}}" nil)
"clojure.lang.LazySeq") "clojure.lang.LazySeq")
"list typename via constant value embedded in the template") "list typename via constant value embedded in the template")
(is (= (render "{{typename(1..5)}}") (is (= (render "{{typename(1..5)}}" nil)
"clojure.lang.LazySeq") "clojure.lang.LazySeq")
"vector typename via constant value embedded in the template") "vector typename via constant value embedded in the template")
(is (= (render "{{typename({a: 1, b: 'foo', c: null})}}") (is (= (render "{{typename({a: 1, b: 'foo', c: null})}}" nil)
"clojure.lang.PersistentArrayMap") "clojure.lang.PersistentArrayMap")
"map typename via constant value embedded in the template") "map typename via constant value embedded in the template")
@ -173,7 +177,7 @@
"a") "a")
"char via model-map") "char via model-map")
(is (= (render "{{identity(x)}}" {:x true}) (is (= (render "{{identity(x)}}" {:x true})
"1") "true")
"boolean via model-map") "boolean via model-map")
(is (= (render "{{identity(x)}}" {:x '(1 2 3 4 5)}) (is (= (render "{{identity(x)}}" {:x '(1 2 3 4 5)})
"[1, 2, 3, 4, 5]") "[1, 2, 3, 4, 5]")
@ -192,29 +196,29 @@
; simple passing / returning... not doing anything exciting with the arguments ; simple passing / returning... not doing anything exciting with the arguments
; using a constant value embedded inside the template ; using a constant value embedded inside the template
(is (= (render "{{identity(42)}}") (is (= (render "{{identity(42)}}" nil)
"42") "42")
"integer via constant value embedded in the template") "integer via constant value embedded in the template")
(is (= (render "{{identity(3.14)}}") (is (= (render "{{identity(3.14)}}" nil)
"3.14") "3.14")
"float via constant value embedded in the template") "float via constant value embedded in the template")
(is (= (render "{{identity('foobar')}}") (is (= (render "{{identity('foobar')}}" nil)
"foobar") "foobar")
"string via constant value embedded in the template") "string via constant value embedded in the template")
(is (= (render "{{identity('a')}}") (is (= (render "{{identity('a')}}" nil)
"a") "a")
"char via constant value embedded in the template") "char via constant value embedded in the template")
(is (= (render "{{identity(true)}}") (is (= (render "{{identity(true)}}" nil)
"1") "true")
"boolean via constant value embedded in the template") "boolean via constant value embedded in the template")
(is (= (render "{{identity([1, 2, 3, 4, 5])}}") (is (= (render "{{identity([1, 2, 3, 4, 5])}}" nil)
"[1, 2, 3, 4, 5]") "[1, 2, 3, 4, 5]")
"enumerated list via constant value embedded in the template") "enumerated list via constant value embedded in the template")
(is (= (render "{{identity(1..5)}}") (is (= (render "{{identity(1..5)}}" nil)
"[1, 2, 3, 4, 5]") "[1, 2, 3, 4, 5]")
"list by comprehension via constant value embedded in the template") "list by comprehension via constant value embedded in the template")
; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order)
(is (= (render "{{identity({a: 1, b: 'foo', c: null})}}") (is (= (render "{{identity({a: 1, b: 'foo', c: null})}}" nil)
"{b=foo, c=null, a=1}") "{b=foo, c=null, a=1}")
"map via constant value embedded in the template") "map via constant value embedded in the template")
@ -228,7 +232,7 @@
"vector (iterating over a model-map var passed to a function and returned from it)") "vector (iterating over a model-map var passed to a function and returned from it)")
; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order)
(is (= (render "{% for k, v in identity(x) %}{{k}}: {{v}} {% endfor %}" {:x {:a 1 :b "foo" :c nil}}) (is (= (render "{% for k, v in identity(x) %}{{k}}: {{v}} {% endfor %}" {:x {:a 1 :b "foo" :c nil}})
"b: foo c: a: 1 ") "b: foo c: null a: 1 ")
"map (iterating over a model-map var passed to a function and returned from it)") "map (iterating over a model-map var passed to a function and returned from it)")
(is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x #{1 2 3 4 5}}) (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x #{1 2 3 4 5}})
"1 4 3 2 5 ") "1 4 3 2 5 ")
@ -236,15 +240,15 @@
; iterating over passed sequence/collection type arguments passed to a custom function from a constant ; iterating over passed sequence/collection type arguments passed to a custom function from a constant
; value embedded in the template and being returned ; value embedded in the template and being returned
(is (= (render "{% for i in identity([1, 2, 3, 4, 5]) %}{{i}} {% endfor %}") (is (= (render "{% for i in identity([1, 2, 3, 4, 5]) %}{{i}} {% endfor %}" nil)
"1 2 3 4 5 ") "1 2 3 4 5 ")
"enumerated list (iterating over a model-map var passed to a function and returned from it)") "enumerated list (iterating over a model-map var passed to a function and returned from it)")
(is (= (render "{% for i in identity(1..5) %}{{i}} {% endfor %}") (is (= (render "{% for i in identity(1..5) %}{{i}} {% endfor %}" nil)
"1 2 3 4 5 ") "1 2 3 4 5 ")
"list by comprehension (iterating over a model-map var passed to a function and returned from it)") "list by comprehension (iterating over a model-map var passed to a function and returned from it)")
; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order)
(is (= (render "{% for k, v in identity({a: 1, b: 'foo', c: null}) %}{{k}}: {{v}} {% endfor %}") (is (= (render "{% for k, v in identity({a: 1, b: 'foo', c: null}) %}{{k}}: {{v}} {% endfor %}" nil)
"b: foo c: a: 1 ") "b: foo c: null a: 1 ")
"map (iterating over a model-map var passed to a function and returned from it)") "map (iterating over a model-map var passed to a function and returned from it)")
(reset-functions!)))) (reset-functions!))))
@ -268,40 +272,40 @@
(is (true? (function-exists? "sort_descending_by")))) (is (true? (function-exists? "sort_descending_by"))))
(testing "blank_if_null" (testing "blank_if_null"
(is (= (render "{{ a|blank_if_null }}") (is (= (render "{{ a|blank_if_null }}" nil)
"")) ""))
(is (= (render "{{ a|blank_if_null }}" {:a nil}) (is (= (render "{{ a|blank_if_null }}" {:a nil})
"")) ""))
(is (= (render "{{ a|blank_if_null }}" {:a "foo"}) (is (= (render "{{ a|blank_if_null }}" {:a "foo"})
"foo")) "foo"))
(is (= (render "{{ a|nonull }}") (is (= (render "{{ a|nonull }}" nil)
""))) "")))
(testing "butlast" (testing "butlast"
(is (= (render "{{ [1, 2, 3, 4, 5]|butlast }}") (is (= (render "{{ [1, 2, 3, 4, 5]|butlast }}" nil)
"[1, 2, 3, 4]"))) "[1, 2, 3, 4]")))
(testing "center" (testing "center"
(is (= (render "{{ center('bat', 5) }}") (is (= (render "{{ center('bat', 5) }}" nil)
" bat ")) " bat "))
(is (= (render "{{ center('bat', 3) }}") (is (= (render "{{ center('bat', 3) }}" nil)
"bat")) "bat"))
(is (= (render "{{ center('bat', 5, 'x') }}") (is (= (render "{{ center('bat', 5, 'x') }}" nil)
"xbatx"))) "xbatx")))
(testing "contains" (testing "contains"
(is (= (render "{{ {a: 1, b: 2, c: 3}|contains(\"b\") }}") (is (= (render "{{ {a: 1, b: 2, c: 3}|contains(\"b\") }}" nil)
"1")) "true"))
(is (= (render "{{ {a: 1, b: 2, c: 3}|contains(\"d\") }}") (is (= (render "{{ {a: 1, b: 2, c: 3}|contains(\"d\") }}" nil)
"0")) "false"))
(is (= (render "{{ [1, 2, 3, 4]|contains(2) }}") (is (= (render "{{ [1, 2, 3, 4]|contains(2) }}" nil)
"1")) "true"))
(is (= (render "{{ [1, 2, 3, 4]|contains(5) }}") (is (= (render "{{ [1, 2, 3, 4]|contains(5) }}" nil)
"0")) "false"))
(is (= (render "{{ \"abcdef\"|contains(\"abc\") }}") (is (= (render "{{ \"abcdef\"|contains(\"abc\") }}" nil)
"1")) "true"))
(is (= (render "{{ \"abcdef\"|contains(\"xyz\") }}") (is (= (render "{{ \"abcdef\"|contains(\"xyz\") }}" nil)
"0"))) "false")))
(testing "dump" (testing "dump"
(is (= (render "{{ a|dump }}" {:a [{:foo "bar"} [1, 2, 3] "hello"]}) (is (= (render "{{ a|dump }}" {:a [{:foo "bar"} [1, 2, 3] "hello"]})
@ -312,114 +316,115 @@
"\n| :a | :b | :c |\n|----+----+-----|\n| 1 | 2 | 3 |\n| 7 | 5 | dog |\n"))) "\n| :a | :b | :c |\n|----+----+-----|\n| 1 | 2 | 3 |\n| 7 | 5 | dog |\n")))
(testing "index_of" (testing "index_of"
(is (= (render "{{ [1, 2, 3, 2, 1]|index_of(2) }}") (is (= (render "{{ [1, 2, 3, 2, 1]|index_of(2) }}" nil)
"1")) "1"))
(is (= (render "{{ [1, 2, 3, 2, 1]|index_of(5) }}") (is (= (render "{{ [1, 2, 3, 2, 1]|index_of(5) }}" nil)
"-1")) "-1"))
(is (= (render "{{ \"abcdcba\"|index_of(\"b\") }}") (is (= (render "{{ \"abcdcba\"|index_of(\"b\") }}" nil)
"1")) "1"))
(is (= (render "{{ \"abcdcba\"|index_of(\"z\") }}") (is (= (render "{{ \"abcdcba\"|index_of(\"z\") }}" nil)
"-1"))) "-1")))
(testing "last_index_of" (testing "last_index_of"
(is (= (render "{{ [1, 2, 3, 2, 1]|last_index_of(2) }}") (is (= (render "{{ [1, 2, 3, 2, 1]|last_index_of(2) }}" nil)
"3")) "3"))
(is (= (render "{{ [1, 2, 3, 2, 1]|last_index_of(5) }}") (is (= (render "{{ [1, 2, 3, 2, 1]|last_index_of(5) }}" nil)
"-1")) "-1"))
(is (= (render "{{ \"abcdcba\"|last_index_of(\"b\") }}") (is (= (render "{{ \"abcdcba\"|last_index_of(\"b\") }}" nil)
"5")) "5"))
(is (= (render "{{ \"abcdcba\"|last_index_of(\"z\") }}") (is (= (render "{{ \"abcdcba\"|last_index_of(\"z\") }}" nil)
"-1"))) "-1")))
(testing "max" (testing "max"
(is (= (render "{{ [2, 1, 5, 3, 4]|max }}") (is (= (render "{{ [2, 1, 5, 3, 4]|max }}" nil)
"5")) "5"))
(is (= (render "{{ max(2, 1, 5, 3, 4) }}") (is (= (render "{{ max(2, 1, 5, 3, 4) }}" nil)
"5"))) "5")))
(testing "min" (testing "min"
(is (= (render "{{ [2, 1, 5, 3, 4]|min }}") (is (= (render "{{ [2, 1, 5, 3, 4]|min }}" nil)
"1")) "1"))
(is (= (render "{{ min(2, 1, 5, 3, 4) }}") (is (= (render "{{ min(2, 1, 5, 3, 4) }}" nil)
"1"))) "1")))
(testing "normalize_space" (testing "normalize_space"
(is (= (render "{{ normalize_space(' hello world ') }}") (is (= (render "{{ normalize_space(' hello world ') }}" nil)
"hello world"))) "hello world")))
(testing "nth" (testing "nth"
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(2) }}") (is (= (render "{{ [1, 2, 3, 4, 5]|nth(2) }}" nil)
"3")) "3"))
(is (thrown? (is (thrown-with-msg?
Exception Exception
(render "{{ [1, 2, 3, 4, 5]|nth(6) }}"))) #"java.lang.IndexOutOfBoundsException"
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(6, \"not found\") }}") (render "{{ [1, 2, 3, 4, 5]|nth(6) }}" nil)))
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(6, \"not found\") }}" nil)
"not found"))) "not found")))
(testing "pad_left" (testing "pad_left"
(is (= (render "{{ pad_left('bat', 5) }}") (is (= (render "{{ pad_left('bat', 5) }}" nil)
" bat")) " bat"))
(is (= (render "{{ pad_left('bat', 3) }}") (is (= (render "{{ pad_left('bat', 3) }}" nil)
"bat")) "bat"))
(is (= (render "{{ pad_left('bat', 5, 'x') }}") (is (= (render "{{ pad_left('bat', 5, 'x') }}" nil)
"xxbat"))) "xxbat")))
(testing "pad_right" (testing "pad_right"
(is (= (render "{{ pad_right('bat', 5) }}") (is (= (render "{{ pad_right('bat', 5) }}" nil)
"bat ")) "bat "))
(is (= (render "{{ pad_right('bat', 3) }}") (is (= (render "{{ pad_right('bat', 3) }}" nil)
"bat")) "bat"))
(is (= (render "{{ pad_right('bat', 5, 'x') }}") (is (= (render "{{ pad_right('bat', 5, 'x') }}" nil)
"batxx"))) "batxx")))
(testing "random" (testing "random"
(is (some #{(render "{{ ['apple', 'orange', 'citrus']|random }}")} (is (some #{(render "{{ ['apple', 'orange', 'citrus']|random }}" nil)}
["apple" "orange" "citrus"])) ["apple" "orange" "citrus"]))
(is (some #{(render "{{ \"ABC\"|random }}")} (is (some #{(render "{{ \"ABC\"|random }}" nil)}
["A" "B" "C"]))) ["A" "B" "C"])))
(testing "range" (testing "range"
(is (= (render "{{ range(1, 5) }}") (is (= (render "{{ range(1, 5) }}" nil)
"[1, 2, 3, 4]")) "[1, 2, 3, 4]"))
(is (= (render "{{ range(1, 5, 2) }}") (is (= (render "{{ range(1, 5, 2) }}" nil)
"[1, 3]"))) "[1, 3]")))
(testing "repeat" (testing "repeat"
(is (= (render "{{ repeat('x', 10) }}") (is (= (render "{{ repeat('x', 10) }}" nil)
"xxxxxxxxxx")) "xxxxxxxxxx"))
(is (= (render "{{ repeat('x', 0) }}") (is (= (render "{{ repeat('x', 0) }}" nil)
""))) "")))
(testing "rest" (testing "rest"
(is (= (render "{{ [1, 2, 3, 4, 5]|rest }}") (is (= (render "{{ [1, 2, 3, 4, 5]|rest }}" nil)
"[2, 3, 4, 5]"))) "[2, 3, 4, 5]")))
(testing "second" (testing "second"
(is (= (render "{{ [1, 2, 3, 4, 5]|second }}") (is (= (render "{{ [1, 2, 3, 4, 5]|second }}" nil)
"2"))) "2")))
(testing "sort" (testing "sort"
(is (= (render "{{ [2, 1, 5, 3, 4]|sort }}") (is (= (render "{{ [2, 1, 5, 3, 4]|sort }}" nil)
"[1, 2, 3, 4, 5]"))) "[1, 2, 3, 4, 5]")))
(testing "sort_descending" (testing "sort_descending"
(is (= (render "{{ [2, 1, 5, 3, 4]|sort_descending }}") (is (= (render "{{ [2, 1, 5, 3, 4]|sort_descending }}" nil)
"[5, 4, 3, 2, 1]"))) "[5, 4, 3, 2, 1]")))
(testing "sort_by" (testing "sort_by"
(is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sort_by(\"a\") }}") (is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sort_by(\"a\") }}" nil)
"[{a=1}, {a=2}, {a=3}, {a=4}, {a=5}]"))) "[{a=1}, {a=2}, {a=3}, {a=4}, {a=5}]")))
(testing "sort_descending_by" (testing "sort_descending_by"
(is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sort_descending_by(\"a\") }}") (is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sort_descending_by(\"a\") }}" nil)
"[{a=5}, {a=4}, {a=3}, {a=2}, {a=1}]"))) "[{a=5}, {a=4}, {a=3}, {a=2}, {a=1}]")))
(testing "wrap" (testing "wrap"
(is (= (render "{{ wrap(\"Here is one line of text that is going to be wrapped after 20 columns.\", 20) }}") (is (= (render "{{ wrap(\"Here is one line of text that is going to be wrapped after 20 columns.\", 20) }}" nil)
"Here is one line of\ntext that is going\nto be wrapped after\n20 columns.")) "Here is one line of\ntext that is going\nto be wrapped after\n20 columns."))
(is (= (render "{{ wrap(\"Here is one line of text that is going to be wrapped after 20 columns.\", 20, false, \"<br />\") }}") (is (= (render "{{ wrap(\"Here is one line of text that is going to be wrapped after 20 columns.\", 20, false, \"<br />\") }}" nil)
"Here is one line of<br />text that is going<br />to be wrapped after<br />20 columns.")) "Here is one line of<br />text that is going<br />to be wrapped after<br />20 columns."))
(is (= (render "{{ wrap(\"Click here to jump to the commons website - http://commons.apache.org\", 20, false) }}") (is (= (render "{{ wrap(\"Click here to jump to the commons website - http://commons.apache.org\", 20, false) }}" nil)
"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org")) "Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org"))
(is (= (render "{{ wrap(\"Click here to jump to the commons website - http://commons.apache.org\", 20, true) }}") (is (= (render "{{ wrap(\"Click here to jump to the commons website - http://commons.apache.org\", 20, true) }}" nil)
"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org")))) "Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org"))))