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.

687 lines
18KB

  1. /*
  2. mustache.js — Logic-less templates in JavaScript
  3. See http://mustache.github.com/ for more info.
  4. This implementation adds a template compiler for faster processing and fixes bugs.
  5. See http://www.saliences.com/projects/mustache/mustache.html for details.
  6. */
  7. var Mustache = (function(undefined) {
  8. var splitFunc = (function() {
  9. // fix up the stupidness that is IE's broken String.split implementation
  10. /* Cross-Browser Split 1.0.1
  11. (c) Steven Levithan <stevenlevithan.com>; MIT License
  12. An ECMA-compliant, uniform cross-browser split method
  13. */
  14. var compliantExecNpcg = /()??/.exec("")[1] === undefined; // NPCG: nonparticipating capturing group
  15. function capturingSplit(separator) {
  16. var str = this;
  17. var limit = undefined;
  18. // if `separator` is not a regex, use the native `split`
  19. if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
  20. return String.prototype.split.call(str, separator, limit);
  21. }
  22. var output = [],
  23. lastLastIndex = 0,
  24. flags = (separator.ignoreCase ? "i" : "") +
  25. (separator.multiline ? "m" : "") +
  26. (separator.sticky ? "y" : ""),
  27. separator = RegExp(separator.source, flags + "g"), // make `global` and avoid `lastIndex` issues by working with a copy
  28. separator2, match, lastIndex, lastLength;
  29. str = str + ""; // type conversion
  30. if (!compliantExecNpcg) {
  31. separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt
  32. }
  33. /* behavior for `limit`: if it's...
  34. - `undefined`: no limit.
  35. - `NaN` or zero: return an empty array.
  36. - a positive number: use `Math.floor(limit)`.
  37. - a negative number: no limit.
  38. - other: type-convert, then use the above rules. */
  39. if (limit === undefined || +limit < 0) {
  40. limit = Infinity;
  41. } else {
  42. limit = Math.floor(+limit);
  43. if (!limit) {
  44. return [];
  45. }
  46. }
  47. while (match = separator.exec(str)) {
  48. lastIndex = match.index + match[0].length; // `separator.lastIndex` is not reliable cross-browser
  49. if (lastIndex > lastLastIndex) {
  50. output.push(str.slice(lastLastIndex, match.index));
  51. // fix browsers whose `exec` methods don't consistently return `undefined` for nonparticipating capturing groups
  52. if (!compliantExecNpcg && match.length > 1) {
  53. match[0].replace(separator2, function () {
  54. for (var i = 1; i < arguments.length - 2; i++) {
  55. if (arguments[i] === undefined) {
  56. match[i] = undefined;
  57. }
  58. }
  59. });
  60. }
  61. if (match.length > 1 && match.index < str.length) {
  62. Array.prototype.push.apply(output, match.slice(1));
  63. }
  64. lastLength = match[0].length;
  65. lastLastIndex = lastIndex;
  66. if (output.length >= limit) {
  67. break;
  68. }
  69. }
  70. if (separator.lastIndex === match.index) {
  71. separator.lastIndex++; // avoid an infinite loop
  72. }
  73. }
  74. if (lastLastIndex === str.length) {
  75. if (lastLength || !separator.test("")) {
  76. output.push("");
  77. }
  78. } else {
  79. output.push(str.slice(lastLastIndex));
  80. }
  81. return output.length > limit ? output.slice(0, limit) : output;
  82. }
  83. if ('lol'.split(/(o)/).length !== 3) {
  84. return capturingSplit;
  85. } else {
  86. return String.prototype.split;
  87. }
  88. })();
  89. /* BEGIN Helpers */
  90. function noop() {}
  91. var escapeCompiledRegex;
  92. function escape_regex(text) {
  93. // thank you Simon Willison
  94. if (!escapeCompiledRegex) {
  95. var specials = [
  96. '/', '.', '*', '+', '?', '|',
  97. '(', ')', '[', ']', '{', '}', '\\'
  98. ];
  99. escapeCompiledRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
  100. }
  101. return text.replace(escapeCompiledRegex, '\\$1');
  102. }
  103. function is_newline(token) {
  104. return token.match(/\r?\n/);
  105. }
  106. function is_function(a) {
  107. return a && typeof a === 'function';
  108. }
  109. function is_object(a) {
  110. return a && typeof a === 'object';
  111. }
  112. function is_array(a) {
  113. return Object.prototype.toString.call(a) === '[object Array]';
  114. }
  115. var MustacheError = function(message, metrics) {
  116. var str = '';
  117. this.prototype = Error.prototype;
  118. this.name = 'MustacheError';
  119. if (metrics) {
  120. str = '(' + metrics.line + ',' + metrics.character + '): ';
  121. if (metrics.partial) {
  122. str = '[' + metrics.partial + ']' + str;
  123. }
  124. }
  125. this.message = str + message;
  126. if (metrics) {
  127. this.line = metrics.line;
  128. this.character = metrics.character;
  129. this.partial = metrics.partial;
  130. }
  131. };
  132. /* END Helpers */
  133. /* BEGIN Compiler */
  134. function compile(state, noReturn) {
  135. var n, c, token;
  136. for (n = state.tokens.length;state.cursor<n && !state.terminated;++state.cursor) {
  137. token = state.tokens[state.cursor];
  138. if (token==='' || token===undefined) {
  139. continue;
  140. }
  141. if (token.indexOf(state.openTag)===0) {
  142. c = token.charAt(state.openTag.length);
  143. if (state.parser[c]) {
  144. state.parser[c](state, token, c);
  145. } else {
  146. state.parser.def(state, token);
  147. }
  148. } else {
  149. state.parser.text(state, token);
  150. }
  151. if (is_newline(token)) {
  152. state.metrics.character = 1;
  153. state.metrics.line++;
  154. } else {
  155. state.metrics.character+=token.length;
  156. }
  157. }
  158. if (state.parser === scan_section_parser && !state.terminated) {
  159. throw new MustacheError('Closing section tag "' + state.section.variable + '" expected.', state.metrics);
  160. }
  161. if (!noReturn) {
  162. var codeList = state.code;
  163. if (codeList.length === 0) {
  164. return noop;
  165. } else if (codeList.length === 1) {
  166. return codeList[0];
  167. } else {
  168. return function(context, send_func) {
  169. for (var i=0,n=codeList.length;i<n;++i) {
  170. codeList[i](context, send_func);
  171. }
  172. }
  173. }
  174. }
  175. }
  176. var default_tokenizer = /(\r?\n)|({{![\s\S]*?!}})|({{[#\^\/&>]?\s*[^!{=]\S*?\s*}})|({{{\s*\S*?\s*}}})|({{=\S*?\s*\S*?=}})/;
  177. function create_compiler_state(template, partials, openTag, closeTag) {
  178. openTag = openTag || '{{';
  179. closeTag = closeTag || '}}';
  180. var tokenizer;
  181. if (openTag === '{{' && closeTag === '}}') {
  182. tokenizer = default_tokenizer;
  183. } else {
  184. var rOTag = escape_regex(openTag),
  185. rETag = escape_regex(closeTag);
  186. var parts = [
  187. '(\\r?\\n)' // new lines
  188. , '(' + rOTag + '![\\s\\S]*?!' + rETag + ')' // comments
  189. , '(' + rOTag + '[#\^\/&>]?\\s*[^!{=]\\S*?\\s*' + rETag + ')' // all other tags
  190. , '(' + rOTag + '{\\s*\\S*?\\s*}' + rETag + ')' // { unescape token
  191. , '(' + rOTag + '=\\S*?\\s*\\S*?=' + rETag + ')' // set delimiter change
  192. ];
  193. tokenizer = new RegExp(parts.join('|'));
  194. }
  195. var code = [], state = {
  196. metrics: {
  197. partial: null
  198. , line: 1
  199. , character: 1
  200. }
  201. , template: template || ''
  202. , partials: partials || {}
  203. , openTag: openTag
  204. , closeTag: closeTag
  205. , parser: default_parser
  206. , pragmas: {}
  207. , code: code
  208. , send_code_func: function(f) {
  209. code.push(f);
  210. }
  211. };
  212. pragmas(state); // use pragmas to control parsing behaviour
  213. // tokenize and initialize a cursor
  214. state.tokens = splitFunc.call(state.template, tokenizer);
  215. state.cursor = 0;
  216. return state;
  217. }
  218. var pragma_directives = {
  219. 'IMPLICIT-ITERATOR': function(state, options) {
  220. state.pragmas['IMPLICIT-ITERATOR'] = {iterator: ((options || {iterator:undefined}).iterator) || '.'};
  221. }
  222. };
  223. function pragmas(state) {
  224. /* includes tag */
  225. function includes(needle, haystack) {
  226. return haystack.indexOf('{{' + needle) !== -1;
  227. }
  228. // no pragmas, easy escape
  229. if(!includes("%", state.template)) {
  230. return state.template;
  231. }
  232. state.template = state.template.replace(/{{%([\w-]+)(\s*)(.*?(?=}}))}}/g, function(match, pragma, space, suffix) {
  233. var options = undefined,
  234. optionPairs, scratch,
  235. i, n;
  236. if (suffix.length>0) {
  237. optionPairs = suffix.split(',');
  238. options = {};
  239. for (i=0, n=optionPairs.length; i<n; ++i) {
  240. scratch = optionPairs[i].split('=');
  241. if (scratch.length !== 2) {
  242. throw new MustacheError('Malformed pragma option "' + optionPairs[i] + '".');
  243. }
  244. options[scratch[0]] = scratch[1];
  245. }
  246. }
  247. if (is_function(pragma_directives[pragma])) {
  248. pragma_directives[pragma](state, options);
  249. } else {
  250. throw new MustacheError('This implementation of mustache does not implement the "' + pragma + '" pragma.', undefined);
  251. }
  252. return ''; // blank out all pragmas
  253. });
  254. }
  255. /* END Compiler */
  256. /* BEGIN Run Time Helpers */
  257. /*
  258. find `name` in current `context`. That is find me a value
  259. from the view object
  260. */
  261. function find(name, context) {
  262. // Checks whether a value is truthy or false or 0
  263. function is_kinda_truthy(bool) {
  264. return bool === false || bool === 0 || bool;
  265. }
  266. var value;
  267. if (is_kinda_truthy(context[name])) {
  268. value = context[name];
  269. }
  270. if (is_function(value)) {
  271. return value.apply(context);
  272. }
  273. return value;
  274. }
  275. function find_in_stack(name, context_stack) {
  276. var value;
  277. value = find(name, context_stack[context_stack.length-1]);
  278. if (value!==undefined) { return value; }
  279. if (context_stack.length>1) {
  280. value = find(name, context_stack[0]);
  281. if (value!==undefined) { return value; }
  282. }
  283. return undefined;
  284. }
  285. function find_with_dot_notation(name, context) {
  286. var name_components = name.split('.'),
  287. i = 1, n = name_components.length,
  288. value = find_in_stack(name_components[0], context);
  289. while (value && i<n) {
  290. value = find(name_components[i], value);
  291. i++;
  292. }
  293. if (i!==n && !value) {
  294. value = undefined;
  295. }
  296. return value;
  297. }
  298. /* END Run Time Helpers */
  299. function text(state, token) {
  300. state.send_code_func(function(context, send_func) { send_func(token); });
  301. }
  302. function interpolate(state, token, mark) {
  303. function escapeHTML(str) {
  304. return str.replace(/&/g,'&amp;')
  305. .replace(/</g,'&lt;')
  306. .replace(/>/g,'&gt;');
  307. }
  308. var escape, prefix, postfix;
  309. if (mark==='{') {
  310. escape = prefix = postfix = true;
  311. } else if (mark==='&') {
  312. escape = prefix = true;
  313. }
  314. var variable = get_variable_name(state, token, prefix, postfix),
  315. implicit_iterator = (state.pragmas['IMPLICIT-ITERATOR'] || {iterator: '.'}).iterator;
  316. state.send_code_func((function(variable, escape) { return function(context, send_func) {
  317. var value;
  318. if ( variable === implicit_iterator ) { // special case for implicit iterator (usually '.')
  319. value = {}; value[implicit_iterator] = context[context.length-1];
  320. value = find(variable, value);
  321. } else {
  322. value = find_with_dot_notation(variable, context);
  323. }
  324. if (value!==undefined) {
  325. if (!escape) {
  326. value = escapeHTML('' + value);
  327. }
  328. send_func('' + value);
  329. }
  330. };})(variable, escape));
  331. }
  332. function partial(state, token) {
  333. var variable = get_variable_name(state, token, true),
  334. template, program;
  335. if (!state.partials[variable]) {
  336. throw new MustacheError('Unknown partial "' + variable + '".', state.metrics);
  337. }
  338. if (!is_function(state.partials[variable])) {
  339. // if the partial has not been compiled yet, do so now
  340. template = state.partials[variable]; // remember what the partial was
  341. state.partials[variable] = noop; // avoid infinite recursion
  342. var new_state = create_compiler_state(
  343. template
  344. , state.partials
  345. );
  346. new_state.metrics.partial = variable;
  347. // TODO: Determine if partials should inherit pragma state from parent
  348. program = compile(new_state);
  349. state.partials[variable] = function(context, send_func) {
  350. var value = find_in_stack(variable, context);
  351. if (value) {
  352. // TODO: According to mustache-spec, partials do not act as implicit sections
  353. // this behaviour was carried over from janl's mustache and should either
  354. // be discarded or replaced with a pragma
  355. context.push(value);
  356. }
  357. program(context, send_func);
  358. if (value) {
  359. // TODO: See above
  360. context.pop();
  361. }
  362. };
  363. }
  364. state.send_code_func(function(context, send_func) { state.partials[variable](context, send_func); });
  365. }
  366. function section(state) {
  367. var s = state.section, template = s.template_buffer.join(''),
  368. program,
  369. new_state = create_compiler_state(template, state.partials, state.openTag, state.closeTag);
  370. new_state.metrics = s.metrics;
  371. new_state.pragmas = state.pragmas;
  372. program = compile(new_state);
  373. if (s.inverted) {
  374. state.send_code_func((function(program, variable){ return function(context, send_func) {
  375. var value = find_with_dot_notation(variable, context);
  376. if (!value || is_array(value) && value.length === 0) { // false or empty list, render it
  377. program(context, send_func);
  378. }
  379. };})(program, s.variable));
  380. } else {
  381. state.send_code_func((function(program, variable, template, partials){ return function(context, send_func) {
  382. var value = find_with_dot_notation(variable, context);
  383. if (is_array(value)) { // Enumerable, Let's loop!
  384. for (var i=0, n=value.length; i<n; ++i) {
  385. context.push(value[i]);
  386. program(context, send_func);
  387. context.pop();
  388. }
  389. } else if (is_object(value)) { // Object, Use it as subcontext!
  390. context.push(value);
  391. program(context, send_func);
  392. context.pop();
  393. } else if (is_function(value)) { // higher order section
  394. // note that HOS triggers a compilation on the hosFragment.
  395. // this is slow (in relation to a fully compiled template)
  396. // since it invokes a call to the parser
  397. send_func(value.call(context[context.length-1], template, function(hosFragment) {
  398. var o = [],
  399. user_send_func = function(str) { o.push(str); };
  400. var new_state = create_compiler_state(hosFragment, partials);
  401. new_state.metrics.partial = 'HOS@@anon';
  402. compile(new_state)(context, user_send_func);
  403. return o.join('');
  404. }));
  405. } else if (value) { // truthy
  406. program(context, send_func);
  407. }
  408. };})(program, s.variable, template, state.partials));
  409. }
  410. }
  411. /* BEGIN Parser */
  412. var default_parser = {
  413. '!': noop,
  414. '#': begin_section,
  415. '^': begin_section,
  416. '/': function(state, token) { throw new MustacheError('Unbalanced End Section tag "' + token + '".', state.metrics); },
  417. '&': interpolate,
  418. '{': interpolate,
  419. '>': partial,
  420. '=': change_delimiter,
  421. def: interpolate,
  422. text: text
  423. };
  424. var scan_section_parser = {
  425. '!': noop,
  426. '#': begin_section,
  427. '^': begin_section,
  428. '/': end_section,
  429. '&': buffer_section,
  430. '{': buffer_section,
  431. '>': buffer_section,
  432. '=': change_delimiter,
  433. def: buffer_section,
  434. text: buffer_section
  435. };
  436. function get_variable_name(state, token, prefix, postfix) {
  437. var fragment = token.substring(
  438. state.openTag.length + (prefix ? 1 : 0)
  439. , token.length - state.closeTag.length - (postfix ? 1 : 0)
  440. );
  441. if (String.prototype.trim) {
  442. fragment = fragment.trim();
  443. } else {
  444. fragment = fragment.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
  445. }
  446. if (fragment.indexOf(' ')!==-1) {
  447. throw new MustacheError('Malformed variable name "' + fragment + '".', state.metrics);
  448. }
  449. return fragment;
  450. }
  451. function change_delimiter(state, token) {
  452. var matches = token.match(new RegExp(escape_regex(state.openTag) + '=(\\S*?)\\s*(\\S*?)=' + escape_regex(state.closeTag)));
  453. if ((matches || []).length!==3) {
  454. throw new MustacheError('Malformed change delimiter token "' + token + '".', state.metrics);
  455. }
  456. var new_state = create_compiler_state(
  457. state.tokens.slice(state.cursor+1).join('')
  458. , state.partials
  459. , matches[1]
  460. , matches[2]);
  461. new_state.code = state.code;
  462. new_state.send_code_func = state.send_code_func;
  463. new_state.parser = state.parser;
  464. new_state.metrics.line = state.metrics.line;
  465. new_state.metrics.character = state.metrics.character + token.length;
  466. new_state.metrics.partial = state.metrics.partial;
  467. new_state.section = state.section;
  468. new_state.pragmas = state.pragmas;
  469. if (new_state.section) {
  470. new_state.section.template_buffer.push(token);
  471. }
  472. state.terminated = true; // finish off this level
  473. compile(new_state, true);
  474. }
  475. function begin_section(state, token, mark) {
  476. var inverted = mark === '^',
  477. variable = get_variable_name(state, token, true);
  478. if (state.parser===default_parser) {
  479. state.parser = scan_section_parser;
  480. state.section = {
  481. variable: variable
  482. , template_buffer: []
  483. , inverted: inverted
  484. , child_sections: []
  485. , metrics: {
  486. partial: state.metrics.partial
  487. , line: state.metrics.line
  488. , character: state.metrics.character + token.length
  489. }
  490. };
  491. } else {
  492. state.section.child_sections.push(variable);
  493. state.section.template_buffer.push(token);
  494. }
  495. }
  496. function buffer_section(state, token) {
  497. state.section.template_buffer.push(token);
  498. }
  499. function end_section(state, token) {
  500. var variable = get_variable_name(state, token, true);
  501. if (state.section.child_sections.length > 0) {
  502. var child_section = state.section.child_sections[state.section.child_sections.length-1];
  503. if (child_section === variable) {
  504. state.section.child_sections.pop();
  505. state.section.template_buffer.push(token);
  506. } else {
  507. throw new MustacheError('Unexpected section end tag "' + variable + '", expected "' + child_section + '".', state.metrics);
  508. }
  509. } else if (state.section.variable===variable) {
  510. section(state);
  511. delete state.section;
  512. state.parser = default_parser;
  513. } else {
  514. throw new MustacheError('Unexpected section end tag "' + variable + '", expected "' + state.section.variable + '".', state.metrics);
  515. }
  516. }
  517. /* END Parser */
  518. return({
  519. name: "mustache.js",
  520. version: "0.5.1-vcs",
  521. /*
  522. Turns a template into a JS function
  523. */
  524. compile: function(template, partials) {
  525. var p = {};
  526. if (partials) {
  527. for (var key in partials) {
  528. if (partials.hasOwnProperty(key)) {
  529. p[key] = partials[key];
  530. }
  531. }
  532. }
  533. var program = compile(create_compiler_state(template, p));
  534. return function(view, send_func) {
  535. var o = [],
  536. user_send_func = send_func || function(str) {
  537. o.push(str);
  538. };
  539. program([view || {}], user_send_func);
  540. if (!send_func) {
  541. return o.join('');
  542. }
  543. }
  544. },
  545. /*
  546. Turns a template and view into HTML
  547. */
  548. to_html: function(template, view, partials, send_func) {
  549. var program = Mustache.compile(template, partials),
  550. result = program(view, send_func);
  551. if (!send_func) {
  552. return result;
  553. }
  554. },
  555. format: function(template/*, args */) {
  556. var program = Mustache.compile(template),
  557. args = Array.prototype.slice.call(arguments),
  558. view = {};
  559. args.shift();
  560. for (var i = 0, n = args.length; i < n; ++i) {
  561. view['' + i] = args[i];
  562. }
  563. return program(view);
  564. },
  565. Error: MustacheError
  566. });
  567. })();