mustache.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  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. factory(exports); // CommonJS
  9. } else {
  10. var mustache = {};
  11. factory(mustache);
  12. if (typeof define === "function" && define.amd) {
  13. define(mustache); // AMD
  14. } else {
  15. root.Mustache = mustache; // <script>
  16. }
  17. }
  18. }(this, function (mustache) {
  19. var whiteRe = /\s*/;
  20. var spaceRe = /\s+/;
  21. var nonSpaceRe = /\S/;
  22. var eqRe = /\s*=/;
  23. var curlyRe = /\s*\}/;
  24. var tagRe = /#|\^|\/|>|\{|&|=|!/;
  25. // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
  26. // See https://github.com/janl/mustache.js/issues/189
  27. var RegExp_test = RegExp.prototype.test;
  28. function testRegExp(re, string) {
  29. return RegExp_test.call(re, string);
  30. }
  31. function isWhitespace(string) {
  32. return !testRegExp(nonSpaceRe, string);
  33. }
  34. var Object_toString = Object.prototype.toString;
  35. var isArray = Array.isArray || function (object) {
  36. return Object_toString.call(object) === '[object Array]';
  37. };
  38. function isFunction(object) {
  39. return typeof object === 'function';
  40. }
  41. function escapeRegExp(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. function Scanner(string) {
  58. this.string = string;
  59. this.tail = string;
  60. this.pos = 0;
  61. }
  62. /**
  63. * Returns `true` if the tail is empty (end of string).
  64. */
  65. Scanner.prototype.eos = function () {
  66. return this.tail === "";
  67. };
  68. /**
  69. * Tries to match the given regular expression at the current position.
  70. * Returns the matched text if it can match, the empty string otherwise.
  71. */
  72. Scanner.prototype.scan = function (re) {
  73. var match = this.tail.match(re);
  74. if (match && match.index === 0) {
  75. var string = match[0];
  76. this.tail = this.tail.substring(string.length);
  77. this.pos += string.length;
  78. return string;
  79. }
  80. return "";
  81. };
  82. /**
  83. * Skips all text until the given regular expression can be matched. Returns
  84. * the skipped string, which is the entire tail if no match can be made.
  85. */
  86. Scanner.prototype.scanUntil = function (re) {
  87. var index = this.tail.search(re), match;
  88. switch (index) {
  89. case -1:
  90. match = this.tail;
  91. this.tail = "";
  92. break;
  93. case 0:
  94. match = "";
  95. break;
  96. default:
  97. match = this.tail.substring(0, index);
  98. this.tail = this.tail.substring(index);
  99. }
  100. this.pos += match.length;
  101. return match;
  102. };
  103. function Context(view, parent) {
  104. this.view = view == null ? {} : view;
  105. this.parent = parent;
  106. this._cache = { '.': this.view };
  107. }
  108. Context.make = function (view) {
  109. return (view instanceof Context) ? view : new Context(view);
  110. };
  111. Context.prototype.push = function (view) {
  112. return new Context(view, this);
  113. };
  114. Context.prototype.lookup = function (name) {
  115. var value;
  116. if (name in this._cache) {
  117. value = this._cache[name];
  118. } else {
  119. var context = this;
  120. while (context) {
  121. if (name.indexOf('.') > 0) {
  122. value = context.view;
  123. var names = name.split('.'), i = 0;
  124. while (value != null && i < names.length) {
  125. value = value[names[i++]];
  126. }
  127. } else {
  128. value = context.view[name];
  129. }
  130. if (value != null) break;
  131. context = context.parent;
  132. }
  133. this._cache[name] = value;
  134. }
  135. if (isFunction(value)) {
  136. value = value.call(this.view);
  137. }
  138. return value;
  139. };
  140. function Writer() {
  141. this.clearCache();
  142. }
  143. Writer.prototype.clearCache = function () {
  144. this._cache = {};
  145. this._partialCache = {};
  146. };
  147. Writer.prototype.compile = function (template, tags) {
  148. var fn = this._cache[template];
  149. if (!fn) {
  150. var tokens = mustache.parse(template, tags);
  151. fn = this._cache[template] = this.compileTokens(tokens, template);
  152. }
  153. return fn;
  154. };
  155. Writer.prototype.compilePartial = function (name, template, tags) {
  156. var fn = this.compile(template, tags);
  157. this._partialCache[name] = fn;
  158. return fn;
  159. };
  160. Writer.prototype.getPartial = function (name) {
  161. if (!(name in this._partialCache) && this._loadPartial) {
  162. this.compilePartial(name, this._loadPartial(name));
  163. }
  164. return this._partialCache[name];
  165. };
  166. Writer.prototype.compileTokens = function (tokens, template) {
  167. var self = this;
  168. return function (view, partials) {
  169. if (partials) {
  170. if (isFunction(partials)) {
  171. self._loadPartial = partials;
  172. } else {
  173. for (var name in partials) {
  174. self.compilePartial(name, partials[name]);
  175. }
  176. }
  177. }
  178. return renderTokens(tokens, self, Context.make(view), template);
  179. };
  180. };
  181. Writer.prototype.render = function (template, view, partials) {
  182. return this.compile(template)(view, partials);
  183. };
  184. /**
  185. * Low-level function that renders the given `tokens` using the given `writer`
  186. * and `context`. The `template` string is only needed for templates that use
  187. * higher-order sections to extract the portion of the original template that
  188. * was contained in that section.
  189. */
  190. function renderTokens(tokens, writer, context, template) {
  191. var buffer = '';
  192. // This function is used to render an artbitrary template
  193. // in the current context by higher-order functions.
  194. function subRender(template) {
  195. return writer.render(template, context);
  196. }
  197. var token, tokenValue, value;
  198. for (var i = 0, len = tokens.length; i < len; ++i) {
  199. token = tokens[i];
  200. tokenValue = token[1];
  201. switch (token[0]) {
  202. case '#':
  203. value = context.lookup(tokenValue);
  204. if (typeof value === 'object' || typeof value === 'string') {
  205. if (isArray(value)) {
  206. for (var j = 0, jlen = value.length; j < jlen; ++j) {
  207. buffer += renderTokens(token[4], writer, context.push(value[j]), template);
  208. }
  209. } else if (value) {
  210. buffer += renderTokens(token[4], writer, context.push(value), template);
  211. }
  212. } else if (isFunction(value)) {
  213. var text = template == null ? null : template.slice(token[3], token[5]);
  214. value = value.call(context.view, text, subRender);
  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 (isFunction(value)) 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 += mustache.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(escapeRegExp(tags[0]) + "\\s*"),
  302. new RegExp("\\s*" + escapeRegExp(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. function parseTemplate(template, tags) {
  312. template = template || '';
  313. tags = tags || mustache.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, openSection;
  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*' + escapeRegExp('}' + 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. openSection = sections.pop();
  383. if (!openSection) {
  384. throw new Error('Unopened section "' + value + '" at ' + start);
  385. }
  386. if (openSection[1] !== value) {
  387. throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
  388. }
  389. } else if (type === 'name' || type === '{' || type === '&') {
  390. nonSpace = true;
  391. } else if (type === '=') {
  392. // Set the tags for the next time around.
  393. tags = value.split(spaceRe);
  394. if (tags.length !== 2) {
  395. throw new Error('Invalid tags at ' + start + ': ' + tags.join(', '));
  396. }
  397. tagRes = escapeTags(tags);
  398. }
  399. }
  400. // Make sure there are no open sections when we're done.
  401. openSection = sections.pop();
  402. if (openSection) {
  403. throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
  404. }
  405. return nestTokens(squashTokens(tokens));
  406. }
  407. mustache.name = "mustache.js";
  408. mustache.version = "0.7.3";
  409. mustache.tags = ["{{", "}}"];
  410. mustache.Scanner = Scanner;
  411. mustache.Context = Context;
  412. mustache.Writer = Writer;
  413. mustache.parse = parseTemplate;
  414. // Export the escaping function so that the user may override it.
  415. // See https://github.com/janl/mustache.js/issues/244
  416. mustache.escape = escapeHtml;
  417. // All Mustache.* functions use this writer.
  418. var defaultWriter = new Writer();
  419. /**
  420. * Clears all cached templates and partials in the default writer.
  421. */
  422. mustache.clearCache = function () {
  423. return defaultWriter.clearCache();
  424. };
  425. /**
  426. * Compiles the given `template` to a reusable function using the default
  427. * writer.
  428. */
  429. mustache.compile = function (template, tags) {
  430. return defaultWriter.compile(template, tags);
  431. };
  432. /**
  433. * Compiles the partial with the given `name` and `template` to a reusable
  434. * function using the default writer.
  435. */
  436. mustache.compilePartial = function (name, template, tags) {
  437. return defaultWriter.compilePartial(name, template, tags);
  438. };
  439. /**
  440. * Compiles the given array of tokens (the output of a parse) to a reusable
  441. * function using the default writer.
  442. */
  443. mustache.compileTokens = function (tokens, template) {
  444. return defaultWriter.compileTokens(tokens, template);
  445. };
  446. /**
  447. * Renders the `template` with the given `view` and `partials` using the
  448. * default writer.
  449. */
  450. mustache.render = function (template, view, partials) {
  451. return defaultWriter.render(template, view, partials);
  452. };
  453. // This is here for backwards compatibility with 0.4.x.
  454. mustache.to_html = function (template, view, partials, send) {
  455. var result = mustache.render(template, view, partials);
  456. if (isFunction(send)) {
  457. send(result);
  458. } else {
  459. return result;
  460. }
  461. };
  462. }));