update core template rendering, and big changes to function handling

This commit is contained in:
Gered 2014-06-18 09:48:10 -04:00
parent d3eb6e844c
commit f1558b72a1
5 changed files with 208 additions and 231 deletions

View file

@ -1,8 +1,11 @@
(ns clj-jtwig.core
"wrapper functions for working with JTwig from clojure"
(:import (com.lyncode.jtwig JtwigTemplate JtwigContext JtwigModelMap)
(com.lyncode.jtwig.content.api Renderable)
(com.lyncode.jtwig.configuration JtwigConfiguration)
(com.lyncode.jtwig.render RenderContext)
(com.lyncode.jtwig.render.config RenderConfiguration)
(com.lyncode.jtwig.resource ClasspathJtwigResource)
(com.lyncode.jtwig.tree.api Content)
(java.io File FileNotFoundException ByteArrayOutputStream)
(java.net URL))
(:require [clojure.walk :refer [stringify-keys]])
@ -10,6 +13,8 @@
[clj-jtwig.utils]
[clj-jtwig.options]))
(defonce configuration (JtwigConfiguration.))
(declare flush-template-cache!)
(defn set-options!
@ -26,7 +31,7 @@
(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
; last modification timestamp and :template which is a com.lyncode.jtwig.tree.api.Content object which has been
; last modification timestamp and :template which is a com.lyncode.jtwig.content.api.Renderable object which has been
; compiled already and can be rendered by calling it's 'render' method
(defonce compiled-templates (atom {}))
@ -119,12 +124,11 @@
(new JtwigContext model-map-obj @functions)))
(defn- render-compiled-template
[^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)]
(.render compiled-template stream context)
[^Renderable renderable model-map]
(with-open [stream (new ByteArrayOutputStream)]
(let [context (make-context model-map)
render-context (RenderContext/create (.render configuration) context stream)]
(.render renderable render-context)
(.toString stream))))
(defn render
@ -132,15 +136,15 @@
as the model for the template. templates rendered using this function are always
parsed, compiled and rendered. the compiled results are never cached."
[s model-map]
(let [compiled-template (compile-template-string s)]
(render-compiled-template compiled-template model-map)))
(let [renderable (compile-template-string s)]
(render-compiled-template renderable model-map)))
(defn render-file
"renders a template from a file, using the values in model-map as the model for the template"
[^String filename model-map]
(let [file (new File filename)
compiled-template (compile-template! file)]
(render-compiled-template compiled-template model-map)))
(let [file (new File filename)
renderable (compile-template! file)]
(render-compiled-template renderable model-map)))
(defn render-resource
"renders a template from a resource file, using the values in the model-map as the model for

View file

@ -0,0 +1,33 @@
(ns clj-jtwig.function-utils
(: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,37 +1,26 @@
(ns clj-jtwig.functions
"custom template function/filter support functions."
(:import (com.lyncode.jtwig.functions JtwigFunction)
(com.lyncode.jtwig.functions.repository DefaultFunctionRepository)
(com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException))
(:require [clj-jtwig.convert :refer [java->clojure clojure->java]])
(:import (com.lyncode.jtwig.functions.repository FunctionResolver)
(com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException)
(com.lyncode.jtwig.functions.annotations JtwigFunction)
(com.lyncode.jtwig.functions.parameters GivenParameters)
(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]
[clj-jtwig.web.web-functions]))
(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)))))))
(defn- make-aliased-array [aliases]
(let [n (count aliases)
array (make-array String n)]
(doseq [index (range n)]
(aset array index (nth aliases index)))
array))
(def object-array-type (Class/forName "[Ljava.lang.Object;"))
(def function-parameters (doto (GivenParameters.)
(.add (to-array [object-array-type]))))
(defn- add-function-library! [repository functions]
(doseq [[name {:keys [aliases fn]}] functions]
(.add repository
(make-function-handler fn)
name
(make-aliased-array aliases)))
(doseq [fn-obj functions]
(.store repository fn-obj))
repository)
(defn- create-function-repository []
(doto (new DefaultFunctionRepository (make-array JtwigFunction 0))
(doto (new FunctionResolver)
(add-function-library! standard-functions)
(add-function-library! web-functions)))
@ -44,36 +33,30 @@
[]
(reset! functions (create-function-repository)))
(defn function-exists? [^String name]
(defn get-function [^String name]
(try
(.retrieve @functions name)
true
(catch FunctionNotFoundException ex
false)))
(.get @functions name function-parameters)
(catch FunctionNotFoundException ex)))
(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))))
(defn function-exists? [^String name]
(not (nil? (get-function name))))
(defmacro deftwigfn
"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
template. functions defined this way have no aliases and can only be called by the name given."
[fn-name args & body]
`(do
(add-function! ~fn-name (fn ~args ~@body))))
`(let [f# (fn ~args ~@body)]
(.store
@functions
(make-function-handler fn-name nil f#))))
(defmacro defaliasedtwigfn
"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
the last form in body is returned to the template."
[fn-name args aliases & body]
`(do
(add-function! ~fn-name ~aliases (fn ~args ~@body))))
`(let [f# (fn ~args ~@body)]
(.store
@functions
(make-function-handler fn-name aliases f#))))

View file

@ -4,6 +4,7 @@
(: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]
@ -12,192 +13,152 @@
(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).
(deflibrary standard-functions
(library-aliased-function "blank_if_null" ["nonull"] [x]
(if (nil? x) "" x))
; 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.
(library-function "butlast" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals butlast)
(butlast sequence)))
(defonce standard-functions
{"blank_if_null"
{:fn (fn [x]
(if (nil? x) "" x))
:aliases ["nonull"]}
(library-function "center" [s size & [padding-string]]
(StringUtils/center s size (or padding-string " ")))
"butlast"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals butlast)
(butlast sequence)))}
(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))))))
"center"
{:fn (fn [s size & [padding-string]]
(StringUtils/center s size (or padding-string " ")))}
(library-function "dump" [x]
(with-out-str
(clojure.pprint/pprint x)))
"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))))))}
(library-function "dump_table" [x]
(with-out-str
(clojure.pprint/print-table x)))
"dump"
{:fn (fn [x]
(with-out-str
(clojure.pprint/pprint 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))))))
"dump_table"
{:fn (fn [x]
(with-out-str
(clojure.pprint/print-table x)))}
(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))))))
"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))))))}
(library-function "max" [& numbers]
(if (coll? (first numbers))
(apply max (first numbers))
(apply max numbers)))
"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))))))}
(library-function "min" [& numbers]
(if (coll? (first numbers))
(apply min (first numbers))
(apply min numbers)))
"max"
{:fn (fn [& numbers]
(if (coll? (first numbers))
(apply max (first numbers))
(apply max numbers)))}
(library-function "normalize_space" [s]
(StringUtils/normalizeSpace s))
"min"
{:fn (fn [& numbers]
(if (coll? (first numbers))
(apply min (first numbers))
(apply min numbers)))}
(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 (first optional-not-found))
(nth values index))))
"normalize_space"
{:fn (fn [s]
(StringUtils/normalizeSpace s))}
(library-function "pad_left" [s size & [padding-string]]
(StringUtils/leftPad s size (or padding-string " ")))
"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))))}
(library-function "pad_right" [s size & [padding-string]]
(StringUtils/rightPad s size (or padding-string " ")))
"pad_left"
{:fn (fn [s size & [padding-string]]
(StringUtils/leftPad 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)
"pad_right"
{:fn (fn [s size & [padding-string]]
(StringUtils/rightPad s size (or padding-string " ")))}
(> (count values) 1)
(rand-nth values)
"random"
{:fn (fn [& values]
(let [first-value (first values)]
(cond
(and (= (count values) 1)
(coll? first-value))
(rand-nth first-value)
(string? first-value)
(rand-nth (seq first-value))
(> (count values) 1)
(rand-nth values)
(number? first-value)
(rand-int first-value)
(string? first-value)
(rand-nth (seq first-value))
:else
(rand))))
(number? first-value)
(rand-int first-value)
(library-function "range" [low high & [step]]
(range low high (or step 1)))
:else
(rand))))}
(library-function "repeat" [s n]
(StringUtils/repeat s n))
"range"
{:fn (fn [low high & [step]]
(range low high (or step 1)))}
(library-function "rest" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals rest)
(rest sequence)))
"repeat"
{:fn (fn [s n]
(StringUtils/repeat s n))}
(library-function "second" [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals second)
(second sequence)))
"rest"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals rest)
(rest sequence)))}
(library-function "sort" [sequence]
(sort < sequence))
"second"
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation
(if (map? sequence)
(-> sequence vals second)
(second sequence)))}
(library-aliased-function "sort_descending" ["sort_desc"] [sequence]
(sort > sequence))
"sort"
{:fn (fn [sequence]
(sort < sequence))}
(library-function "sort_by" [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) coll)))
"sort_descending"
{:fn (fn [sequence]
(sort > sequence))
:aliases ["sort_desc"]}
(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)))
"sort_by"
{:fn (fn [coll k]
(let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) coll)))}
(library-function "to_double" [x]
(Double/parseDouble x))
"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"]}
(library-function "to_float" [x]
(Float/parseFloat x))
"to_double"
{:fn (fn [x]
(Double/parseDouble x))}
(library-function "to_int" [x]
(Integer/parseInt x))
"to_float"
{:fn (fn [x]
(Float/parseFloat x))}
(library-function "to_keyword" [x]
(keyword x))
"to_int"
{:fn (fn [x]
(Integer/parseInt x))}
(library-function "to_long" [x]
(Long/parseLong x))
"to_keyword"
{:fn (fn [x]
(keyword 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)))
"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?)))}})
(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

@ -5,7 +5,8 @@
(:import (java.net URI))
(:require [clj-jtwig.web.middleware :refer [*servlet-context-path*]]
[clojure.string :as str])
(:use [clj-jtwig.utils]
(:use [clj-jtwig.function-utils]
[clj-jtwig.utils]
[clj-jtwig.options]))
;; TODO: while 'public' is the default with Compojure, applications can override with something else ...
@ -55,23 +56,18 @@
minified-url
url))))
; defined using the same type of map structure as in clj-jtwig.standard-functions
(deflibrary web-functions
(library-function "path" [url]
(get-context-url url))
(defonce web-functions
{"path"
{:fn (fn [url]
(get-context-url url))}
(library-function "stylesheet" [url & [media]]
(let [fmt (if media
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" media=\"%s\" />"
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" />")
resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path) media)))
"stylesheet"
{:fn (fn [url & [media]]
(let [fmt (if media
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" media=\"%s\" />"
"<link href=\"%s\" rel=\"stylesheet\" type=\"text/css\" />")
resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path) media)))}
"javascript"
{:fn (fn [url]
(let [fmt "<script type=\"text/javascript\" src=\"%s\"></script>"
resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path))))}})
(library-function "javascript" [url]
(let [fmt "<script type=\"text/javascript\" src=\"%s\"></script>"
resource-path (get-minified-resource-url url)]
(format fmt (get-url-string resource-path)))))