| @@ -2,320 +2,287 @@ | |||||
| mustache.js — Logic-less templates in JavaScript | mustache.js — Logic-less templates in JavaScript | ||||
| See http://mustache.github.com/ for more info. | See http://mustache.github.com/ for more info. | ||||
| Rewrite as parser by Nathan Vander Wilt, 2010 May 22 | |||||
| */ | */ | ||||
| var Mustache = function() { | 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 = { | Renderer.prototype = { | ||||
| otag: "{{", | otag: "{{", | ||||
| ctag: "}}", | ctag: "}}", | ||||
| pragmas: {}, | |||||
| buffer: [], | |||||
| pragmas_implemented: { | pragmas_implemented: { | ||||
| "IMPLICIT-ITERATOR": true | "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 { | } 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; | 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"]) { | } 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({ | return({ | ||||
| name: "mustache.js", | 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); | renderer.render(template, view, partials); | ||||
| if(!send_fun) { | |||||
| return renderer.buffer.join("\n"); | |||||
| if (!sender) { | |||||
| return buffer.join(""); | |||||
| } | } | ||||
| } | } | ||||
| }); | }); | ||||
| }(); | |||||
| }(); | |||||