mustache.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. /*!
  2. * mustache.js - Logic-less {{mustache}} templates with JavaScript
  3. * http://github.com/janl/mustache.js
  4. */
  5. /*global define: false Mustache: true*/
  6. (function defineMustache (global, factory) {
  7. if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') {
  8. factory(exports); // CommonJS
  9. } else if (typeof define === 'function' && define.amd) {
  10. define(['exports'], factory); // AMD
  11. } else {
  12. global.Mustache = {};
  13. factory(global.Mustache); // script, wsh, asp
  14. }
  15. }(this, function mustacheFactory (mustache) {
  16. var objectToString = Object.prototype.toString;
  17. var isArray = Array.isArray || function isArrayPolyfill (object) {
  18. return objectToString.call(object) === '[object Array]';
  19. };
  20. function isFunction (object) {
  21. return typeof object === 'function';
  22. }
  23. /**
  24. * More correct typeof string handling array
  25. * which normally returns typeof 'object'
  26. */
  27. function typeStr (obj) {
  28. return isArray(obj) ? 'array' : typeof obj;
  29. }
  30. function escapeRegExp (string) {
  31. return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
  32. }
  33. /**
  34. * Null safe way of checking whether or not an object,
  35. * including its prototype, has a given property
  36. */
  37. function hasProperty (obj, propName) {
  38. return obj != null && typeof obj === 'object' && (propName in obj);
  39. }
  40. // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
  41. // See https://github.com/janl/mustache.js/issues/189
  42. var regExpTest = RegExp.prototype.test;
  43. function testRegExp (re, string) {
  44. return regExpTest.call(re, string);
  45. }
  46. var nonSpaceRe = /\S/;
  47. function isWhitespace (string) {
  48. return !testRegExp(nonSpaceRe, string);
  49. }
  50. var entityMap = {
  51. '&': '&',
  52. '<': '&lt;',
  53. '>': '&gt;',
  54. '"': '&quot;',
  55. "'": '&#39;',
  56. '/': '&#x2F;',
  57. '`': '&#x60;',
  58. '=': '&#x3D;'
  59. };
  60. function escapeHtml (string) {
  61. return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
  62. return entityMap[s];
  63. });
  64. }
  65. var whiteRe = /\s*/;
  66. var spaceRe = /\s+/;
  67. var equalsRe = /\s*=/;
  68. var curlyRe = /\s*\}/;
  69. var tagRe = /#|\^|\/|>|\{|&|=|!/;
  70. /**
  71. * Breaks up the given `template` string into a tree of tokens. If the `tags`
  72. * argument is given here it must be an array with two string values: the
  73. * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
  74. * course, the default is to use mustaches (i.e. mustache.tags).
  75. *
  76. * A token is an array with at least 4 elements. The first element is the
  77. * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
  78. * did not contain a symbol (i.e. {{myValue}}) this element is "name". For
  79. * all text that appears outside a symbol this element is "text".
  80. *
  81. * The second element of a token is its "value". For mustache tags this is
  82. * whatever else was inside the tag besides the opening symbol. For text tokens
  83. * this is the text itself.
  84. *
  85. * The third and fourth elements of the token are the start and end indices,
  86. * respectively, of the token in the original template.
  87. *
  88. * Tokens that are the root node of a subtree contain two more elements: 1) an
  89. * array of tokens in the subtree and 2) the index in the original template at
  90. * which the closing tag for that section begins.
  91. */
  92. function parseTemplate (template, tags) {
  93. if (!template)
  94. return [];
  95. var sections = []; // Stack to hold section tokens
  96. var tokens = []; // Buffer to hold the tokens
  97. var spaces = []; // Indices of whitespace tokens on the current line
  98. var hasTag = false; // Is there a {{tag}} on the current line?
  99. var nonSpace = false; // Is there a non-space char on the current line?
  100. // Strips all whitespace tokens array for the current line
  101. // if there was a {{#tag}} on it and otherwise only space.
  102. function stripSpace () {
  103. if (hasTag && !nonSpace) {
  104. while (spaces.length)
  105. delete tokens[spaces.pop()];
  106. } else {
  107. spaces = [];
  108. }
  109. hasTag = false;
  110. nonSpace = false;
  111. }
  112. var openingTagRe, closingTagRe, closingCurlyRe;
  113. function compileTags (tagsToCompile) {
  114. if (typeof tagsToCompile === 'string')
  115. tagsToCompile = tagsToCompile.split(spaceRe, 2);
  116. if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
  117. throw new Error('Invalid tags: ' + tagsToCompile);
  118. openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
  119. closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
  120. closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
  121. }
  122. compileTags(tags || mustache.tags);
  123. var scanner = new Scanner(template);
  124. var start, type, value, chr, token, openSection;
  125. while (!scanner.eos()) {
  126. start = scanner.pos;
  127. // Match any text between tags.
  128. value = scanner.scanUntil(openingTagRe);
  129. if (value) {
  130. for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
  131. chr = value.charAt(i);
  132. if (isWhitespace(chr)) {
  133. spaces.push(tokens.length);
  134. } else {
  135. nonSpace = true;
  136. }
  137. tokens.push([ 'text', chr, start, start + 1 ]);
  138. start += 1;
  139. // Check for whitespace on the current line.
  140. if (chr === '\n')
  141. stripSpace();
  142. }
  143. }
  144. // Match the opening tag.
  145. if (!scanner.scan(openingTagRe))
  146. break;
  147. hasTag = true;
  148. // Get the tag type.
  149. type = scanner.scan(tagRe) || 'name';
  150. scanner.scan(whiteRe);
  151. // Get the tag value.
  152. if (type === '=') {
  153. value = scanner.scanUntil(equalsRe);
  154. scanner.scan(equalsRe);
  155. scanner.scanUntil(closingTagRe);
  156. } else if (type === '{') {
  157. value = scanner.scanUntil(closingCurlyRe);
  158. scanner.scan(curlyRe);
  159. scanner.scanUntil(closingTagRe);
  160. type = '&';
  161. } else {
  162. value = scanner.scanUntil(closingTagRe);
  163. }
  164. // Match the closing tag.
  165. if (!scanner.scan(closingTagRe))
  166. throw new Error('Unclosed tag at ' + scanner.pos);
  167. token = [ type, value, start, scanner.pos ];
  168. tokens.push(token);
  169. if (type === '#' || type === '^') {
  170. sections.push(token);
  171. } else if (type === '/') {
  172. // Check section nesting.
  173. openSection = sections.pop();
  174. if (!openSection)
  175. throw new Error('Unopened section "' + value + '" at ' + start);
  176. if (openSection[1] !== value)
  177. throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
  178. } else if (type === 'name' || type === '{' || type === '&') {
  179. nonSpace = true;
  180. } else if (type === '=') {
  181. // Set the tags for the next time around.
  182. compileTags(value);
  183. }
  184. }
  185. // Make sure there are no open sections when we're done.
  186. openSection = sections.pop();
  187. if (openSection)
  188. throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
  189. return nestTokens(squashTokens(tokens));
  190. }
  191. /**
  192. * Combines the values of consecutive text tokens in the given `tokens` array
  193. * to a single token.
  194. */
  195. function squashTokens (tokens) {
  196. var squashedTokens = [];
  197. var token, lastToken;
  198. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  199. token = tokens[i];
  200. if (token) {
  201. if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
  202. lastToken[1] += token[1];
  203. lastToken[3] = token[3];
  204. } else {
  205. squashedTokens.push(token);
  206. lastToken = token;
  207. }
  208. }
  209. }
  210. return squashedTokens;
  211. }
  212. /**
  213. * Forms the given array of `tokens` into a nested tree structure where
  214. * tokens that represent a section have two additional items: 1) an array of
  215. * all tokens that appear in that section and 2) the index in the original
  216. * template that represents the end of that section.
  217. */
  218. function nestTokens (tokens) {
  219. var nestedTokens = [];
  220. var collector = nestedTokens;
  221. var sections = [];
  222. var token, section;
  223. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  224. token = tokens[i];
  225. switch (token[0]) {
  226. case '#':
  227. case '^':
  228. collector.push(token);
  229. sections.push(token);
  230. collector = token[4] = [];
  231. break;
  232. case '/':
  233. section = sections.pop();
  234. section[5] = token[2];
  235. collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
  236. break;
  237. default:
  238. collector.push(token);
  239. }
  240. }
  241. return nestedTokens;
  242. }
  243. /**
  244. * A simple string scanner that is used by the template parser to find
  245. * tokens in template strings.
  246. */
  247. function Scanner (string) {
  248. this.string = string;
  249. this.tail = string;
  250. this.pos = 0;
  251. }
  252. /**
  253. * Returns `true` if the tail is empty (end of string).
  254. */
  255. Scanner.prototype.eos = function eos () {
  256. return this.tail === '';
  257. };
  258. /**
  259. * Tries to match the given regular expression at the current position.
  260. * Returns the matched text if it can match, the empty string otherwise.
  261. */
  262. Scanner.prototype.scan = function scan (re) {
  263. var match = this.tail.match(re);
  264. if (!match || match.index !== 0)
  265. return '';
  266. var string = match[0];
  267. this.tail = this.tail.substring(string.length);
  268. this.pos += string.length;
  269. return string;
  270. };
  271. /**
  272. * Skips all text until the given regular expression can be matched. Returns
  273. * the skipped string, which is the entire tail if no match can be made.
  274. */
  275. Scanner.prototype.scanUntil = function scanUntil (re) {
  276. var index = this.tail.search(re), match;
  277. switch (index) {
  278. case -1:
  279. match = this.tail;
  280. this.tail = '';
  281. break;
  282. case 0:
  283. match = '';
  284. break;
  285. default:
  286. match = this.tail.substring(0, index);
  287. this.tail = this.tail.substring(index);
  288. }
  289. this.pos += match.length;
  290. return match;
  291. };
  292. /**
  293. * Represents a rendering context by wrapping a view object and
  294. * maintaining a reference to the parent context.
  295. */
  296. function Context (view, parentContext) {
  297. this.view = view;
  298. this.cache = { '.': this.view };
  299. this.parent = parentContext;
  300. }
  301. /**
  302. * Creates a new context using the given view with this context
  303. * as the parent.
  304. */
  305. Context.prototype.push = function push (view) {
  306. return new Context(view, this);
  307. };
  308. /**
  309. * Returns the value of the given name in this context, traversing
  310. * up the context hierarchy if the value is absent in this context's view.
  311. */
  312. Context.prototype.lookup = function lookup (name) {
  313. var cache = this.cache;
  314. var value;
  315. if (cache.hasOwnProperty(name)) {
  316. value = cache[name];
  317. } else {
  318. var context = this, names, index, lookupHit = false;
  319. while (context) {
  320. if (name.indexOf('.') > 0) {
  321. value = context.view;
  322. names = name.split('.');
  323. index = 0;
  324. /**
  325. * Using the dot notion path in `name`, we descend through the
  326. * nested objects.
  327. *
  328. * To be certain that the lookup has been successful, we have to
  329. * check if the last object in the path actually has the property
  330. * we are looking for. We store the result in `lookupHit`.
  331. *
  332. * This is specially necessary for when the value has been set to
  333. * `undefined` and we want to avoid looking up parent contexts.
  334. **/
  335. while (value != null && index < names.length) {
  336. if (index === names.length - 1)
  337. lookupHit = hasProperty(value, names[index]);
  338. value = value[names[index++]];
  339. }
  340. } else {
  341. value = context.view[name];
  342. lookupHit = hasProperty(context.view, name);
  343. }
  344. if (lookupHit)
  345. break;
  346. context = context.parent;
  347. }
  348. cache[name] = value;
  349. }
  350. if (isFunction(value))
  351. value = value.call(this.view);
  352. return value;
  353. };
  354. /**
  355. * A Writer knows how to take a stream of tokens and render them to a
  356. * string, given a context. It also maintains a cache of templates to
  357. * avoid the need to parse the same template twice.
  358. */
  359. function Writer () {
  360. this.cache = {};
  361. }
  362. /**
  363. * Clears all cached templates in this writer.
  364. */
  365. Writer.prototype.clearCache = function clearCache () {
  366. this.cache = {};
  367. };
  368. /**
  369. * Parses and caches the given `template` and returns the array of tokens
  370. * that is generated from the parse.
  371. */
  372. Writer.prototype.parse = function parse (template, tags) {
  373. var cache = this.cache;
  374. var tokens = cache[template];
  375. if (tokens == null)
  376. tokens = cache[template] = parseTemplate(template, tags);
  377. return tokens;
  378. };
  379. /**
  380. * High-level method that is used to render the given `template` with
  381. * the given `view`.
  382. *
  383. * The optional `partials` argument may be an object that contains the
  384. * names and templates of partials that are used in the template. It may
  385. * also be a function that is used to load partial templates on the fly
  386. * that takes a single argument: the name of the partial.
  387. */
  388. Writer.prototype.render = function render (template, view, partials) {
  389. var tokens = this.parse(template);
  390. var context = (view instanceof Context) ? view : new Context(view);
  391. return this.renderTokens(tokens, context, partials, template);
  392. };
  393. /**
  394. * Low-level method that renders the given array of `tokens` using
  395. * the given `context` and `partials`.
  396. *
  397. * Note: The `originalTemplate` is only ever used to extract the portion
  398. * of the original template that was contained in a higher-order section.
  399. * If the template doesn't use higher-order sections, this argument may
  400. * be omitted.
  401. */
  402. Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) {
  403. var buffer = '';
  404. var token, symbol, value;
  405. for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
  406. value = undefined;
  407. token = tokens[i];
  408. symbol = token[0];
  409. if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate);
  410. else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate);
  411. else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate);
  412. else if (symbol === '&') value = this.unescapedValue(token, context);
  413. else if (symbol === 'name') value = this.escapedValue(token, context);
  414. else if (symbol === 'text') value = this.rawValue(token);
  415. if (value !== undefined)
  416. buffer += value;
  417. }
  418. return buffer;
  419. };
  420. Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
  421. var self = this;
  422. var buffer = '';
  423. var value = context.lookup(token[1]);
  424. // This function is used to render an arbitrary template
  425. // in the current context by higher-order sections.
  426. function subRender (template) {
  427. return self.render(template, context, partials);
  428. }
  429. if (!value) return;
  430. if (isArray(value)) {
  431. for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
  432. buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
  433. }
  434. } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
  435. buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
  436. } else if (isFunction(value)) {
  437. if (typeof originalTemplate !== 'string')
  438. throw new Error('Cannot use higher-order sections without the original template');
  439. // Extract the portion of the original template that the section contains.
  440. value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
  441. if (value != null)
  442. buffer += value;
  443. } else {
  444. buffer += this.renderTokens(token[4], context, partials, originalTemplate);
  445. }
  446. return buffer;
  447. };
  448. Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
  449. var value = context.lookup(token[1]);
  450. // Use JavaScript's definition of falsy. Include empty arrays.
  451. // See https://github.com/janl/mustache.js/issues/186
  452. if (!value || (isArray(value) && value.length === 0))
  453. return this.renderTokens(token[4], context, partials, originalTemplate);
  454. };
  455. Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
  456. if (!partials) return;
  457. var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
  458. if (value != null)
  459. return this.renderTokens(this.parse(value), context, partials, value);
  460. };
  461. Writer.prototype.unescapedValue = function unescapedValue (token, context) {
  462. var value = context.lookup(token[1]);
  463. if (value != null)
  464. return value;
  465. };
  466. Writer.prototype.escapedValue = function escapedValue (token, context) {
  467. var value = context.lookup(token[1]);
  468. if (value != null)
  469. return mustache.escape(value);
  470. };
  471. Writer.prototype.rawValue = function rawValue (token) {
  472. return token[1];
  473. };
  474. mustache.name = 'mustache.js';
  475. mustache.version = '2.3.2';
  476. mustache.tags = [ '{{', '}}' ];
  477. // All high-level mustache.* functions use this writer.
  478. var defaultWriter = new Writer();
  479. /**
  480. * Clears all cached templates in the default writer.
  481. */
  482. mustache.clearCache = function clearCache () {
  483. return defaultWriter.clearCache();
  484. };
  485. /**
  486. * Parses and caches the given template in the default writer and returns the
  487. * array of tokens it contains. Doing this ahead of time avoids the need to
  488. * parse templates on the fly as they are rendered.
  489. */
  490. mustache.parse = function parse (template, tags) {
  491. return defaultWriter.parse(template, tags);
  492. };
  493. /**
  494. * Renders the `template` with the given `view` and `partials` using the
  495. * default writer.
  496. */
  497. mustache.render = function render (template, view, partials) {
  498. if (typeof template !== 'string') {
  499. throw new TypeError('Invalid template! Template should be a "string" ' +
  500. 'but "' + typeStr(template) + '" was given as the first ' +
  501. 'argument for mustache#render(template, view, partials)');
  502. }
  503. return defaultWriter.render(template, view, partials);
  504. };
  505. // This is here for backwards compatibility with 0.4.x.,
  506. /*eslint-disable */ // eslint wants camel cased function name
  507. mustache.to_html = function to_html (template, view, partials, send) {
  508. /*eslint-enable*/
  509. var result = mustache.render(template, view, partials);
  510. if (isFunction(send)) {
  511. send(result);
  512. } else {
  513. return result;
  514. }
  515. };
  516. // Export the escaping function so that the user may override it.
  517. // See https://github.com/janl/mustache.js/issues/244
  518. mustache.escape = escapeHtml;
  519. // Export these mainly for testing, but also for advanced usage.
  520. mustache.Scanner = Scanner;
  521. mustache.Context = Context;
  522. mustache.Writer = Writer;
  523. return mustache;
  524. }));