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 (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.configuration JtwigConfiguration)
(com.lyncode.jtwig.render RenderContext)
(com.lyncode.jtwig.render.config RenderConfiguration)
(com.lyncode.jtwig.resource ClasspathJtwigResource) (com.lyncode.jtwig.resource ClasspathJtwigResource)
(com.lyncode.jtwig.tree.api Content)
(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]])
@ -10,6 +13,8 @@
[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!
@ -26,7 +31,7 @@
(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.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 ; compiled already and can be rendered by calling it's 'render' method
(defonce compiled-templates (atom {})) (defonce compiled-templates (atom {}))
@ -119,12 +124,11 @@
(new JtwigContext model-map-obj @functions))) (new JtwigContext model-map-obj @functions)))
(defn- render-compiled-template (defn- render-compiled-template
[^Content compiled-template model-map] [^Renderable renderable 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)]
(.render compiled-template stream context) (let [context (make-context model-map)
render-context (RenderContext/create (.render configuration) context stream)]
(.render renderable render-context)
(.toString stream)))) (.toString stream))))
(defn render (defn render
@ -132,15 +136,15 @@
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."
[s model-map] [s model-map]
(let [compiled-template (compile-template-string s)] (let [renderable (compile-template-string s)]
(render-compiled-template compiled-template model-map))) (render-compiled-template renderable 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)
compiled-template (compile-template! file)] renderable (compile-template! file)]
(render-compiled-template compiled-template model-map))) (render-compiled-template renderable 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

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 (ns clj-jtwig.functions
"custom template function/filter support functions." "custom template function/filter support functions."
(:import (com.lyncode.jtwig.functions JtwigFunction) (:import (com.lyncode.jtwig.functions.repository FunctionResolver)
(com.lyncode.jtwig.functions.repository DefaultFunctionRepository) (com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException)
(com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException)) (com.lyncode.jtwig.functions.annotations JtwigFunction)
(:require [clj-jtwig.convert :refer [java->clojure clojure->java]]) (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] (:use [clj-jtwig.standard-functions]
[clj-jtwig.web.web-functions])) [clj-jtwig.web.web-functions]))
(defn- make-function-handler [f] (def object-array-type (Class/forName "[Ljava.lang.Object;"))
(reify JtwigFunction (def function-parameters (doto (GivenParameters.)
(execute [_ arguments] (.add (to-array [object-array-type]))))
(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))
(defn- add-function-library! [repository functions] (defn- add-function-library! [repository functions]
(doseq [[name {:keys [aliases fn]}] functions] (doseq [fn-obj functions]
(.add repository (.store repository fn-obj))
(make-function-handler fn)
name
(make-aliased-array aliases)))
repository) repository)
(defn- create-function-repository [] (defn- create-function-repository []
(doto (new DefaultFunctionRepository (make-array JtwigFunction 0)) (doto (new FunctionResolver)
(add-function-library! standard-functions) (add-function-library! standard-functions)
(add-function-library! web-functions))) (add-function-library! web-functions)))
@ -44,36 +33,30 @@
[] []
(reset! functions (create-function-repository))) (reset! functions (create-function-repository)))
(defn function-exists? [^String name] (defn get-function [^String name]
(try (try
(.retrieve @functions name) (.get @functions name function-parameters)
true (catch FunctionNotFoundException ex)))
(catch FunctionNotFoundException ex
false)))
(defn add-function! (defn function-exists? [^String name]
"adds a new template function under the name specified. templates can call the function by the (not (nil? (get-function name))))
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]
`(do `(let [f# (fn ~args ~@body)]
(add-function! ~fn-name (fn ~args ~@body)))) (.store
@functions
(make-function-handler fn-name nil f#))))
(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]
`(do `(let [f# (fn ~args ~@body)]
(add-function! ~fn-name ~aliases (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) (:import (org.apache.commons.lang3.text WordUtils)
(org.apache.commons.lang3 StringUtils)) (org.apache.commons.lang3 StringUtils))
(:use [clojure.pprint] (:use [clojure.pprint]
[clj-jtwig.function-utils]
[clj-jtwig.options])) [clj-jtwig.options]))
(defn- possible-keyword-string [x] (defn- possible-keyword-string [x]
@ -12,98 +13,75 @@
(keyword x) (keyword x)
x)) x))
; we are using a separate map to hold the standard functions instead of using deftwigfn, etc. because doing it this (deflibrary standard-functions
; way makes it easy to re-add all these functions when/if the JTwig function repository object needs to be (library-aliased-function "blank_if_null" ["nonull"] [x]
; 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)) (if (nil? x) "" x))
:aliases ["nonull"]}
"butlast" (library-function "butlast" [sequence]
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation ; matching behaviour of jtwig's first/last implementation
(if (map? sequence) (if (map? sequence)
(-> sequence vals butlast) (-> sequence vals butlast)
(butlast sequence)))} (butlast sequence)))
"center" (library-function "center" [s size & [padding-string]]
{:fn (fn [s size & [padding-string]] (StringUtils/center s size (or padding-string " ")))
(StringUtils/center s size (or padding-string " ")))}
"contains" (library-function "contains" [coll value]
{:fn (fn [coll value]
(cond (cond
(map? coll) (contains? coll (possible-keyword-string value)) (map? coll) (contains? coll (possible-keyword-string value))
(string? coll) (.contains coll value) (string? coll) (.contains coll value)
; explicit use of '=' to allow testing for falsey values ; explicit use of '=' to allow testing for falsey values
(coll? coll) (not (nil? (some #(= value %) coll))) (coll? coll) (not (nil? (some #(= value %) coll)))
:else (throw (new Exception (str "'contains' passed invalid collection type: " (type coll))))))} :else (throw (new Exception (str "'contains' passed invalid collection type: " (type coll))))))
"dump" (library-function "dump" [x]
{:fn (fn [x]
(with-out-str (with-out-str
(clojure.pprint/pprint x)))} (clojure.pprint/pprint x)))
"dump_table" (library-function "dump_table" [x]
{:fn (fn [x]
(with-out-str (with-out-str
(clojure.pprint/print-table x)))} (clojure.pprint/print-table x)))
"index_of" (library-function "index_of" [coll value]
{:fn (fn [coll value]
(cond (cond
(instance? java.util.List coll) (.indexOf coll value) (instance? java.util.List coll) (.indexOf coll value)
(string? coll) (.indexOf coll (if (char? value) (int value) value)) (string? coll) (.indexOf coll (if (char? value) (int value) value))
:else (throw (new Exception (str "'index_of' passed invalid collection type: " (type coll))))))} :else (throw (new Exception (str "'index_of' passed invalid collection type: " (type coll))))))
"last_index_of" (library-function "last_index_of" [coll value]
{:fn (fn [coll value]
(cond (cond
(instance? java.util.List coll) (.lastIndexOf coll value) (instance? java.util.List coll) (.lastIndexOf coll value)
(string? coll) (.lastIndexOf coll (if (char? value) (int value) 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))))))} :else (throw (new Exception (str "'last_index_of' passed invalid collection type: " (type coll))))))
"max" (library-function "max" [& numbers]
{:fn (fn [& numbers]
(if (coll? (first numbers)) (if (coll? (first numbers))
(apply max (first numbers)) (apply max (first numbers))
(apply max numbers)))} (apply max numbers)))
"min" (library-function "min" [& numbers]
{:fn (fn [& numbers]
(if (coll? (first numbers)) (if (coll? (first numbers))
(apply min (first numbers)) (apply min (first numbers))
(apply min numbers)))} (apply min numbers)))
"normalize_space" (library-function "normalize_space" [s]
{:fn (fn [s] (StringUtils/normalizeSpace s))
(StringUtils/normalizeSpace s))}
"nth" (library-function "nth" [sequence index & optional-not-found]
{:fn (fn [sequence index & optional-not-found]
(let [values (if (map? sequence) ; map instance check to match behaviour of jtwig's first/last implementation (let [values (if (map? sequence) ; map instance check to match behaviour of jtwig's first/last implementation
(-> sequence vals) (-> sequence vals)
sequence)] sequence)]
(if optional-not-found (if optional-not-found
(nth values index (first optional-not-found)) (nth values index (first optional-not-found))
(nth values index))))} (nth values index))))
"pad_left" (library-function "pad_left" [s size & [padding-string]]
{:fn (fn [s size & [padding-string]] (StringUtils/leftPad s size (or padding-string " ")))
(StringUtils/leftPad s size (or padding-string " ")))}
"pad_right" (library-function "pad_right" [s size & [padding-string]]
{:fn (fn [s size & [padding-string]] (StringUtils/rightPad s size (or padding-string " ")))
(StringUtils/rightPad s size (or padding-string " ")))}
"random" (library-function "random" [& values]
{:fn (fn [& values]
(let [first-value (first values)] (let [first-value (first values)]
(cond (cond
(and (= (count values) 1) (and (= (count values) 1)
@ -120,84 +98,67 @@
(rand-int first-value) (rand-int first-value)
:else :else
(rand))))} (rand))))
"range" (library-function "range" [low high & [step]]
{:fn (fn [low high & [step]] (range low high (or step 1)))
(range low high (or step 1)))}
"repeat" (library-function "repeat" [s n]
{:fn (fn [s n] (StringUtils/repeat s n))
(StringUtils/repeat s n))}
"rest" (library-function "rest" [sequence]
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation ; matching behaviour of jtwig's first/last implementation
(if (map? sequence) (if (map? sequence)
(-> sequence vals rest) (-> sequence vals rest)
(rest sequence)))} (rest sequence)))
"second" (library-function "second" [sequence]
{:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation ; matching behaviour of jtwig's first/last implementation
(if (map? sequence) (if (map? sequence)
(-> sequence vals second) (-> sequence vals second)
(second sequence)))} (second sequence)))
"sort" (library-function "sort" [sequence]
{:fn (fn [sequence] (sort < sequence))
(sort < sequence))}
"sort_descending" (library-aliased-function "sort_descending" ["sort_desc"] [sequence]
{:fn (fn [sequence]
(sort > sequence)) (sort > sequence))
:aliases ["sort_desc"]}
"sort_by" (library-function "sort_by" [coll k]
{:fn (fn [coll k]
(let [sort-key (possible-keyword-string k)] (let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) coll)))} (sort-by #(get % sort-key) coll)))
"sort_descending_by" (library-aliased-function "sort_descending_by" ["sort_desc_by"] [coll k]
{:fn (fn [coll k]
(let [sort-key (possible-keyword-string k)] (let [sort-key (possible-keyword-string k)]
(sort-by #(get % sort-key) #(compare %2 %1) coll))) (sort-by #(get % sort-key) #(compare %2 %1) coll)))
:aliases ["sort_desc_by"]}
"to_double" (library-function "to_double" [x]
{:fn (fn [x] (Double/parseDouble x))
(Double/parseDouble x))}
"to_float" (library-function "to_float" [x]
{:fn (fn [x] (Float/parseFloat x))
(Float/parseFloat x))}
"to_int" (library-function "to_int" [x]
{:fn (fn [x] (Integer/parseInt x))
(Integer/parseInt x))}
"to_keyword" (library-function "to_keyword" [x]
{:fn (fn [x] (keyword x))
(keyword x))}
"to_long" (library-function "to_long" [x]
{:fn (fn [x] (Long/parseLong x))
(Long/parseLong x))}
"to_string" (library-function "to_string" [x]
{:fn (fn [x]
(cond (cond
(keyword? x) (name x) (keyword? x) (name x)
(instance? clojure.lang.LazySeq x) (str (seq x)) (instance? clojure.lang.LazySeq x) (str (seq x))
(coll? x) (str x) (coll? x) (str x)
:else (.toString x)))} :else (.toString x)))
"wrap" (library-function "wrap" [s length & [wrap-long-words? new-line-string]]
{:fn (fn [s length & [wrap-long-words? new-line-string]]
(WordUtils/wrap (WordUtils/wrap
s s
length length
new-line-string new-line-string
(if (nil? wrap-long-words?) (if (nil? wrap-long-words?)
false false
wrap-long-words?)))}}) wrap-long-words?))))

View file

@ -5,7 +5,8 @@
(: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.utils] (:use [clj-jtwig.function-utils]
[clj-jtwig.utils]
[clj-jtwig.options])) [clj-jtwig.options]))
;; TODO: while 'public' is the default with Compojure, applications can override with something else ... ;; TODO: while 'public' is the default with Compojure, applications can override with something else ...
@ -55,23 +56,18 @@
minified-url minified-url
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 (library-function "stylesheet" [url & [media]]
{"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)))
"javascript" (library-function "javascript" [url]
{: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)))))