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