diff --git a/mustache.js b/mustache.js index 19c8417..78e3949 100644 --- a/mustache.js +++ b/mustache.js @@ -2,320 +2,287 @@ mustache.js — Logic-less templates in JavaScript See http://mustache.github.com/ for more info. + + Rewrite as parser by Nathan Vander Wilt, 2010 May 22 */ var Mustache = function() { - var Renderer = function() {}; + // `sender` is a function to buffer or stream parsed chunks + var Renderer = function(sender) { + this.send = sender; + }; Renderer.prototype = { otag: "{{", ctag: "}}", - pragmas: {}, - buffer: [], pragmas_implemented: { "IMPLICIT-ITERATOR": true }, - context: {}, - - render: function(template, context, partials, in_recursion) { - // reset buffer & set context - if(!in_recursion) { - this.context = context; - this.buffer = []; // TODO: make this non-lazy - } - - // fail fast - if(!this.includes("", template)) { - if(in_recursion) { - return template; - } else { - this.send(template); - return; - } + + // state created per-instance to avoid sharing + pragmas: null, + + // the main parser (return value for internal use only) + render: function(template, context, partials) { + //console.log("Context", context); + this.pragmas = {}; + var tokens = this.splitTemplate(template); + //console.log("Tokens", tokens); + var tree = this.formTree(tokens); + //console.log("Tree", tree); + this.renderTree(tree, context, partials, template); + }, + + // returns {tag, start, end} or nothing + findToken: function(template, startPos) { + var tokenStart = template.indexOf(this.otag, startPos); + if (tokenStart == -1) return; + var tokenEnd = template.indexOf(this.ctag, tokenStart + this.otag.length); + if (tokenEnd == -1) { + var context = template.substr(tokenStart, 15); + throw new Error("Unclosed token '" + context + "...'."); } - - template = this.render_pragmas(template); - var html = this.render_section(template, context, partials); - if(in_recursion) { - return this.render_tags(html, context, partials, in_recursion); + + var tokenInnards = template.slice(tokenStart + this.otag.length, tokenEnd); + var tokenParts = tokenInnards.match(/([=%!{&>#^\/])?\s*(.+?)\s*\1?$/); + var token = { + "operator": tokenParts[1], + "tag": tokenParts[2], + "text": this.otag + tokenInnards + this.ctag, + "start": tokenStart, + "end": tokenEnd + this.ctag.length + }; + if (token.operator == "{" && template[token.end] == "}") { + // adjust for symmetrical unescaped tag with default delimiters + token.end += 1; } - - this.render_tags(html, context, partials, in_recursion); + return token; }, - - /* - Sends parsed lines - */ - send: function(line) { - if(line != "") { - this.buffer.push(line); + + splitTemplate: function(template) { + var tokens = []; + var token; + var parsePosition = 0; + while (token = this.findToken(template, parsePosition)) { + var text = template.slice(parsePosition, token.start); + tokens.push({"text": text, "start": parsePosition, "end": token.start}); + tokens.push(token); + parsePosition = token.end; + + if (token.operator == "=") { + // set new delimiters + var delimiters = token.tag.split(" "); + this.otag = delimiters[0]; + this.ctag = delimiters[1]; + } else if (token.operator == "%") { + // store pragma + var pragmaInfo = token.tag.match(/([\w_-]+) ?([\w]+=[\w]+)?/); + var pragma = pragmaInfo[1]; + if (!this.pragmas_implemented[pragma]) { + throw new Error("This mustache implementation doesn't understand the '" + + pragma + "' pragma"); + } + var options = {} + var optionStr = pragmaInfo[2]; + if (optionStr) { + var opts = optionStr.split("="); + options[opts[0]] = opts[1]; + } + this.pragmas[pragma] = options; + } } + var finalText = template.slice(parsePosition, template.length); + tokens.push({"text": finalText, "start": parsePosition, "end": template.length}); + return tokens; }, - - /* - Looks for %PRAGMAS - */ - render_pragmas: function(template) { - // no pragmas - if(!this.includes("%", template)) { - return template; - } - - var that = this; - var regex = new RegExp(this.otag + "%([\\w_-]+) ?([\\w]+=[\\w]+)?" + - this.ctag); - return template.replace(regex, function(match, pragma, options) { - if(!that.pragmas_implemented[pragma]) { - throw({message: - "This implementation of mustache doesn't understand the '" + - pragma + "' pragma"}); - } - that.pragmas[pragma] = {}; - if(options) { - var opts = options.split("="); - that.pragmas[pragma][opts[0]] = opts[1]; + + // NOTE: empties tokens parameter and modifies its former subobjects + formTree: function(tokens, section) { + var tree = []; + var token; + while (token = tokens.shift()) { + if (token.start == token.end) { + // drop empty tokens + continue; + } else if (token.tag) { + if (token.operator == "#" || token.operator == "^") { + token.section = true; + token.invert = (token.operator == "^"); + token.tree = this.formTree(tokens, token.tag); + } else if (token.operator == "/") { + if (token.tag != section) { + throw new Error("Badly nested section '" + section + "'" + + " (left via '" + token.tag +"')."); + } + break; + } else if (token.operator == ">") { + token.partial = true; + } else if (token.operator == "{" || token.operator == "&") { + token.noEscape = true; + } } - return ""; - // ignore unknown pragmas silently - }); - }, - - /* - Tries to find a partial in the curent scope and render it - */ - render_partial: function(name, context, partials) { - name = this.trim(name); - if(!partials || !partials[name]) { - throw({message: "unknown_partial '" + name + "'"}); - } - if(typeof(context[name]) != "object") { - return this.render(partials[name], context, partials, true); + tree.push(token); } - return this.render(partials[name], context[name], partials, true); + return tree; }, - - /* - Renders inverted (^) and normal (#) sections - */ - render_section: function(template, context, partials) { - if(!this.includes("#", template) && !this.includes("^", template)) { - return template; - } - - var that = this; - // CSW - Added "+?" so it finds the tighest bound, not the widest - var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + - "\\s*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + - "\\s*", "mg"); - - // for each {{#foo}}{{/foo}} section do... - return template.replace(regex, function(match, type, name, content) { - var value = that.find(name, context); - if(type == "^") { // inverted section - if(!value || that.is_array(value) && value.length === 0) { - // false or empty list, render it - return that.render(content, context, partials, true); - } else { - return ""; + + renderTree: function(tree, context, partials, template) { + for (var i = 0, len = tree.length; i < len; ++i) { + var item = tree[i]; + if (item.section) { + var iterator = this.valueIterator(item.tag, context); + var value; + if (item.invert) { + value = iterator(); + if (!value) { + this.renderTree(item.tree, context, partials, template); + } + } else while (value = iterator()) { + if (value instanceof Function) { + var subtree = item.tree; + var lastSubitem = subtree[subtree.length-1]; + var subtext = template.slice(item.end, lastSubitem && lastSubitem.end); + var renderer = function(text) { + return Mustache.to_html(text, context, partials); + } + var lambdaResult = value.call(context, subtext, renderer); + if (lambdaResult) { + this.send(lambdaResult); + } + } else { + var subContext = this.mergedCopy(context, value); + this.renderTree(item.tree, subContext, partials, template); + } } - } else if(type == "#") { // normal section - if(that.is_array(value)) { // Enumerable, Let's loop! - return that.map(value, function(row) { - return that.render(content, that.create_context(row), - partials, true); - }).join(""); - } else if(that.is_object(value)) { // Object, Use it as subcontext! - return that.render(content, that.create_context(value), - partials, true); - } else if(typeof value === "function") { - // higher order section - return value.call(context, content, function(text) { - return that.render(text, context, partials, true); - }); - } else if(value) { // boolean section - return that.render(content, context, partials, true); + } else if (item.partial) { + var subTemplate = partials[item.tag]; + if (!subTemplate) { + throw new Error("Unknown partial '" + item.tag + "'"); + } + this.render(subTemplate, context, partials); + // TODO: below matches @janl's mustache, but not mustache(5) + /* + var subContext = context[item.tag]; + if (typeof(subContext) == "object") { + this.render(subTemplate, subContext, partials); } else { - return ""; + this.send(subTemplate); } - } - }); - }, - - /* - Replace {{foo}} and friends with values from our view - */ - render_tags: function(template, context, partials, in_recursion) { - // tit for tat - var that = this; - - var new_regex = function() { - return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + - that.ctag + "+", "g"); - }; - - var regex = new_regex(); - var tag_replace_callback = function(match, operator, name) { - switch(operator) { - case "!": // ignore comments - return ""; - case "=": // set new delimiters, rebuild the replace regexp - that.set_delimiters(name); - regex = new_regex(); - return ""; - case ">": // render partial - return that.render_partial(name, context, partials); - case "{": // the triple mustache is unescaped - return that.find(name, context); - default: // escape the value - return that.escape(that.find(name, context)); - } - }; - var lines = template.split("\n"); - for(var i = 0; i < lines.length; i++) { - lines[i] = lines[i].replace(regex, tag_replace_callback, this); - if(!in_recursion) { - this.send(lines[i]); + */ + } else if (item.operator && !item.noEscape) { + // ignore other operators + } else if (item.tag) { + var rawValue = this.lookupValue(item.tag, context); + if (rawValue) { + var value = rawValue.toString(); + this.send((item.noEscape) ? value : this.escapeHTML(value)); + } + } else { + this.send(item.text); } } - - if(in_recursion) { - return lines.join("\n"); - } - }, - - set_delimiters: function(delimiters) { - var dels = delimiters.split(" "); - this.otag = this.escape_regex(dels[0]); - this.ctag = this.escape_regex(dels[1]); - }, - - escape_regex: function(text) { - // thank you Simon Willison - if(!arguments.callee.sRE) { - var specials = [ - '/', '.', '*', '+', '?', '|', - '(', ')', '[', ']', '{', '}', '\\' - ]; - arguments.callee.sRE = new RegExp( - '(\\' + specials.join('|\\') + ')', 'g' - ); - } - return text.replace(arguments.callee.sRE, '\\$1'); }, - - /* - find `name` in current `context`. That is find me a value - from the view object - */ - find: function(name, context) { - name = this.trim(name); - - // Checks whether a value is thruthy 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]; - } else if(is_kinda_truthy(this.context[name])) { - value = this.context[name]; + + // find `name` value in current view `context` + lookupValue: function(name, context) { + var value = context[name]; + // evaluate plain-function value (only once) + if (value instanceof Function && !value.iterator) { + value = value.apply(context); } - - if(typeof value === "function") { - return value.apply(context); + // silently ignore unkown variables + if (!value) { + value = ""; } - if(value !== undefined) { + return value; + }, + + objectValue: function(value, context) { + if (value instanceof Function) { return value; } - // silently ignore unkown variables - return ""; - }, - - // Utility methods - - /* includes tag */ - includes: function(needle, haystack) { - return haystack.indexOf(this.otag + needle) != -1; - }, - - /* - Does away with nasty characters - */ - escape: function(s) { - s = String(s === null ? "" : s); - return s.replace(/&(?!\w+;)|["<>\\]/g, function(s) { - switch(s) { - case "&": return "&"; - case "\\": return "\\\\"; - case '"': return '\"'; - case "<": return "<"; - case ">": return ">"; - default: return s; - } - }); - }, - - // by @langalex, support for arrays of strings - create_context: function(_context) { - if(this.is_object(_context)) { - return _context; + + var obj = (value != null) ? {} : null; + if (Object.prototype.toString.call(value) == '[object Object]') { + obj = value; } else if(this.pragmas["IMPLICIT-ITERATOR"]) { - var iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator || "."; - var ctx = {}; - ctx[iterator] = _context; - return ctx; + // original credit to @langalex, support for arrays of strings + var iteratorKey = this.pragmas["IMPLICIT-ITERATOR"].iterator || "."; + obj[iteratorKey] = value; } + return obj; }, - - is_object: function(a) { - return a && typeof a == "object"; - }, - - is_array: function(a) { - return Object.prototype.toString.call(a) === '[object Array]'; + + // always returns iterator function returning object/null + valueIterator: function(name, context) { + var value = this.lookupValue(name, context); + var me = this; + if (!value) { + return function(){}; + } else if (value instanceof Function && value.iterator) { + return value; + } else if (value instanceof Array) { + var i = 0; + var l = value.length; + return function() { + return (i < l) ? me.objectValue(value[i++], context) : null; + } + } else { + return function() { + var v = value; + value = null; + return me.objectValue(v, context); + }; + } }, - - /* - Gets rid of leading and trailing whitespace - */ - trim: function(s) { - return s.replace(/^\s*|\s*$/g, ""); + + // copies contents of `b` over copy of `a` + mergedCopy: function(a, b) { + var copy = {}; + for (var key in a) if (a.hasOwnProperty(key)) { + copy[key] = a[key]; + } + for (var key in b) if (b.hasOwnProperty(key)) { + copy[key] = b[key]; + } + return copy; }, - - /* - Why, why, why? Because IE. Cry, cry cry. - */ - map: function(array, fn) { - if (typeof array.map == "function") { - return array.map(fn); - } else { - var r = []; - var l = array.length; - for(var i = 0; i < l; i++) { - r.push(fn(array[i])); + + // converts special HTML characters + escapeHTML: function(s) { + var htmlCharsRE = new RegExp("&(?!\\w+;)|[\"<>\\\\]", "g"); + return s.replace(htmlCharsRE, function(c) { + switch(c) { + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return c; } - return r; - } + }); } }; return({ name: "mustache.js", - version: "0.3.0-dev", - - /* - Turns a template and view into HTML - */ - to_html: function(template, view, partials, send_fun) { - var renderer = new Renderer(); - if(send_fun) { - renderer.send = send_fun; + version: "0.4.0-dev", + + // wrap internal render function + to_html: function(template, view, partials, sender) { + var buffer = []; + var renderSender = sender || function(chunk) { + if (chunk.length) { + buffer.push(chunk); + } } + var renderer = new Renderer(renderSender); + renderer.render(template, view, partials); - if(!send_fun) { - return renderer.buffer.join("\n"); + + if (!sender) { + return buffer.join(""); } } }); -}(); +}(); \ No newline at end of file