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.

290 lines
9.2KB

  1. /*
  2. mustache.js — Logic-less templates in JavaScript
  3. See http://mustache.github.com/ for more info.
  4. Rewrite as parser by Nathan Vander Wilt, 2010 May 22
  5. */
  6. var Mustache = function() {
  7. // `sender` is a function to buffer or stream parsed chunks
  8. var Renderer = function(sender) {
  9. this.send = sender;
  10. };
  11. Renderer.prototype = {
  12. otag: "{{",
  13. ctag: "}}",
  14. pragmas_implemented: {
  15. "IMPLICIT-ITERATOR": true
  16. },
  17. // state created per-instance to avoid sharing
  18. pragmas: null,
  19. // the main parser (return value for internal use only)
  20. render: function(template, context, partials) {
  21. //console.log("Context", context);
  22. this.pragmas = {};
  23. var tokens = this.splitTemplate(template);
  24. //console.log("Tokens", tokens);
  25. var tree = this.formTree(tokens);
  26. //console.log("Tree", tree);
  27. this.renderTree(tree, context, partials, template);
  28. },
  29. // returns {tag, start, end} or nothing
  30. findToken: function(template, startPos) {
  31. var tokenStart = template.indexOf(this.otag, startPos);
  32. if (tokenStart == -1) return;
  33. var tokenEnd = template.indexOf(this.ctag, tokenStart + this.otag.length);
  34. if (tokenEnd == -1) {
  35. var context = template.substr(tokenStart, 15);
  36. throw new Error("Unclosed token '" + context + "...'.");
  37. }
  38. var tokenInnards = template.slice(tokenStart + this.otag.length, tokenEnd);
  39. var tokenParts = tokenInnards.match(/([=%!{&>#^\/])?\s*(.+?)\s*\1?$/);
  40. var token = {
  41. "operator": tokenParts[1],
  42. "tag": tokenParts[2],
  43. "text": this.otag + tokenInnards + this.ctag,
  44. "start": tokenStart,
  45. "end": tokenEnd + this.ctag.length
  46. };
  47. if (token.operator == "{" && template[token.end] == "}") {
  48. // adjust for symmetrical unescaped tag with default delimiters
  49. token.end += 1;
  50. }
  51. return token;
  52. },
  53. splitTemplate: function(template) {
  54. var tokens = [];
  55. var token;
  56. var parsePosition = 0;
  57. while (token = this.findToken(template, parsePosition)) {
  58. var text = template.slice(parsePosition, token.start);
  59. tokens.push({"text": text, "start": parsePosition, "end": token.start});
  60. tokens.push(token);
  61. parsePosition = token.end;
  62. if (token.operator == "=") {
  63. // set new delimiters
  64. var delimiters = token.tag.split(" ");
  65. this.otag = delimiters[0];
  66. this.ctag = delimiters[1];
  67. } else if (token.operator == "%") {
  68. // store pragma
  69. var pragmaInfo = token.tag.match(/([\w_-]+) ?([\w]+=[\w]+)?/);
  70. var pragma = pragmaInfo[1];
  71. if (!this.pragmas_implemented[pragma]) {
  72. throw new Error("This implementation of mustache doesn't understand the '" +
  73. pragma + "' pragma");
  74. }
  75. var options = {}
  76. var optionStr = pragmaInfo[2];
  77. if (optionStr) {
  78. var opts = optionStr.split("=");
  79. options[opts[0]] = opts[1];
  80. }
  81. this.pragmas[pragma] = options;
  82. }
  83. }
  84. var finalText = template.slice(parsePosition, template.length);
  85. tokens.push({"text": finalText, "start": parsePosition, "end": template.length});
  86. return tokens;
  87. },
  88. // NOTE: empties tokens parameter and modifies its former subobjects
  89. formTree: function(tokens, section) {
  90. var tree = [];
  91. var token;
  92. while (token = tokens.shift()) {
  93. if (token.start == token.end) {
  94. // drop empty tokens
  95. continue;
  96. } else if (token.tag) {
  97. if (token.operator == "#" || token.operator == "^") {
  98. token.section = true;
  99. token.invert = (token.operator == "^");
  100. token.tree = this.formTree(tokens, token.tag);
  101. } else if (token.operator == "/") {
  102. if (token.tag != section) {
  103. throw new Error("Badly nested section '" + section + "'" +
  104. " (left via '" + token.tag +"').");
  105. }
  106. break;
  107. } else if (token.operator == ">") {
  108. token.partial = true;
  109. } else if (token.operator == "{" || token.operator == "&") {
  110. token.noEscape = true;
  111. }
  112. }
  113. tree.push(token);
  114. }
  115. return tree;
  116. },
  117. renderTree: function(tree, context, partials, template) {
  118. for (var i = 0, len = tree.length; i < len; ++i) {
  119. var item = tree[i];
  120. if (item.section) {
  121. var iterator = this.valueIterator(item.tag, context);
  122. var value;
  123. if (item.invert) {
  124. value = iterator();
  125. if (!value) {
  126. this.renderTree(item.tree, context, partials, template);
  127. }
  128. } else while (value = iterator()) {
  129. if (value instanceof Function) {
  130. var subtree = item.tree;
  131. var lastSubitem = subtree[subtree.length-1];
  132. var subtext = template.slice(item.end, lastSubitem && lastSubitem.end);
  133. var renderer = function(text) {
  134. return Mustache.to_html(text, context, partials);
  135. }
  136. var lambdaResult = value.call(context, subtext, renderer);
  137. if (lambdaResult) {
  138. this.send(lambdaResult);
  139. }
  140. } else {
  141. var subContext = this.mergedCopy(context, value);
  142. this.renderTree(item.tree, subContext, partials, template);
  143. }
  144. }
  145. } else if (item.partial) {
  146. var subTemplate = partials[item.tag];
  147. if (!subTemplate) {
  148. throw new Error("Unknown partial '" + item.tag + "'");
  149. }
  150. // this is the mustache(5) way
  151. // this.render(subTemplate, context, partials);
  152. // this is @janl's way
  153. var subContext = context[item.tag];
  154. if (typeof(subContext) == "object") {
  155. this.render(subTemplate, subContext, partials);
  156. } else {
  157. this.send(subTemplate);
  158. }
  159. } else if (item.operator && !item.noEscape) {
  160. // ignore other operators
  161. } else if (item.tag) {
  162. var rawValue = this.lookupValue(item.tag, context);
  163. if (rawValue) {
  164. var value = rawValue.toString();
  165. this.send((item.noEscape) ? value : this.escapeHTML(value));
  166. }
  167. } else {
  168. this.send(item.text);
  169. }
  170. }
  171. },
  172. // find `name` value in current view `context`
  173. lookupValue: function(name, context) {
  174. var value = context[name];
  175. // evaluate plain-function value (only once)
  176. if (value instanceof Function && !value.iterator) {
  177. value = value.apply(context);
  178. }
  179. // silently ignore unkown variables
  180. if (!value) {
  181. value = "";
  182. }
  183. return value;
  184. },
  185. objectValue: function(value, context) {
  186. if (value instanceof Function) {
  187. return value;
  188. }
  189. var obj = (value != null) ? {} : null;
  190. if (Object.prototype.toString.call(value) == '[object Object]') {
  191. obj = value;
  192. } else if(this.pragmas["IMPLICIT-ITERATOR"]) {
  193. // original credit to @langalex, support for arrays of strings
  194. var iteratorKey = this.pragmas["IMPLICIT-ITERATOR"].iterator || ".";
  195. obj[iteratorKey] = value;
  196. }
  197. return obj;
  198. },
  199. // always returns iterator function returning object/null
  200. valueIterator: function(name, context) {
  201. var value = this.lookupValue(name, context);
  202. var me = this;
  203. if (!value) {
  204. return function(){};
  205. } else if (value instanceof Function && value.iterator) {
  206. return value;
  207. } else if (value instanceof Array) {
  208. var i = 0;
  209. var l = value.length;
  210. return function() {
  211. return (i < l) ? me.objectValue(value[i++], context) : null;
  212. }
  213. } else {
  214. return function() {
  215. var v = value;
  216. value = null;
  217. return me.objectValue(v, context);
  218. };
  219. }
  220. },
  221. // copies contents of `b` over copy of `a`
  222. mergedCopy: function(a, b) {
  223. var copy = {};
  224. for (var key in a) if (a.hasOwnProperty(key)) {
  225. copy[key] = a[key];
  226. }
  227. for (var key in b) if (b.hasOwnProperty(key)) {
  228. copy[key] = b[key];
  229. }
  230. return copy;
  231. },
  232. // converts special HTML characters
  233. escapeHTML: function(s) {
  234. var htmlCharsRE = new RegExp("&(?!\\w+;)|[\"<>\\\\]", "g");
  235. return s.replace(htmlCharsRE, function(c) {
  236. switch(c) {
  237. case "&": return "&amp;";
  238. case "\\": return "\\\\";
  239. case '"': return '\"';
  240. case "<": return "&lt;";
  241. case ">": return "&gt;";
  242. default: return c;
  243. }
  244. });
  245. }
  246. };
  247. return({
  248. name: "mustache.js",
  249. version: "0.4.0-dev",
  250. // wrap internal render function
  251. to_html: function(template, view, partials, sender) {
  252. var buffer = [];
  253. var renderSender = sender || function(chunk) {
  254. if (chunk.length) {
  255. buffer.push(chunk);
  256. }
  257. }
  258. var renderer = new Renderer(renderSender);
  259. renderer.render(template, view, partials);
  260. if (!sender) {
  261. return buffer.join("");
  262. }
  263. }
  264. });
  265. }();