Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

614 строки
15KB

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