You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

537 lines
14KB

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