| @@ -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(""); | |||
| } | |||
| } | |||
| }); | |||
| }(); | |||
| }(); | |||