/* mustache.js — Logic-less templates in JavaScript See http://mustache.github.com/ for more info. */ var Mustache = function() { function ParserException(message) { this.message = message; } ParserException.prototype = {}; var Renderer = function(send_func, mode) { this.user_send_func = send_func; if (mode==='interpreter' || !mode) { this.commandSet = this.interpreter; this.send_func = function(text) { this.user_send_func(text); } } else if (mode==='compiler') { this.commandSet = this.compiler; this.cached_output = []; this.send_func = function(text) { this.cached_output.push(text); } } else { throw new ParserException('Unsupported mode.'); } this.pragmas = {}; // Fix up the stupidness that is IE's split implementation this._compliantExecNpcg = /()??/.exec("")[1] === undefined; // NPCG: nonparticipating capturing group var hasCapturingSplit = '{{hi}}'.split(/(hi)/).length === 3; if (!hasCapturingSplit) { this.splitFunc = this.capturingSplit; } else { this.splitFunc = String.prototype.split; } }; Renderer.prototype = { capturingSplit: function(separator) { // fix up the stupidness that is IE's broken String.split implementation // originally by Steven Levithan /* Cross-Browser Split 1.0.1 (c) Steven Levithan ; MIT License An ECMA-compliant, uniform cross-browser split method */ var str = this; var limit = undefined; // if `separator` is not a regex, use the native `split` if (Object.prototype.toString.call(separator) !== "[object RegExp]") { return String.prototype.split.call(str, separator, limit); } var output = [], lastLastIndex = 0, flags = (separator.ignoreCase ? "i" : "") + (separator.multiline ? "m" : "") + (separator.sticky ? "y" : ""), separator = RegExp(separator.source, flags + "g"), // make `global` and avoid `lastIndex` issues by working with a copy separator2, match, lastIndex, lastLength; str = str + ""; // type conversion if (!this._compliantExecNpcg) { separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt } /* behavior for `limit`: if it's... - `undefined`: no limit. - `NaN` or zero: return an empty array. - a positive number: use `Math.floor(limit)`. - a negative number: no limit. - other: type-convert, then use the above rules. */ if (limit === undefined || +limit < 0) { limit = Infinity; } else { limit = Math.floor(+limit); if (!limit) { return []; } } while (match = separator.exec(str)) { lastIndex = match.index + match[0].length; // `separator.lastIndex` is not reliable cross-browser if (lastIndex > lastLastIndex) { output.push(str.slice(lastLastIndex, match.index)); // fix browsers whose `exec` methods don't consistently return `undefined` for nonparticipating capturing groups if (!this._compliantExecNpcg && match.length > 1) { match[0].replace(separator2, function () { for (var i = 1; i < arguments.length - 2; i++) { if (arguments[i] === undefined) { match[i] = undefined; } } }); } if (match.length > 1 && match.index < str.length) { Array.prototype.push.apply(output, match.slice(1)); } lastLength = match[0].length; lastLastIndex = lastIndex; if (output.length >= limit) { break; } } if (separator.lastIndex === match.index) { separator.lastIndex++; // avoid an infinite loop } } if (lastLastIndex === str.length) { if (lastLength || !separator.test("")) { output.push(""); } } else { output.push(str.slice(lastLastIndex)); } return output.length > limit ? output.slice(0, limit) : output; }, render: function(template, context, partials) { template = this.parse_pragmas(template, '{{', '}}'); var tokens = this.tokenize(template, '{{', '}}'); this.parse(this.createParserContext(tokens, partials, '{{', '}}'), [context]); }, createParserContext: function(tokens, partials, openTag, closeTag) { return { tokens: tokens, token: function() { return this.tokens[this.index]; }, index: 0, length: tokens.length, partials: partials, stack: [], openTag: openTag, closeTag: closeTag }; }, tokenize: function(template, openTag, closeTag) { var delimiters = [ '\\{', '&', '\\}', '#', '\\^', '\\/', '>', '=', '%', '!', '\\s+' ]; delimiters.unshift(this.escape_regex(openTag)); delimiters.unshift(this.escape_regex(closeTag)); var regex = new RegExp('(' + delimiters.join('|') + ')'); var tokens = this.splitFunc.call(template, regex); var cleaned_tokens = []; for (var i = 0, n = tokens.length; i0) { var optionPairs = suffix.split(','); var scratch; options = {}; for (var i=0, n=optionPairs.length; i': parserContext.stack.push({tagType:'partial'}); return 'simpleKeyName'; case '=': parserContext.stack.push({tagType: 'setDelimiter'}); return 'setDelimiterStart'; case '!': return 'discard'; case '%': throw new ParserException('Pragmas are only supported as a preprocessing directive.'); case '/': // close mustache throw new ParserException('Unexpected closing tag.'); case '}': // close triple mustache throw new ParserException('Unexpected token encountered.'); default: parserContext.stack.push({tagType:'variable'}); return this.stateMachine.keyName.call(this, parserContext, contextStack); } }, closeMustache: function(parserContext, contextStack) { if (this.isWhitespace(parserContext.token())) { return 'closeMustache'; } else if (parserContext.token()===parserContext.closeTag) { return this.dispatchCommand(parserContext, contextStack); } }, expectClosingMustache: function(parserContext, contextStack) { if (parserContext.closeTag==='}}' && parserContext.token()==='}}') { return 'expectClosingParenthesis'; } else if (parserContext.token()==='}') { return 'closeMustache'; } else { throw new ParserException('Unexpected token encountered.'); } }, expectClosingParenthesis: function(parserContext, contextStack) { if (parserContext.token()==='}') { return this.dispatchCommand(parserContext, contextStack); } else { throw new ParserException('Unexpected token encountered.'); } }, keyName: function(parserContext, contextStack) { var result = this.stateMachine.simpleKeyName.call(this, parserContext, contextStack); if (result==='closeMustache') { var tagKey = parserContext.stack.pop(); var tag = parserContext.stack.pop(); if (tag.tagType==='unescapedVariable' && tag.subtype==='tripleMustache') { parserContext.stack.push({tagType:'unescapedVariable'}); parserContext.stack.push(tagKey); return 'expectClosingMustache'; } else { parserContext.stack.push(tag); parserContext.stack.push(tagKey); return 'closeMustache'; } } else if (result==='simpleKeyName') { return 'keyName'; } else { throw new ParserException('Unexpected branch in tag name: ' + result); } }, simpleKeyName: function(parserContext, contextStack) { if (this.isWhitespace(parserContext.token())) { return 'simpleKeyName'; } else { parserContext.stack.push(parserContext.token()); return 'closeMustache'; } }, setDelimiterStart: function(parserContext, contextStack) { if (this.isWhitespace(parserContext.token()) || parserContext.token()==='=') { throw new ParserException('Syntax error in Set Delimiter tag'); } else { parserContext.stack.push(parserContext.token()); return 'setDelimiterStartOrWhitespace'; } }, setDelimiterStartOrWhitespace: function(parserContext, contextStack) { if (this.isWhitespace(parserContext.token())) { return 'setDelimiterEnd'; } else if (parserContext.token()==='='){ throw new ParserException('Syntax error in Set Delimiter tag'); } else { parserContext.stack.push(parserContext.stack.pop() + parserContext.token()); return 'setDelimiterStartOrWhitespace'; } }, setDelimiterEnd: function(parserContext, contextStack) { if (this.isWhitespace(parserContext.token())) { return 'setDelimiterEnd'; } else if (parserContext.token()==='=') { throw new ParserException('Syntax error in Set Delimiter tag'); } else { parserContext.stack.push(parserContext.token()); return 'setDelimiterEndOrEqualSign'; } }, setDelimiterEndOrEqualSign: function(parserContext, contextStack) { if (parserContext.token()==='=') { return 'setDelimiterExpectClosingTag'; } else if (this.isWhitespace(parserContext.token())) { throw new ParserException('Syntax error in Set Delimiter tag'); } else { parserContext.stack.push(parserContext.stack.pop() + parserContext.token()); return 'setDelimiterEndOrEqualSign'; } }, setDelimiterExpectClosingTag: function(parserContext, contextStack) { if (parserContext.token()===parserContext.closeTag) { var newCloseTag = parserContext.stack.pop(); var newOpenTag = parserContext.stack.pop(); var command = parserContext.stack.pop(); if (command.tagType!=='setDelimiter') { throw new ParserException('Syntax error in Set Delimiter tag'); } else { var tokens = this.tokenize( parserContext.tokens.slice(parserContext.index+1).join(''), newOpenTag, newCloseTag); var newParserContext = this.createParserContext(tokens, parserContext.partials, newOpenTag, newCloseTag); parserContext.tokens = newParserContext.tokens; parserContext.index = -1; parserContext.length = newParserContext.length; parserContext.openTag = newParserContext.openTag; parserContext.closeTag = newParserContext.closeTag; return 'text'; } } else { throw new ParserException('Syntax error in Set Delimiter tag'); } }, endSectionScan: function(parserContext, contextStack) { switch (parserContext.token()) { case parserContext.openTag: return 'expectSectionOrEndSection'; default: parserContext.stack[parserContext.stack.length-1].content.push(parserContext.token()); return 'endSectionScan'; } }, expectSectionOrEndSection: function(parserContext, contextStack) { switch (parserContext.token()) { case '#': case '^': parserContext.stack[parserContext.stack.length-1].depth++; parserContext.stack[parserContext.stack.length-1].content.push(parserContext.openTag + parserContext.token()); return 'endSectionScan'; case '/': parserContext.stack.push({tagType:'endSection'}); return 'simpleKeyName'; default: parserContext.stack[parserContext.stack.length-1].content.push(parserContext.openTag + parserContext.token()); return 'endSectionScan'; } }, discard: function(parserContext, contextStack) { if (parserContext.token()==='!') { return 'closeComment'; } else { return 'discard'; } }, closeComment: function(parserContext, contextStack) { if (parserContext.token()!==parserContext.closeTag) { return 'discard'; } else { return 'text'; } }, endOfDoc: function(parserContext, contextStack) { // eventually we may want to give better error messages throw new ParserException('Unexpected end of document.'); } }, dispatchCommand: function(parserContext, contextStack) { var key = parserContext.stack.pop(); var command = parserContext.stack.pop(); switch (command.tagType) { case 'section': case 'invertedSection': parserContext.stack.push({sectionType:command.tagType, key:key, content:[], depth:1}); return 'endSectionScan'; case 'variable': this.commandSet.variable.call(this, key, contextStack); return 'text'; case 'unescapedVariable': this.commandSet.unescaped_variable.call(this, key, contextStack); return 'text'; case 'partial': this.commandSet.partial.call(this, key, contextStack, parserContext.partials, parserContext.openTag, parserContext.closeTag); return 'text'; case 'endSection': var section = parserContext.stack.pop(); if (--section.depth === 0) { if (section.key === key) { this.commandSet.section.call(this, section.sectionType, section.content.join(''), key, contextStack, parserContext.partials, parserContext.openTag, parserContext.closeTag); return 'text'; } else { throw new ParserException('Unbalanced open/close section tags'); } } else { section.content.push('{{/' + key + '}}'); parserContext.stack.push(section); return 'endSectionScan'; } default: throw new ParserException('Unknown dispatch command: ' + command.tagType); } }, pragmaDirectives: { 'IMPLICIT-ITERATOR': function(options) { this.pragmas['IMPLICIT-ITERATOR'] = {}; if (options) { this.pragmas['IMPLICIT-ITERATOR'].iterator = options['iterator']; } } }, /* find `name` in current `context`. That is find me a value from the view object */ find: function(name, context) { // Checks whether a value is truthy or false or 0 function is_kinda_truthy(bool) { return bool === false || bool === 0 || bool; } var value; if (is_kinda_truthy(context[name])) { value = context[name]; } if (this.is_function(value)) { return value.apply(context); } return value; }, find_in_stack: function(name, contextStack) { var value; value = this.find(name, contextStack[contextStack.length-1]); if (value!==undefined) { return value; } if (contextStack.length>1) { value = this.find(name, contextStack[0]); if (value!==undefined) { return value; } } return undefined; }, is_function: function(a) { return a && typeof a === 'function'; }, is_object: function(a) { return a && typeof a === 'object'; }, is_array: function(a) { return Object.prototype.toString.call(a) === '[object Array]'; }, interpreter: { text: function() { // in this implementation, rendering text is meaningless // since the send_func method simply forwards to user_send_func }, variable: function(key, contextStack) { function escapeHTML(str) { return ('' + str).replace(/&/g,'&') .replace(//g,'>'); } var result = this.find_in_stack(key, contextStack); if (result!==undefined) { this.user_send_func(escapeHTML(result)); } }, unescaped_variable: function(key, contextStack) { var result = this.find_in_stack(key, contextStack); if (result!==undefined) { this.user_send_func(result); } }, partial: function(key, contextStack, partials, openTag, closeTag) { if (!partials || partials[key] === undefined) { throw new ParserException('Unknown partial \'' + key + '\''); } var res = this.find_in_stack(key, contextStack); if (this.is_object(res)) { contextStack.push(res); } var tokens = this.tokenize(partials[key], openTag, closeTag); this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); if (this.is_object(res)) { contextStack.pop(); } }, section: function(sectionType, mustacheFragment, key, contextStack, partials, openTag, closeTag) { // by @langalex, support for arrays of strings var that = this; function create_context(_context) { if(that.is_object(_context)) { return _context; } else { var iterator = '.'; if(that.pragmas["IMPLICIT-ITERATOR"] && that.pragmas["IMPLICIT-ITERATOR"].iterator) { iterator = that.pragmas["IMPLICIT-ITERATOR"].iterator; } var ctx = {}; ctx[iterator] = _context; return ctx; } } var value = this.find_in_stack(key, contextStack); var tokens; if (sectionType==='invertedSection') { if (!value || this.is_array(value) && value.length === 0) { // false or empty list, render it tokens = this.tokenize(mustacheFragment, openTag, closeTag); this.parse(this.createParserContext(tokens, partials, openTag, closeTag), contextStack); } } else if (sectionType==='section') { if (this.is_array(value)) { // Enumerable, Let's loop! tokens = this.tokenize(mustacheFragment, openTag, closeTag); for (var i=0, n=value.length; i/g,'>'); } var that = this; this.user_send_func(function(contextStack, send_func) { var result = that.find_in_stack(key, contextStack); if (result!==undefined) { send_func(escapeHTML(result)); } }); }, unescaped_variable: function(key/*, contextStack*/) { var that = this; this.user_send_func(function(contextStack, send_func) { var result = that.find_in_stack(key, contextStack); if (result!==undefined) { send_func(result); } }); }, partial: function(key, reserved/*contextStack*/, partials, openTag, closeTag) { if (!partials || partials[key] === undefined) { throw new ParserException('Unknown partial \'' + key + '\''); } if (!this.is_function(partials[key])) { var old_user_send_func = this.user_send_func; var commands = []; this.user_send_func = function(command) { commands.push(command); }; var tokens = this.tokenize(partials[key], openTag, closeTag); partials[key] = function() {}; // blank out the paritals so that infinite recursion doesn't happen this.parse(this.createParserContext(tokens, partials, openTag, closeTag), reserved); this.user_send_func = old_user_send_func; var that = this; partials[key] = function(contextStack, send_func) { var res = that.find_in_stack(key, contextStack); if (that.is_object(res)) { contextStack.push(res); } for (var i=0,n=commands.length; i