mustache.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /*!
  2. * mustache.js - Logic-less {{mustache}} templates with JavaScript
  3. * http://github.com/janl/mustache.js
  4. */
  5. /*global define: false*/
  6. (function (root, factory) {
  7. if (typeof exports === "object" && exports) {
  8. module.exports = factory; // CommonJS
  9. } else if (typeof define === "function" && define.amd) {
  10. define(factory); // AMD
  11. } else {
  12. root.Mustache = factory; // <script>
  13. }
  14. }(this, (function () {
  15. var exports = {};
  16. exports.name = "mustache.js";
  17. exports.version = "0.7.2";
  18. exports.tags = ["{{", "}}"];
  19. exports.Scanner = Scanner;
  20. exports.Context = Context;
  21. exports.Writer = Writer;
  22. var whiteRe = /\s*/;
  23. var spaceRe = /\s+/;
  24. var nonSpaceRe = /\S/;
  25. var eqRe = /\s*=/;
  26. var curlyRe = /\s*\}/;
  27. var tagRe = /#|\^|\/|>|\{|&|=|!/;
  28. var _test = RegExp.prototype.test;
  29. var _toString = Object.prototype.toString;
  30. // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
  31. // See https://github.com/janl/mustache.js/issues/189
  32. function testRe(re, string) {
  33. return _test.call(re, string);
  34. }
  35. function isWhitespace(string) {
  36. return !testRe(nonSpaceRe, string);
  37. }
  38. var isArray = Array.isArray || function (obj) {
  39. return _toString.call(obj) === '[object Array]';
  40. };
  41. function escapeRe(string) {
  42. return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
  43. }
  44. var entityMap = {
  45. "&": "&amp;",
  46. "<": "&lt;",
  47. ">": "&gt;",
  48. '"': '&quot;',
  49. "'": '&#39;',
  50. "/": '&#x2F;'
  51. };
  52. function escapeHtml(string) {
  53. return String(string).replace(/[&<>"'\/]/g, function (s) {
  54. return entityMap[s];
  55. });
  56. }
  57. // Export the escaping function so that the user may override it.
  58. // See https://github.com/janl/mustache.js/issues/244
  59. exports.escape = escapeHtml;
  60. function Scanner(string) {
  61. this.string = string;
  62. this.tail = string;
  63. this.pos = 0;
  64. }
  65. /**
  66. * Returns `true` if the tail is empty (end of string).
  67. */
  68. Scanner.prototype.eos = function () {
  69. return this.tail === "";
  70. };
  71. /**
  72. * Tries to match the given regular expression at the current position.
  73. * Returns the matched text if it can match, the empty string otherwise.
  74. */
  75. Scanner.prototype.scan = function (re) {
  76. var match = this.tail.match(re);
  77. if (match && match.index === 0) {
  78. this.tail = this.tail.substring(match[0].length);
  79. this.pos += match[0].length;
  80. return match[0];
  81. }
  82. return "";
  83. };
  84. /**
  85. * Skips all text until the given regular expression can be matched. Returns
  86. * the skipped string, which is the entire tail if no match can be made.
  87. */
  88. Scanner.prototype.scanUntil = function (re) {
  89. var match, pos = this.tail.search(re);
  90. switch (pos) {
  91. case -1:
  92. match = this.tail;
  93. this.pos += this.tail.length;
  94. this.tail = "";
  95. break;
  96. case 0:
  97. match = "";
  98. break;
  99. default:
  100. match = this.tail.substring(0, pos);
  101. this.tail = this.tail.substring(pos);
  102. this.pos += pos;
  103. }
  104. return match;
  105. };
  106. function Context(view, parent) {
  107. this.view = view;
  108. this.parent = parent;
  109. this._cache = {};
  110. }
  111. Context.make = function (view) {
  112. return (view instanceof Context) ? view : new Context(view);
  113. };
  114. Context.prototype.push = function (view) {
  115. return new Context(view, this);
  116. };
  117. Context.prototype.lookup = function (name) {
  118. var value = this._cache[name];
  119. if (!value) {
  120. if (name == '.') {
  121. value = this.view;
  122. } else {
  123. var context = this;
  124. while (context) {
  125. if (name.indexOf('.') > 0) {
  126. value = context.view;
  127. var names = name.split('.'), i = 0;
  128. while (value && i < names.length) {
  129. value = value[names[i++]];
  130. }
  131. } else {
  132. value = context.view[name];
  133. }
  134. if (value != null) break;
  135. context = context.parent;
  136. }
  137. }
  138. this._cache[name] = value;
  139. }
  140. if (typeof value === 'function') value = value.call(this.view);
  141. return value;
  142. };
  143. function Writer() {
  144. this.clearCache();
  145. }
  146. Writer.prototype.clearCache = function () {
  147. this._cache = {};
  148. this._partialCache = {};
  149. };
  150. Writer.prototype.compile = function (template, tags) {
  151. var fn = this._cache[template];
  152. if (!fn) {
  153. var tokens = exports.parse(template, tags);
  154. fn = this._cache[template] = this.compileTokens(tokens, template);
  155. }
  156. return fn;
  157. };
  158. Writer.prototype.compilePartial = function (name, template, tags) {
  159. var fn = this.compile(template, tags);
  160. this._partialCache[name] = fn;
  161. return fn;
  162. };
  163. Writer.prototype.getPartial = function (name) {
  164. if (!(name in this._partialCache) && this._loadPartial) {
  165. this.compilePartial(name, this._loadPartial(name));
  166. }
  167. return this._partialCache[name];
  168. };
  169. Writer.prototype.compileTokens = function (tokens, template) {
  170. var self = this;
  171. return function (view, partials) {
  172. if (partials) {
  173. if (typeof partials === 'function') {
  174. self._loadPartial = partials;
  175. } else {
  176. for (var name in partials) {
  177. self.compilePartial(name, partials[name]);
  178. }
  179. }
  180. }
  181. return renderTokens(tokens, self, Context.make(view), template);
  182. };
  183. };
  184. Writer.prototype.render = function (template, view, partials) {
  185. return this.compile(template)(view, partials);
  186. };
  187. /**
  188. * Low-level function that renders the given `tokens` using the given `writer`
  189. * and `context`. The `template` string is only needed for templates that use
  190. * higher-order sections to extract the portion of the original template that
  191. * was contained in that section.
  192. */
  193. function renderTokens(tokens, writer, context, template) {
  194. var buffer = '';
  195. var token, tokenValue, value;
  196. for (var i = 0, len = tokens.length; i < len; ++i) {
  197. token = tokens[i];
  198. tokenValue = token[1];
  199. switch (token[0]) {
  200. case '#':
  201. value = context.lookup(tokenValue);
  202. if (typeof value === 'object') {
  203. if (isArray(value)) {
  204. for (var j = 0, jlen = value.length; j < jlen; ++j) {
  205. buffer += renderTokens(token[4], writer, context.push(value[j]), template);
  206. }
  207. } else if (value) {
  208. buffer += renderTokens(token[4], writer, context.push(value), template);
  209. }
  210. } else if (typeof value === 'function') {
  211. var text = template == null ? null : template.slice(token[3], token[5]);
  212. value = value.call(context.view, text, function (template) {
  213. return writer.render(template, context);
  214. });
  215. if (value != null) buffer += value;
  216. } else if (value) {
  217. buffer += renderTokens(token[4], writer, context, template);
  218. }
  219. break;
  220. case '^':
  221. value = context.lookup(tokenValue);
  222. // Use JavaScript's definition of falsy. Include empty arrays.
  223. // See https://github.com/janl/mustache.js/issues/186
  224. if (!value || (isArray(value) && value.length === 0)) {
  225. buffer += renderTokens(token[4], writer, context, template);
  226. }
  227. break;
  228. case '>':
  229. value = writer.getPartial(tokenValue);
  230. if (typeof value === 'function') buffer += value(context);
  231. break;
  232. case '&':
  233. value = context.lookup(tokenValue);
  234. if (value != null) buffer += value;
  235. break;
  236. case 'name':
  237. value = context.lookup(tokenValue);
  238. if (value != null) buffer += exports.escape(value);
  239. break;
  240. case 'text':
  241. buffer += tokenValue;
  242. break;
  243. }
  244. }
  245. return buffer;
  246. }
  247. /**
  248. * Forms the given array of `tokens` into a nested tree structure where
  249. * tokens that represent a section have two additional items: 1) an array of
  250. * all tokens that appear in that section and 2) the index in the original
  251. * template that represents the end of that section.
  252. */
  253. function nestTokens(tokens) {
  254. var tree = [];
  255. var collector = tree;
  256. var sections = [];
  257. var token;
  258. for (var i = 0, len = tokens.length; i < len; ++i) {
  259. token = tokens[i];
  260. switch (token[0]) {
  261. case '#':
  262. case '^':
  263. sections.push(token);
  264. collector.push(token);
  265. collector = token[4] = [];
  266. break;
  267. case '/':
  268. var section = sections.pop();
  269. section[5] = token[2];
  270. collector = sections.length > 0 ? sections[sections.length - 1][4] : tree;
  271. break;
  272. default:
  273. collector.push(token);
  274. }
  275. }
  276. return tree;
  277. }
  278. /**
  279. * Combines the values of consecutive text tokens in the given `tokens` array
  280. * to a single token.
  281. */
  282. function squashTokens(tokens) {
  283. var squashedTokens = [];
  284. var token, lastToken;
  285. for (var i = 0, len = tokens.length; i < len; ++i) {
  286. token = tokens[i];
  287. if (token) {
  288. if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
  289. lastToken[1] += token[1];
  290. lastToken[3] = token[3];
  291. } else {
  292. lastToken = token;
  293. squashedTokens.push(token);
  294. }
  295. }
  296. }
  297. return squashedTokens;
  298. }
  299. function escapeTags(tags) {
  300. return [
  301. new RegExp(escapeRe(tags[0]) + "\\s*"),
  302. new RegExp("\\s*" + escapeRe(tags[1]))
  303. ];
  304. }
  305. /**
  306. * Breaks up the given `template` string into a tree of token objects. If
  307. * `tags` is given here it must be an array with two string values: the
  308. * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
  309. * course, the default is to use mustaches (i.e. Mustache.tags).
  310. */
  311. exports.parse = function (template, tags) {
  312. template = template || '';
  313. tags = tags || exports.tags;
  314. if (typeof tags === 'string') tags = tags.split(spaceRe);
  315. if (tags.length !== 2) throw new Error('Invalid tags: ' + tags.join(', '));
  316. var tagRes = escapeTags(tags);
  317. var scanner = new Scanner(template);
  318. var sections = []; // Stack to hold section tokens
  319. var tokens = []; // Buffer to hold the tokens
  320. var spaces = []; // Indices of whitespace tokens on the current line
  321. var hasTag = false; // Is there a {{tag}} on the current line?
  322. var nonSpace = false; // Is there a non-space char on the current line?
  323. // Strips all whitespace tokens array for the current line
  324. // if there was a {{#tag}} on it and otherwise only space.
  325. function stripSpace() {
  326. if (hasTag && !nonSpace) {
  327. while (spaces.length) {
  328. delete tokens[spaces.pop()];
  329. }
  330. } else {
  331. spaces = [];
  332. }
  333. hasTag = false;
  334. nonSpace = false;
  335. }
  336. var start, type, value, chr, token;
  337. while (!scanner.eos()) {
  338. start = scanner.pos;
  339. // Match any text between tags.
  340. value = scanner.scanUntil(tagRes[0]);
  341. if (value) {
  342. for (var i = 0, len = value.length; i < len; ++i) {
  343. chr = value.charAt(i);
  344. if (isWhitespace(chr)) {
  345. spaces.push(tokens.length);
  346. } else {
  347. nonSpace = true;
  348. }
  349. tokens.push(['text', chr, start, start + 1]);
  350. start += 1;
  351. // Check for whitespace on the current line.
  352. if (chr == '\n') stripSpace();
  353. }
  354. }
  355. // Match the opening tag.
  356. if (!scanner.scan(tagRes[0])) break;
  357. hasTag = true;
  358. // Get the tag type.
  359. type = scanner.scan(tagRe) || 'name';
  360. scanner.scan(whiteRe);
  361. // Get the tag value.
  362. if (type === '=') {
  363. value = scanner.scanUntil(eqRe);
  364. scanner.scan(eqRe);
  365. scanner.scanUntil(tagRes[1]);
  366. } else if (type === '{') {
  367. value = scanner.scanUntil(new RegExp('\\s*' + escapeRe('}' + tags[1])));
  368. scanner.scan(curlyRe);
  369. scanner.scanUntil(tagRes[1]);
  370. type = '&';
  371. } else {
  372. value = scanner.scanUntil(tagRes[1]);
  373. }
  374. // Match the closing tag.
  375. if (!scanner.scan(tagRes[1])) throw new Error('Unclosed tag at ' + scanner.pos);
  376. token = [type, value, start, scanner.pos];
  377. tokens.push(token);
  378. if (type === '#' || type === '^') {
  379. sections.push(token);
  380. } else if (type === '/') {
  381. // Check section nesting.
  382. if (sections.length === 0) throw new Error('Unopened section "' + value + '" at ' + start);
  383. var openSection = sections.pop();
  384. if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
  385. } else if (type === 'name' || type === '{' || type === '&') {
  386. nonSpace = true;
  387. } else if (type === '=') {
  388. // Set the tags for the next time around.
  389. tags = value.split(spaceRe);
  390. if (tags.length !== 2) throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
  391. tagRes = escapeTags(tags);
  392. }
  393. }
  394. // Make sure there are no open sections when we're done.
  395. var openSection = sections.pop();
  396. if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
  397. tokens = squashTokens(tokens);
  398. return nestTokens(tokens);
  399. };
  400. // All Mustache.* functions use this writer.
  401. var _writer = new Writer();
  402. /**
  403. * Clears all cached templates and partials in the default writer.
  404. */
  405. exports.clearCache = function () {
  406. return _writer.clearCache();
  407. };
  408. /**
  409. * Compiles the given `template` to a reusable function using the default
  410. * writer.
  411. */
  412. exports.compile = function (template, tags) {
  413. return _writer.compile(template, tags);
  414. };
  415. /**
  416. * Compiles the partial with the given `name` and `template` to a reusable
  417. * function using the default writer.
  418. */
  419. exports.compilePartial = function (name, template, tags) {
  420. return _writer.compilePartial(name, template, tags);
  421. };
  422. /**
  423. * Compiles the given array of tokens (the output of a parse) to a reusable
  424. * function using the default writer.
  425. */
  426. exports.compileTokens = function (tokens, template) {
  427. return _writer.compileTokens(tokens, template);
  428. };
  429. /**
  430. * Renders the `template` with the given `view` and `partials` using the
  431. * default writer.
  432. */
  433. exports.render = function (template, view, partials) {
  434. return _writer.render(template, view, partials);
  435. };
  436. // This is here for backwards compatibility with 0.4.x.
  437. exports.to_html = function (template, view, partials, send) {
  438. var result = exports.render(template, view, partials);
  439. if (typeof send === "function") {
  440. send(result);
  441. } else {
  442. return result;
  443. }
  444. };
  445. return exports;
  446. }())));