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.

581 lines
14KB

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