-
Notifications
You must be signed in to change notification settings - Fork 62
/
parser.clj
383 lines (353 loc) · 15.8 KB
/
parser.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
(ns clostache.parser
"A parser for mustache templates."
(:use [clojure.string :only (split)]
[clojure.core.incubator :only (seqable?)])
(:refer-clojure :exclude (seqable?))
(:require [clojure.java.io :as io]
[clojure.string :as str])
(:import java.util.regex.Matcher))
(defn- ^String map-str
"Apply f to each element of coll, concatenate all results into a
String."
[f coll]
(apply str (map f coll)))
(defrecord Section [name body start end inverted])
(defn- replace-all
"Applies all replacements from the replacement list to the string.
Replacements are a sequence of two element sequences where the first element
is the pattern to match and the second is the replacement.
An optional third boolean argument can be set to true if the replacement
should not be quoted."
[string replacements]
(reduce (fn [string [from to dont-quote]]
(.replaceAll (str string) from
(if dont-quote
to
(Matcher/quoteReplacement to))))
string replacements))
(defn- escape-html
"Replaces angle brackets with the respective HTML entities."
[string]
(replace-all string [["&" "&"]
["\"" """]
["<" "<"]
[">" ">"]
["'","'"]]))
(defn- indent-partial
"Indent all lines of the partial by indent."
[partial indent]
(replace-all partial [["(\r\n|[\r\n])(.+)" (str "$1" indent "$2") true]]))
(def regex-chars ["\\" "{" "}" "[" "]" "(" ")" "." "?" "^" "+" "-" "|"])
(defn- escape-regex
"Escapes characters that have special meaning in regular expressions."
[regex]
(replace-all regex (map #(repeat 2 (str "\\" %)) regex-chars)))
(defn- unescape-regex
"Unescapes characters that have special meaning in regular expressions."
[regex]
(replace-all regex (map (fn [char] [(str "\\\\\\" char) char true])
regex-chars)))
(defn- process-set-delimiters
"Replaces custom set delimiters with mustaches."
[^String template data]
(let [builder (StringBuilder. template)
data (atom data)
open-delim (atom "\\{\\{")
close-delim (atom "\\}\\}")
set-delims (fn [open close]
(doseq [[var delim]
[[open-delim open] [close-delim close]]]
(swap! var (constantly (escape-regex delim)))))]
(loop [offset 0]
(let [string (.toString builder)
custom-delim (not (= "\\{\\{" @open-delim))
matcher (re-matcher
(re-pattern (str "(" @open-delim ".*?" @close-delim
(if custom-delim
(str "|\\{\\{.*?\\}\\}"))
")"))
string)]
(if (.find matcher offset)
(let [match-result (.toMatchResult matcher)
match-start (.start match-result)
match-end (.end match-result)
match (.substring string match-start match-end)]
(if (and custom-delim
(= "{{" (.substring string match-start (+ match-start 2))))
(if-let [tag (re-find #"\{\{(.*?)\}\}" match)]
(do
(.replace builder match-start match-end
(str "\\{\\{" (second tag) "\\}\\}"))
(recur match-end)))
(if-let [delim-change (re-find
(re-pattern (str @open-delim
"=\\s*(.*?) (.*?)\\s*="
@close-delim))
match)]
(do
(apply set-delims (rest delim-change))
(.delete builder match-start match-end)
(recur match-start))
(if-let [tag (re-find
(re-pattern (str @open-delim "(.*?)"
@close-delim))
match)]
(let [section-start (re-find (re-pattern
(str "^"
@open-delim
"\\s*#\\s*(.*?)\\s*"
@close-delim))
(first tag))
key (if section-start (keyword (second section-start)))
value (if key (key @data))]
(if (and value (fn? value)
(not (and (= @open-delim "\\{\\{")
(= @close-delim "\\}\\}"))))
(swap! data
#(update-in % [key]
(fn [old]
(fn [data]
(str "{{="
(unescape-regex @open-delim)
" "
(unescape-regex @close-delim)
"=}}"
(old data)))))))
(.replace builder match-start match-end
(str "{{" (second tag) "}}"))
(recur match-end)))))))))
[(.toString builder) @data]))
(defn- create-partial-replacements
"Creates pairs of partial replacements."
[template partials]
(apply concat
(for [k (keys partials)]
(let [regex (re-pattern (str "(\r\n|[\r\n]|^)([ \\t]*)\\{\\{>\\s*"
(name k) "\\s*\\}\\}"))
indent (nth (first (re-seq (re-pattern regex) template)) 2)]
[[(str "\\{\\{>\\s*" (name k) "\\s*\\}\\}")
(first (process-set-delimiters (indent-partial (str (k partials))
indent) {}))]]))))
(defn- include-partials
"Include partials within the template."
[template partials]
(replace-all template (create-partial-replacements template partials)))
(defn- remove-comments
"Removes comments from the template."
[template]
(let [comment-regex "\\{\\{\\![^\\}]*\\}\\}"]
(replace-all template [[(str "(^|[\n\r])[ \t]*" comment-regex
"(\r\n|[\r\n]|$)") "$1" true]
[comment-regex ""]])))
(defn- next-index
"Return the next index of the supplied regex."
([section regex]
(next-index section regex 0))
([^String section regex index]
(if (= index -1)
-1
(let [s (.substring section index)
matcher (re-matcher regex s)]
(if (nil? (re-find matcher))
-1
(+ index (.start (.toMatchResult matcher))))))))
(defn- find-section-start-tag
"Find the next section start tag, starting to search at index."
[^String template index]
(next-index template #"\{\{[#\^]" index))
(defn- find-section-end-tag
"Find the matching end tag for a section at the specified level,
starting to search at index."
[^String template ^long index ^long level]
(let [next-start (find-section-start-tag template index)
next-end (.indexOf ^String template "{{/" index)]
(if (= next-end -1)
-1
(if (and (not (= next-start -1)) (< next-start next-end))
(find-section-end-tag template (+ next-start 3) (inc level))
(if (= level 1)
next-end
(find-section-end-tag template (+ next-end 3) (dec level)))))))
(defn- extract-section
"Extracts the outer section from the template."
[^String template]
(let [^Long start (find-section-start-tag template 0)]
(if (= start -1)
nil
(let [inverted (= (str (.charAt template (+ start 2))) "^")
^Long end-tag (find-section-end-tag template (+ start 3) 1)]
(if (= end-tag -1)
nil
(let [end (+ (.indexOf template "}}" end-tag) 2)
section (.substring template start end)
body-start (+ (.indexOf section "}}") 2)
body-end (.lastIndexOf section "{{")
body (if (or (= body-start -1) (= body-end -1)
(< body-end body-start))
""
(.substring section body-start body-end))
section-name (.trim (.substring section 3
(.indexOf section "}}")))]
(Section. section-name body start end inverted)))))))
(defn- replace-all-callback
"Replaces each occurrence of the regex with the return value of the callback."
[^String string regex callback]
(str/replace string regex #(callback %)))
(declare render-template)
(defn replace-variables
"Replaces variables in the template with their values from the data."
[template data partials]
(let [regex #"\{\{(\{|\&|\>|)\s*(.*?)\s*\}{2,3}"]
(replace-all-callback template regex
#(let [var-name (nth % 2)
var-k (keyword var-name)
var-type (second %)
var-value (var-k data)
var-value (if (fn? var-value)
(render-template
(var-value)
(dissoc data var-name)
partials)
var-value)
var-value (str var-value)]
(cond (= var-type "") (escape-html var-value)
(= var-type ">") (render-template (var-k partials) data partials)
:else var-value)))))
(defn- join-standalone-delimiter-tags
"Remove newlines after standalone (i.e. on their own line) delimiter tags."
[template]
(replace-all
template
(let [eol-start "(\r\n|[\r\n]|^)"
eol-end "(\r\n|[\r\n]|$)"]
[[(str eol-start "[ \t]*(\\{\\{=[^\\}]*\\}\\})" eol-end) "$1$2"
true]])))
(defn- path-data
"Extract the data for the supplied path."
[elements data]
(get-in data (map keyword elements)))
(defn- convert-path
"Convert a tag with a dotted name to nested sections, using the
supplied delimiters to access the value."
[tag open-delim close-delim data]
(let [tag-type (last open-delim)
section-tag (some #{tag-type} [\# \^ \/])
section-end-tag (= tag-type \/)
builder (StringBuilder.)
tail-builder (if section-tag nil (StringBuilder.))
elements (split tag #"\.")
element-to-invert (if (= tag-type \^)
(loop [path [(first elements)]
remaining-elements (rest elements)]
(if (not (empty? remaining-elements))
(if (nil? (path-data path data))
(last path)
(recur (conj path (first remaining-elements))
(next remaining-elements))))))]
(if (and (not section-tag) (nil? (path-data elements data)))
""
(let [elements (if section-end-tag (reverse elements) elements)]
(do
(doseq [element (butlast elements)]
(.append builder (str "{{" (if section-end-tag "/"
(if (= element element-to-invert)
"^" "#"))
element "}}"))
(if (not (nil? tail-builder))
(.insert tail-builder 0 (str "{{/" element "}}"))))
(.append builder (str open-delim (last elements) close-delim))
(str (.toString builder) (if (not (nil? tail-builder))
(.toString tail-builder))))))))
(defn- convert-paths
"Converts tags with dotted tag names to nested sections."
[^String template data]
(loop [^String s ^String template]
(let [matcher (re-matcher #"(\{\{[\{&#\^/]?)([^\}]+\.[^\}]+)(\}{2,3})" s)]
(if-let [match (re-find matcher)]
(let [match-start (.start matcher)
match-end (.end matcher)
converted (convert-path (str/trim (nth match 2)) (nth match 1)
(nth match 3) data)]
(recur (str (.substring s 0 match-start) converted
(.substring s match-end))))
s))))
(defn- join-standalone-tags
"Remove newlines after standalone (i.e. on their own line) section/partials
tags."
[template]
(replace-all
template
(let [eol-start "(\r\n|[\r\n]|^)"
eol-end "(\r\n|[\r\n]|$)"]
[[(str eol-start
"\\{\\{[#\\^][^\\}]*\\}\\}(\r\n|[\r\n])\\{\\{/[^\\}]*\\}\\}"
eol-end)
"$1" true]
[(str eol-start "[ \t]*(\\{\\{[#\\^/][^\\}]*\\}\\})" eol-end) "$1$2"
true]
[(str eol-start "([ \t]*\\{\\{>\\s*[^\\}]*\\s*\\}\\})" eol-end) "$1$2"
true]])))
(defn- preprocess
"Preprocesses template and data (e.g. removing comments)."
[template data partials]
(let [template (join-standalone-delimiter-tags template)
[template data] (process-set-delimiters template data)
template (join-standalone-tags template)
template (remove-comments template)
template (include-partials template partials)
template (convert-paths template data)]
[template data]))
(defn- render-section
[section data partials]
(let [section-data ((keyword (:name section)) data)]
(if (:inverted section)
(if (or (and (seqable? section-data) (empty? section-data))
(not section-data))
(:body section))
(if section-data
(if (fn? section-data)
(let [result (section-data (:body section))]
(if (fn? result)
(result #(render-template % data partials))
result))
(let [section-data (cond (sequential? section-data) section-data
(map? section-data) [section-data]
(seqable? section-data) (seq section-data)
:else [{}])
section-data (if (map? (first section-data))
section-data
(map (fn [e] {(keyword ".") e})
section-data))
section-data (map #(conj data %) section-data)]
(map-str (fn [m]
(render-template (:body section) m partials))
section-data)))))))
(defn- render-template
"Renders the template with the data and partials."
[^String template data partials]
(let [[^String template data] (preprocess template data partials)
^String section (extract-section template)]
(if (nil? section)
(replace-variables template data partials)
(let [before (.substring template 0 (:start section))
after (.substring template (:end section))]
(recur (str before (render-section section data partials) after) data
partials)))))
(defn render
"Renders the template with the data and, if supplied, partials."
([template]
(render template {} {}))
([template data]
(render template data {}))
([template data partials]
(replace-all (render-template template data partials)
[["\\\\\\{\\\\\\{" "{{"]
["\\\\\\}\\\\\\}" "}}"]])))
(defn render-resource
"Renders a resource located on the classpath"
([^String path]
(render (slurp (io/resource path)) {}))
([^String path data]
(render (slurp (io/resource path)) data))
([^String path data partials]
(render (slurp (io/resource path)) data partials)))