/*! * mustache.js - Logic-less {{mustache}} templates with JavaScript * http://github.com/janl/mustache.js */ var Mustache = (typeof module !== "undefined" && module.exports) || {}; (function (exports) { exports.name = "mustache.js"; exports.version = "0.5.0-dev"; exports.tags = ["{{", "}}"]; exports.parse = parse; exports.compile = compile; exports.render = render; exports.clearCache = clearCache; // 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 _toString = Object.prototype.toString; var _isArray = Array.isArray; var _forEach = Array.prototype.forEach; var _trim = String.prototype.trim; var isArray; if (_isArray) { isArray = _isArray; } else { isArray = function (obj) { return _toString.call(obj) === "[object Array]"; }; } var forEach; if (_forEach) { forEach = function (obj, callback, scope) { return _forEach.call(obj, callback, scope); }; } else { forEach = function (obj, callback, scope) { for (var i = 0, len = obj.length; i < len; ++i) { callback.call(scope, obj[i], i, obj); } }; } var spaceRe = /^\s*$/; function isWhitespace(string) { return spaceRe.test(string); } var trim; if (_trim) { trim = function (string) { return string == null ? "" : _trim.call(string); }; } else { var trimLeft, trimRight; if (isWhitespace("\xA0")) { trimLeft = /^\s+/; trimRight = /\s+$/; } else { // IE doesn't match non-breaking spaces with \s, thanks jQuery. trimLeft = /^[\s\xA0]+/; trimRight = /[\s\xA0]+$/; } trim = function (string) { return string == null ? "" : String(string).replace(trimLeft, "").replace(trimRight, ""); }; } var escapeMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''' }; function escapeHTML(string) { return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { return escapeMap[s] || s; }); } /** * Adds the `template`, `line`, and `file` properties to the given error * object and alters the message to provide more useful debugging information. */ function debug(e, template, line, file) { file = file || "<template>"; var lines = template.split("\n"), start = Math.max(line - 3, 0), end = Math.min(lines.length, line + 3), context = lines.slice(start, end); var c; for (var i = 0, len = context.length; i < len; ++i) { c = i + start + 1; context[i] = (c === line ? " >> " : " ") + context[i]; } e.template = template; e.line = line; e.file = file; e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n"); return e; } /** * Looks up the value of the given `name` in the given context `stack`. */ function lookup(name, stack, defaultValue) { if (name === ".") { return stack[stack.length - 1]; } var names = name.split("."); var lastIndex = names.length - 1; var target = names[lastIndex]; var value, context, i = stack.length, j, localStack; while (i) { localStack = stack.slice(0); context = stack[--i]; j = 0; while (j < lastIndex) { context = context[names[j++]]; if (context == null) { break; } localStack.push(context); } if (context && target in context) { value = context[target]; break; } } // If the value is a function, call it in the current context. if (typeof value === "function") { value = value.call(localStack[localStack.length - 1]); } if (value == null) { return defaultValue; } return value; } function renderSection(name, stack, callback, inverted) { var buffer = ""; var value = lookup(name, stack); if (inverted) { // 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)) { buffer += callback(); } } else if (isArray(value)) { forEach(value, function (value) { stack.push(value); buffer += callback(); stack.pop(); }); } else if (typeof value === "object") { stack.push(value); buffer += callback(); stack.pop(); } else if (typeof value === "function") { var scope = stack[stack.length - 1]; var scopedRender = function (template) { return render(template, scope); }; buffer += value.call(scope, callback(), scopedRender) || ""; } else if (value) { buffer += callback(); } return buffer; } /** * Parses the given `template` and returns the source of a function that, * with the proper arguments, will render the template. Recognized options * include the following: * * - file The name of the file the template comes from (displayed in * error messages) * - tags An array of open and close tags the `template` uses. Defaults * to the value of Mustache.tags * - debug Set `true` to log the body of the generated function to the * console * - space Set `true` to preserve whitespace from lines that otherwise * contain only a {{tag}}. Defaults to `false` */ function parse(template, options) { options = options || {}; var tags = options.tags || exports.tags, openTag = tags[0], closeTag = tags[tags.length - 1]; var code = [ 'var buffer = "";', // output buffer "\nvar line = 1;", // keep track of source line number "\ntry {", '\nbuffer += "' ]; var spaces = [], // indices of whitespace in code 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 space characters from the code array for the current line // if there was a {{tag}} on it and otherwise only spaces. var stripSpace = function () { if (hasTag && !nonSpace && !options.space) { while (spaces.length) { code.splice(spaces.pop(), 1); } } else { spaces = []; } hasTag = false; nonSpace = false; }; var sectionStack = [], updateLine, nextOpenTag, nextCloseTag; var setTags = function (source) { tags = trim(source).split(/\s+/); nextOpenTag = tags[0]; nextCloseTag = tags[tags.length - 1]; }; var includePartial = function (source) { code.push( '";', updateLine, '\nvar partial = partials["' + trim(source) + '"];', '\nif (partial) {', '\n buffer += render(partial,stack[stack.length - 1],partials);', '\n}', '\nbuffer += "' ); }; var openSection = function (source, inverted) { var name = trim(source); if (name === "") { throw debug(new Error("Section name may not be empty"), template, line, options.file); } sectionStack.push({name: name, inverted: inverted}); code.push( '";', updateLine, '\nvar name = "' + name + '";', '\nvar callback = (function () {', '\n return function () {', '\n var buffer = "";', '\nbuffer += "' ); }; var openInvertedSection = function (source) { openSection(source, true); }; var closeSection = function (source) { var name = trim(source); var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name; if (!openName || name != openName) { throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file); } var section = sectionStack.pop(); code.push( '";', '\n return buffer;', '\n };', '\n})();' ); if (section.inverted) { code.push("\nbuffer += renderSection(name,stack,callback,true);"); } else { code.push("\nbuffer += renderSection(name,stack,callback);"); } code.push('\nbuffer += "'); }; var sendPlain = function (source) { code.push( '";', updateLine, '\nbuffer += lookup("' + trim(source) + '",stack,"");', '\nbuffer += "' ); }; var sendEscaped = function (source) { code.push( '";', updateLine, '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));', '\nbuffer += "' ); }; var line = 1, c, callback; for (var i = 0, len = template.length; i < len; ++i) { if (template.slice(i, i + openTag.length) === openTag) { i += openTag.length; c = template.substr(i, 1); updateLine = '\nline = ' + line + ';'; nextOpenTag = openTag; nextCloseTag = closeTag; hasTag = true; switch (c) { case "!": // comment i++; callback = null; break; case "=": // change open/close tags, e.g. {{=<% %>=}} i++; closeTag = "=" + closeTag; callback = setTags; break; case ">": // include partial i++; callback = includePartial; break; case "#": // start section i++; callback = openSection; break; case "^": // start inverted section i++; callback = openInvertedSection; break; case "/": // end section i++; callback = closeSection; break; case "{": // plain variable closeTag = "}" + closeTag; // fall through case "&": // plain variable i++; nonSpace = true; callback = sendPlain; break; default: // escaped variable nonSpace = true; callback = sendEscaped; } var end = template.indexOf(closeTag, i); if (end === -1) { throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file); } var source = template.substring(i, end); if (callback) { callback(source); } // Maintain line count for \n in source. var n = 0; while (~(n = source.indexOf("\n", n))) { line++; n++; } i = end + closeTag.length - 1; openTag = nextOpenTag; closeTag = nextCloseTag; } else { c = template.substr(i, 1); switch (c) { case '"': case "\\": nonSpace = true; code.push("\\" + c); break; case "\r": // Ignore carriage returns. break; case "\n": spaces.push(code.length); code.push("\\n"); stripSpace(); // Check for whitespace on the current line. line++; break; default: if (isWhitespace(c)) { spaces.push(code.length); } else { nonSpace = true; } code.push(c); } } } if (sectionStack.length != 0) { throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file); } // Clean up any whitespace from a closing {{tag}} that was at the end // of the template without a trailing \n. stripSpace(); code.push( '";', "\nreturn buffer;", "\n} catch (e) { throw {error: e, line: line}; }" ); // Ignore `buffer += "";` statements. var body = code.join("").replace(/buffer \+= "";\n/g, ""); if (options.debug) { if (typeof console != "undefined" && console.log) { console.log(body); } else if (typeof print === "function") { print(body); } } return body; } /** * Used by `compile` to generate a reusable function for the given `template`. */ function _compile(template, options) { var args = "view,partials,stack,lookup,escapeHTML,renderSection,render"; var body = parse(template, options); var fn = new Function(args, body); // This anonymous function wraps the generated function so we can do // argument coercion, setup some variables, and handle any errors // encountered while executing it. return function (view, partials) { partials = partials || {}; var stack = [view]; // context stack try { return fn(view, partials, stack, lookup, escapeHTML, renderSection, render); } catch (e) { throw debug(e.error, template, e.line, options.file); } }; } // Cache of pre-compiled templates. var _cache = {}; /** * Clear the cache of compiled templates. */ function clearCache() { _cache = {}; } /** * Compiles the given `template` into a reusable function using the given * `options`. In addition to the options accepted by Mustache.parse, * recognized options include the following: * * - cache Set `false` to bypass any pre-compiled version of the given * template. Otherwise, a given `template` string will be cached * the first time it is parsed */ function compile(template, options) { options = options || {}; // Use a pre-compiled version from the cache if we have one. if (options.cache !== false) { if (!_cache[template]) { _cache[template] = _compile(template, options); } return _cache[template]; } return _compile(template, options); } /** * High-level function that renders the given `template` using the given * `view` and `partials`. If you need to use any of the template options (see * `compile` above), you must compile in a separate step, and then call that * compiled function. */ function render(template, view, partials) { return compile(template)(view, partials); } })(Mustache);