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.

630 lines
16KB

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