15 pragmas_implemented: { |
60 pragmas_implemented: { |
16 "IMPLICIT-ITERATOR": true |
61 "IMPLICIT-ITERATOR": true |
17 }, |
62 }, |
18 context: {}, |
63 context: {}, |
19 |
64 |
20 render: function(template, context, partials, in_recursion) { |
65 render: function (template, context, partials, in_recursion) { |
21 // reset buffer & set context |
66 // reset buffer & set context |
22 if(!in_recursion) { |
67 if (!in_recursion) { |
23 this.context = context; |
68 this.context = context; |
24 this.buffer = []; // TODO: make this non-lazy |
69 this.buffer = []; // TODO: make this non-lazy |
25 } |
70 } |
26 |
71 |
27 // fail fast |
72 // fail fast |
28 if(!this.includes("", template)) { |
73 if (!this.includes("", template)) { |
29 if(in_recursion) { |
74 if (in_recursion) { |
30 return template; |
75 return template; |
31 } else { |
76 } else { |
32 this.send(template); |
77 this.send(template); |
33 return; |
78 return; |
34 } |
79 } |
35 } |
80 } |
36 |
81 |
|
82 // get the pragmas together |
37 template = this.render_pragmas(template); |
83 template = this.render_pragmas(template); |
|
84 |
|
85 // render the template |
38 var html = this.render_section(template, context, partials); |
86 var html = this.render_section(template, context, partials); |
39 if(in_recursion) { |
87 |
40 return this.render_tags(html, context, partials, in_recursion); |
88 // render_section did not find any sections, we still need to render the tags |
41 } |
89 if (html === false) { |
42 |
90 html = this.render_tags(template, context, partials, in_recursion); |
43 this.render_tags(html, context, partials, in_recursion); |
91 } |
|
92 |
|
93 if (in_recursion) { |
|
94 return html; |
|
95 } else { |
|
96 this.sendLines(html); |
|
97 } |
44 }, |
98 }, |
45 |
99 |
46 /* |
100 /* |
47 Sends parsed lines |
101 Sends parsed lines |
48 */ |
102 */ |
49 send: function(line) { |
103 send: function (line) { |
50 if(line !== "") { |
104 if (line !== "") { |
51 this.buffer.push(line); |
105 this.buffer.push(line); |
52 } |
106 } |
53 }, |
107 }, |
54 |
108 |
|
109 sendLines: function (text) { |
|
110 if (text) { |
|
111 var lines = text.split("\n"); |
|
112 for (var i = 0; i < lines.length; i++) { |
|
113 this.send(lines[i]); |
|
114 } |
|
115 } |
|
116 }, |
|
117 |
55 /* |
118 /* |
56 Looks for %PRAGMAS |
119 Looks for %PRAGMAS |
57 */ |
120 */ |
58 render_pragmas: function(template) { |
121 render_pragmas: function (template) { |
59 // no pragmas |
122 // no pragmas |
60 if(!this.includes("%", template)) { |
123 if (!this.includes("%", template)) { |
61 return template; |
124 return template; |
62 } |
125 } |
63 |
126 |
64 var that = this; |
127 var that = this; |
65 var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + |
128 var regex = this.getCachedRegex("render_pragmas", function (otag, ctag) { |
66 this.ctag, "g"); |
129 return new RegExp(otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" + ctag, "g"); |
67 return template.replace(regex, function(match, pragma, options) { |
130 }); |
68 if(!that.pragmas_implemented[pragma]) { |
131 |
69 throw({message: |
132 return template.replace(regex, function (match, pragma, options) { |
|
133 if (!that.pragmas_implemented[pragma]) { |
|
134 throw({message: |
70 "This implementation of mustache doesn't understand the '" + |
135 "This implementation of mustache doesn't understand the '" + |
71 pragma + "' pragma"}); |
136 pragma + "' pragma"}); |
72 } |
137 } |
73 that.pragmas[pragma] = {}; |
138 that.pragmas[pragma] = {}; |
74 if(options) { |
139 if (options) { |
75 var opts = options.split("="); |
140 var opts = options.split("="); |
76 that.pragmas[pragma][opts[0]] = opts[1]; |
141 that.pragmas[pragma][opts[0]] = opts[1]; |
77 } |
142 } |
78 return ""; |
143 return ""; |
79 // ignore unknown pragmas silently |
144 // ignore unknown pragmas silently |
81 }, |
146 }, |
82 |
147 |
83 /* |
148 /* |
84 Tries to find a partial in the curent scope and render it |
149 Tries to find a partial in the curent scope and render it |
85 */ |
150 */ |
86 render_partial: function(name, context, partials) { |
151 render_partial: function (name, context, partials) { |
87 name = this.trim(name); |
152 name = trim(name); |
88 if(!partials || partials[name] === undefined) { |
153 if (!partials || partials[name] === undefined) { |
89 throw({message: "unknown_partial '" + name + "'"}); |
154 throw({message: "unknown_partial '" + name + "'"}); |
90 } |
155 } |
91 if(typeof(context[name]) != "object") { |
156 if (!context || typeof context[name] != "object") { |
92 return this.render(partials[name], context, partials, true); |
157 return this.render(partials[name], context, partials, true); |
93 } |
158 } |
94 return this.render(partials[name], context[name], partials, true); |
159 return this.render(partials[name], context[name], partials, true); |
95 }, |
160 }, |
96 |
161 |
97 /* |
162 /* |
98 Renders inverted (^) and normal (#) sections |
163 Renders inverted (^) and normal (#) sections |
99 */ |
164 */ |
100 render_section: function(template, context, partials) { |
165 render_section: function (template, context, partials) { |
101 if(!this.includes("#", template) && !this.includes("^", template)) { |
166 if (!this.includes("#", template) && !this.includes("^", template)) { |
102 return template; |
167 // did not render anything, there were no sections |
|
168 return false; |
103 } |
169 } |
104 |
170 |
105 var that = this; |
171 var that = this; |
106 // CSW - Added "+?" so it finds the tighest bound, not the widest |
172 |
107 var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag + |
173 var regex = this.getCachedRegex("render_section", function (otag, ctag) { |
108 "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag + |
174 // This regex matches _the first_ section ({{#foo}}{{/foo}}), and captures the remainder |
109 "\\s*", "mg"); |
175 return new RegExp( |
|
176 "^([\\s\\S]*?)" + // all the crap at the beginning that is not {{*}} ($1) |
|
177 |
|
178 otag + // {{ |
|
179 "(\\^|\\#)\\s*(.+)\\s*" + // #foo (# == $2, foo == $3) |
|
180 ctag + // }} |
|
181 |
|
182 "\n*([\\s\\S]*?)" + // between the tag ($2). leading newlines are dropped |
|
183 |
|
184 otag + // {{ |
|
185 "\\/\\s*\\3\\s*" + // /foo (backreference to the opening tag). |
|
186 ctag + // }} |
|
187 |
|
188 "\\s*([\\s\\S]*)$", // everything else in the string ($4). leading whitespace is dropped. |
|
189 |
|
190 "g"); |
|
191 }); |
|
192 |
110 |
193 |
111 // for each {{#foo}}{{/foo}} section do... |
194 // for each {{#foo}}{{/foo}} section do... |
112 return template.replace(regex, function(match, type, name, content) { |
195 return template.replace(regex, function (match, before, type, name, content, after) { |
113 var value = that.find(name, context); |
196 // before contains only tags, no sections |
114 if(type == "^") { // inverted section |
197 var renderedBefore = before ? that.render_tags(before, context, partials, true) : "", |
115 if(!value || that.is_array(value) && value.length === 0) { |
198 |
|
199 // after may contain both sections and tags, so use full rendering function |
|
200 renderedAfter = after ? that.render(after, context, partials, true) : "", |
|
201 |
|
202 // will be computed below |
|
203 renderedContent, |
|
204 |
|
205 value = that.find(name, context); |
|
206 |
|
207 if (type === "^") { // inverted section |
|
208 if (!value || Array.isArray(value) && value.length === 0) { |
116 // false or empty list, render it |
209 // false or empty list, render it |
117 return that.render(content, context, partials, true); |
210 renderedContent = that.render(content, context, partials, true); |
118 } else { |
211 } else { |
119 return ""; |
212 renderedContent = ""; |
120 } |
213 } |
121 } else if(type == "#") { // normal section |
214 } else if (type === "#") { // normal section |
122 if(that.is_array(value)) { // Enumerable, Let's loop! |
215 if (Array.isArray(value)) { // Enumerable, Let's loop! |
123 return that.map(value, function(row) { |
216 renderedContent = that.map(value, function (row) { |
124 return that.render(content, that.create_context(row), |
217 return that.render(content, that.create_context(row), partials, true); |
125 partials, true); |
|
126 }).join(""); |
218 }).join(""); |
127 } else if(that.is_object(value)) { // Object, Use it as subcontext! |
219 } else if (that.is_object(value)) { // Object, Use it as subcontext! |
128 return that.render(content, that.create_context(value), |
220 renderedContent = that.render(content, that.create_context(value), |
129 partials, true); |
221 partials, true); |
130 } else if(typeof value === "function") { |
222 } else if (typeof value == "function") { |
131 // higher order section |
223 // higher order section |
132 return value.call(context, content, function(text) { |
224 renderedContent = value.call(context, content, function (text) { |
133 return that.render(text, context, partials, true); |
225 return that.render(text, context, partials, true); |
134 }); |
226 }); |
135 } else if(value) { // boolean section |
227 } else if (value) { // boolean section |
136 return that.render(content, context, partials, true); |
228 renderedContent = that.render(content, context, partials, true); |
137 } else { |
229 } else { |
138 return ""; |
230 renderedContent = ""; |
139 } |
231 } |
140 } |
232 } |
|
233 |
|
234 return renderedBefore + renderedContent + renderedAfter; |
141 }); |
235 }); |
142 }, |
236 }, |
143 |
237 |
144 /* |
238 /* |
145 Replace {{foo}} and friends with values from our view |
239 Replace {{foo}} and friends with values from our view |
146 */ |
240 */ |
147 render_tags: function(template, context, partials, in_recursion) { |
241 render_tags: function (template, context, partials, in_recursion) { |
148 // tit for tat |
242 // tit for tat |
149 var that = this; |
243 var that = this; |
150 |
244 |
151 var new_regex = function() { |
245 var new_regex = function () { |
152 return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" + |
246 return that.getCachedRegex("render_tags", function (otag, ctag) { |
153 that.ctag + "+", "g"); |
247 return new RegExp(otag + "(=|!|>|&|\\{|%)?([^#\\^]+?)\\1?" + ctag + "+", "g"); |
|
248 }); |
154 }; |
249 }; |
155 |
250 |
156 var regex = new_regex(); |
251 var regex = new_regex(); |
157 var tag_replace_callback = function(match, operator, name) { |
252 var tag_replace_callback = function (match, operator, name) { |
158 switch(operator) { |
253 switch(operator) { |
159 case "!": // ignore comments |
254 case "!": // ignore comments |
160 return ""; |
255 return ""; |
161 case "=": // set new delimiters, rebuild the replace regexp |
256 case "=": // set new delimiters, rebuild the replace regexp |
162 that.set_delimiters(name); |
257 that.set_delimiters(name); |
163 regex = new_regex(); |
258 regex = new_regex(); |
164 return ""; |
259 return ""; |
165 case ">": // render partial |
260 case ">": // render partial |
166 return that.render_partial(name, context, partials); |
261 return that.render_partial(name, context, partials); |
167 case "{": // the triple mustache is unescaped |
262 case "{": // the triple mustache is unescaped |
|
263 case "&": // & operator is an alternative unescape method |
168 return that.find(name, context); |
264 return that.find(name, context); |
169 default: // escape the value |
265 default: // escape the value |
170 return that.escape(that.find(name, context)); |
266 return escapeHTML(that.find(name, context)); |
171 } |
267 } |
172 }; |
268 }; |
173 var lines = template.split("\n"); |
269 var lines = template.split("\n"); |
174 for(var i = 0; i < lines.length; i++) { |
270 for(var i = 0; i < lines.length; i++) { |
175 lines[i] = lines[i].replace(regex, tag_replace_callback, this); |
271 lines[i] = lines[i].replace(regex, tag_replace_callback, this); |
176 if(!in_recursion) { |
272 if (!in_recursion) { |
177 this.send(lines[i]); |
273 this.send(lines[i]); |
178 } |
274 } |
179 } |
275 } |
180 |
276 |
181 if(in_recursion) { |
277 if (in_recursion) { |
182 return lines.join("\n"); |
278 return lines.join("\n"); |
183 } |
279 } |
184 }, |
280 }, |
185 |
281 |
186 set_delimiters: function(delimiters) { |
282 set_delimiters: function (delimiters) { |
187 var dels = delimiters.split(" "); |
283 var dels = delimiters.split(" "); |
188 this.otag = this.escape_regex(dels[0]); |
284 this.otag = this.escape_regex(dels[0]); |
189 this.ctag = this.escape_regex(dels[1]); |
285 this.ctag = this.escape_regex(dels[1]); |
190 }, |
286 }, |
191 |
287 |
192 escape_regex: function(text) { |
288 escape_regex: function (text) { |
193 // thank you Simon Willison |
289 // thank you Simon Willison |
194 if(!arguments.callee.sRE) { |
290 if (!arguments.callee.sRE) { |
195 var specials = [ |
291 var specials = [ |
196 '/', '.', '*', '+', '?', '|', |
292 '/', '.', '*', '+', '?', '|', |
197 '(', ')', '[', ']', '{', '}', '\\' |
293 '(', ')', '[', ']', '{', '}', '\\' |
198 ]; |
294 ]; |
199 arguments.callee.sRE = new RegExp( |
295 arguments.callee.sRE = new RegExp( |
205 |
301 |
206 /* |
302 /* |
207 find `name` in current `context`. That is find me a value |
303 find `name` in current `context`. That is find me a value |
208 from the view object |
304 from the view object |
209 */ |
305 */ |
210 find: function(name, context) { |
306 find: function (name, context) { |
211 name = this.trim(name); |
307 name = trim(name); |
212 |
308 |
213 // Checks whether a value is thruthy or false or 0 |
309 // Checks whether a value is thruthy or false or 0 |
214 function is_kinda_truthy(bool) { |
310 function is_kinda_truthy(bool) { |
215 return bool === false || bool === 0 || bool; |
311 return bool === false || bool === 0 || bool; |
216 } |
312 } |
217 |
313 |
218 var value; |
314 var value; |
219 if(is_kinda_truthy(context[name])) { |
315 |
220 value = context[name]; |
316 // check for dot notation eg. foo.bar |
221 } else if(is_kinda_truthy(this.context[name])) { |
317 if (name.match(/([a-z_]+)\./ig)) { |
222 value = this.context[name]; |
318 var childValue = this.walk_context(name, context); |
223 } |
319 if (is_kinda_truthy(childValue)) { |
224 |
320 value = childValue; |
225 if(typeof value === "function") { |
321 } |
|
322 } else { |
|
323 if (is_kinda_truthy(context[name])) { |
|
324 value = context[name]; |
|
325 } else if (is_kinda_truthy(this.context[name])) { |
|
326 value = this.context[name]; |
|
327 } |
|
328 } |
|
329 |
|
330 if (typeof value == "function") { |
226 return value.apply(context); |
331 return value.apply(context); |
227 } |
332 } |
228 if(value !== undefined) { |
333 if (value !== undefined) { |
229 return value; |
334 return value; |
230 } |
335 } |
231 // silently ignore unkown variables |
336 // silently ignore unkown variables |
232 return ""; |
337 return ""; |
233 }, |
338 }, |
234 |
339 |
|
340 walk_context: function (name, context) { |
|
341 var path = name.split('.'); |
|
342 // if the var doesn't exist in current context, check the top level context |
|
343 var value_context = (context[path[0]] != undefined) ? context : this.context; |
|
344 var value = value_context[path.shift()]; |
|
345 while (value != undefined && path.length > 0) { |
|
346 value_context = value; |
|
347 value = value[path.shift()]; |
|
348 } |
|
349 // if the value is a function, call it, binding the correct context |
|
350 if (typeof value == "function") { |
|
351 return value.apply(value_context); |
|
352 } |
|
353 return value; |
|
354 }, |
|
355 |
235 // Utility methods |
356 // Utility methods |
236 |
357 |
237 /* includes tag */ |
358 /* includes tag */ |
238 includes: function(needle, haystack) { |
359 includes: function (needle, haystack) { |
239 return haystack.indexOf(this.otag + needle) != -1; |
360 return haystack.indexOf(this.otag + needle) != -1; |
240 }, |
361 }, |
241 |
362 |
242 /* |
|
243 Does away with nasty characters |
|
244 */ |
|
245 escape: function(s) { |
|
246 s = String(s === null ? "" : s); |
|
247 return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) { |
|
248 switch(s) { |
|
249 case "&": return "&"; |
|
250 case "\\": return "\\\\"; |
|
251 case '"': return '"'; |
|
252 case "'": return '''; |
|
253 case "<": return "<"; |
|
254 case ">": return ">"; |
|
255 default: return s; |
|
256 } |
|
257 }); |
|
258 }, |
|
259 |
|
260 // by @langalex, support for arrays of strings |
363 // by @langalex, support for arrays of strings |
261 create_context: function(_context) { |
364 create_context: function (_context) { |
262 if(this.is_object(_context)) { |
365 if (this.is_object(_context)) { |
263 return _context; |
366 return _context; |
264 } else { |
367 } else { |
265 var iterator = "."; |
368 var iterator = "."; |
266 if(this.pragmas["IMPLICIT-ITERATOR"]) { |
369 if (this.pragmas["IMPLICIT-ITERATOR"]) { |
267 iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; |
370 iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator; |
268 } |
371 } |
269 var ctx = {}; |
372 var ctx = {}; |
270 ctx[iterator] = _context; |
373 ctx[iterator] = _context; |
271 return ctx; |
374 return ctx; |
272 } |
375 } |
273 }, |
376 }, |
274 |
377 |
275 is_object: function(a) { |
378 is_object: function (a) { |
276 return a && typeof a == "object"; |
379 return a && typeof a == "object"; |
277 }, |
380 }, |
278 |
381 |
279 is_array: function(a) { |
|
280 return Object.prototype.toString.call(a) === '[object Array]'; |
|
281 }, |
|
282 |
|
283 /* |
|
284 Gets rid of leading and trailing whitespace |
|
285 */ |
|
286 trim: function(s) { |
|
287 return s.replace(/^\s*|\s*$/g, ""); |
|
288 }, |
|
289 |
|
290 /* |
382 /* |
291 Why, why, why? Because IE. Cry, cry cry. |
383 Why, why, why? Because IE. Cry, cry cry. |
292 */ |
384 */ |
293 map: function(array, fn) { |
385 map: function (array, fn) { |
294 if (typeof array.map == "function") { |
386 if (typeof array.map == "function") { |
295 return array.map(fn); |
387 return array.map(fn); |
296 } else { |
388 } else { |
297 var r = []; |
389 var r = []; |
298 var l = array.length; |
390 var l = array.length; |
299 for(var i = 0; i < l; i++) { |
391 for(var i = 0; i < l; i++) { |
300 r.push(fn(array[i])); |
392 r.push(fn(array[i])); |
301 } |
393 } |
302 return r; |
394 return r; |
303 } |
395 } |
|
396 }, |
|
397 |
|
398 getCachedRegex: function (name, generator) { |
|
399 var byOtag = regexCache[this.otag]; |
|
400 if (!byOtag) { |
|
401 byOtag = regexCache[this.otag] = {}; |
|
402 } |
|
403 |
|
404 var byCtag = byOtag[this.ctag]; |
|
405 if (!byCtag) { |
|
406 byCtag = byOtag[this.ctag] = {}; |
|
407 } |
|
408 |
|
409 var regex = byCtag[name]; |
|
410 if (!regex) { |
|
411 regex = byCtag[name] = generator(this.otag, this.ctag); |
|
412 } |
|
413 |
|
414 return regex; |
304 } |
415 } |
305 }; |
416 }; |
306 |
417 |
307 return({ |
418 return({ |
308 name: "mustache.js", |
419 name: "mustache.js", |
309 version: "0.3.1-dev", |
420 version: "0.5.0-dev", |
310 |
421 |
311 /* |
422 /* |
312 Turns a template and view into HTML |
423 Turns a template and view into HTML |
313 */ |
424 */ |
314 to_html: function(template, view, partials, send_fun) { |
425 to_html: function (template, view, partials, send_fun) { |
315 var renderer = new Renderer(); |
426 var renderer = new Renderer(); |
316 if(send_fun) { |
427 if (send_fun) { |
317 renderer.send = send_fun; |
428 renderer.send = send_fun; |
318 } |
429 } |
319 renderer.render(template, view, partials); |
430 renderer.render(template, view || {}, partials); |
320 if(!send_fun) { |
431 if (!send_fun) { |
321 return renderer.buffer.join("\n"); |
432 return renderer.buffer.join("\n"); |
322 } |
433 } |
323 } |
434 } |
324 }); |
435 }); |
325 }(); |
436 }(); |