Merge "Special:Upload should not crash on failing previews"
[mediawiki.git] / resources / lib / mustache / mustache.js
blobdbc982319559b073d76e79a1b2bda03435e9c2ae
1 /*!
2  * mustache.js - Logic-less {{mustache}} templates with JavaScript
3  * http://github.com/janl/mustache.js
4  */
6 /*global define: false*/
8 (function (global, factory) {
9   if (typeof exports === "object" && exports) {
10     factory(exports); // CommonJS
11   } else if (typeof define === "function" && define.amd) {
12     define(['exports'], factory); // AMD
13   } else {
14     factory(global.Mustache = {}); // <script>
15   }
16 }(this, function (mustache) {
18   var Object_toString = Object.prototype.toString;
19   var isArray = Array.isArray || function (object) {
20     return Object_toString.call(object) === '[object Array]';
21   };
23   function isFunction(object) {
24     return typeof object === 'function';
25   }
27   function escapeRegExp(string) {
28     return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
29   }
31   // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
32   // See https://github.com/janl/mustache.js/issues/189
33   var RegExp_test = RegExp.prototype.test;
34   function testRegExp(re, string) {
35     return RegExp_test.call(re, string);
36   }
38   var nonSpaceRe = /\S/;
39   function isWhitespace(string) {
40     return !testRegExp(nonSpaceRe, string);
41   }
43   var entityMap = {
44     "&": "&amp;",
45     "<": "&lt;",
46     ">": "&gt;",
47     '"': '&quot;',
48     "'": '&#39;',
49     "/": '&#x2F;'
50   };
52   function escapeHtml(string) {
53     return String(string).replace(/[&<>"'\/]/g, function (s) {
54       return entityMap[s];
55     });
56   }
58   var whiteRe = /\s*/;
59   var spaceRe = /\s+/;
60   var equalsRe = /\s*=/;
61   var curlyRe = /\s*\}/;
62   var tagRe = /#|\^|\/|>|\{|&|=|!/;
64   /**
65    * Breaks up the given `template` string into a tree of tokens. If the `tags`
66    * argument is given here it must be an array with two string values: the
67    * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
68    * course, the default is to use mustaches (i.e. mustache.tags).
69    *
70    * A token is an array with at least 4 elements. The first element is the
71    * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
72    * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
73    * all text that appears outside a symbol this element is "text".
74    *
75    * The second element of a token is its "value". For mustache tags this is
76    * whatever else was inside the tag besides the opening symbol. For text tokens
77    * this is the text itself.
78    *
79    * The third and fourth elements of the token are the start and end indices,
80    * respectively, of the token in the original template.
81    *
82    * Tokens that are the root node of a subtree contain two more elements: 1) an
83    * array of tokens in the subtree and 2) the index in the original template at
84    * which the closing tag for that section begins.
85    */
86   function parseTemplate(template, tags) {
87     if (!template)
88       return [];
90     var sections = [];     // Stack to hold section tokens
91     var tokens = [];       // Buffer to hold the tokens
92     var spaces = [];       // Indices of whitespace tokens on the current line
93     var hasTag = false;    // Is there a {{tag}} on the current line?
94     var nonSpace = false;  // Is there a non-space char on the current line?
96     // Strips all whitespace tokens array for the current line
97     // if there was a {{#tag}} on it and otherwise only space.
98     function stripSpace() {
99       if (hasTag && !nonSpace) {
100         while (spaces.length)
101           delete tokens[spaces.pop()];
102       } else {
103         spaces = [];
104       }
106       hasTag = false;
107       nonSpace = false;
108     }
110     var openingTagRe, closingTagRe, closingCurlyRe;
111     function compileTags(tags) {
112       if (typeof tags === 'string')
113         tags = tags.split(spaceRe, 2);
115       if (!isArray(tags) || tags.length !== 2)
116         throw new Error('Invalid tags: ' + tags);
118       openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
119       closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
120       closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
121     }
123     compileTags(tags || mustache.tags);
125     var scanner = new Scanner(template);
127     var start, type, value, chr, token, openSection;
128     while (!scanner.eos()) {
129       start = scanner.pos;
131       // Match any text between tags.
132       value = scanner.scanUntil(openingTagRe);
134       if (value) {
135         for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
136           chr = value.charAt(i);
138           if (isWhitespace(chr)) {
139             spaces.push(tokens.length);
140           } else {
141             nonSpace = true;
142           }
144           tokens.push([ 'text', chr, start, start + 1 ]);
145           start += 1;
147           // Check for whitespace on the current line.
148           if (chr === '\n')
149             stripSpace();
150         }
151       }
153       // Match the opening tag.
154       if (!scanner.scan(openingTagRe))
155         break;
157       hasTag = true;
159       // Get the tag type.
160       type = scanner.scan(tagRe) || 'name';
161       scanner.scan(whiteRe);
163       // Get the tag value.
164       if (type === '=') {
165         value = scanner.scanUntil(equalsRe);
166         scanner.scan(equalsRe);
167         scanner.scanUntil(closingTagRe);
168       } else if (type === '{') {
169         value = scanner.scanUntil(closingCurlyRe);
170         scanner.scan(curlyRe);
171         scanner.scanUntil(closingTagRe);
172         type = '&';
173       } else {
174         value = scanner.scanUntil(closingTagRe);
175       }
177       // Match the closing tag.
178       if (!scanner.scan(closingTagRe))
179         throw new Error('Unclosed tag at ' + scanner.pos);
181       token = [ type, value, start, scanner.pos ];
182       tokens.push(token);
184       if (type === '#' || type === '^') {
185         sections.push(token);
186       } else if (type === '/') {
187         // Check section nesting.
188         openSection = sections.pop();
190         if (!openSection)
191           throw new Error('Unopened section "' + value + '" at ' + start);
193         if (openSection[1] !== value)
194           throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
195       } else if (type === 'name' || type === '{' || type === '&') {
196         nonSpace = true;
197       } else if (type === '=') {
198         // Set the tags for the next time around.
199         compileTags(value);
200       }
201     }
203     // Make sure there are no open sections when we're done.
204     openSection = sections.pop();
206     if (openSection)
207       throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
209     return nestTokens(squashTokens(tokens));
210   }
212   /**
213    * Combines the values of consecutive text tokens in the given `tokens` array
214    * to a single token.
215    */
216   function squashTokens(tokens) {
217     var squashedTokens = [];
219     var token, lastToken;
220     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
221       token = tokens[i];
223       if (token) {
224         if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
225           lastToken[1] += token[1];
226           lastToken[3] = token[3];
227         } else {
228           squashedTokens.push(token);
229           lastToken = token;
230         }
231       }
232     }
234     return squashedTokens;
235   }
237   /**
238    * Forms the given array of `tokens` into a nested tree structure where
239    * tokens that represent a section have two additional items: 1) an array of
240    * all tokens that appear in that section and 2) the index in the original
241    * template that represents the end of that section.
242    */
243   function nestTokens(tokens) {
244     var nestedTokens = [];
245     var collector = nestedTokens;
246     var sections = [];
248     var token, section;
249     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
250       token = tokens[i];
252       switch (token[0]) {
253       case '#':
254       case '^':
255         collector.push(token);
256         sections.push(token);
257         collector = token[4] = [];
258         break;
259       case '/':
260         section = sections.pop();
261         section[5] = token[2];
262         collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
263         break;
264       default:
265         collector.push(token);
266       }
267     }
269     return nestedTokens;
270   }
272   /**
273    * A simple string scanner that is used by the template parser to find
274    * tokens in template strings.
275    */
276   function Scanner(string) {
277     this.string = string;
278     this.tail = string;
279     this.pos = 0;
280   }
282   /**
283    * Returns `true` if the tail is empty (end of string).
284    */
285   Scanner.prototype.eos = function () {
286     return this.tail === "";
287   };
289   /**
290    * Tries to match the given regular expression at the current position.
291    * Returns the matched text if it can match, the empty string otherwise.
292    */
293   Scanner.prototype.scan = function (re) {
294     var match = this.tail.match(re);
296     if (!match || match.index !== 0)
297       return '';
299     var string = match[0];
301     this.tail = this.tail.substring(string.length);
302     this.pos += string.length;
304     return string;
305   };
307   /**
308    * Skips all text until the given regular expression can be matched. Returns
309    * the skipped string, which is the entire tail if no match can be made.
310    */
311   Scanner.prototype.scanUntil = function (re) {
312     var index = this.tail.search(re), match;
314     switch (index) {
315     case -1:
316       match = this.tail;
317       this.tail = "";
318       break;
319     case 0:
320       match = "";
321       break;
322     default:
323       match = this.tail.substring(0, index);
324       this.tail = this.tail.substring(index);
325     }
327     this.pos += match.length;
329     return match;
330   };
332   /**
333    * Represents a rendering context by wrapping a view object and
334    * maintaining a reference to the parent context.
335    */
336   function Context(view, parentContext) {
337     this.view = view == null ? {} : view;
338     this.cache = { '.': this.view };
339     this.parent = parentContext;
340   }
342   /**
343    * Creates a new context using the given view with this context
344    * as the parent.
345    */
346   Context.prototype.push = function (view) {
347     return new Context(view, this);
348   };
350   /**
351    * Returns the value of the given name in this context, traversing
352    * up the context hierarchy if the value is absent in this context's view.
353    */
354   Context.prototype.lookup = function (name) {
355     var cache = this.cache;
357     var value;
358     if (name in cache) {
359       value = cache[name];
360     } else {
361       var context = this, names, index;
363       while (context) {
364         if (name.indexOf('.') > 0) {
365           value = context.view;
366           names = name.split('.');
367           index = 0;
369           while (value != null && index < names.length)
370             value = value[names[index++]];
371         } else if (typeof context.view == 'object') {
372           value = context.view[name];
373         }
375         if (value != null)
376           break;
378         context = context.parent;
379       }
381       cache[name] = value;
382     }
384     if (isFunction(value))
385       value = value.call(this.view);
387     return value;
388   };
390   /**
391    * A Writer knows how to take a stream of tokens and render them to a
392    * string, given a context. It also maintains a cache of templates to
393    * avoid the need to parse the same template twice.
394    */
395   function Writer() {
396     this.cache = {};
397   }
399   /**
400    * Clears all cached templates in this writer.
401    */
402   Writer.prototype.clearCache = function () {
403     this.cache = {};
404   };
406   /**
407    * Parses and caches the given `template` and returns the array of tokens
408    * that is generated from the parse.
409    */
410   Writer.prototype.parse = function (template, tags) {
411     var cache = this.cache;
412     var tokens = cache[template];
414     if (tokens == null)
415       tokens = cache[template] = parseTemplate(template, tags);
417     return tokens;
418   };
420   /**
421    * High-level method that is used to render the given `template` with
422    * the given `view`.
423    *
424    * The optional `partials` argument may be an object that contains the
425    * names and templates of partials that are used in the template. It may
426    * also be a function that is used to load partial templates on the fly
427    * that takes a single argument: the name of the partial.
428    */
429   Writer.prototype.render = function (template, view, partials) {
430     var tokens = this.parse(template);
431     var context = (view instanceof Context) ? view : new Context(view);
432     return this.renderTokens(tokens, context, partials, template);
433   };
435   /**
436    * Low-level method that renders the given array of `tokens` using
437    * the given `context` and `partials`.
438    *
439    * Note: The `originalTemplate` is only ever used to extract the portion
440    * of the original template that was contained in a higher-order section.
441    * If the template doesn't use higher-order sections, this argument may
442    * be omitted.
443    */
444   Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
445     var buffer = '';
447     // This function is used to render an arbitrary template
448     // in the current context by higher-order sections.
449     var self = this;
450     function subRender(template) {
451       return self.render(template, context, partials);
452     }
454     var token, value;
455     for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
456       token = tokens[i];
458       switch (token[0]) {
459       case '#':
460         value = context.lookup(token[1]);
462         if (!value)
463           continue;
465         if (isArray(value)) {
466           for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
467             buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
468           }
469         } else if (typeof value === 'object' || typeof value === 'string') {
470           buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
471         } else if (isFunction(value)) {
472           if (typeof originalTemplate !== 'string')
473             throw new Error('Cannot use higher-order sections without the original template');
475           // Extract the portion of the original template that the section contains.
476           value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
478           if (value != null)
479             buffer += value;
480         } else {
481           buffer += this.renderTokens(token[4], context, partials, originalTemplate);
482         }
484         break;
485       case '^':
486         value = context.lookup(token[1]);
488         // Use JavaScript's definition of falsy. Include empty arrays.
489         // See https://github.com/janl/mustache.js/issues/186
490         if (!value || (isArray(value) && value.length === 0))
491           buffer += this.renderTokens(token[4], context, partials, originalTemplate);
493         break;
494       case '>':
495         if (!partials)
496           continue;
498         value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
500         if (value != null)
501           buffer += this.renderTokens(this.parse(value), context, partials, value);
503         break;
504       case '&':
505         value = context.lookup(token[1]);
507         if (value != null)
508           buffer += value;
510         break;
511       case 'name':
512         value = context.lookup(token[1]);
514         if (value != null)
515           buffer += mustache.escape(value);
517         break;
518       case 'text':
519         buffer += token[1];
520         break;
521       }
522     }
524     return buffer;
525   };
527   mustache.name = "mustache.js";
528   mustache.version = "0.8.2";
529   mustache.tags = [ "{{", "}}" ];
531   // All high-level mustache.* functions use this writer.
532   var defaultWriter = new Writer();
534   /**
535    * Clears all cached templates in the default writer.
536    */
537   mustache.clearCache = function () {
538     return defaultWriter.clearCache();
539   };
541   /**
542    * Parses and caches the given template in the default writer and returns the
543    * array of tokens it contains. Doing this ahead of time avoids the need to
544    * parse templates on the fly as they are rendered.
545    */
546   mustache.parse = function (template, tags) {
547     return defaultWriter.parse(template, tags);
548   };
550   /**
551    * Renders the `template` with the given `view` and `partials` using the
552    * default writer.
553    */
554   mustache.render = function (template, view, partials) {
555     return defaultWriter.render(template, view, partials);
556   };
558   // This is here for backwards compatibility with 0.4.x.
559   mustache.to_html = function (template, view, partials, send) {
560     var result = mustache.render(template, view, partials);
562     if (isFunction(send)) {
563       send(result);
564     } else {
565       return result;
566     }
567   };
569   // Export the escaping function so that the user may override it.
570   // See https://github.com/janl/mustache.js/issues/244
571   mustache.escape = escapeHtml;
573   // Export these mainly for testing, but also for advanced usage.
574   mustache.Scanner = Scanner;
575   mustache.Context = Context;
576   mustache.Writer = Writer;
578 }));