Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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