|
1 /* |
|
2 mustache.js — Logic-less templates in JavaScript |
|
3 |
|
4 See http://mustache.github.com/ for more info. |
|
5 */ |
|
6 |
|
7 var Mustache = function () { |
|
8 var _toString = Object.prototype.toString; |
|
9 |
|
10 Array.isArray = Array.isArray || function (obj) { |
|
11 return _toString.call(obj) == "[object Array]"; |
|
12 } |
|
13 |
|
14 var _trim = String.prototype.trim, trim; |
|
15 |
|
16 if (_trim) { |
|
17 trim = function (text) { |
|
18 return text == null ? "" : _trim.call(text); |
|
19 } |
|
20 } else { |
|
21 var trimLeft, trimRight; |
|
22 |
|
23 // IE doesn't match non-breaking spaces with \s. |
|
24 if ((/\S/).test("\xA0")) { |
|
25 trimLeft = /^[\s\xA0]+/; |
|
26 trimRight = /[\s\xA0]+$/; |
|
27 } else { |
|
28 trimLeft = /^\s+/; |
|
29 trimRight = /\s+$/; |
|
30 } |
|
31 |
|
32 trim = function (text) { |
|
33 return text == null ? "" : |
|
34 text.toString().replace(trimLeft, "").replace(trimRight, ""); |
|
35 } |
|
36 } |
|
37 |
|
38 var escapeMap = { |
|
39 "&": "&", |
|
40 "<": "<", |
|
41 ">": ">", |
|
42 '"': '"', |
|
43 "'": ''' |
|
44 }; |
|
45 |
|
46 function escapeHTML(string) { |
|
47 return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { |
|
48 return escapeMap[s] || s; |
|
49 }); |
|
50 } |
|
51 |
|
52 var regexCache = {}; |
|
53 var Renderer = function () {}; |
|
54 |
|
55 Renderer.prototype = { |
|
56 otag: "{{", |
|
57 ctag: "}}", |
|
58 pragmas: {}, |
|
59 buffer: [], |
|
60 pragmas_implemented: { |
|
61 "IMPLICIT-ITERATOR": true |
|
62 }, |
|
63 context: {}, |
|
64 |
|
65 render: function (template, context, partials, in_recursion) { |
|
66 // reset buffer & set context |
|
67 if (!in_recursion) { |
|
68 this.context = context; |
|
69 this.buffer = []; // TODO: make this non-lazy |
|
70 } |
|
71 |
|
72 // fail fast |
|
73 if (!this.includes("", template)) { |
|
74 if (in_recursion) { |
|
75 return template; |
|
76 } else { |
|
77 this.send(template); |
|
78 return; |
|
79 } |
|
80 } |
|
81 |
|
82 // get the pragmas together |
|
83 template = this.render_pragmas(template); |
|
84 |
|
85 // render the template |
|
86 var html = this.render_section(template, context, partials); |
|
87 |
|
88 // render_section did not find any sections, we still need to render the tags |
|
89 if (html === false) { |
|
90 html = this.render_tags(template, context, partials, in_recursion); |
|
91 } |
|
92 |
|
93 if (in_recursion) { |
|
94 return html; |
|
95 } else { |
|
96 this.sendLines(html); |
|
97 } |
|
98 }, |
|
99 |
|
100 /* |
|
101 Sends parsed lines |
|
102 */ |
|
103 send: function (line) { |
|
104 if (line !== "") { |
|
105 this.buffer.push(line); |
|
106 } |
|
107 }, |
|
108 |
|
109 sendLines: function (text) { |
|
110 if (text) { |
|
111 var lines = text.split("\n"); |
|
112 for (var i = 0; i < lines.length; i++) { |
|
113 this.send(lines[i]); |
|
114 } |
|
115 } |
|
116 }, |
|
117 |
|
118 /* |
|
119 Looks for %PRAGMAS |
|
120 */ |
|
121 render_pragmas: function (template) { |
|
122 // no pragmas |
|
123 if (!this.includes("%", template)) { |
|
124 return template; |
|
125 } |
|
126 |
|
127 var that = this; |
|
128 var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) { |
|
129 return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); |
|
130 }); |
|
131 |
|
132 return template.replace(regex, function (match, pragma, options) { |
|
133 if (!that.pragmas_implemented[pragma]) { |
|
134 throw({message: |
|
135 "This implementation of mustache doesn't understand the '" + |
|
136 pragma + "' pragma"}); |
|
137 } |
|
138 that.pragmas[pragma] = {}; |
|
139 if (options) { |
|
140 var opts = options.split("="); |
|
141 that.pragmas[pragma][opts[0]] = opts[1]; |
|
142 } |
|
143 return ""; |
|
144 // ignore unknown pragmas silently |
|
145 }); |
|
146 }, |
|
147 |
|
148 /* |
|
149 Tries to find a partial in the curent scope and render it |
|
150 */ |
|
151 render_partial: function (name, context, partials) { |
|
152 name = trim(name); |
|
153 if (!partials || partials[name] === undefined) { |
|
154 throw({message: "unknown_partial '" + name + "'"}); |
|
155 } |
|
156 if (!context || typeof context[name] != "object") { |
|
157 return this.render(partials[name], context, partials, true); |
|
158 } |
|
159 return this.render(partials[name], context[name], partials, true); |
|
160 }, |
|
161 |
|
162 /* |
|
163 Renders inverted (^) and normal (#) sections |
|
164 */ |
|
165 render_section: function (template, context, partials) { |
|
166 if (!this.includes("#", template) && !this.includes("^", template)) { |
|
167 // did not render anything, there were no sections |
|
168 return false; |
|
169 } |
|
170 |
|
171 var that = this; |
|
172 |
|
173 var regex = this.getCachedRegex("render_section", function (otag, ctag) { |
|
174 // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder |
|
175 return new RegExp( |
|
176 "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) |
|
177 |
|
178 otag + // {{ |
|
179 "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) |
|
180 ctag + // }} |
|
181 |
|
182 "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped |
|
183 |
|
184 otag + // {{ |
|
185 "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). |
|
186 ctag + // }} |
|
187 |
|
188 "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. |
|
189 |
|
190 "g"); |
|
191 }); |
|
192 |
|
193 |
|
194 // for each {{#foo}}{{/foo}} section do... |
|
195 return template.replace(regex, function (match, before, type, name, content, after) { |
|
196 // before contains only tags, no sections |
|
197 var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", |
|
198 |
|
199 // after may contain both sections and tags, so use full rendering function |
|
200 renderedAfter = after ? that.render(after, context, partials, true) : "", |
|
201 |
|
202 // will be computed below |
|
203 renderedContent, |
|
204 |
|
205 value = that.find(name, context); |
|
206 |
|
207 if (type === "^") { // inverted section |
|
208 if (!value || Array.isArray(value) && value.length === 0) { |
|
209 // false or empty list, render it |
|
210 renderedContent = that.render(content, context, partials, true); |
|
211 } else { |
|
212 renderedContent = ""; |
|
213 } |
|
214 } else if (type === "#") { // normal section |
|
215 if (Array.isArray(value)) { // Enumerable, Let's loop! |
|
216 renderedContent = that.map(value, function (row) { |
|
217 return that.render(content, that.create_context(row), partials, true); |
|
218 }).join(""); |
|
219 } else if (that.is_object(value)) { // Object, Use it as subcontext! |
|
220 renderedContent = that.render(content, that.create_context(value), |
|
221 partials, true); |
|
222 } else if (typeof value == "function") { |
|
223 // higher order section |
|
224 renderedContent = value.call(context, content, function (text) { |
|
225 return that.render(text, context, partials, true); |
|
226 }); |
|
227 } else if (value) { // boolean section |
|
228 renderedContent = that.render(content, context, partials, true); |
|
229 } else { |
|
230 renderedContent = ""; |
|
231 } |
|
232 } |
|
233 |
|
234 return renderedBefore + renderedContent + renderedAfter; |
|
235 }); |
|
236 }, |
|
237 |
|
238 /* |
|
239 Replace {{foo}} and friends with values from our view |
|
240 */ |
|
241 render_tags: function (template, context, partials, in_recursion) { |
|
242 // tit for tat |
|
243 var that = this; |
|
244 |
|
245 var new_regex = function () { |
|
246 return that.getCachedRegex("render_tags", function (otag, ctag) { |
|
247 return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g"); |
|
248 }); |
|
249 }; |
|
250 |
|
251 var regex = new_regex(); |
|
252 var tag_replace_callback = function (match, operator, name) { |
|
253 switch(operator) { |
|
254 case "!": // ignore comments |
|
255 return ""; |
|
256 case "=": // set new delimiters, rebuild the replace regexp |
|
257 that.set_delimiters(name); |
|
258 regex = new_regex(); |
|
259 return ""; |
|
260 case ">": // render partial |
|
261 return that.render_partial(name, context, partials); |
|
262 case "{": // the triple mustache is unescaped |
|
263 case "&": // & operator is an alternative unescape method |
|
264 return that.find(name, context); |
|
265 default: // escape the value |
|
266 return escapeHTML(that.find(name, context)); |
|
267 } |
|
268 }; |
|
269 var lines = template.split("\n"); |
|
270 for(var i = 0; i < lines.length; i++) { |
|
271 lines[i] = lines[i].replace(regex, tag_replace_callback, this); |
|
272 if (!in_recursion) { |
|
273 this.send(lines[i]); |
|
274 } |
|
275 } |
|
276 |
|
277 if (in_recursion) { |
|
278 return lines.join("\n"); |
|
279 } |
|
280 }, |
|
281 |
|
282 set_delimiters: function (delimiters) { |
|
283 var dels = delimiters.split(" "); |
|
284 this.otag = this.escape_regex(dels[0]); |
|
285 this.ctag = this.escape_regex(dels[1]); |
|
286 }, |
|
287 |
|
288 escape_regex: function (text) { |
|
289 // thank you Simon Willison |
|
290 if (!arguments.callee.sRE) { |
|
291 var specials = [ |
|
292 '/', '.', '*', '+', '?', '|', |
|
293 '(', ')', '[', ']', '{', '}', '\\' |
|
294 ]; |
|
295 arguments.callee.sRE = new RegExp( |
|
296 '(\\' + specials.join('|\\') + ')', 'g' |
|
297 ); |
|
298 } |
|
299 return text.replace(arguments.callee.sRE, '\\$1'); |
|
300 }, |
|
301 |
|
302 /* |
|
303 find `name` in current `context`. That is find me a value |
|
304 from the view object |
|
305 */ |
|
306 find: function (name, context) { |
|
307 name = trim(name); |
|
308 |
|
309 // Checks whether a value is thruthy or false or 0 |
|
310 function is_kinda_truthy(bool) { |
|
311 return bool === false || bool === 0 || bool; |
|
312 } |
|
313 |
|
314 var value; |
|
315 |
|
316 // check for dot notation eg. foo.bar |
|
317 if (name.match(/([a-z_]+)\./ig)) { |
|
318 var childValue = this.walk_context(name, context); |
|
319 if (is_kinda_truthy(childValue)) { |
|
320 value = childValue; |
|
321 } |
|
322 } else { |
|
323 if (is_kinda_truthy(context[name])) { |
|
324 value = context[name]; |
|
325 } else if (is_kinda_truthy(this.context[name])) { |
|
326 value = this.context[name]; |
|
327 } |
|
328 } |
|
329 |
|
330 if (typeof value == "function") { |
|
331 return value.apply(context); |
|
332 } |
|
333 if (value !== undefined) { |
|
334 return value; |
|
335 } |
|
336 // silently ignore unkown variables |
|
337 return ""; |
|
338 }, |
|
339 |
|
340 walk_context: function (name, context) { |
|
341 var path = name.split('.'); |
|
342 // if the var doesn't exist in current context, check the top level context |
|
343 var value_context = (context[path[0]] != undefined) ? context : this.context; |
|
344 var value = value_context[path.shift()]; |
|
345 while (value != undefined && path.length > 0) { |
|
346 value_context = value; |
|
347 value = value[path.shift()]; |
|
348 } |
|
349 // if the value is a function, call it, binding the correct context |
|
350 if (typeof value == "function") { |
|
351 return value.apply(value_context); |
|
352 } |
|
353 return value; |
|
354 }, |
|
355 |
|
356 // Utility methods |
|
357 |
|
358 /* includes tag */ |
|
359 includes: function (needle, haystack) { |
|
360 return haystack.indexOf(this.otag + needle) != -1; |
|
361 }, |
|
362 |
|
363 // by @langalex, support for arrays of strings |
|
364 create_context: function (_context) { |
|
365 if (this.is_object(_context)) { |
|
366 return _context; |
|
367 } else { |
|
368 var iterator = "."; |
|
369 if (this.pragmas["IMPLICIT-ITERATOR"]) { |
|
370 iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; |
|
371 } |
|
372 var ctx = {}; |
|
373 ctx[iterator] = _context; |
|
374 return ctx; |
|
375 } |
|
376 }, |
|
377 |
|
378 is_object: function (a) { |
|
379 return a && typeof a == "object"; |
|
380 }, |
|
381 |
|
382 /* |
|
383 Why, why, why? Because IE. Cry, cry cry. |
|
384 */ |
|
385 map: function (array, fn) { |
|
386 if (typeof array.map == "function") { |
|
387 return array.map(fn); |
|
388 } else { |
|
389 var r = []; |
|
390 var l = array.length; |
|
391 for(var i = 0; i < l; i++) { |
|
392 r.push(fn(array[i])); |
|
393 } |
|
394 return r; |
|
395 } |
|
396 }, |
|
397 |
|
398 getCachedRegex: function (name, generator) { |
|
399 var byOtag = regexCache[this.otag]; |
|
400 if (!byOtag) { |
|
401 byOtag = regexCache[this.otag] = {}; |
|
402 } |
|
403 |
|
404 var byCtag = byOtag[this.ctag]; |
|
405 if (!byCtag) { |
|
406 byCtag = byOtag[this.ctag] = {}; |
|
407 } |
|
408 |
|
409 var regex = byCtag[name]; |
|
410 if (!regex) { |
|
411 regex = byCtag[name] = generator(this.otag, this.ctag); |
|
412 } |
|
413 |
|
414 return regex; |
|
415 } |
|
416 }; |
|
417 |
|
418 return({ |
|
419 name: "mustache.js", |
|
420 version: "0.5.0-dev", |
|
421 |
|
422 /* |
|
423 Turns a template and view into HTML |
|
424 */ |
|
425 to_html: function (template, view, partials, send_fun) { |
|
426 var renderer = new Renderer(); |
|
427 if (send_fun) { |
|
428 renderer.send = send_fun; |
|
429 } |
|
430 renderer.render(template, view || {}, partials); |
|
431 if (!send_fun) { |
|
432 return renderer.buffer.join("\n"); |
|
433 } |
|
434 } |
|
435 }); |
|
436 }(); |