2 * mustache.js - Logic-less {{mustache}} templates with JavaScript
3 * http://github.com/janl/mustache.js
6 /*global define: false*/
8 (function (root, factory) {
9 if (typeof exports === "object" && exports) {
10 factory(exports); // CommonJS
14 if (typeof define === "function" && define.amd) {
15 define(mustache); // AMD
17 root.Mustache = mustache; // <script>
20 }(this, function (mustache) {
24 var nonSpaceRe = /\S/;
26 var curlyRe = /\s*\}/;
27 var tagRe = /#|\^|\/|>|\{|&|=|!/;
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);
36 function isWhitespace(string) {
37 return !testRegExp(nonSpaceRe, string);
40 var Object_toString = Object.prototype.toString;
41 var isArray = Array.isArray || function (obj) {
42 return Object_toString.call(obj) === '[object Array]';
45 function escapeRegExp(string) {
46 return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
58 function escapeHtml(string) {
59 return String(string).replace(/[&<>"'\/]/g, function (s) {
64 function Scanner(string) {
71 * Returns `true` if the tail is empty (end of string).
73 Scanner.prototype.eos = function () {
74 return this.tail === "";
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.
81 Scanner.prototype.scan = function (re) {
82 var match = this.tail.match(re);
84 if (match && match.index === 0) {
85 this.tail = this.tail.substring(match[0].length);
86 this.pos += match[0].length;
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.
97 Scanner.prototype.scanUntil = function (re) {
98 var match, pos = this.tail.search(re);
103 this.pos += this.tail.length;
110 match = this.tail.substring(0, pos);
111 this.tail = this.tail.substring(pos);
118 function Context(view, parent) {
119 this.view = view || {};
120 this.parent = parent;
124 Context.make = function (view) {
125 return (view instanceof Context) ? view : new Context(view);
128 Context.prototype.push = function (view) {
129 return new Context(view, this);
132 Context.prototype.lookup = function (name) {
133 var value = this._cache[name];
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++]];
149 value = context.view[name];
152 if (value != null) break;
154 context = context.parent;
158 this._cache[name] = value;
161 if (typeof value === 'function') value = value.call(this.view);
170 Writer.prototype.clearCache = function () {
172 this._partialCache = {};
175 Writer.prototype.compile = function (template, tags) {
176 var fn = this._cache[template];
179 var tokens = mustache.parse(template, tags);
180 fn = this._cache[template] = this.compileTokens(tokens, template);
186 Writer.prototype.compilePartial = function (name, template, tags) {
187 var fn = this.compile(template, tags);
188 this._partialCache[name] = fn;
192 Writer.prototype.getPartial = function (name) {
193 if (!(name in this._partialCache) && this._loadPartial) {
194 this.compilePartial(name, this._loadPartial(name));
197 return this._partialCache[name];
200 Writer.prototype.compileTokens = function (tokens, template) {
202 return function (view, partials) {
204 if (typeof partials === 'function') {
205 self._loadPartial = partials;
207 for (var name in partials) {
208 self.compilePartial(name, partials[name]);
213 return renderTokens(tokens, self, Context.make(view), template);
217 Writer.prototype.render = function (template, view, partials) {
218 return this.compile(template)(view, partials);
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.
227 function renderTokens(tokens, writer, context, template) {
230 var token, tokenValue, value;
231 for (var i = 0, len = tokens.length; i < len; ++i) {
233 tokenValue = token[1];
237 value = context.lookup(tokenValue);
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);
245 buffer += renderTokens(token[4], writer, context.push(value), template);
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);
252 if (value != null) buffer += value;
254 buffer += renderTokens(token[4], writer, context, template);
259 value = context.lookup(tokenValue);
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);
269 value = writer.getPartial(tokenValue);
270 if (typeof value === 'function') buffer += value(context);
273 value = context.lookup(tokenValue);
274 if (value != null) buffer += value;
277 value = context.lookup(tokenValue);
278 if (value != null) buffer += mustache.escape(value);
281 buffer += tokenValue;
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.
295 function nestTokens(tokens) {
297 var collector = tree;
301 for (var i = 0, len = tokens.length; i < len; ++i) {
306 sections.push(token);
307 collector.push(token);
308 collector = token[4] = [];
311 var section = sections.pop();
312 section[5] = token[2];
313 collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
316 collector.push(token);
324 * Combines the values of consecutive text tokens in the given `tokens` array
327 function squashTokens(tokens) {
328 var squashedTokens = [];
330 var token, lastToken;
331 for (var i = 0, len = tokens.length; i < len; ++i) {
334 if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
335 lastToken[1] += token[1];
336 lastToken[3] = token[3];
339 squashedTokens.push(token);
344 return squashedTokens;
347 function escapeTags(tags) {
349 new RegExp(escapeRegExp(tags[0]) + "\\s*"),
350 new RegExp("\\s*" + escapeRegExp(tags[1]))
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).
360 function parseTemplate(template, tags) {
361 template = template || '';
362 tags = tags || mustache.tags;
364 if (typeof tags === 'string') tags = tags.split(spaceRe);
365 if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', '));
367 var tagRes = escapeTags(tags);
368 var scanner = new Scanner(template);
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?
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()];
391 var start, type, value, chr, token;
392 while (!scanner.eos()) {
395 // Match any text between tags.
396 value = scanner.scanUntil(tagRes[0]);
398 for (var i = 0, len = value.length; i < len; ++i) {
399 chr = value.charAt(i);
401 if (isWhitespace(chr)) {
402 spaces.push(tokens.length);
407 tokens.push(['text', chr, start, start + 1]);
410 // Check for whitespace on the current line.
411 if (chr == '\n') stripSpace();
415 // Match the opening tag.
416 if (!scanner.scan(tagRes[0])) break;
420 type = scanner.scan(tagRe) || 'name';
421 scanner.scan(whiteRe);
423 // Get the tag value.
425 value = scanner.scanUntil(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]);
434 value = scanner.scanUntil(tagRes[1]);
437 // Match the closing tag.
438 if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos);
440 token = [type, value, start, scanner.pos];
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 === '&') {
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);
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);
464 tokens = squashTokens(tokens);
466 return nestTokens(tokens);
469 mustache.name = "mustache.js";
470 mustache.version = "0.7.2";
471 mustache.tags = ["{{", "}}"];
473 mustache.Scanner = Scanner;
474 mustache.Context = Context;
475 mustache.Writer = Writer;
477 mustache.parse = parseTemplate;
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;
483 // All Mustache.* functions use this writer.
484 var defaultWriter = new Writer();
487 * Clears all cached templates and partials in the default writer.
489 mustache.clearCache = function () {
490 return defaultWriter.clearCache();
494 * Compiles the given `template` to a reusable function using the default
497 mustache.compile = function (template, tags) {
498 return defaultWriter.compile(template, tags);
502 * Compiles the partial with the given `name` and `template` to a reusable
503 * function using the default writer.
505 mustache.compilePartial = function (name, template, tags) {
506 return defaultWriter.compilePartial(name, template, tags);
510 * Compiles the given array of tokens (the output of a parse) to a reusable
511 * function using the default writer.
513 mustache.compileTokens = function (tokens, template) {
514 return defaultWriter.compileTokens(tokens, template);
518 * Renders the `template` with the given `view` and `partials` using the
521 mustache.render = function (template, view, partials) {
522 return defaultWriter.render(template, view, partials);
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);
529 if (typeof send === "function") {