-
Notifications
You must be signed in to change notification settings - Fork 19
/
milk.coffee
260 lines (220 loc) · 11.1 KB
/
milk.coffee
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
# Milk is a simple, fast way to get more Mustache into your CoffeeScript and
# Javascript.
#
# Mustache templates are reasonably simple -- plain text templates are
# sprinkled with "tags", which are (by default) a pair of curly braces
# surrounding some bit of content. A good resource for Mustache can be found
# [here](mustache.github.com).
TemplateCache = {}
# Tags used for working with data get their data by looking up a name in a
# context stack. This name corresponds to a key in a hash, and the stack is
# searched top to bottom for an object with given key. Dots in names are
# special: a single dot ('.') is "top of stack", and dotted names like 'a.b.c'
# do a chained lookups.
Find = (name, stack, value = null) ->
return stack[stack.length - 1] if name == '.'
[name, parts...] = name.split(/\./)
for i in [stack.length - 1...-1]
continue unless stack[i]?
continue unless typeof stack[i] == 'object' and name of (ctx = stack[i])
value = ctx[name]
break
value = Find(part, [value]) for part in parts
# If we find a function in the stack, we'll treat it as a method, and call it
# with `this` bound to the element it came from. If a method returns a
# function, we treat it as a lambda, which doesn't have a bound `this`.
if value instanceof Function
value = do (value) -> ->
val = value.apply(ctx, arguments)
return (val instanceof Function) and val.apply(null, arguments) or val
# Null values will be coerced to the empty string.
return value
# Parsed templates are expanded by simply calling each function in turn.
Expand = (obj, tmpl, args...) -> (f.call(obj, args...) for f in tmpl).join('')
# For parsing, we'll basically need a template string to parse. We do need to
# remember to take the tag delimiters into account for the cache -- different
# parse trees can exist for the same template string!
Parse = (template, delimiters = ['{{','}}'], section = null) ->
cache = (TemplateCache[delimiters.join(' ')] ||= {})
return cache[template] if template of cache
buffer = []
# We'll use a regular expression to handle tag discovery. A proper parser
# might be faster, but this is simpler, and certainly fast enough for now.
# Since the tag delimiters may change over time, we'll want to rebuild the
# regex when they change.
BuildRegex = ->
[tagOpen, tagClose] = delimiters
return ///
([\s\S]*?) # Capture the pre-tag content
([#{' '}\t]*) # Capture the pre-tag whitespace
(?: #{tagOpen} \s* # Match the opening tag
(?:
(!) \s* ([\s\S]+?) | # Comments
(=) \s* ([\s\S]+?) \s* = | # Set Delimiters
({) \s* (\w[\S]*?) \s* } | # Triple Mustaches
([^0-9a-zA-Z._!={]?) \s* ([\w.][\S]*?) # Everything else
)
\s* #{tagClose} ) # Match the closing tag
///gm
tagPattern = BuildRegex()
tagPattern.lastIndex = pos = (section || { start: 0 }).start
# Useful errors should always be prefered - we should compile as much
# relevant information as possible.
parseError = (pos, msg) ->
(endOfLine = /$/gm).lastIndex = pos
endOfLine.exec(template)
parsedLines = template.substr(0, pos).split('\n')
lineNo = parsedLines.length
lastLine = parsedLines[lineNo - 1]
tagStart = contentEnd + whitespace.length
lastTag = template.substr(tagStart + 1, pos - tagStart - 1)
indent = new Array(lastLine.length - lastTag.length + 1).join(' ')
carets = new Array(lastTag.length + 1).join('^')
lastLine = lastLine + template.substr(pos, endOfLine.lastIndex - pos)
error = new Error()
error[key] = e[key] for key of e =
"message": "#{msg}\n\nLine #{lineNo}:\n#{lastLine}\n#{indent}#{carets}"
"error": msg, "line": lineNo, "char": indent.length, "tag": lastTag
return error
# As we start matching things, let's pull out our captures and build indices.
while match = tagPattern.exec(template)
[content, whitespace] = match[1..2]
type = match[3] || match[5] || match[7] || match[9]
tag = match[4] || match[6] || match[8] || match[10]
contentEnd = (pos + content.length) - 1
pos = tagPattern.lastIndex
# Standalone tags are tags on lines without any non-whitespace characters.
isStandalone = (contentEnd == -1 or template.charAt(contentEnd) == '\n') &&
template.charAt(pos) in [ undefined, '', '\r', '\n' ]
# We should just add static content to the buffer.
buffer.push(do (content) -> -> content) if content
# If we're dealing with a standalone tag that's not interpolation, we
# should consume the newline immediately following the tag. If we're not,
# we need to buffer the whitespace we captured earlier.
if isStandalone and type not in ['', '&', '{']
pos += 1 if template.charAt(pos) == '\r'
pos += 1 if template.charAt(pos) == '\n'
else if whitespace
buffer.push(do (whitespace) -> -> whitespace)
contentEnd += whitespace.length
whitespace = ''
# Now we'll handle the tag itself:
switch type
# Comment tags should simply be ignored.
when '!' then break
# Interpolations are handled by finding the value in the context stack,
# calling and rendering lambdas, and escaping the value if appropriate.
when '', '&', '{'
buildInterpolationTag = (name, is_unescaped) ->
return (context) ->
if (value = Find(name, context) ? '') instanceof Function
value = Expand(this, Parse("#{value()}"), arguments...)
value = @escape("#{value}") unless is_unescaped
return "#{value}"
buffer.push(buildInterpolationTag(tag, type))
# Partial data is looked up lazily by the given function, indented as
# appropriate, and then rendered.
when '>'
buildPartialTag = (name, indentation) ->
return (context, partials) ->
partial = partials(name).toString()
partial = partial.replace(/^(?=.)/gm, indentation) if indentation
return Expand(this, Parse(partial), arguments...)
buffer.push(buildPartialTag(tag, whitespace))
# Sections and Inverted Sections make a recursive parsing pass, allowing
# us to use the call stack to handle section parsing. This will go until
# it reaches the matching End Section tag, when it will return the
# (cached!) template it parsed, along with the index it stopped at.
when '#', '^'
sectionInfo =
name: tag, start: pos
error: parseError(tagPattern.lastIndex, "Unclosed section '#{tag}'!")
[tmpl, pos] = Parse(template, delimiters, sectionInfo)
# Sections are rendered by finding the value in the context stack,
# coercing it into an array (unless the value is falsey), and rendering
# the template with each element of the array taking a turn atop the
# context stack. If the value was a function, the template is filtered
# through it before rendering.
sectionInfo['#'] = buildSectionTag = (name, delims, raw) ->
return (context) ->
value = Find(name, context) || []
tmpl = if value instanceof Function then value(raw) else raw
value = [value] unless value instanceof Array
parsed = Parse(tmpl || '', delims)
context.push(value)
result = for v in value
context[context.length - 1] = v
Expand(this, parsed, arguments...)
context.pop()
return result.join('')
# Inverted Sections render under almost opposite conditions: their
# contents will only be rendered when the retrieved value is either
# falsey or an empty array.
sectionInfo['^'] = buildInvertedSectionTag = (name, delims, raw) ->
return (context) ->
value = Find(name, context) || []
value = [1] unless value instanceof Array
value = if value.length is 0 then Parse(raw, delims) else []
return Expand(this, value, arguments...)
buffer.push(sectionInfo[type](tag, delimiters, tmpl))
# When the parser encounters an End Section tag, it runs a couple of
# quick sanity checks, then returns control back to its caller.
when '/'
unless section?
error = "End Section tag '#{tag}' found, but not in section!"
else if tag != (name = section.name)
error = "End Section tag closes '#{tag}'; expected '#{name}'!"
throw parseError(tagPattern.lastIndex, error) if error
template = template[section.start..contentEnd]
cache[template] = buffer
return [template, pos]
# The Set Delimiters tag needs to update the delimiters after some error
# checking, and rebuild the regular expression we're using to match tags.
when '='
unless (delimiters = tag.split(/\s+/)).length == 2
error = "Set Delimiters tags should have two and only two values!"
throw parseError(tagPattern.lastIndex, error) if error
escape = /[-[\]{}()*+?.,\\^$|#]/g
delimiters = (d.replace(escape, "\\$&") for d in delimiters)
tagPattern = BuildRegex()
# Any other tag type is probably a typo.
else
throw parseError(tagPattern.lastIndex, "Unknown tag type -- #{type}")
# Now that we've finished with this tag, we prepare to parse the next one!
tagPattern.lastIndex = if pos? then pos else template.length
# At this point, we've parsed all the tags. If we've still got a `section`,
# someone left a section tag open.
throw section.error if section?
# All the tags is not all the content; if there's anything left over, append
# it to the buffer. Then we'll cache the buffer and return it!
buffer.push(-> template[pos..]) unless template.length == pos
return cache[template] = buffer
# ### Public API
# The exported object (globally `Milk` in browsers) forms Milk's public API:
Milk =
VERSION: '1.2.0'
# Helpers are a form of context, implicitly on the bottom of the stack. This
# is a global value, and may be either an object or an array.
helpers: []
# Partials may also be provided globally.
partials: null
# The `escape` method performs basic content escaping, and may be either
# called or overridden with an alternate escaping mechanism.
escape: (value) ->
entities = { '&': 'amp', '"': 'quot', '<': 'lt', '>': 'gt' }
return value.replace(/[&"<>]/g, (ch) -> "&#{ entities[ch] };")
# Rendering is simple: given a template and some data, it populates the
# template. If your template uses Partial Tags, you may also supply a hash or
# a function, or simply override `Milk.partials`. There is no Step Three.
render: (template, data, partials = null) ->
unless (partials ||= @partials || {}) instanceof Function
partials = do (partials) -> (name) ->
throw "Unknown partial '#{name}'!" unless name of partials
return Find(name, [partials])
context = if @helpers instanceof Array then @helpers else [@helpers]
return Expand(this, Parse(template), context.concat([data]), partials)
# Happy hacking!
if exports?
exports[key] = Milk[key] for key of Milk
else
this.Milk = Milk