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
15KB

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