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.

598 lines
15KB

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