mustache.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. mustache.js — Logic-less templates in JavaScript
  3. See http://mustache.github.com/ for more info.
  4. */
  5. var Mustache = function() {
  6. var Renderer = function() {};
  7. Renderer.prototype = {
  8. otag: "{{",
  9. ctag: "}}",
  10. pragmas: {},
  11. buffer: [],
  12. pragmas_implemented: {
  13. "IMPLICIT-ITERATOR": true
  14. },
  15. context: {},
  16. render: function(template, context, partials, in_recursion) {
  17. // reset buffer & set context
  18. if(!in_recursion) {
  19. this.context = context;
  20. this.buffer = []; // TODO: make this non-lazy
  21. }
  22. // fail fast
  23. if(!this.includes("", template)) {
  24. if(in_recursion) {
  25. return template;
  26. } else {
  27. this.send(template);
  28. return;
  29. }
  30. }
  31. template = this.render_pragmas(template);
  32. var html = this.render_section(template, context, partials);
  33. if(in_recursion) {
  34. return this.render_tags(html, context, partials, in_recursion);
  35. }
  36. this.render_tags(html, context, partials, in_recursion);
  37. },
  38. /*
  39. Sends parsed lines
  40. */
  41. send: function(line) {
  42. if(line != "") {
  43. this.buffer.push(line);
  44. }
  45. },
  46. /*
  47. Looks for %PRAGMAS
  48. */
  49. render_pragmas: function(template) {
  50. // no pragmas
  51. if(!this.includes("%", template)) {
  52. return template;
  53. }
  54. var that = this;
  55. var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
  56. this.ctag);
  57. return template.replace(regex, function(match, pragma, options) {
  58. if(!that.pragmas_implemented[pragma]) {
  59. throw({message:
  60. "This implementation of mustache doesn't understand the '" +
  61. pragma + "' pragma"});
  62. }
  63. that.pragmas[pragma] = {};
  64. if(options) {
  65. var opts = options.split("=");
  66. that.pragmas[pragma][opts[0]] = opts[1];
  67. }
  68. return "";
  69. // ignore unknown pragmas silently
  70. });
  71. },
  72. /*
  73. Tries to find a partial in the curent scope and render it
  74. */
  75. render_partial: function(name, context, partials) {
  76. name = this.trim(name);
  77. if(!partials || partials[name] === undefined) {
  78. throw({message: "unknown_partial '" + name + "'"});
  79. }
  80. if(typeof(context[name]) != "object") {
  81. return this.render(partials[name], context, partials, true);
  82. }
  83. return this.render(partials[name], context[name], partials, true);
  84. },
  85. /*
  86. Renders inverted (^) and normal (#) sections
  87. */
  88. render_section: function(template, context, partials) {
  89. if(!this.includes("#", template) && !this.includes("^", template)) {
  90. return template;
  91. }
  92. var that = this;
  93. // CSW - Added "+?" so it finds the tighest bound, not the widest
  94. var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
  95. "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
  96. "\\s*", "mg");
  97. // for each {{#foo}}{{/foo}} section do...
  98. return template.replace(regex, function(match, type, name, content) {
  99. var value = that.find(name, context);
  100. if(type == "^") { // inverted section
  101. if(!value || that.is_array(value) && value.length === 0) {
  102. // false or empty list, render it
  103. return that.render(content, context, partials, true);
  104. } else {
  105. return "";
  106. }
  107. } else if(type == "#") { // normal section
  108. if(that.is_array(value)) { // Enumerable, Let's loop!
  109. return that.map(value, function(row) {
  110. return that.render(content, that.create_context(row),
  111. partials, true);
  112. }).join("");
  113. } else if(that.is_object(value)) { // Object, Use it as subcontext!
  114. return that.render(content, that.create_context(value),
  115. partials, true);
  116. } else if(typeof value === "function") {
  117. // higher order section
  118. return value.call(context, content, function(text) {
  119. return that.render(text, context, partials, true);
  120. });
  121. } else if(value) { // boolean section
  122. return that.render(content, context, partials, true);
  123. } else {
  124. return "";
  125. }
  126. }
  127. });
  128. },
  129. /*
  130. Replace {{foo}} and friends with values from our view
  131. */
  132. render_tags: function(template, context, partials, in_recursion) {
  133. // tit for tat
  134. var that = this;
  135. var new_regex = function() {
  136. return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
  137. that.ctag + "+", "g");
  138. };
  139. var regex = new_regex();
  140. var tag_replace_callback = function(match, operator, name) {
  141. switch(operator) {
  142. case "!": // ignore comments
  143. return "";
  144. case "=": // set new delimiters, rebuild the replace regexp
  145. that.set_delimiters(name);
  146. regex = new_regex();
  147. return "";
  148. case ">": // render partial
  149. return that.render_partial(name, context, partials);
  150. case "{": // the triple mustache is unescaped
  151. return that.find(name, context);
  152. default: // escape the value
  153. return that.escape(that.find(name, context));
  154. }
  155. };
  156. var lines = template.split("\n");
  157. for(var i = 0; i < lines.length; i++) {
  158. lines[i] = lines[i].replace(regex, tag_replace_callback, this);
  159. if(!in_recursion) {
  160. this.send(lines[i]);
  161. }
  162. }
  163. if(in_recursion) {
  164. return lines.join("\n");
  165. }
  166. },
  167. set_delimiters: function(delimiters) {
  168. var dels = delimiters.split(" ");
  169. this.otag = this.escape_regex(dels[0]);
  170. this.ctag = this.escape_regex(dels[1]);
  171. },
  172. escape_regex: function(text) {
  173. // thank you Simon Willison
  174. if(!arguments.callee.sRE) {
  175. var specials = [
  176. '/', '.', '*', '+', '?', '|',
  177. '(', ')', '[', ']', '{', '}', '\\'
  178. ];
  179. arguments.callee.sRE = new RegExp(
  180. '(\\' + specials.join('|\\') + ')', 'g'
  181. );
  182. }
  183. return text.replace(arguments.callee.sRE, '\\$1');
  184. },
  185. /*
  186. find `name` in current `context`. That is find me a value
  187. from the view object
  188. */
  189. find: function(name, context) {
  190. name = this.trim(name);
  191. // Checks whether a value is thruthy or false or 0
  192. function is_kinda_truthy(bool) {
  193. return bool === false || bool === 0 || bool;
  194. }
  195. var value;
  196. if(is_kinda_truthy(context[name])) {
  197. value = context[name];
  198. } else if(is_kinda_truthy(this.context[name])) {
  199. value = this.context[name];
  200. }
  201. if(typeof value === "function") {
  202. return value.apply(context);
  203. }
  204. if(value !== undefined) {
  205. return value;
  206. }
  207. // silently ignore unknown variables
  208. return "";
  209. },
  210. // Utility methods
  211. /* includes tag */
  212. includes: function(needle, haystack) {
  213. return haystack.indexOf(this.otag + needle) != -1;
  214. },
  215. /*
  216. Does away with nasty characters
  217. */
  218. escape: function(s) {
  219. s = String(s === null ? "" : s);
  220. return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
  221. switch(s) {
  222. case "&": return "&amp;";
  223. case "\\": return "\\\\";
  224. case '"': return '&quot;';
  225. case "'": return '&#39;';
  226. case "<": return "&lt;";
  227. case ">": return "&gt;";
  228. default: return s;
  229. }
  230. });
  231. },
  232. // by @langalex, support for arrays of strings
  233. create_context: function(_context) {
  234. if(this.is_object(_context)) {
  235. return _context;
  236. } else {
  237. var iterator = ".";
  238. if(this.pragmas["IMPLICIT-ITERATOR"]) {
  239. iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
  240. }
  241. var ctx = {};
  242. ctx[iterator] = _context;
  243. return ctx;
  244. }
  245. },
  246. is_object: function(a) {
  247. return a && typeof a == "object";
  248. },
  249. is_array: function(a) {
  250. return Object.prototype.toString.call(a) === '[object Array]';
  251. },
  252. /*
  253. Gets rid of leading and trailing whitespace
  254. */
  255. trim: function(s) {
  256. return s.replace(/^\s*|\s*$/g, "");
  257. },
  258. /*
  259. Why, why, why? Because IE. Cry, cry cry.
  260. */
  261. map: function(array, fn) {
  262. if (typeof array.map == "function") {
  263. return array.map(fn);
  264. } else {
  265. var r = [];
  266. var l = array.length;
  267. for(var i = 0; i < l; i++) {
  268. r.push(fn(array[i]));
  269. }
  270. return r;
  271. }
  272. }
  273. };
  274. return({
  275. name: "mustache.js",
  276. version: "0.3.1-dev",
  277. /*
  278. Turns a template and view into HTML
  279. */
  280. to_html: function(template, view, partials, send_fun) {
  281. var renderer = new Renderer();
  282. if(send_fun) {
  283. renderer.send = send_fun;
  284. }
  285. renderer.render(template, view, partials);
  286. if(!send_fun) {
  287. return renderer.buffer.join("\n");
  288. }
  289. }
  290. });
  291. }();