1 /*! |
|
2 * mustache.js - Logic-less {{mustache}} templates with JavaScript |
|
3 * http://github.com/janl/mustache.js |
|
4 */ |
|
5 |
|
6 /*global define: false Mustache: true*/ |
|
7 |
|
8 (function defineMustache (global, factory) { |
|
9 if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { |
|
10 factory(exports); // CommonJS |
|
11 } else if (typeof define === 'function' && define.amd) { |
|
12 define(['exports'], factory); // AMD |
|
13 } else { |
|
14 global.Mustache = {}; |
|
15 factory(Mustache); // script, wsh, asp |
|
16 } |
|
17 }(this, function mustacheFactory (mustache) { |
|
18 |
|
19 var objectToString = Object.prototype.toString; |
|
20 var isArray = Array.isArray || function isArrayPolyfill (object) { |
|
21 return objectToString.call(object) === '[object Array]'; |
|
22 }; |
|
23 |
|
24 function isFunction (object) { |
|
25 return typeof object === 'function'; |
|
26 } |
|
27 |
|
28 /** |
|
29 * More correct typeof string handling array |
|
30 * which normally returns typeof 'object' |
|
31 */ |
|
32 function typeStr (obj) { |
|
33 return isArray(obj) ? 'array' : typeof obj; |
|
34 } |
|
35 |
|
36 function escapeRegExp (string) { |
|
37 return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); |
|
38 } |
|
39 |
|
40 /** |
|
41 * Null safe way of checking whether or not an object, |
|
42 * including its prototype, has a given property |
|
43 */ |
|
44 function hasProperty (obj, propName) { |
|
45 return obj != null && typeof obj === 'object' && (propName in obj); |
|
46 } |
|
47 |
|
48 // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 |
|
49 // See https://github.com/janl/mustache.js/issues/189 |
|
50 var regExpTest = RegExp.prototype.test; |
|
51 function testRegExp (re, string) { |
|
52 return regExpTest.call(re, string); |
|
53 } |
|
54 |
|
55 var nonSpaceRe = /\S/; |
|
56 function isWhitespace (string) { |
|
57 return !testRegExp(nonSpaceRe, string); |
|
58 } |
|
59 |
|
60 var entityMap = { |
|
61 '&': '&', |
|
62 '<': '<', |
|
63 '>': '>', |
|
64 '"': '"', |
|
65 "'": ''', |
|
66 '/': '/' |
|
67 }; |
|
68 |
|
69 function escapeHtml (string) { |
|
70 return String(string).replace(/[&<>"'\/]/g, function fromEntityMap (s) { |
|
71 return entityMap[s]; |
|
72 }); |
|
73 } |
|
74 |
|
75 var whiteRe = /\s*/; |
|
76 var spaceRe = /\s+/; |
|
77 var equalsRe = /\s*=/; |
|
78 var curlyRe = /\s*\}/; |
|
79 var tagRe = /#|\^|\/|>|\{|&|=|!/; |
|
80 |
|
81 /** |
|
82 * Breaks up the given `template` string into a tree of tokens. If the `tags` |
|
83 * argument is given here it must be an array with two string values: the |
|
84 * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of |
|
85 * course, the default is to use mustaches (i.e. mustache.tags). |
|
86 * |
|
87 * A token is an array with at least 4 elements. The first element is the |
|
88 * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag |
|
89 * did not contain a symbol (i.e. {{myValue}}) this element is "name". For |
|
90 * all text that appears outside a symbol this element is "text". |
|
91 * |
|
92 * The second element of a token is its "value". For mustache tags this is |
|
93 * whatever else was inside the tag besides the opening symbol. For text tokens |
|
94 * this is the text itself. |
|
95 * |
|
96 * The third and fourth elements of the token are the start and end indices, |
|
97 * respectively, of the token in the original template. |
|
98 * |
|
99 * Tokens that are the root node of a subtree contain two more elements: 1) an |
|
100 * array of tokens in the subtree and 2) the index in the original template at |
|
101 * which the closing tag for that section begins. |
|
102 */ |
|
103 function parseTemplate (template, tags) { |
|
104 if (!template) |
|
105 return []; |
|
106 |
|
107 var sections = []; // Stack to hold section tokens |
|
108 var tokens = []; // Buffer to hold the tokens |
|
109 var spaces = []; // Indices of whitespace tokens on the current line |
|
110 var hasTag = false; // Is there a {{tag}} on the current line? |
|
111 var nonSpace = false; // Is there a non-space char on the current line? |
|
112 |
|
113 // Strips all whitespace tokens array for the current line |
|
114 // if there was a {{#tag}} on it and otherwise only space. |
|
115 function stripSpace () { |
|
116 if (hasTag && !nonSpace) { |
|
117 while (spaces.length) |
|
118 delete tokens[spaces.pop()]; |
|
119 } else { |
|
120 spaces = []; |
|
121 } |
|
122 |
|
123 hasTag = false; |
|
124 nonSpace = false; |
|
125 } |
|
126 |
|
127 var openingTagRe, closingTagRe, closingCurlyRe; |
|
128 function compileTags (tagsToCompile) { |
|
129 if (typeof tagsToCompile === 'string') |
|
130 tagsToCompile = tagsToCompile.split(spaceRe, 2); |
|
131 |
|
132 if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) |
|
133 throw new Error('Invalid tags: ' + tagsToCompile); |
|
134 |
|
135 openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); |
|
136 closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); |
|
137 closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); |
|
138 } |
|
139 |
|
140 compileTags(tags || mustache.tags); |
|
141 |
|
142 var scanner = new Scanner(template); |
|
143 |
|
144 var start, type, value, chr, token, openSection; |
|
145 while (!scanner.eos()) { |
|
146 start = scanner.pos; |
|
147 |
|
148 // Match any text between tags. |
|
149 value = scanner.scanUntil(openingTagRe); |
|
150 |
|
151 if (value) { |
|
152 for (var i = 0, valueLength = value.length; i < valueLength; ++i) { |
|
153 chr = value.charAt(i); |
|
154 |
|
155 if (isWhitespace(chr)) { |
|
156 spaces.push(tokens.length); |
|
157 } else { |
|
158 nonSpace = true; |
|
159 } |
|
160 |
|
161 tokens.push([ 'text', chr, start, start + 1 ]); |
|
162 start += 1; |
|
163 |
|
164 // Check for whitespace on the current line. |
|
165 if (chr === '\n') |
|
166 stripSpace(); |
|
167 } |
|
168 } |
|
169 |
|
170 // Match the opening tag. |
|
171 if (!scanner.scan(openingTagRe)) |
|
172 break; |
|
173 |
|
174 hasTag = true; |
|
175 |
|
176 // Get the tag type. |
|
177 type = scanner.scan(tagRe) || 'name'; |
|
178 scanner.scan(whiteRe); |
|
179 |
|
180 // Get the tag value. |
|
181 if (type === '=') { |
|
182 value = scanner.scanUntil(equalsRe); |
|
183 scanner.scan(equalsRe); |
|
184 scanner.scanUntil(closingTagRe); |
|
185 } else if (type === '{') { |
|
186 value = scanner.scanUntil(closingCurlyRe); |
|
187 scanner.scan(curlyRe); |
|
188 scanner.scanUntil(closingTagRe); |
|
189 type = '&'; |
|
190 } else { |
|
191 value = scanner.scanUntil(closingTagRe); |
|
192 } |
|
193 |
|
194 // Match the closing tag. |
|
195 if (!scanner.scan(closingTagRe)) |
|
196 throw new Error('Unclosed tag at ' + scanner.pos); |
|
197 |
|
198 token = [ type, value, start, scanner.pos ]; |
|
199 tokens.push(token); |
|
200 |
|
201 if (type === '#' || type === '^') { |
|
202 sections.push(token); |
|
203 } else if (type === '/') { |
|
204 // Check section nesting. |
|
205 openSection = sections.pop(); |
|
206 |
|
207 if (!openSection) |
|
208 throw new Error('Unopened section "' + value + '" at ' + start); |
|
209 |
|
210 if (openSection[1] !== value) |
|
211 throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); |
|
212 } else if (type === 'name' || type === '{' || type === '&') { |
|
213 nonSpace = true; |
|
214 } else if (type === '=') { |
|
215 // Set the tags for the next time around. |
|
216 compileTags(value); |
|
217 } |
|
218 } |
|
219 |
|
220 // Make sure there are no open sections when we're done. |
|
221 openSection = sections.pop(); |
|
222 |
|
223 if (openSection) |
|
224 throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); |
|
225 |
|
226 return nestTokens(squashTokens(tokens)); |
|
227 } |
|
228 |
|
229 /** |
|
230 * Combines the values of consecutive text tokens in the given `tokens` array |
|
231 * to a single token. |
|
232 */ |
|
233 function squashTokens (tokens) { |
|
234 var squashedTokens = []; |
|
235 |
|
236 var token, lastToken; |
|
237 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { |
|
238 token = tokens[i]; |
|
239 |
|
240 if (token) { |
|
241 if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { |
|
242 lastToken[1] += token[1]; |
|
243 lastToken[3] = token[3]; |
|
244 } else { |
|
245 squashedTokens.push(token); |
|
246 lastToken = token; |
|
247 } |
|
248 } |
|
249 } |
|
250 |
|
251 return squashedTokens; |
|
252 } |
|
253 |
|
254 /** |
|
255 * Forms the given array of `tokens` into a nested tree structure where |
|
256 * tokens that represent a section have two additional items: 1) an array of |
|
257 * all tokens that appear in that section and 2) the index in the original |
|
258 * template that represents the end of that section. |
|
259 */ |
|
260 function nestTokens (tokens) { |
|
261 var nestedTokens = []; |
|
262 var collector = nestedTokens; |
|
263 var sections = []; |
|
264 |
|
265 var token, section; |
|
266 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { |
|
267 token = tokens[i]; |
|
268 |
|
269 switch (token[0]) { |
|
270 case '#': |
|
271 case '^': |
|
272 collector.push(token); |
|
273 sections.push(token); |
|
274 collector = token[4] = []; |
|
275 break; |
|
276 case '/': |
|
277 section = sections.pop(); |
|
278 section[5] = token[2]; |
|
279 collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; |
|
280 break; |
|
281 default: |
|
282 collector.push(token); |
|
283 } |
|
284 } |
|
285 |
|
286 return nestedTokens; |
|
287 } |
|
288 |
|
289 /** |
|
290 * A simple string scanner that is used by the template parser to find |
|
291 * tokens in template strings. |
|
292 */ |
|
293 function Scanner (string) { |
|
294 this.string = string; |
|
295 this.tail = string; |
|
296 this.pos = 0; |
|
297 } |
|
298 |
|
299 /** |
|
300 * Returns `true` if the tail is empty (end of string). |
|
301 */ |
|
302 Scanner.prototype.eos = function eos () { |
|
303 return this.tail === ''; |
|
304 }; |
|
305 |
|
306 /** |
|
307 * Tries to match the given regular expression at the current position. |
|
308 * Returns the matched text if it can match, the empty string otherwise. |
|
309 */ |
|
310 Scanner.prototype.scan = function scan (re) { |
|
311 var match = this.tail.match(re); |
|
312 |
|
313 if (!match || match.index !== 0) |
|
314 return ''; |
|
315 |
|
316 var string = match[0]; |
|
317 |
|
318 this.tail = this.tail.substring(string.length); |
|
319 this.pos += string.length; |
|
320 |
|
321 return string; |
|
322 }; |
|
323 |
|
324 /** |
|
325 * Skips all text until the given regular expression can be matched. Returns |
|
326 * the skipped string, which is the entire tail if no match can be made. |
|
327 */ |
|
328 Scanner.prototype.scanUntil = function scanUntil (re) { |
|
329 var index = this.tail.search(re), match; |
|
330 |
|
331 switch (index) { |
|
332 case -1: |
|
333 match = this.tail; |
|
334 this.tail = ''; |
|
335 break; |
|
336 case 0: |
|
337 match = ''; |
|
338 break; |
|
339 default: |
|
340 match = this.tail.substring(0, index); |
|
341 this.tail = this.tail.substring(index); |
|
342 } |
|
343 |
|
344 this.pos += match.length; |
|
345 |
|
346 return match; |
|
347 }; |
|
348 |
|
349 /** |
|
350 * Represents a rendering context by wrapping a view object and |
|
351 * maintaining a reference to the parent context. |
|
352 */ |
|
353 function Context (view, parentContext) { |
|
354 this.view = view; |
|
355 this.cache = { '.': this.view }; |
|
356 this.parent = parentContext; |
|
357 } |
|
358 |
|
359 /** |
|
360 * Creates a new context using the given view with this context |
|
361 * as the parent. |
|
362 */ |
|
363 Context.prototype.push = function push (view) { |
|
364 return new Context(view, this); |
|
365 }; |
|
366 |
|
367 /** |
|
368 * Returns the value of the given name in this context, traversing |
|
369 * up the context hierarchy if the value is absent in this context's view. |
|
370 */ |
|
371 Context.prototype.lookup = function lookup (name) { |
|
372 var cache = this.cache; |
|
373 |
|
374 var value; |
|
375 if (cache.hasOwnProperty(name)) { |
|
376 value = cache[name]; |
|
377 } else { |
|
378 var context = this, names, index, lookupHit = false; |
|
379 |
|
380 while (context) { |
|
381 if (name.indexOf('.') > 0) { |
|
382 value = context.view; |
|
383 names = name.split('.'); |
|
384 index = 0; |
|
385 |
|
386 /** |
|
387 * Using the dot notion path in `name`, we descend through the |
|
388 * nested objects. |
|
389 * |
|
390 * To be certain that the lookup has been successful, we have to |
|
391 * check if the last object in the path actually has the property |
|
392 * we are looking for. We store the result in `lookupHit`. |
|
393 * |
|
394 * This is specially necessary for when the value has been set to |
|
395 * `undefined` and we want to avoid looking up parent contexts. |
|
396 **/ |
|
397 while (value != null && index < names.length) { |
|
398 if (index === names.length - 1) |
|
399 lookupHit = hasProperty(value, names[index]); |
|
400 |
|
401 value = value[names[index++]]; |
|
402 } |
|
403 } else { |
|
404 value = context.view[name]; |
|
405 lookupHit = hasProperty(context.view, name); |
|
406 } |
|
407 |
|
408 if (lookupHit) |
|
409 break; |
|
410 |
|
411 context = context.parent; |
|
412 } |
|
413 |
|
414 cache[name] = value; |
|
415 } |
|
416 |
|
417 if (isFunction(value)) |
|
418 value = value.call(this.view); |
|
419 |
|
420 return value; |
|
421 }; |
|
422 |
|
423 /** |
|
424 * A Writer knows how to take a stream of tokens and render them to a |
|
425 * string, given a context. It also maintains a cache of templates to |
|
426 * avoid the need to parse the same template twice. |
|
427 */ |
|
428 function Writer () { |
|
429 this.cache = {}; |
|
430 } |
|
431 |
|
432 /** |
|
433 * Clears all cached templates in this writer. |
|
434 */ |
|
435 Writer.prototype.clearCache = function clearCache () { |
|
436 this.cache = {}; |
|
437 }; |
|
438 |
|
439 /** |
|
440 * Parses and caches the given `template` and returns the array of tokens |
|
441 * that is generated from the parse. |
|
442 */ |
|
443 Writer.prototype.parse = function parse (template, tags) { |
|
444 var cache = this.cache; |
|
445 var tokens = cache[template]; |
|
446 |
|
447 if (tokens == null) |
|
448 tokens = cache[template] = parseTemplate(template, tags); |
|
449 |
|
450 return tokens; |
|
451 }; |
|
452 |
|
453 /** |
|
454 * High-level method that is used to render the given `template` with |
|
455 * the given `view`. |
|
456 * |
|
457 * The optional `partials` argument may be an object that contains the |
|
458 * names and templates of partials that are used in the template. It may |
|
459 * also be a function that is used to load partial templates on the fly |
|
460 * that takes a single argument: the name of the partial. |
|
461 */ |
|
462 Writer.prototype.render = function render (template, view, partials) { |
|
463 var tokens = this.parse(template); |
|
464 var context = (view instanceof Context) ? view : new Context(view); |
|
465 return this.renderTokens(tokens, context, partials, template); |
|
466 }; |
|
467 |
|
468 /** |
|
469 * Low-level method that renders the given array of `tokens` using |
|
470 * the given `context` and `partials`. |
|
471 * |
|
472 * Note: The `originalTemplate` is only ever used to extract the portion |
|
473 * of the original template that was contained in a higher-order section. |
|
474 * If the template doesn't use higher-order sections, this argument may |
|
475 * be omitted. |
|
476 */ |
|
477 Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) { |
|
478 var buffer = ''; |
|
479 |
|
480 var token, symbol, value; |
|
481 for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { |
|
482 value = undefined; |
|
483 token = tokens[i]; |
|
484 symbol = token[0]; |
|
485 |
|
486 if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); |
|
487 else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); |
|
488 else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate); |
|
489 else if (symbol === '&') value = this.unescapedValue(token, context); |
|
490 else if (symbol === 'name') value = this.escapedValue(token, context); |
|
491 else if (symbol === 'text') value = this.rawValue(token); |
|
492 |
|
493 if (value !== undefined) |
|
494 buffer += value; |
|
495 } |
|
496 |
|
497 return buffer; |
|
498 }; |
|
499 |
|
500 Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { |
|
501 var self = this; |
|
502 var buffer = ''; |
|
503 var value = context.lookup(token[1]); |
|
504 |
|
505 // This function is used to render an arbitrary template |
|
506 // in the current context by higher-order sections. |
|
507 function subRender (template) { |
|
508 return self.render(template, context, partials); |
|
509 } |
|
510 |
|
511 if (!value) return; |
|
512 |
|
513 if (isArray(value)) { |
|
514 for (var j = 0, valueLength = value.length; j < valueLength; ++j) { |
|
515 buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); |
|
516 } |
|
517 } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { |
|
518 buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); |
|
519 } else if (isFunction(value)) { |
|
520 if (typeof originalTemplate !== 'string') |
|
521 throw new Error('Cannot use higher-order sections without the original template'); |
|
522 |
|
523 // Extract the portion of the original template that the section contains. |
|
524 value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); |
|
525 |
|
526 if (value != null) |
|
527 buffer += value; |
|
528 } else { |
|
529 buffer += this.renderTokens(token[4], context, partials, originalTemplate); |
|
530 } |
|
531 return buffer; |
|
532 }; |
|
533 |
|
534 Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { |
|
535 var value = context.lookup(token[1]); |
|
536 |
|
537 // Use JavaScript's definition of falsy. Include empty arrays. |
|
538 // See https://github.com/janl/mustache.js/issues/186 |
|
539 if (!value || (isArray(value) && value.length === 0)) |
|
540 return this.renderTokens(token[4], context, partials, originalTemplate); |
|
541 }; |
|
542 |
|
543 Writer.prototype.renderPartial = function renderPartial (token, context, partials) { |
|
544 if (!partials) return; |
|
545 |
|
546 var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; |
|
547 if (value != null) |
|
548 return this.renderTokens(this.parse(value), context, partials, value); |
|
549 }; |
|
550 |
|
551 Writer.prototype.unescapedValue = function unescapedValue (token, context) { |
|
552 var value = context.lookup(token[1]); |
|
553 if (value != null) |
|
554 return value; |
|
555 }; |
|
556 |
|
557 Writer.prototype.escapedValue = function escapedValue (token, context) { |
|
558 var value = context.lookup(token[1]); |
|
559 if (value != null) |
|
560 return mustache.escape(value); |
|
561 }; |
|
562 |
|
563 Writer.prototype.rawValue = function rawValue (token) { |
|
564 return token[1]; |
|
565 }; |
|
566 |
|
567 mustache.name = 'mustache.js'; |
|
568 mustache.version = '2.1.3'; |
|
569 mustache.tags = [ '{{', '}}' ]; |
|
570 |
|
571 // All high-level mustache.* functions use this writer. |
|
572 var defaultWriter = new Writer(); |
|
573 |
|
574 /** |
|
575 * Clears all cached templates in the default writer. |
|
576 */ |
|
577 mustache.clearCache = function clearCache () { |
|
578 return defaultWriter.clearCache(); |
|
579 }; |
|
580 |
|
581 /** |
|
582 * Parses and caches the given template in the default writer and returns the |
|
583 * array of tokens it contains. Doing this ahead of time avoids the need to |
|
584 * parse templates on the fly as they are rendered. |
|
585 */ |
|
586 mustache.parse = function parse (template, tags) { |
|
587 return defaultWriter.parse(template, tags); |
|
588 }; |
|
589 |
|
590 /** |
|
591 * Renders the `template` with the given `view` and `partials` using the |
|
592 * default writer. |
|
593 */ |
|
594 mustache.render = function render (template, view, partials) { |
|
595 if (typeof template !== 'string') { |
|
596 throw new TypeError('Invalid template! Template should be a "string" ' + |
|
597 'but "' + typeStr(template) + '" was given as the first ' + |
|
598 'argument for mustache#render(template, view, partials)'); |
|
599 } |
|
600 |
|
601 return defaultWriter.render(template, view, partials); |
|
602 }; |
|
603 |
|
604 // This is here for backwards compatibility with 0.4.x., |
|
605 /*eslint-disable */ // eslint wants camel cased function name |
|
606 mustache.to_html = function to_html (template, view, partials, send) { |
|
607 /*eslint-enable*/ |
|
608 |
|
609 var result = mustache.render(template, view, partials); |
|
610 |
|
611 if (isFunction(send)) { |
|
612 send(result); |
|
613 } else { |
|
614 return result; |
|
615 } |
|
616 }; |
|
617 |
|
618 // Export the escaping function so that the user may override it. |
|
619 // See https://github.com/janl/mustache.js/issues/244 |
|
620 mustache.escape = escapeHtml; |
|
621 |
|
622 // Export these mainly for testing, but also for advanced usage. |
|
623 mustache.Scanner = Scanner; |
|
624 mustache.Context = Context; |
|
625 mustache.Writer = Writer; |
|
626 |
|
627 })); |
|