not really known
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.

610 lines
15 KiB

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