diff --git a/mustache.js b/mustache.js index 8b52bc1..bcdf7c2 100644 --- a/mustache.js +++ b/mustache.js @@ -4,41 +4,11 @@ See http://mustache.github.com/ for more info. */ -var Mustache = function() { - function ParserException(message) { - this.message = message; - } - ParserException.prototype = {}; - - var Renderer = function(send_func) { - this._escapeCompiledRegex = null; - if (!Renderer.TokenizerRegex) { - Renderer.TokenizerRegex = this._createTokenizerRegex('{{', '}}'); - } - - this.user_send_func = send_func; - this.commandSet = this.compiler; - - this.cached_output = []; - this.send_func = function(text) { - this.cached_output.push(text); - } - - this.pragmas = {}; - +var Mustache = (function(undefined) { + var splitFunc = (function() { // 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.TokenizerRegex = null; - - Renderer.prototype = { - capturingSplit: function(separator) { + var compliantExecNpcg = /()??/.exec("")[1] === undefined; // NPCG: nonparticipating capturing group + function capturingSplit(separator) { // fix up the stupidness that is IE's broken String.split implementation // originally by Steven Levithan /* Cross-Browser Split 1.0.1 @@ -61,7 +31,7 @@ var Mustache = function() { separator2, match, lastIndex, lastLength; str = str + ""; // type conversion - if (!this._compliantExecNpcg) { + if (!compliantExecNpcg) { separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt } @@ -87,7 +57,7 @@ var Mustache = function() { 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) { + if (!compliantExecNpcg && match.length > 1) { match[0].replace(separator2, function () { for (var i = 1; i < arguments.length - 2; i++) { if (arguments[i] === undefined) { @@ -123,680 +93,444 @@ var Mustache = function() { } return output.length > limit ? output.slice(0, limit) : output; - }, + } - render: function(template, partials) { - template = this.parse_pragmas(template, '{{', '}}'); - - var tokens = this.tokenize(template, '{{', '}}'); - - this.parse(this.createParserContext(tokens, partials, '{{', '}}')); - }, + if ('lal'.split(/(a)/).length !== 3) { + return capturingSplit; + } else { + return String.prototype.split; + } + })(); + + var escapeCompiledRegex; + function escape_regex(text) { + // thank you Simon Willison + if (!escapeCompiledRegex) { + var specials = [ + '/', '.', '*', '+', '?', '|', + '(', ')', '[', ']', '{', '}', '\\' + ]; + escapeCompiledRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g'); + } - createParserContext: function(tokens, partials, openTag, closeTag) { - return { - tokens: tokens, - token: tokens[0], - index: 0, - length: tokens.length, - partials: partials, - stack: [], - openTag: openTag, - closeTag: closeTag - }; - }, + return text.replace(escapeCompiledRegex, '\\$1'); + } - _createTokenizerRegex: function(openTag, closeTag) { - var delimiters = [ - '\\{', - '&', - '\\}', - '#', - '\\^', - '\\/', - '>', - '=', - '%', - '!', - '\\s+' - ]; - delimiters.unshift(this.escape_regex(openTag)); - delimiters.unshift(this.escape_regex(closeTag)); + function isWhitespace(token) { + return token.match(/^\s+$/)!==null; + } + + function isNewline(token) { + return token.match(/\r?\n/)!==null; + } + + function create_parser_context(template, partials, view, send_func, openTag, closeTag) { + openTag = openTag || '{{'; + closeTag = closeTag || '}}'; + + var tokenizer = new RegExp('(\\r?\\n)|(' + escape_regex(openTag) + '[!#\^\/&{>=]?\\s*\\S*?\\s*}?' + escape_regex(closeTag) + ')|(' + escape_regex(openTag) + '=\\S*\\s*\\S*=' + escape_regex(closeTag) + ')'); + + var context = { + template: template || '' + , partials: partials || {} + , contextStack: [view || {}] + , user_send_func: send_func + , openTag: openTag + , closeTag: closeTag + , state: 'normal' + , pragmas: {} + }; + + // prefilter pragmas + pragmas(context); + + // tokenize and initialize a cursor + context.tokens = splitFunc.call(context.template, tokenizer); + context.cursor = 0; + + return context; + } + + function is_function(a) { + return a && typeof a === 'function'; + } + + function is_object(a) { + return a && typeof a === 'object'; + } + + function is_array(a) { + return Object.prototype.toString.call(a) === '[object Array]'; + } + + /* + find `name` in current `context`. That is find me a value + from the view object + */ + function find(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 (is_function(value)) { + return value.apply(context); + } + + return value; + } + + function find_in_stack(name, contextStack) { + var value; + + value = find(name, contextStack[contextStack.length-1]); + if (value!==undefined) { return value; } + + if (contextStack.length>1) { + value = find(name, contextStack[0]); + if (value!==undefined) { return value; } + } + + return undefined; + } + + function get_variable_name(parserContext, token, prefixes, postfixes) { + var matches = token.match(new RegExp(escape_regex(parserContext.openTag) + + '[' + escape_regex((prefixes || []).join('')) + + ']?\\s*(\\S*?)\\s*[' + + escape_regex((postfixes || []).join('')) + + ']?' + + escape_regex(parserContext.closeTag))); - return new RegExp('(' + delimiters.join('|') + ')'); - }, + if ((matches || []).length!==2) { + throw new Error('Malformed mustache tag: ' + token); + } else { + return matches[1]; + } + } + + function interpolate(parserContext, token, escape) { + function escapeHTML(str) { + return str.replace(/&/g,'&') + .replace(//g,'>'); + } - tokenize: function(template, openTag, closeTag) { - var regex; - if (openTag==='{{' && closeTag==='}}') { - // the common case, use the stored compiled regex - regex = Renderer.TokenizerRegex; - } else { - regex = this._createTokenizerRegex(openTag, closeTag); + var prefix = [], postfix = []; + if (escape==='{') { + prefix = ['{']; + postfix = ['}']; + } else if (escape==='&') { + prefix = ['&']; + } + + var res = find_in_stack(get_variable_name(parserContext, token, prefix, postfix), parserContext.contextStack); + if (res!==undefined) { + if (!escape) { + res = escapeHTML('' + res); } - return this.splitFunc.call(template, regex); - }, + parserContext.user_send_func('' + res); + } + } + + function partial(parserContext, token) { + var variable = get_variable_name(parserContext, token, ['>']); - /* - Looks for %PRAGMAS - */ - parse_pragmas: function(template, openTag, closeTag) { - /* includes tag */ - function includes(needle, haystack) { - return haystack.indexOf(openTag + needle) !== -1; - } + var value = find_in_stack(variable, parserContext.contextStack); + + var new_parser_context = create_parser_context( + parserContext.partials[variable] || '' + , parserContext.partials + , null + , parserContext.user_send_func); - // no pragmas, easy escape - if(!includes("%", template)) { - return template; - } + new_parser_context.contextStack = parserContext.contextStack; - var that = this; - var regex = new RegExp(this.escape_regex(openTag) + "%([\\w-]+)(\\s*)(.*?(?=" + this.escape_regex(closeTag) + "))" + this.escape_regex(closeTag)); - return template.replace(regex, function(match, pragma, space, suffix) { - var options = undefined; - - if (suffix.length>0) { - 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); - } - }, - closeMustache: function(parserContext) { - if (this.isWhitespace(parserContext.token)) { - return 'closeMustache'; - } else if (parserContext.token===parserContext.closeTag) { - return this.dispatchCommand(parserContext); - } - }, - expectClosingMustache: function(parserContext) { - if (parserContext.closeTag==='}}' && - parserContext.token==='}}') { - return 'expectClosingParenthesis'; - } else if (parserContext.token==='}') { - return 'closeMustache'; - } else { - throw new ParserException('Unexpected token encountered.'); - } - }, - expectClosingParenthesis: function(parserContext) { - if (parserContext.token==='}') { - return this.dispatchCommand(parserContext); - } else { - throw new ParserException('Unexpected token encountered.'); - } - }, - keyName: function(parserContext) { - var result = this.stateMachine.simpleKeyName.call(this, parserContext); + if (s.inverted) { + if (!value || is_array(value) && value.length === 0) { // false or empty list, render it + new_parser_context = create_section_context(s); + parse(new_parser_context); + } + } else { + if (is_array(value)) { // Enumerable, Let's loop! + new_parser_context = create_section_context(s); - if (result==='closeMustache') { - var tagKey = parserContext.stack[parserContext.stack.length-1], - tag = parserContext.stack[parserContext.stack.length-2]; - - if (tag.tagType==='unescapedVariable' && tag.subtype==='tripleMustache') { - parserContext.stack[parserContext.stack.length-2] = {tagType:'unescapedVariable'}; - - return 'expectClosingMustache'; - } else { - return 'closeMustache'; - } - } else if (result==='simpleKeyName') { - return 'keyName'; - } else { - throw new ParserException('Unexpected branch in tag name: ' + result); - } - }, - simpleKeyName: function(parserContext) { - if (this.isWhitespace(parserContext.token)) { - return 'simpleKeyName'; - } else { - parserContext.stack.push(parserContext.token); - - return 'closeMustache'; + for (i=0, n=value.length; i1) { - value = this.find(name, contextStack[0]); - if (value!==undefined) { return value; } + if (suffix.length>0) { + var optionPairs = suffix.split(','); + var scratch; + + options = {}; + for (var i=0, n=optionPairs.length; i/g,'>'); - } + var context = create_parser_context( + parserContext.tokens.slice(parserContext.cursor+1).join('') + , parserContext.partials + , null + , parserContext.user_send_func + , matches[1] + , matches[2]); - 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) { - 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, 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)); - - 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), - isObj = that.is_object(res); - - if (isObj) { - contextStack.push(res); - } - - for (var i=0,n=commands.length; i 0 && + parserContext.section.child_sections[parserContext.section.child_sections.length-1] === variable) { + + parserContext.section.child_sections.pop(); + parserContext.section.template_buffer.push(token); + } else if (parserContext.section.variable===variable) { + section(parserContext); + delete parserContext.section; + parserContext.state = 'normal'; + } else { + throw new Error('Unexpected section end tag. Expected: ' + parserContext.section.variable); + } + } + + function parse(parserContext) { + var n, token; + + for (n = parserContext.tokens.length;parserContext.cursor': // partial + partial(parserContext, token); + break; + case '=': // set delimiter change + change_delimiter(parserContext, token); + break; + default: // escaped variable + interpolate(parserContext, token); + break; + } + } else { + // plain jane text + parserContext.user_send_func(token); + } + } + , 'scan_section': function(parserContext, token) { + if (token.indexOf(parserContext.openTag)===0) { + switch (token.charAt(parserContext.openTag.length)) { + case '!': // comments + // comments are just discarded, nothing to do + break; + case '#': // section + begin_section(parserContext, token, false); + break; + case '^': // inverted section + begin_section(parserContext, token, true); + break; + case '/': // end section + end_section(parserContext, token); + break; + case '=': // set delimiter change + change_delimiter(parserContext, token); + break; + default: // all others + parserContext.section.template_buffer.push(token); + break; } + } else { + parserContext.section.template_buffer.push(token); } } } return({ name: "mustache.js", - version: "0.4.0-vcs", + version: "0.5.0-vcs", /* Turns a template and view into HTML */ to_html: function(template, view, partials, send_func) { - return (Mustache.compile(template, partials))(view, send_func); - }, - - /* - Compiles a template into an equivalent JS function for faster - repeated execution. - */ - compile: function(template, partials) { - if (!template) { return function() { return '' }; } - - var p = {}; - if (partials) { - for (var key in partials) { - if (partials.hasOwnProperty(key)) { - p[key] = partials[key]; - } - } - } - - var commands = []; - var s = function(command) { commands.push(command); }; + var o = []; + var user_send_func = send_func || function(str) { + o.push(str); + }; - (new Renderer(s)).render(template, p); - - return function(view, send_func) { - view = [view || {}]; - - var o = send_func ? undefined : []; - var s = send_func || function(output) { o.push(output); }; + parse(create_parser_context(template, partials, view, user_send_func)); - for (var i=0,n=commands.length; ipartial}}', partials ); + //var partials = { 'partial' : '{{key}}' }; + //Mustache.compile('{{>partial}}', partials ); - equals(partials['partial'], '{{key}}', 'Partials compiler must be non-destructive'); + //equals(partials['partial'], '{{key}}', 'Partials compiler must be non-destructive'); }); test("Basic Variables", function() { @@ -474,7 +474,7 @@ test("'%' (Pragmas)", function() { ); ok(false); } catch (e) { - equals(e.message, 'This implementation of mustache doesn\'t understand the \'I-HAVE-THE-GREATEST-MUSTACHE\' pragma'); + equals(e.message, 'This implementation of mustache does not implement the "I-HAVE-THE-GREATEST-MUSTACHE" pragma'); } equals(