1 /*! |
|
2 * mustache.js - Logic-less {{mustache}} templates with JavaScript |
|
3 * http://github.com/janl/mustache.js |
|
4 */ |
|
5 |
|
6 /*global define: false*/ |
|
7 |
|
8 (function (root, factory) { |
|
9 if (typeof exports === "object" && exports) { |
|
10 factory(exports); // CommonJS |
|
11 } else { |
|
12 var mustache = {}; |
|
13 factory(mustache); |
|
14 if (typeof define === "function" && define.amd) { |
|
15 define(mustache); // AMD |
|
16 } else { |
|
17 root.Mustache = mustache; // <script> |
|
18 } |
|
19 } |
|
20 }(this, function (mustache) { |
|
21 |
|
22 var whiteRe = /\s*/; |
|
23 var spaceRe = /\s+/; |
|
24 var nonSpaceRe = /\S/; |
|
25 var eqRe = /\s*=/; |
|
26 var curlyRe = /\s*\}/; |
|
27 var tagRe = /#|\^|\/|>|\{|&|=|!/; |
|
28 |
|
29 // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 |
|
30 // See https://github.com/janl/mustache.js/issues/189 |
|
31 var RegExp_test = RegExp.prototype.test; |
|
32 function testRegExp(re, string) { |
|
33 return RegExp_test.call(re, string); |
|
34 } |
|
35 |
|
36 function isWhitespace(string) { |
|
37 return !testRegExp(nonSpaceRe, string); |
|
38 } |
|
39 |
|
40 var Object_toString = Object.prototype.toString; |
|
41 var isArray = Array.isArray || function (obj) { |
|
42 return Object_toString.call(obj) === '[object Array]'; |
|
43 }; |
|
44 |
|
45 function escapeRegExp(string) { |
|
46 return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); |
|
47 } |
|
48 |
|
49 var entityMap = { |
|
50 "&": "&", |
|
51 "<": "<", |
|
52 ">": ">", |
|
53 '"': '"', |
|
54 "'": ''', |
|
55 "/": '/' |
|
56 }; |
|
57 |
|
58 function escapeHtml(string) { |
|
59 return String(string).replace(/[&<>"'\/]/g, function (s) { |
|
60 return entityMap[s]; |
|
61 }); |
|
62 } |
|
63 |
|
64 function Scanner(string) { |
|
65 this.string = string; |
|
66 this.tail = string; |
|
67 this.pos = 0; |
|
68 } |
|
69 |
|
70 /** |
|
71 * Returns `true` if the tail is empty (end of string). |
|
72 */ |
|
73 Scanner.prototype.eos = function () { |
|
74 return this.tail === ""; |
|
75 }; |
|
76 |
|
77 /** |
|
78 * Tries to match the given regular expression at the current position. |
|
79 * Returns the matched text if it can match, the empty string otherwise. |
|
80 */ |
|
81 Scanner.prototype.scan = function (re) { |
|
82 var match = this.tail.match(re); |
|
83 |
|
84 if (match && match.index === 0) { |
|
85 this.tail = this.tail.substring(match[0].length); |
|
86 this.pos += match[0].length; |
|
87 return match[0]; |
|
88 } |
|
89 |
|
90 return ""; |
|
91 }; |
|
92 |
|
93 /** |
|
94 * Skips all text until the given regular expression can be matched. Returns |
|
95 * the skipped string, which is the entire tail if no match can be made. |
|
96 */ |
|
97 Scanner.prototype.scanUntil = function (re) { |
|
98 var match, pos = this.tail.search(re); |
|
99 |
|
100 switch (pos) { |
|
101 case -1: |
|
102 match = this.tail; |
|
103 this.pos += this.tail.length; |
|
104 this.tail = ""; |
|
105 break; |
|
106 case 0: |
|
107 match = ""; |
|
108 break; |
|
109 default: |
|
110 match = this.tail.substring(0, pos); |
|
111 this.tail = this.tail.substring(pos); |
|
112 this.pos += pos; |
|
113 } |
|
114 |
|
115 return match; |
|
116 }; |
|
117 |
|
118 function Context(view, parent) { |
|
119 this.view = view || {}; |
|
120 this.parent = parent; |
|
121 this._cache = {}; |
|
122 } |
|
123 |
|
124 Context.make = function (view) { |
|
125 return (view instanceof Context) ? view : new Context(view); |
|
126 }; |
|
127 |
|
128 Context.prototype.push = function (view) { |
|
129 return new Context(view, this); |
|
130 }; |
|
131 |
|
132 Context.prototype.lookup = function (name) { |
|
133 var value = this._cache[name]; |
|
134 |
|
135 if (!value) { |
|
136 if (name == '.') { |
|
137 value = this.view; |
|
138 } else { |
|
139 var context = this; |
|
140 |
|
141 while (context) { |
|
142 if (name.indexOf('.') > 0) { |
|
143 value = context.view; |
|
144 var names = name.split('.'), i = 0; |
|
145 while (value && i < names.length) { |
|
146 value = value[names[i++]]; |
|
147 } |
|
148 } else { |
|
149 value = context.view[name]; |
|
150 } |
|
151 |
|
152 if (value != null) break; |
|
153 |
|
154 context = context.parent; |
|
155 } |
|
156 } |
|
157 |
|
158 this._cache[name] = value; |
|
159 } |
|
160 |
|
161 if (typeof value === 'function') value = value.call(this.view); |
|
162 |
|
163 return value; |
|
164 }; |
|
165 |
|
166 function Writer() { |
|
167 this.clearCache(); |
|
168 } |
|
169 |
|
170 Writer.prototype.clearCache = function () { |
|
171 this._cache = {}; |
|
172 this._partialCache = {}; |
|
173 }; |
|
174 |
|
175 Writer.prototype.compile = function (template, tags) { |
|
176 var fn = this._cache[template]; |
|
177 |
|
178 if (!fn) { |
|
179 var tokens = mustache.parse(template, tags); |
|
180 fn = this._cache[template] = this.compileTokens(tokens, template); |
|
181 } |
|
182 |
|
183 return fn; |
|
184 }; |
|
185 |
|
186 Writer.prototype.compilePartial = function (name, template, tags) { |
|
187 var fn = this.compile(template, tags); |
|
188 this._partialCache[name] = fn; |
|
189 return fn; |
|
190 }; |
|
191 |
|
192 Writer.prototype.getPartial = function (name) { |
|
193 if (!(name in this._partialCache) && this._loadPartial) { |
|
194 this.compilePartial(name, this._loadPartial(name)); |
|
195 } |
|
196 |
|
197 return this._partialCache[name]; |
|
198 }; |
|
199 |
|
200 Writer.prototype.compileTokens = function (tokens, template) { |
|
201 var self = this; |
|
202 return function (view, partials) { |
|
203 if (partials) { |
|
204 if (typeof partials === 'function') { |
|
205 self._loadPartial = partials; |
|
206 } else { |
|
207 for (var name in partials) { |
|
208 self.compilePartial(name, partials[name]); |
|
209 } |
|
210 } |
|
211 } |
|
212 |
|
213 return renderTokens(tokens, self, Context.make(view), template); |
|
214 }; |
|
215 }; |
|
216 |
|
217 Writer.prototype.render = function (template, view, partials) { |
|
218 return this.compile(template)(view, partials); |
|
219 }; |
|
220 |
|
221 /** |
|
222 * Low-level function that renders the given `tokens` using the given `writer` |
|
223 * and `context`. The `template` string is only needed for templates that use |
|
224 * higher-order sections to extract the portion of the original template that |
|
225 * was contained in that section. |
|
226 */ |
|
227 function renderTokens(tokens, writer, context, template) { |
|
228 var buffer = ''; |
|
229 |
|
230 var token, tokenValue, value; |
|
231 for (var i = 0, len = tokens.length; i < len; ++i) { |
|
232 token = tokens[i]; |
|
233 tokenValue = token[1]; |
|
234 |
|
235 switch (token[0]) { |
|
236 case '#': |
|
237 value = context.lookup(tokenValue); |
|
238 |
|
239 if (typeof value === 'object') { |
|
240 if (isArray(value)) { |
|
241 for (var j = 0, jlen = value.length; j < jlen; ++j) { |
|
242 buffer += renderTokens(token[4], writer, context.push(value[j]), template); |
|
243 } |
|
244 } else if (value) { |
|
245 buffer += renderTokens(token[4], writer, context.push(value), template); |
|
246 } |
|
247 } else if (typeof value === 'function') { |
|
248 var text = template == null ? null : template.slice(token[3], token[5]); |
|
249 value = value.call(context.view, text, function (template) { |
|
250 return writer.render(template, context); |
|
251 }); |
|
252 if (value != null) buffer += value; |
|
253 } else if (value) { |
|
254 buffer += renderTokens(token[4], writer, context, template); |
|
255 } |
|
256 |
|
257 break; |
|
258 case '^': |
|
259 value = context.lookup(tokenValue); |
|
260 |
|
261 // Use JavaScript's definition of falsy. Include empty arrays. |
|
262 // See https://github.com/janl/mustache.js/issues/186 |
|
263 if (!value || (isArray(value) && value.length === 0)) { |
|
264 buffer += renderTokens(token[4], writer, context, template); |
|
265 } |
|
266 |
|
267 break; |
|
268 case '>': |
|
269 value = writer.getPartial(tokenValue); |
|
270 if (typeof value === 'function') buffer += value(context); |
|
271 break; |
|
272 case '&': |
|
273 value = context.lookup(tokenValue); |
|
274 if (value != null) buffer += value; |
|
275 break; |
|
276 case 'name': |
|
277 value = context.lookup(tokenValue); |
|
278 if (value != null) buffer += mustache.escape(value); |
|
279 break; |
|
280 case 'text': |
|
281 buffer += tokenValue; |
|
282 break; |
|
283 } |
|
284 } |
|
285 |
|
286 return buffer; |
|
287 } |
|
288 |
|
289 /** |
|
290 * Forms the given array of `tokens` into a nested tree structure where |
|
291 * tokens that represent a section have two additional items: 1) an array of |
|
292 * all tokens that appear in that section and 2) the index in the original |
|
293 * template that represents the end of that section. |
|
294 */ |
|
295 function nestTokens(tokens) { |
|
296 var tree = []; |
|
297 var collector = tree; |
|
298 var sections = []; |
|
299 |
|
300 var token; |
|
301 for (var i = 0, len = tokens.length; i < len; ++i) { |
|
302 token = tokens[i]; |
|
303 switch (token[0]) { |
|
304 case '#': |
|
305 case '^': |
|
306 sections.push(token); |
|
307 collector.push(token); |
|
308 collector = token[4] = []; |
|
309 break; |
|
310 case '/': |
|
311 var section = sections.pop(); |
|
312 section[5] = token[2]; |
|
313 collector = sections.length > 0 ? sections[sections.length - 1][4] : tree; |
|
314 break; |
|
315 default: |
|
316 collector.push(token); |
|
317 } |
|
318 } |
|
319 |
|
320 return tree; |
|
321 } |
|
322 |
|
323 /** |
|
324 * Combines the values of consecutive text tokens in the given `tokens` array |
|
325 * to a single token. |
|
326 */ |
|
327 function squashTokens(tokens) { |
|
328 var squashedTokens = []; |
|
329 |
|
330 var token, lastToken; |
|
331 for (var i = 0, len = tokens.length; i < len; ++i) { |
|
332 token = tokens[i]; |
|
333 if (token) { |
|
334 if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { |
|
335 lastToken[1] += token[1]; |
|
336 lastToken[3] = token[3]; |
|
337 } else { |
|
338 lastToken = token; |
|
339 squashedTokens.push(token); |
|
340 } |
|
341 } |
|
342 } |
|
343 |
|
344 return squashedTokens; |
|
345 } |
|
346 |
|
347 function escapeTags(tags) { |
|
348 return [ |
|
349 new RegExp(escapeRegExp(tags[0]) + "\\s*"), |
|
350 new RegExp("\\s*" + escapeRegExp(tags[1])) |
|
351 ]; |
|
352 } |
|
353 |
|
354 /** |
|
355 * Breaks up the given `template` string into a tree of token objects. If |
|
356 * `tags` is given here it must be an array with two string values: the |
|
357 * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of |
|
358 * course, the default is to use mustaches (i.e. Mustache.tags). |
|
359 */ |
|
360 function parseTemplate(template, tags) { |
|
361 template = template || ''; |
|
362 tags = tags || mustache.tags; |
|
363 |
|
364 if (typeof tags === 'string') tags = tags.split(spaceRe); |
|
365 if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', ')); |
|
366 |
|
367 var tagRes = escapeTags(tags); |
|
368 var scanner = new Scanner(template); |
|
369 |
|
370 var sections = []; // Stack to hold section tokens |
|
371 var tokens = []; // Buffer to hold the tokens |
|
372 var spaces = []; // Indices of whitespace tokens on the current line |
|
373 var hasTag = false; // Is there a {{tag}} on the current line? |
|
374 var nonSpace = false; // Is there a non-space char on the current line? |
|
375 |
|
376 // Strips all whitespace tokens array for the current line |
|
377 // if there was a {{#tag}} on it and otherwise only space. |
|
378 function stripSpace() { |
|
379 if (hasTag && !nonSpace) { |
|
380 while (spaces.length) { |
|
381 delete tokens[spaces.pop()]; |
|
382 } |
|
383 } else { |
|
384 spaces = []; |
|
385 } |
|
386 |
|
387 hasTag = false; |
|
388 nonSpace = false; |
|
389 } |
|
390 |
|
391 var start, type, value, chr, token; |
|
392 while (!scanner.eos()) { |
|
393 start = scanner.pos; |
|
394 |
|
395 // Match any text between tags. |
|
396 value = scanner.scanUntil(tagRes[0]); |
|
397 if (value) { |
|
398 for (var i = 0, len = value.length; i < len; ++i) { |
|
399 chr = value.charAt(i); |
|
400 |
|
401 if (isWhitespace(chr)) { |
|
402 spaces.push(tokens.length); |
|
403 } else { |
|
404 nonSpace = true; |
|
405 } |
|
406 |
|
407 tokens.push(['text', chr, start, start + 1]); |
|
408 start += 1; |
|
409 |
|
410 // Check for whitespace on the current line. |
|
411 if (chr == '\n') stripSpace(); |
|
412 } |
|
413 } |
|
414 |
|
415 // Match the opening tag. |
|
416 if (!scanner.scan(tagRes[0])) break; |
|
417 hasTag = true; |
|
418 |
|
419 // Get the tag type. |
|
420 type = scanner.scan(tagRe) || 'name'; |
|
421 scanner.scan(whiteRe); |
|
422 |
|
423 // Get the tag value. |
|
424 if (type === '=') { |
|
425 value = scanner.scanUntil(eqRe); |
|
426 scanner.scan(eqRe); |
|
427 scanner.scanUntil(tagRes[1]); |
|
428 } else if (type === '{') { |
|
429 value = scanner.scanUntil(new RegExp('\\s*' + escapeRegExp('}' + tags[1]))); |
|
430 scanner.scan(curlyRe); |
|
431 scanner.scanUntil(tagRes[1]); |
|
432 type = '&'; |
|
433 } else { |
|
434 value = scanner.scanUntil(tagRes[1]); |
|
435 } |
|
436 |
|
437 // Match the closing tag. |
|
438 if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos); |
|
439 |
|
440 token = [type, value, start, scanner.pos]; |
|
441 tokens.push(token); |
|
442 |
|
443 if (type === '#' || type === '^') { |
|
444 sections.push(token); |
|
445 } else if (type === '/') { |
|
446 // Check section nesting. |
|
447 if (sections.length === 0) throw new Error('Unopened section "' + value + '" at ' + start); |
|
448 var openSection = sections.pop(); |
|
449 if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); |
|
450 } else if (type === 'name' || type === '{' || type === '&') { |
|
451 nonSpace = true; |
|
452 } else if (type === '=') { |
|
453 // Set the tags for the next time around. |
|
454 tags = value.split(spaceRe); |
|
455 if (tags.length !== 2) throw new Error('Invalid tags at ' + start + ': ' + tags.join(', ')); |
|
456 tagRes = escapeTags(tags); |
|
457 } |
|
458 } |
|
459 |
|
460 // Make sure there are no open sections when we're done. |
|
461 var openSection = sections.pop(); |
|
462 if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); |
|
463 |
|
464 tokens = squashTokens(tokens); |
|
465 |
|
466 return nestTokens(tokens); |
|
467 } |
|
468 |
|
469 mustache.name = "mustache.js"; |
|
470 mustache.version = "0.7.2"; |
|
471 mustache.tags = ["{{", "}}"]; |
|
472 |
|
473 mustache.Scanner = Scanner; |
|
474 mustache.Context = Context; |
|
475 mustache.Writer = Writer; |
|
476 |
|
477 mustache.parse = parseTemplate; |
|
478 |
|
479 // Export the escaping function so that the user may override it. |
|
480 // See https://github.com/janl/mustache.js/issues/244 |
|
481 mustache.escape = escapeHtml; |
|
482 |
|
483 // All Mustache.* functions use this writer. |
|
484 var defaultWriter = new Writer(); |
|
485 |
|
486 /** |
|
487 * Clears all cached templates and partials in the default writer. |
|
488 */ |
|
489 mustache.clearCache = function () { |
|
490 return defaultWriter.clearCache(); |
|
491 }; |
|
492 |
|
493 /** |
|
494 * Compiles the given `template` to a reusable function using the default |
|
495 * writer. |
|
496 */ |
|
497 mustache.compile = function (template, tags) { |
|
498 return defaultWriter.compile(template, tags); |
|
499 }; |
|
500 |
|
501 /** |
|
502 * Compiles the partial with the given `name` and `template` to a reusable |
|
503 * function using the default writer. |
|
504 */ |
|
505 mustache.compilePartial = function (name, template, tags) { |
|
506 return defaultWriter.compilePartial(name, template, tags); |
|
507 }; |
|
508 |
|
509 /** |
|
510 * Compiles the given array of tokens (the output of a parse) to a reusable |
|
511 * function using the default writer. |
|
512 */ |
|
513 mustache.compileTokens = function (tokens, template) { |
|
514 return defaultWriter.compileTokens(tokens, template); |
|
515 }; |
|
516 |
|
517 /** |
|
518 * Renders the `template` with the given `view` and `partials` using the |
|
519 * default writer. |
|
520 */ |
|
521 mustache.render = function (template, view, partials) { |
|
522 return defaultWriter.render(template, view, partials); |
|
523 }; |
|
524 |
|
525 // This is here for backwards compatibility with 0.4.x. |
|
526 mustache.to_html = function (template, view, partials, send) { |
|
527 var result = mustache.render(template, view, partials); |
|
528 |
|
529 if (typeof send === "function") { |
|
530 send(result); |
|
531 } else { |
|
532 return result; |
|
533 } |
|
534 }; |
|
535 |
|
536 })); |
|