mustache.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /*!
  2. * mustache.js - Logic-less {{mustache}} templates with JavaScript
  3. * http://github.com/janl/mustache.js
  4. */
  5. var Mustache = (typeof module !== "undefined" && module.exports) || {};
  6. (function (exports) {
  7. exports.name = "mustache.js";
  8. exports.version = "0.5.0-dev";
  9. exports.tags = ["{{", "}}"];
  10. exports.parse = parse;
  11. exports.compile = compile;
  12. exports.render = render;
  13. exports.clearCache = clearCache;
  14. // This is here for backwards compatibility with 0.4.x.
  15. exports.to_html = function (template, view, partials, send) {
  16. var result = render(template, view, partials);
  17. if (typeof send === "function") {
  18. send(result);
  19. } else {
  20. return result;
  21. }
  22. };
  23. var _toString = Object.prototype.toString;
  24. var _isArray = Array.isArray;
  25. var _forEach = Array.prototype.forEach;
  26. var _trim = String.prototype.trim;
  27. var isArray;
  28. if (_isArray) {
  29. isArray = _isArray;
  30. } else {
  31. isArray = function (obj) {
  32. return _toString.call(obj) === "[object Array]";
  33. };
  34. }
  35. var forEach;
  36. if (_forEach) {
  37. forEach = function (obj, callback, scope) {
  38. return _forEach.call(obj, callback, scope);
  39. };
  40. } else {
  41. forEach = function (obj, callback, scope) {
  42. for (var i = 0, len = obj.length; i < len; ++i) {
  43. callback.call(scope, obj[i], i, obj);
  44. }
  45. };
  46. }
  47. var spaceRe = /^\s*$/;
  48. function isWhitespace(string) {
  49. return spaceRe.test(string);
  50. }
  51. var trim;
  52. if (_trim) {
  53. trim = function (string) {
  54. return string == null ? "" : _trim.call(string);
  55. };
  56. } else {
  57. var trimLeft, trimRight;
  58. if (isWhitespace("\xA0")) {
  59. trimLeft = /^\s+/;
  60. trimRight = /\s+$/;
  61. } else {
  62. // IE doesn't match non-breaking spaces with \s, thanks jQuery.
  63. trimLeft = /^[\s\xA0]+/;
  64. trimRight = /[\s\xA0]+$/;
  65. }
  66. trim = function (string) {
  67. return string == null ? "" :
  68. String(string).replace(trimLeft, "").replace(trimRight, "");
  69. };
  70. }
  71. var escapeMap = {
  72. "&": "&amp;",
  73. "<": "&lt;",
  74. ">": "&gt;",
  75. '"': '&quot;',
  76. "'": '&#39;'
  77. };
  78. function escapeHTML(string) {
  79. return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) {
  80. return escapeMap[s] || s;
  81. });
  82. }
  83. /**
  84. * Adds the `template`, `line`, and `file` properties to the given error
  85. * object and alters the message to provide more useful debugging information.
  86. */
  87. function debug(e, template, line, file) {
  88. file = file || "<template>";
  89. var lines = template.split("\n"),
  90. start = Math.max(line - 3, 0),
  91. end = Math.min(lines.length, line + 3),
  92. context = lines.slice(start, end);
  93. var c;
  94. for (var i = 0, len = context.length; i < len; ++i) {
  95. c = i + start + 1;
  96. context[i] = (c === line ? " >> " : " ") + context[i];
  97. }
  98. e.template = template;
  99. e.line = line;
  100. e.file = file;
  101. e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n");
  102. return e;
  103. }
  104. /**
  105. * Looks up the value of the given `name` in the given context `stack`.
  106. */
  107. function lookup(name, stack, defaultValue) {
  108. if (name === ".") {
  109. return stack[stack.length - 1];
  110. }
  111. var names = name.split(".");
  112. var lastIndex = names.length - 1;
  113. var target = names[lastIndex];
  114. var value, context, i = stack.length, j, localStack;
  115. while (i) {
  116. localStack = stack.slice(0);
  117. context = stack[--i];
  118. j = 0;
  119. while (j < lastIndex) {
  120. context = context[names[j++]];
  121. if (context == null) {
  122. break;
  123. }
  124. localStack.push(context);
  125. }
  126. if (context && typeof context === "object" && target in context) {
  127. value = context[target];
  128. break;
  129. }
  130. }
  131. // If the value is a function, call it in the current context.
  132. if (typeof value === "function") {
  133. value = value.call(localStack[localStack.length - 1]);
  134. }
  135. if (value == null) {
  136. return defaultValue;
  137. }
  138. return value;
  139. }
  140. function renderSection(name, stack, callback, inverted) {
  141. var buffer = "";
  142. var value = lookup(name, stack);
  143. if (inverted) {
  144. // From the spec: inverted sections may render text once based on the
  145. // inverse value of the key. That is, they will be rendered if the key
  146. // doesn't exist, is false, or is an empty list.
  147. if (value == null || value === false || (isArray(value) && value.length === 0)) {
  148. buffer += callback();
  149. }
  150. } else if (isArray(value)) {
  151. forEach(value, function (value) {
  152. stack.push(value);
  153. buffer += callback();
  154. stack.pop();
  155. });
  156. } else if (typeof value === "object") {
  157. stack.push(value);
  158. buffer += callback();
  159. stack.pop();
  160. } else if (typeof value === "function") {
  161. var scope = stack[stack.length - 1];
  162. var scopedRender = function (template) {
  163. return render(template, scope);
  164. };
  165. buffer += value.call(scope, callback(), scopedRender) || "";
  166. } else if (value) {
  167. buffer += callback();
  168. }
  169. return buffer;
  170. }
  171. /**
  172. * Parses the given `template` and returns the source of a function that,
  173. * with the proper arguments, will render the template. Recognized options
  174. * include the following:
  175. *
  176. * - file The name of the file the template comes from (displayed in
  177. * error messages)
  178. * - tags An array of open and close tags the `template` uses. Defaults
  179. * to the value of Mustache.tags
  180. * - debug Set `true` to log the body of the generated function to the
  181. * console
  182. * - space Set `true` to preserve whitespace from lines that otherwise
  183. * contain only a {{tag}}. Defaults to `false`
  184. */
  185. function parse(template, options) {
  186. options = options || {};
  187. var tags = options.tags || exports.tags,
  188. openTag = tags[0],
  189. closeTag = tags[tags.length - 1];
  190. var code = [
  191. 'var buffer = "";', // output buffer
  192. "\nvar line = 1;", // keep track of source line number
  193. "\ntry {",
  194. '\nbuffer += "'
  195. ];
  196. var spaces = [], // indices of whitespace in code on the current line
  197. hasTag = false, // is there a {{tag}} on the current line?
  198. nonSpace = false; // is there a non-space char on the current line?
  199. // Strips all space characters from the code array for the current line
  200. // if there was a {{tag}} on it and otherwise only spaces.
  201. var stripSpace = function () {
  202. if (hasTag && !nonSpace && !options.space) {
  203. while (spaces.length) {
  204. code.splice(spaces.pop(), 1);
  205. }
  206. } else {
  207. spaces = [];
  208. }
  209. hasTag = false;
  210. nonSpace = false;
  211. };
  212. var sectionStack = [], updateLine, nextOpenTag, nextCloseTag;
  213. var setTags = function (source) {
  214. tags = trim(source).split(/\s+/);
  215. nextOpenTag = tags[0];
  216. nextCloseTag = tags[tags.length - 1];
  217. };
  218. var includePartial = function (source) {
  219. code.push(
  220. '";',
  221. updateLine,
  222. '\nvar partial = partials["' + trim(source) + '"];',
  223. '\nif (partial) {',
  224. '\n buffer += render(partial,stack[stack.length - 1],partials);',
  225. '\n}',
  226. '\nbuffer += "'
  227. );
  228. };
  229. var openSection = function (source, inverted) {
  230. var name = trim(source);
  231. if (name === "") {
  232. throw debug(new Error("Section name may not be empty"), template, line, options.file);
  233. }
  234. sectionStack.push({name: name, inverted: inverted});
  235. code.push(
  236. '";',
  237. updateLine,
  238. '\nvar name = "' + name + '";',
  239. '\nvar callback = (function () {',
  240. '\n return function () {',
  241. '\n var buffer = "";',
  242. '\nbuffer += "'
  243. );
  244. };
  245. var openInvertedSection = function (source) {
  246. openSection(source, true);
  247. };
  248. var closeSection = function (source) {
  249. var name = trim(source);
  250. var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name;
  251. if (!openName || name != openName) {
  252. throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file);
  253. }
  254. var section = sectionStack.pop();
  255. code.push(
  256. '";',
  257. '\n return buffer;',
  258. '\n };',
  259. '\n})();'
  260. );
  261. if (section.inverted) {
  262. code.push("\nbuffer += renderSection(name,stack,callback,true);");
  263. } else {
  264. code.push("\nbuffer += renderSection(name,stack,callback);");
  265. }
  266. code.push('\nbuffer += "');
  267. };
  268. var sendPlain = function (source) {
  269. code.push(
  270. '";',
  271. updateLine,
  272. '\nbuffer += lookup("' + trim(source) + '",stack,"");',
  273. '\nbuffer += "'
  274. );
  275. };
  276. var sendEscaped = function (source) {
  277. code.push(
  278. '";',
  279. updateLine,
  280. '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));',
  281. '\nbuffer += "'
  282. );
  283. };
  284. var line = 1, c, callback;
  285. for (var i = 0, len = template.length; i < len; ++i) {
  286. if (template.slice(i, i + openTag.length) === openTag) {
  287. i += openTag.length;
  288. c = template.substr(i, 1);
  289. updateLine = '\nline = ' + line + ';';
  290. nextOpenTag = openTag;
  291. nextCloseTag = closeTag;
  292. hasTag = true;
  293. switch (c) {
  294. case "!": // comment
  295. i++;
  296. callback = null;
  297. break;
  298. case "=": // change open/close tags, e.g. {{=<% %>=}}
  299. i++;
  300. closeTag = "=" + closeTag;
  301. callback = setTags;
  302. break;
  303. case ">": // include partial
  304. i++;
  305. callback = includePartial;
  306. break;
  307. case "#": // start section
  308. i++;
  309. callback = openSection;
  310. break;
  311. case "^": // start inverted section
  312. i++;
  313. callback = openInvertedSection;
  314. break;
  315. case "/": // end section
  316. i++;
  317. callback = closeSection;
  318. break;
  319. case "{": // plain variable
  320. closeTag = "}" + closeTag;
  321. // fall through
  322. case "&": // plain variable
  323. i++;
  324. nonSpace = true;
  325. callback = sendPlain;
  326. break;
  327. default: // escaped variable
  328. nonSpace = true;
  329. callback = sendEscaped;
  330. }
  331. var end = template.indexOf(closeTag, i);
  332. if (end === -1) {
  333. throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file);
  334. }
  335. var source = template.substring(i, end);
  336. if (callback) {
  337. callback(source);
  338. }
  339. // Maintain line count for \n in source.
  340. var n = 0;
  341. while (~(n = source.indexOf("\n", n))) {
  342. line++;
  343. n++;
  344. }
  345. i = end + closeTag.length - 1;
  346. openTag = nextOpenTag;
  347. closeTag = nextCloseTag;
  348. } else {
  349. c = template.substr(i, 1);
  350. switch (c) {
  351. case '"':
  352. case "\\":
  353. nonSpace = true;
  354. code.push("\\" + c);
  355. break;
  356. case "\r":
  357. // Ignore carriage returns.
  358. break;
  359. case "\n":
  360. spaces.push(code.length);
  361. code.push("\\n");
  362. stripSpace(); // Check for whitespace on the current line.
  363. line++;
  364. break;
  365. default:
  366. if (isWhitespace(c)) {
  367. spaces.push(code.length);
  368. } else {
  369. nonSpace = true;
  370. }
  371. code.push(c);
  372. }
  373. }
  374. }
  375. if (sectionStack.length != 0) {
  376. throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file);
  377. }
  378. // Clean up any whitespace from a closing {{tag}} that was at the end
  379. // of the template without a trailing \n.
  380. stripSpace();
  381. code.push(
  382. '";',
  383. "\nreturn buffer;",
  384. "\n} catch (e) { throw {error: e, line: line}; }"
  385. );
  386. // Ignore `buffer += "";` statements.
  387. var body = code.join("").replace(/buffer \+= "";\n/g, "");
  388. if (options.debug) {
  389. if (typeof console != "undefined" && console.log) {
  390. console.log(body);
  391. } else if (typeof print === "function") {
  392. print(body);
  393. }
  394. }
  395. return body;
  396. }
  397. /**
  398. * Used by `compile` to generate a reusable function for the given `template`.
  399. */
  400. function _compile(template, options) {
  401. var args = "view,partials,stack,lookup,escapeHTML,renderSection,render";
  402. var body = parse(template, options);
  403. var fn = new Function(args, body);
  404. // This anonymous function wraps the generated function so we can do
  405. // argument coercion, setup some variables, and handle any errors
  406. // encountered while executing it.
  407. return function (view, partials) {
  408. partials = partials || {};
  409. var stack = [view]; // context stack
  410. try {
  411. return fn(view, partials, stack, lookup, escapeHTML, renderSection, render);
  412. } catch (e) {
  413. throw debug(e.error, template, e.line, options.file);
  414. }
  415. };
  416. }
  417. // Cache of pre-compiled templates.
  418. var _cache = {};
  419. /**
  420. * Clear the cache of compiled templates.
  421. */
  422. function clearCache() {
  423. _cache = {};
  424. }
  425. /**
  426. * Compiles the given `template` into a reusable function using the given
  427. * `options`. In addition to the options accepted by Mustache.parse,
  428. * recognized options include the following:
  429. *
  430. * - cache Set `false` to bypass any pre-compiled version of the given
  431. * template. Otherwise, a given `template` string will be cached
  432. * the first time it is parsed
  433. */
  434. function compile(template, options) {
  435. options = options || {};
  436. // Use a pre-compiled version from the cache if we have one.
  437. if (options.cache !== false) {
  438. if (!_cache[template]) {
  439. _cache[template] = _compile(template, options);
  440. }
  441. return _cache[template];
  442. }
  443. return _compile(template, options);
  444. }
  445. /**
  446. * High-level function that renders the given `template` using the given
  447. * `view` and `partials`. If you need to use any of the template options (see
  448. * `compile` above), you must compile in a separate step, and then call that
  449. * compiled function.
  450. */
  451. function render(template, view, partials) {
  452. return compile(template)(view, partials);
  453. }
  454. })(Mustache);