/*! * mustache.js - Logic-less {{mustache}} templates with JavaScript * http://github.com/janl/mustache.js */ var Mustache; (function (exports) { if (typeof module !== "undefined") { module.exports = exports; // CommonJS } else if (typeof define === "function") { define(exports); // AMD } else { Mustache = exports; // <script> } }(function () { var exports = {}; exports.name = "mustache.js"; exports.version = "0.5.1-dev"; exports.tags = ["{{", "}}"]; exports.parse = parse; exports.clearCache = clearCache; exports.compile = compile; exports.compilePartial = compilePartial; exports.render = render; exports.Scanner = Scanner; exports.Context = Context; exports.Renderer = Renderer; // This is here for backwards compatibility with 0.4.x. exports.to_html = function (template, view, partials, send) { var result = render(template, view, partials); if (typeof send === "function") { send(result); } else { return result; } }; var whiteRe = /\s*/; var spaceRe = /\s+/; var nonSpaceRe = /\S/; var eqRe = /\s*=/; var curlyRe = /\s*\}/; var tagRe = /#|\^|\/|>|\{|&|=|!/; // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 // See https://github.com/janl/mustache.js/issues/189 function testRe(re, string) { return RegExp.prototype.test.call(re, string); } function isWhitespace(string) { return !testRe(nonSpaceRe, string); } var isArray = Array.isArray || function (obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; // OSWASP Guidelines: escape all non alphanumeric characters in ASCII space. var jsCharsRe = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm; function quote(text) { var escaped = text.replace(jsCharsRe, function (c) { return "\\u" + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); }); return '"' + escaped + '"'; } function escapeRe(string) { return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); } var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; function escapeHtml(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); } // Export these utility functions. exports.isWhitespace = isWhitespace; exports.isArray = isArray; exports.quote = quote; exports.escapeRe = escapeRe; exports.escapeHtml = escapeHtml; function Scanner(string) { this.string = string; this.tail = string; this.pos = 0; } /** * Returns `true` if the tail is empty (end of string). */ Scanner.prototype.eos = function () { return this.tail === ""; }; /** * Tries to match the given regular expression at the current position. * Returns the matched text if it can match, `null` otherwise. */ Scanner.prototype.scan = function (re) { var match = this.tail.match(re); if (match && match.index === 0) { this.tail = this.tail.substring(match[0].length); this.pos += match[0].length; return match[0]; } return null; }; /** * Skips all text until the given regular expression can be matched. Returns * the skipped string, which is the entire tail of this scanner if no match * can be made. */ Scanner.prototype.scanUntil = function (re) { var match, pos = this.tail.search(re); switch (pos) { case -1: match = this.tail; this.pos += this.tail.length; this.tail = ""; break; case 0: match = null; break; default: match = this.tail.substring(0, pos); this.tail = this.tail.substring(pos); this.pos += pos; } return match; }; function Context(view, parent) { this.view = view; this.parent = parent; this.clearCache(); } Context.make = function (view) { return (view instanceof Context) ? view : new Context(view); }; Context.prototype.clearCache = function () { this._cache = {}; }; Context.prototype.push = function (view) { return new Context(view, this); }; Context.prototype.lookup = function (name) { var value = this._cache[name]; if (!value) { if (name === ".") { value = this.view; } else { var context = this; while (context) { if (name.indexOf(".") > 0) { var names = name.split("."), i = 0; value = context.view; while (value && i < names.length) { value = value[names[i++]]; } } else { value = context.view[name]; } if (value != null) { break; } context = context.parent; } } this._cache[name] = value; } if (typeof value === "function") { value = value.call(this.view); } return value; }; function Renderer() { this.clearCache(); } Renderer.prototype.clearCache = function () { this._cache = {}; this._partialCache = {}; }; Renderer.prototype.compile = function (tokens, tags) { if (typeof tokens === "string") { tokens = parse(tokens, tags); } var fn = compileTokens(tokens), self = this; return function (view) { return fn(Context.make(view), self); }; }; Renderer.prototype.compilePartial = function (name, tokens, tags) { this._partialCache[name] = this.compile(tokens, tags); return this._partialCache[name]; }; Renderer.prototype.render = function (template, view) { var fn = this._cache[template]; if (!fn) { fn = this.compile(template); this._cache[template] = fn; } return fn(view); }; Renderer.prototype._section = function (name, context, callback) { var value = context.lookup(name); switch (typeof value) { case "object": if (isArray(value)) { var buffer = ""; for (var i = 0, len = value.length; i < len; ++i) { buffer += callback(context.push(value[i]), this); } return buffer; } return value ? callback(context.push(value), this) : ""; case "function": // TODO: The text should be passed to the callback plain, not rendered. var sectionText = callback(context, this), self = this; var scopedRender = function (template) { return self.render(template, context); }; return value.call(context.view, sectionText, scopedRender) || ""; default: if (value) { return callback(context, this); } } return ""; }; Renderer.prototype._inverted = function (name, context, callback) { var value = context.lookup(name); // From the spec: inverted sections may render text once based on the // inverse value of the key. That is, they will be rendered if the key // doesn't exist, is false, or is an empty list. if (value == null || value === false || (isArray(value) && value.length === 0)) { return callback(context, this); } return ""; }; Renderer.prototype._partial = function (name, context) { var fn = this._partialCache[name]; if (fn) { return fn(context, this); } return ""; }; Renderer.prototype._name = function (name, context, escape) { var value = context.lookup(name); if (typeof value === "function") { value = value.call(context.view); } var string = (value == null) ? "" : String(value); if (escape) { return escapeHtml(string); } return string; }; /** * Low-level function that compiles the given `tokens` into a * function that accepts two arguments: a Context and a * Renderer. Returns the body of the function as a string if * `returnBody` is true. */ function compileTokens(tokens, returnBody) { var body = ['""']; var token, method, escape; for (var i = 0, len = tokens.length; i < len; ++i) { token = tokens[i]; switch (token.type) { case "#": case "^": method = (token.type === "#") ? "_section" : "_inverted"; body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" + " " + compileTokens(token.tokens, true) + "\n" + "})"); break; case "{": case "&": case "name": escape = token.type === "name" ? "true" : "false"; body.push("r._name(" + quote(token.value) + ", c, " + escape + ")"); break; case ">": body.push("r._partial(" + quote(token.value) + ", c)"); break; case "text": body.push(quote(token.value)); break; } } // Convert to a string body. body = "return " + body.join(" + ") + ";"; // Good for debugging. // console.log(body); if (returnBody) { return body; } // For great evil! return new Function("c, r", body); } function escapeTags(tags) { if (tags.length === 2) { return [ new RegExp(escapeRe(tags[0]) + "\\s*"), new RegExp("\\s*" + escapeRe(tags[1])) ]; } throw new Error("Invalid tags: " + tags.join(" ")); } /** * Forms the given linear array of `tokens` into a nested tree structure * where tokens that represent a section have a "tokens" array property * that contains all tokens that are in that section. */ function nestTokens(tokens) { var tree = []; var collector = tree; var sections = []; var token, section; for (var i = 0; i < tokens.length; ++i) { token = tokens[i]; switch (token.type) { case "#": case "^": token.tokens = []; sections.push(token); collector.push(token); collector = token.tokens; break; case "/": if (sections.length === 0) { throw new Error("Unopened section: " + token.value); } section = sections.pop(); if (section.value !== token.value) { throw new Error("Unclosed section: " + section.value); } if (sections.length > 0) { collector = sections[sections.length - 1].tokens; } else { collector = tree; } break; default: collector.push(token); } } // Make sure there were no open sections when we're done. section = sections.pop(); if (section) { throw new Error("Unclosed section: " + section.value); } return tree; } /** * Combines the values of consecutive text tokens in the given `tokens` array * to a single token. */ function squashTokens(tokens) { var lastToken; for (var i = 0; i < tokens.length; ++i) { var token = tokens[i]; if (lastToken && lastToken.type === "text" && token.type === "text") { lastToken.value += token.value; tokens.splice(i--, 1); // Remove this token from the array. } else { lastToken = token; } } } /** * Breaks up the given `template` string into a tree of token objects. If * `tags` is given here it must be an array with two string values: the * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of * course, the default is to use mustaches (i.e. Mustache.tags). */ function parse(template, tags) { tags = tags || exports.tags; var tagRes = escapeTags(tags); var scanner = new Scanner(template); var tokens = [], // Buffer to hold the tokens spaces = [], // Indices of whitespace tokens on the current line hasTag = false, // Is there a {{tag}} on the current line? nonSpace = false; // Is there a non-space char on the current line? // Strips all whitespace tokens array for the current line // if there was a {{#tag}} on it and otherwise only space. var stripSpace = function () { if (hasTag && !nonSpace) { while (spaces.length) { tokens.splice(spaces.pop(), 1); } } else { spaces = []; } hasTag = false; nonSpace = false; }; var type, value, chr; while (!scanner.eos()) { value = scanner.scanUntil(tagRes[0]); if (value) { for (var i = 0, len = value.length; i < len; ++i) { chr = value.charAt(i); if (isWhitespace(chr)) { spaces.push(tokens.length); } else { nonSpace = true; } tokens.push({type: "text", value: chr}); if (chr === "\n") { stripSpace(); // Check for whitespace on the current line. } } } // Match the opening tag. if (!scanner.scan(tagRes[0])) { break; } hasTag = true; type = scanner.scan(tagRe) || "name"; // Skip any whitespace between tag and value. scanner.scan(whiteRe); // Extract the tag value. if (type === "=") { value = scanner.scanUntil(eqRe); scanner.scan(eqRe); scanner.scanUntil(tagRes[1]); } else if (type === "{") { var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1])); value = scanner.scanUntil(closeRe); scanner.scan(curlyRe); scanner.scanUntil(tagRes[1]); } else { value = scanner.scanUntil(tagRes[1]); } // Match the closing tag. if (!scanner.scan(tagRes[1])) { throw new Error("Unclosed tag at " + scanner.pos); } tokens.push({type: type, value: value}); if (type === "name" || type === "{" || type === "&") { nonSpace = true; } // Set the tags for the next time around. if (type === "=") { tags = value.split(spaceRe); tagRes = escapeTags(tags); } } squashTokens(tokens); return nestTokens(tokens); } // The high-level clearCache, compile, compilePartial, and render functions // use this default renderer. var _renderer = new Renderer(); /** * Clears all cached templates and partials. */ function clearCache() { _renderer.clearCache(); } /** * High-level API for compiling the given `tokens` down to a reusable * function. If `tokens` is a string it will be parsed using the given `tags` * before it is compiled. */ function compile(tokens, tags) { return _renderer.compile(tokens, tags); } /** * High-level API for compiling the `tokens` for the partial with the given * `name` down to a reusable function. If `tokens` is a string it will be * parsed using the given `tags` before it is compiled. */ function compilePartial(name, tokens, tags) { return _renderer.compilePartial(name, tokens, tags); } /** * High-level API for rendering the `template` using the given `view`. The * optional `partials` object may be given here for convenience, but note that * it will cause all partials to be re-compiled, thus hurting performance. Of * course, this only matters if you're going to render the same template more * than once. If so, it is best to call `compilePartial` before calling this * function and to leave the `partials` argument blank. */ function render(template, view, partials) { if (partials) { for (var name in partials) { compilePartial(name, partials[name]); } } return _renderer.render(template, view); } return exports; }()));