Merge branch 'master' into java6

Conflicts:
	project.clj
This commit is contained in:
Gered 2014-03-23 11:47:42 -04:00
commit b66cdbf79e
8 changed files with 241 additions and 54 deletions

View file

@ -1,7 +1,8 @@
(defproject clj-jtwig-java6 "0.2.2" (defproject clj-jtwig-java6 "0.3"
:description "Clojure wrapper for JTwig (Java 6 dependencies)" :description "Clojure wrapper for JTwig (Java 6 dependencies)"
:url "https://github.com/gered/clj-jtwig/tree/java6" :url "https://github.com/gered/clj-jtwig/tree/java6"
: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"}
:dependencies [[org.clojure/clojure "1.5.1"] :dependencies [[org.clojure/clojure "1.5.1"]
[com.lyncode/jtwig-core-java6 "2.1.2"]]) [com.lyncode/jtwig-core-java6 "2.1.2"]
[org.apache.commons/commons-lang3 "3.1"]])

View file

@ -1,11 +1,13 @@
(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.resource ClasspathJtwigResource)
(com.lyncode.jtwig.tree.api Content) (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]])
(:use [clj-jtwig.functions])) (:use [clj-jtwig.functions]
[clj-jtwig.utils]))
; global options ; global options
(defonce options (atom {; true/false to enable/disable compiled template caching when using templates from (defonce options (atom {; true/false to enable/disable compiled template caching when using templates from
@ -55,20 +57,15 @@
(.compile))) (.compile)))
(defn- compile-template-file [^File file] (defn- compile-template-file [^File file]
(->> file
(new JtwigTemplate)
(.compile)))
(defn- inside-jar? [^File file]
(-> file
(.getPath)
; the path of a file inside a jar looks something like "jar:file:/path/to/file.jar!/path/to/file"
(.contains "jar!")))
(defn- get-file-last-modified [^File file]
(if (inside-jar? file) (if (inside-jar? file)
0 (->> (.getPath file)
(.lastModified file))) (get-jar-resource-filename)
(new ClasspathJtwigResource)
(new JtwigTemplate)
(.compile))
(->> file
(new JtwigTemplate)
(.compile))))
(defn- newer? [^File file other-timestamp] (defn- newer? [^File file other-timestamp]
(let [file-last-modified (get-file-last-modified file)] (let [file-last-modified (get-file-last-modified file)]
@ -83,7 +80,7 @@
; this function really only exists so i can easily change the exception type / message in the future ; this function really only exists so i can easily change the exception type / message in the future
; since this file-exists check is needed in a few places ; since this file-exists check is needed in a few places
(defn- err-if-no-file [^File file] (defn- err-if-no-file [^File file]
(if-not (.exists file) (if-not (exists? file)
(throw (new FileNotFoundException (str "Template file \"" file "\" not found."))))) (throw (new FileNotFoundException (str "Template file \"" file "\" not found.")))))
(defn- cache-compiled-template! [^File file create-fn] (defn- cache-compiled-template! [^File file create-fn]
@ -129,12 +126,6 @@
[] []
(reset! compiled-templates {})) (reset! compiled-templates {}))
(defn- get-resource-path
(^URL [^String filename]
(-> (Thread/currentThread)
(.getContextClassLoader)
(.getResource filename))))
(defn- make-model-map [model-map-values {:keys [skip-model-map-stringify?] :as options}] (defn- make-model-map [model-map-values {:keys [skip-model-map-stringify?] :as options}]
(let [model-map-obj (new JtwigModelMap) (let [model-map-obj (new JtwigModelMap)
values (if-not skip-model-map-stringify? values (if-not skip-model-map-stringify?
@ -177,4 +168,5 @@
the template." the template."
[^String filename model-map & [options]] [^String filename model-map & [options]]
(if-let [resource-filename (get-resource-path filename)] (if-let [resource-filename (get-resource-path filename)]
(render-file (.getPath resource-filename) model-map options))) (render-file (.getPath resource-filename) model-map options)
(throw (new FileNotFoundException (str "Template file \"" filename "\" not found.")))))

View file

@ -4,7 +4,8 @@
(com.lyncode.jtwig.functions.repository DefaultFunctionRepository) (com.lyncode.jtwig.functions.repository DefaultFunctionRepository)
(com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException)) (com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException))
(:require [clj-jtwig.convert :refer [java->clojure clojure->java]]) (:require [clj-jtwig.convert :refer [java->clojure clojure->java]])
(:use [clj-jtwig.standard-functions])) (:use [clj-jtwig.standard-functions]
[clj-jtwig.web.web-functions]))
(defn- make-function-handler [f] (defn- make-function-handler [f]
(reify JtwigFunction (reify JtwigFunction
@ -21,15 +22,18 @@
(aset array index (nth aliases index))) (aset array index (nth aliases index)))
array)) array))
(defn- add-function-library! [repository functions]
(doseq [[name {:keys [aliases fn]}] functions]
(.add repository
(make-function-handler fn)
name
(make-aliased-array aliases)))
repository)
(defn- create-function-repository [] (defn- create-function-repository []
(let [repository (new DefaultFunctionRepository (make-array JtwigFunction 0))] (doto (new DefaultFunctionRepository (make-array JtwigFunction 0))
; always add our standard functions to new repository objects (add-function-library! standard-functions)
(doseq [[name {:keys [aliases fn]}] standard-functions] (add-function-library! web-functions)))
(.add repository
(make-function-handler fn)
name
(make-aliased-array aliases)))
repository))
; we'll be reusing the same function repository object for all contexts created when rendering templates. ; we'll be reusing the same function repository object for all contexts created when rendering templates.
; any custom functions added will be added to this instance ; any custom functions added will be added to this instance

View file

@ -1,6 +1,8 @@
(ns clj-jtwig.standard-functions (ns clj-jtwig.standard-functions
"standard function definitions. these are functions that are not yet included in JTwig's standard function "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." 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])) (:use [clojure.pprint]))
; we are using a separate map to hold the standard functions instead of using deftwigfn, etc. because doing it this ; we are using a separate map to hold the standard functions instead of using deftwigfn, etc. because doing it this
@ -23,6 +25,14 @@
(-> sequence vals butlast) (-> sequence vals butlast)
(butlast sequence)))} (butlast sequence)))}
"capitalize_all"
{:fn (fn [s]
(WordUtils/capitalize s))}
"center"
{:fn (fn [s size & [padding-string]]
(StringUtils/center s size (or padding-string " ")))}
"dump" "dump"
{:fn (fn [x] {:fn (fn [x]
(with-out-str (with-out-str
@ -33,15 +43,6 @@
(with-out-str (with-out-str
(clojure.pprint/print-table x)))} (clojure.pprint/print-table x)))}
"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))))}
"max" "max"
{:fn (fn [& numbers] {:fn (fn [& numbers]
(if (coll? (first numbers)) (if (coll? (first numbers))
@ -54,6 +55,27 @@
(apply min (first numbers)) (apply min (first numbers))
(apply min 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" "random"
{:fn (fn [& values] {:fn (fn [& values]
(let [first-value (first values)] (let [first-value (first values)]
@ -78,6 +100,10 @@
{:fn (fn [low high & [step]] {:fn (fn [low high & [step]]
(range low high (or step 1)))} (range low high (or step 1)))}
"repeat"
{:fn (fn [s n]
(StringUtils/repeat s n))}
"rest" "rest"
{:fn (fn [sequence] {:fn (fn [sequence]
; matching behaviour of jtwig's first/last implementation ; matching behaviour of jtwig's first/last implementation
@ -108,4 +134,14 @@
"sort_descending_by" "sort_descending_by"
{:fn (fn [coll k] {:fn (fn [coll k]
(sort-by #(get % k) #(compare %2 %1) coll)) (sort-by #(get % k) #(compare %2 %1) coll))
:aliases ["sort_desc_by"]}}) :aliases ["sort_desc_by"]}
"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?)))}})

51
src/clj_jtwig/utils.clj Normal file
View file

@ -0,0 +1,51 @@
(ns clj-jtwig.utils
"various helper / utility functions"
(:import (java.net URL)
(java.io File)
(java.util.jar JarFile)))
(defn inside-jar? [^File file]
(-> file
(.getPath)
; the path of a file inside a jar looks something like "jar:file:/path/to/file.jar!/path/inside/jar/to/file"
(.contains "jar!")))
(defn get-jar-resource-filename [^String resource-filename]
(let [pos (.indexOf resource-filename "jar!")]
(if-not (= -1 pos)
(subs resource-filename (+ pos 5))
resource-filename)))
(defn get-jar-filename [^String resource-filename]
(let [start (.indexOf resource-filename "file:")
end (.indexOf resource-filename "jar!")]
(if (and (not= -1 start)
(not= -1 end))
(subs resource-filename 5 (+ end 3))
resource-filename)))
(defn exists? [^File file]
(if (inside-jar? file)
(let [filename (.getPath file)
jar-file (new JarFile (get-jar-filename filename))
jar-entry (.getJarEntry jar-file (get-jar-resource-filename filename))]
(not (nil? jar-entry)))
(.exists file)))
(defn get-file-last-modified [^File file]
(if (inside-jar? file)
0
(.lastModified file)))
(defn get-resource-path
(^URL [^String filename]
(-> (Thread/currentThread)
(.getContextClassLoader)
(.getResource filename))))
(defn get-resource-modification-date [^String filename]
(when-let [resource-filename (get-resource-path filename)]
(->> resource-filename
(.getPath)
(new File)
(get-file-last-modified))))

View file

@ -0,0 +1,12 @@
(ns clj-jtwig.web.middleware)
(declare ^:dynamic *servlet-context-path*)
(defn wrap-servlet-context-path
"Binds the current request's context path to a var which we can use in
various jtwig functions that need it without having to explicitly
pass the path in as a function parameter."
[handler]
(fn [req]
(binding [*servlet-context-path* (:context req)]
(handler req))))

View file

@ -0,0 +1,43 @@
(ns clj-jtwig.web.web-functions
"web functions, intended to be used by web applications only. most of these will require the
current servlet context path, so use of clj-jtwig.web.middleware.wrap-servlet-context-path
is a prerequisite for these functions."
(:import (java.net URI))
(:require [clj-jtwig.web.middleware :refer [*servlet-context-path*]]
[clojure.string :as str])
(:use [clj-jtwig.utils]))
(defn- get-context-url [url]
(str *servlet-context-path* url))
(defn- relative-url? [url]
(if-not (str/blank? url)
(let [uri (new URI url)]
(str/blank? (.getScheme uri)))))
(defn- get-url-string [url]
(if-let [modification-timestamp (if (relative-url? url)
;; TODO: while 'public' is the default with Compojure, applications can override with something else ...
(->> (str "public" url)
(get-context-url)
(get-resource-modification-date)))]
(str url "?" modification-timestamp)
url))
; defined using the same type of map structure as in clj-jtwig.standard-functions
(defonce web-functions
{"path"
{:fn (fn [url]
(get-context-url url))}
"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\" />")]
(format fmt (get-url-string url) media)))}
"javascript"
{:fn (fn [url]
(format "<script type=\"text/javascript\" src=\"%s\"></script>" (get-url-string url)))}})

View file

@ -284,6 +284,18 @@
(is (= (render "{{ [1, 2, 3, 4, 5]|butlast }}" nil) (is (= (render "{{ [1, 2, 3, 4, 5]|butlast }}" nil)
"[1, 2, 3, 4]"))) "[1, 2, 3, 4]")))
(testing "capitalize_all"
(is (= (render "{{ capitalize_all('hello world') }}" nil)
"Hello World")))
(testing "center"
(is (= (render "{{ center('bat', 5) }}" nil)
" bat "))
(is (= (render "{{ center('bat', 3) }}" nil)
"bat"))
(is (= (render "{{ center('bat', 5, 'x') }}" nil)
"xbatx")))
(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"]})
"({\"foo\" \"bar\"} (1 2 3) \"hello\")\n"))) "({\"foo\" \"bar\"} (1 2 3) \"hello\")\n")))
@ -292,16 +304,6 @@
(is (= (render "{{ t|dump_table }}", {:t [{:a 1 :b 2 :c 3} {:b 5 :a 7 :c "dog"}]}) (is (= (render "{{ t|dump_table }}", {:t [{:a 1 :b 2 :c 3} {:b 5 :a 7 :c "dog"}]})
"\n| b | c | a |\n|---+-----+---|\n| 2 | 3 | 1 |\n| 5 | dog | 7 |\n"))) "\n| b | c | a |\n|---+-----+---|\n| 2 | 3 | 1 |\n| 5 | dog | 7 |\n")))
(testing "nth"
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(2) }}" nil)
"3"))
(is (thrown-with-msg?
Exception
#"java.lang.IndexOutOfBoundsException"
(render "{{ [1, 2, 3, 4, 5]|nth(6) }}" nil)))
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(6, \"not found\") }}" nil)
"not found")))
(testing "max" (testing "max"
(is (= (render "{{ [2, 1, 5, 3, 4]|max }}" nil) (is (= (render "{{ [2, 1, 5, 3, 4]|max }}" nil)
"5")) "5"))
@ -314,6 +316,36 @@
(is (= (render "{{ min(2, 1, 5, 3, 4) }}" nil) (is (= (render "{{ min(2, 1, 5, 3, 4) }}" nil)
"1"))) "1")))
(testing "normalize_space"
(is (= (render "{{ normalize_space(' hello world ') }}" nil)
"hello world")))
(testing "nth"
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(2) }}" nil)
"3"))
(is (thrown-with-msg?
Exception
#"java.lang.IndexOutOfBoundsException"
(render "{{ [1, 2, 3, 4, 5]|nth(6) }}" nil)))
(is (= (render "{{ [1, 2, 3, 4, 5]|nth(6, \"not found\") }}" nil)
"not found")))
(testing "pad_left"
(is (= (render "{{ pad_left('bat', 5) }}" nil)
" bat"))
(is (= (render "{{ pad_left('bat', 3) }}" nil)
"bat"))
(is (= (render "{{ pad_left('bat', 5, 'x') }}" nil)
"xxbat")))
(testing "pad_right"
(is (= (render "{{ pad_right('bat', 5) }}" nil)
"bat "))
(is (= (render "{{ pad_right('bat', 3) }}" nil)
"bat"))
(is (= (render "{{ pad_right('bat', 5, 'x') }}" nil)
"batxx")))
(testing "random" (testing "random"
(is (some #{(render "{{ ['apple', 'orange', 'citrus']|random }}" nil)} (is (some #{(render "{{ ['apple', 'orange', 'citrus']|random }}" nil)}
["apple" "orange" "citrus"])) ["apple" "orange" "citrus"]))
@ -326,6 +358,12 @@
(is (= (render "{{ range(1, 5, 2) }}" nil) (is (= (render "{{ range(1, 5, 2) }}" nil)
"[1, 3]"))) "[1, 3]")))
(testing "repeat"
(is (= (render "{{ repeat('x', 10) }}" nil)
"xxxxxxxxxx"))
(is (= (render "{{ repeat('x', 0) }}" nil)
"")))
(testing "rest" (testing "rest"
(is (= (render "{{ [1, 2, 3, 4, 5]|rest }}" nil) (is (= (render "{{ [1, 2, 3, 4, 5]|rest }}" nil)
"[2, 3, 4, 5]"))) "[2, 3, 4, 5]")))
@ -348,4 +386,14 @@
(testing "sort_descending_by" (testing "sort_descending_by"
(is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sort_descending_by(\"a\") }}" nil) (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"
(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."))
(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."))
(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"))
(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"))))