Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

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