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.

321 lines
8.8KB

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