/
util.clj
206 lines (172 loc) · 6.68 KB
/
util.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
(ns selmer.util
(:import java.io.StringReader
java.util.regex.Pattern))
(defmacro exception [& [param & more :as params]]
(if (class? param)
`(throw (new ~param (str ~@more)))
`(throw (Exception. (str ~@params)))))
(def ^:dynamic *custom-resource-path* nil)
(defn set-custom-resource-path!
[path]
(alter-var-root #'*custom-resource-path* (constantly path))
(when (thread-bound? #'*custom-resource-path*)
(set! *custom-resource-path* path)))
(def ^:dynamic *escape-variables* true)
(defn turn-off-escaping! []
(alter-var-root #'*escape-variables*
(constantly false)))
(defn turn-on-escaping! []
(alter-var-root #'*escape-variables*
(constantly true)))
(defmacro with-escaping [& body]
`(binding [*escape-variables* true]
~@body))
(defmacro without-escaping [& body]
`(binding [*escape-variables* false]
~@body))
(defn pattern [& content]
(re-pattern (clojure.string/join content)))
(defn read-char [^java.io.Reader rdr]
(let [ch (.read rdr)]
(if-not (== -1 ch) (char ch))))
(defn assoc-in*
"Works best for small collections seemingly."
[m ks v]
(let [k (first ks)]
(if (zero? (count ks))
(assoc m k (assoc-in* (get m k) (next ks) v))
(assoc m k v))))
;; default tag characters
(def ^:dynamic ^Character *tag-open* \{)
(def ^:dynamic ^Character *tag-close* \})
(def ^:dynamic ^Character *filter-open* \{)
(def ^:dynamic ^Character *filter-close* \})
(def ^:dynamic ^Character *tag-second* \%)
(def ^:dynamic ^Character *short-comment-second* \#)
;; tag regex patterns
(def ^:dynamic ^Pattern *tag-second-pattern* nil)
(def ^:dynamic ^Pattern *filter-open-pattern* nil)
(def ^:dynamic ^Pattern *filter-close-pattern* nil)
(def ^:dynamic ^Pattern *filter-pattern* nil)
(def ^:dynamic ^Pattern *tag-open-pattern* nil)
(def ^:dynamic ^Pattern *tag-close-pattern* nil)
(def ^:dynamic ^Pattern *tag-pattern* nil)
(def ^:dynamic ^Pattern *include-pattern* nil)
(def ^:dynamic ^Pattern *extends-pattern* nil)
(def ^:dynamic ^Pattern *block-pattern* nil)
(def ^:dynamic ^Pattern *block-super-pattern* nil)
(def ^:dynamic ^Pattern *endblock-pattern* nil)
(defn check-tag-args [args]
(if (even? (count (filter #{\"} args)))
args (exception "malformed tag arguments in " args)))
(defn read-tag-info [rdr]
(let [buf (StringBuilder.)
tag-type (if (= *filter-open* (read-char rdr)) :filter :expr)]
(loop [ch1 (read-char rdr)
ch2 (read-char rdr)]
(when-not (or (nil? ch1)
(and (or (= *filter-close* ch1) (= *tag-second* ch1))
(= *tag-close* ch2)))
(.append buf ch1)
(recur ch2 (read-char rdr))))
(let [content (->> (.toString buf)
(check-tag-args)
(re-seq #"(?:[^\s\"]|\"[^\"]*\")+")
(remove empty?)
(map (fn [^String s] (.trim s))))]
(merge {:tag-type tag-type}
(if (= :filter tag-type)
{:tag-value (first content)}
{:tag-name (keyword (first content))
:args (next content)})))))
(defn peek-rdr [^java.io.Reader rdr]
(.mark rdr 1)
(let [result (read-char rdr)]
(.reset rdr)
result))
(defmacro ->buf [[buf] & body]
`(let [~buf (StringBuilder.)]
(do ~@body)
(.toString ~buf)))
(defn read-tag-content [rdr]
(->buf [buf]
(let [next-ch (peek-rdr rdr)
filter? (not= *tag-second* next-ch)]
(.append buf *tag-open*)
(when next-ch
(loop [ch (read-char rdr)]
(.append buf ch)
(when (and (not= *tag-close* ch) (not= *filter-close* ch))
(recur (read-char rdr))))
(when filter?
(.append buf (read-char rdr)))))))
(defn open-tag? [ch rdr]
(and (= *tag-open* ch)
(let [next-ch (peek-rdr rdr)]
(or (= *filter-open* next-ch)
(= *tag-second* next-ch)))))
(defn open-short-comment? [ch rdr]
(and (= *tag-open* ch)
(let [next-ch (peek-rdr rdr)]
(= *short-comment-second* next-ch))))
(defn split-by-args [s]
(let [rdr (StringReader. s)
buf (StringBuilder.)]
(loop [items []
ch (read-char rdr)
open? false]
(cond
(nil? ch) items
(and open? (= ch \"))
(let [value (.trim (.toString buf))]
(.setLength buf 0)
(recur (conj items value) (read-char rdr) false))
(= ch \")
(recur items (read-char rdr) true)
(and (not open?) (= ch \=))
(let [id (.trim (.toString buf))]
(.setLength buf 0)
(recur (conj items id) (read-char rdr) open?))
:else
(do
(.append buf ch)
(recur items (read-char rdr) open?))))))
(defn get-resource [resource]
(-> (Thread/currentThread)
(.getContextClassLoader)
(.getResource resource)))
(defn resource-path [template]
(if (instance? java.net.URL template)
template
(if-let [path *custom-resource-path*]
(java.net.URL. (str path template))
(get-resource template))))
(defn resource-last-modified [^java.net.URL resource]
(let [path (.getPath resource)]
(try
(.lastModified (java.io.File. ^String path))
(catch NullPointerException _ -1))))
(defn check-template-exists [^java.net.URL resource]
(when-not resource
(exception "template: \"" (.getPath ^java.net.URL resource) "\" not found")))
(def default-missing-value-formatter (constantly ""))
(def ^:dynamic *missing-value-formatter* default-missing-value-formatter)
(def ^:dynamic *filter-missing-values* true)
(defn set-missing-value-formatter!
"Takes a function of two arguments which is called on a missing value.
The function should return the value to be output in place of an empty string
(which is the default from 'default-missing-value-formatter').
Call with named argument :filter-missing-values true to force filtering of missing
values (although for most use cases this will not make sense).
Arguments to missing-value-fn:
tag - map with data for the tag being evaluated.
Contains the key :tag-type with the value :filter or :expr (for filter or expression tag types.
For :filter:
tag-value - the contents of the filter tag as a string.
For :expr:
tag-name - the name of the expression.
args - the args provided to the expression.
context-map - the context-map provided to the render function."
[missing-value-fn & {:keys [filter-missing-values] :or {filter-missing-values false}}]
(alter-var-root #'*missing-value-formatter* (constantly missing-value-fn))
(alter-var-root #'*filter-missing-values* (constantly filter-missing-values)))