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

388 строки
11KB

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