5a920362 |
1 | /* |
2 | Shameless port of a shameless port |
3 | @defunkt => @janl => @aq |
4 | |
5 | See http://github.com/defunkt/mustache for more info. |
6 | */ |
7 | |
8 | ;(function($) { |
9 | |
10 | /*! |
11 | * mustache.js - Logic-less {{mustache}} templates with JavaScript |
12 | * http://github.com/janl/mustache.js |
13 | */ |
14 | var Mustache = (typeof module !== "undefined" && module.exports) || {}; |
15 | |
16 | (function (exports) { |
17 | |
18 | exports.name = "mustache.js"; |
19 | exports.version = "0.5.0-dev"; |
20 | exports.tags = ["{{", "}}"]; |
21 | exports.parse = parse; |
22 | exports.compile = compile; |
23 | exports.render = render; |
24 | exports.clearCache = clearCache; |
25 | |
26 | // This is here for backwards compatibility with 0.4.x. |
27 | exports.to_html = function (template, view, partials, send) { |
28 | var result = render(template, view, partials); |
29 | |
30 | if (typeof send === "function") { |
31 | send(result); |
32 | } else { |
33 | return result; |
34 | } |
35 | }; |
36 | |
37 | var _toString = Object.prototype.toString; |
38 | var _isArray = Array.isArray; |
39 | var _forEach = Array.prototype.forEach; |
40 | var _trim = String.prototype.trim; |
41 | |
42 | var isArray; |
43 | if (_isArray) { |
44 | isArray = _isArray; |
45 | } else { |
46 | isArray = function (obj) { |
47 | return _toString.call(obj) === "[object Array]"; |
48 | }; |
49 | } |
50 | |
51 | var forEach; |
52 | if (_forEach) { |
53 | forEach = function (obj, callback, scope) { |
54 | return _forEach.call(obj, callback, scope); |
55 | }; |
56 | } else { |
57 | forEach = function (obj, callback, scope) { |
58 | for (var i = 0, len = obj.length; i < len; ++i) { |
59 | callback.call(scope, obj[i], i, obj); |
60 | } |
61 | }; |
62 | } |
63 | |
64 | var spaceRe = /^\s*$/; |
65 | |
66 | function isWhitespace(string) { |
67 | return spaceRe.test(string); |
68 | } |
69 | |
70 | var trim; |
71 | if (_trim) { |
72 | trim = function (string) { |
73 | return string == null ? "" : _trim.call(string); |
74 | }; |
75 | } else { |
76 | var trimLeft, trimRight; |
77 | |
78 | if (isWhitespace("\xA0")) { |
79 | trimLeft = /^\s+/; |
80 | trimRight = /\s+$/; |
81 | } else { |
82 | // IE doesn't match non-breaking spaces with \s, thanks jQuery. |
83 | trimLeft = /^[\s\xA0]+/; |
84 | trimRight = /[\s\xA0]+$/; |
85 | } |
86 | |
87 | trim = function (string) { |
88 | return string == null ? "" : |
89 | String(string).replace(trimLeft, "").replace(trimRight, ""); |
90 | }; |
91 | } |
92 | |
93 | var escapeMap = { |
94 | "&": "&", |
95 | "<": "<", |
96 | ">": ">", |
97 | '"': '"', |
98 | "'": ''' |
99 | }; |
100 | |
101 | function escapeHTML(string) { |
102 | return String(string).replace(/&(?!\w+;)|[<>"']/g, function (s) { |
103 | return escapeMap[s] || s; |
104 | }); |
105 | } |
106 | |
107 | /** |
108 | * Adds the `template`, `line`, and `file` properties to the given error |
109 | * object and alters the message to provide more useful debugging information. |
110 | */ |
111 | function debug(e, template, line, file) { |
112 | file = file || "<template>"; |
113 | |
114 | var lines = template.split("\n"), |
115 | start = Math.max(line - 3, 0), |
116 | end = Math.min(lines.length, line + 3), |
117 | context = lines.slice(start, end); |
118 | |
119 | var c; |
120 | for (var i = 0, len = context.length; i < len; ++i) { |
121 | c = i + start + 1; |
122 | context[i] = (c === line ? " >> " : " ") + context[i]; |
123 | } |
124 | |
125 | e.template = template; |
126 | e.line = line; |
127 | e.file = file; |
128 | e.message = [file + ":" + line, context.join("\n"), "", e.message].join("\n"); |
129 | |
130 | return e; |
131 | } |
132 | |
133 | /** |
134 | * Looks up the value of the given `name` in the given context `stack`. |
135 | */ |
136 | function lookup(name, stack, defaultValue) { |
137 | if (name === ".") { |
138 | return stack[stack.length - 1]; |
139 | } |
140 | |
141 | var names = name.split("."); |
142 | var lastIndex = names.length - 1; |
143 | var target = names[lastIndex]; |
144 | |
145 | var value, context, i = stack.length, j, localStack; |
146 | while (i) { |
147 | localStack = stack.slice(0); |
148 | context = stack[--i]; |
149 | |
150 | j = 0; |
151 | while (j < lastIndex) { |
152 | context = context[names[j++]]; |
153 | |
154 | if (context == null) { |
155 | break; |
156 | } |
157 | |
158 | localStack.push(context); |
159 | } |
160 | |
161 | if (context && typeof context === "object" && target in context) { |
162 | value = context[target]; |
163 | break; |
164 | } |
165 | } |
166 | |
167 | // If the value is a function, call it in the current context. |
168 | if (typeof value === "function") { |
169 | value = value.call(localStack[localStack.length - 1]); |
170 | } |
171 | |
172 | if (value == null) { |
173 | return defaultValue; |
174 | } |
175 | |
176 | return value; |
177 | } |
178 | |
179 | function renderSection(name, stack, callback, inverted) { |
180 | var buffer = ""; |
181 | var value = lookup(name, stack); |
182 | |
183 | if (inverted) { |
184 | // From the spec: inverted sections may render text once based on the |
185 | // inverse value of the key. That is, they will be rendered if the key |
186 | // doesn't exist, is false, or is an empty list. |
187 | if (value == null || value === false || (isArray(value) && value.length === 0)) { |
188 | buffer += callback(); |
189 | } |
190 | } else if (isArray(value)) { |
191 | forEach(value, function (value) { |
192 | stack.push(value); |
193 | buffer += callback(); |
194 | stack.pop(); |
195 | }); |
196 | } else if (typeof value === "object") { |
197 | stack.push(value); |
198 | buffer += callback(); |
199 | stack.pop(); |
200 | } else if (typeof value === "function") { |
201 | var scope = stack[stack.length - 1]; |
202 | var scopedRender = function (template) { |
203 | return render(template, scope); |
204 | }; |
205 | buffer += value.call(scope, callback(), scopedRender) || ""; |
206 | } else if (value) { |
207 | buffer += callback(); |
208 | } |
209 | |
210 | return buffer; |
211 | } |
212 | |
213 | /** |
214 | * Parses the given `template` and returns the source of a function that, |
215 | * with the proper arguments, will render the template. Recognized options |
216 | * include the following: |
217 | * |
218 | * - file The name of the file the template comes from (displayed in |
219 | * error messages) |
220 | * - tags An array of open and close tags the `template` uses. Defaults |
221 | * to the value of Mustache.tags |
222 | * - debug Set `true` to log the body of the generated function to the |
223 | * console |
224 | * - space Set `true` to preserve whitespace from lines that otherwise |
225 | * contain only a {{tag}}. Defaults to `false` |
226 | */ |
227 | function parse(template, options) { |
228 | options = options || {}; |
229 | |
230 | var tags = options.tags || exports.tags, |
231 | openTag = tags[0], |
232 | closeTag = tags[tags.length - 1]; |
233 | |
234 | var code = [ |
235 | 'var buffer = "";', // output buffer |
236 | "\nvar line = 1;", // keep track of source line number |
237 | "\ntry {", |
238 | '\nbuffer += "' |
239 | ]; |
240 | |
241 | var spaces = [], // indices of whitespace in code on the current line |
242 | hasTag = false, // is there a {{tag}} on the current line? |
243 | nonSpace = false; // is there a non-space char on the current line? |
244 | |
245 | // Strips all space characters from the code array for the current line |
246 | // if there was a {{tag}} on it and otherwise only spaces. |
247 | var stripSpace = function () { |
248 | if (hasTag && !nonSpace && !options.space) { |
249 | while (spaces.length) { |
250 | code.splice(spaces.pop(), 1); |
251 | } |
252 | } else { |
253 | spaces = []; |
254 | } |
255 | |
256 | hasTag = false; |
257 | nonSpace = false; |
258 | }; |
259 | |
260 | var sectionStack = [], updateLine, nextOpenTag, nextCloseTag; |
261 | |
262 | var setTags = function (source) { |
263 | tags = trim(source).split(/\s+/); |
264 | nextOpenTag = tags[0]; |
265 | nextCloseTag = tags[tags.length - 1]; |
266 | }; |
267 | |
268 | var includePartial = function (source) { |
269 | code.push( |
270 | '";', |
271 | updateLine, |
272 | '\nvar partial = partials["' + trim(source) + '"];', |
273 | '\nif (partial) {', |
274 | '\n buffer += render(partial,stack[stack.length - 1],partials);', |
275 | '\n}', |
276 | '\nbuffer += "' |
277 | ); |
278 | }; |
279 | |
280 | var openSection = function (source, inverted) { |
281 | var name = trim(source); |
282 | |
283 | if (name === "") { |
284 | throw debug(new Error("Section name may not be empty"), template, line, options.file); |
285 | } |
286 | |
287 | sectionStack.push({name: name, inverted: inverted}); |
288 | |
289 | code.push( |
290 | '";', |
291 | updateLine, |
292 | '\nvar name = "' + name + '";', |
293 | '\nvar callback = (function () {', |
294 | '\n return function () {', |
295 | '\n var buffer = "";', |
296 | '\nbuffer += "' |
297 | ); |
298 | }; |
299 | |
300 | var openInvertedSection = function (source) { |
301 | openSection(source, true); |
302 | }; |
303 | |
304 | var closeSection = function (source) { |
305 | var name = trim(source); |
306 | var openName = sectionStack.length != 0 && sectionStack[sectionStack.length - 1].name; |
307 | |
308 | if (!openName || name != openName) { |
309 | throw debug(new Error('Section named "' + name + '" was never opened'), template, line, options.file); |
310 | } |
311 | |
312 | var section = sectionStack.pop(); |
313 | |
314 | code.push( |
315 | '";', |
316 | '\n return buffer;', |
317 | '\n };', |
318 | '\n})();' |
319 | ); |
320 | |
321 | if (section.inverted) { |
322 | code.push("\nbuffer += renderSection(name,stack,callback,true);"); |
323 | } else { |
324 | code.push("\nbuffer += renderSection(name,stack,callback);"); |
325 | } |
326 | |
327 | code.push('\nbuffer += "'); |
328 | }; |
329 | |
330 | var sendPlain = function (source) { |
331 | code.push( |
332 | '";', |
333 | updateLine, |
334 | '\nbuffer += lookup("' + trim(source) + '",stack,"");', |
335 | '\nbuffer += "' |
336 | ); |
337 | }; |
338 | |
339 | var sendEscaped = function (source) { |
340 | code.push( |
341 | '";', |
342 | updateLine, |
343 | '\nbuffer += escapeHTML(lookup("' + trim(source) + '",stack,""));', |
344 | '\nbuffer += "' |
345 | ); |
346 | }; |
347 | |
348 | var line = 1, c, callback; |
349 | for (var i = 0, len = template.length; i < len; ++i) { |
350 | if (template.slice(i, i + openTag.length) === openTag) { |
351 | i += openTag.length; |
352 | c = template.substr(i, 1); |
353 | updateLine = '\nline = ' + line + ';'; |
354 | nextOpenTag = openTag; |
355 | nextCloseTag = closeTag; |
356 | hasTag = true; |
357 | |
358 | switch (c) { |
359 | case "!": // comment |
360 | i++; |
361 | callback = null; |
362 | break; |
363 | case "=": // change open/close tags, e.g. {{=<% %>=}} |
364 | i++; |
365 | closeTag = "=" + closeTag; |
366 | callback = setTags; |
367 | break; |
368 | case ">": // include partial |
369 | i++; |
370 | callback = includePartial; |
371 | break; |
372 | case "#": // start section |
373 | i++; |
374 | callback = openSection; |
375 | break; |
376 | case "^": // start inverted section |
377 | i++; |
378 | callback = openInvertedSection; |
379 | break; |
380 | case "/": // end section |
381 | i++; |
382 | callback = closeSection; |
383 | break; |
384 | case "{": // plain variable |
385 | closeTag = "}" + closeTag; |
386 | // fall through |
387 | case "&": // plain variable |
388 | i++; |
389 | nonSpace = true; |
390 | callback = sendPlain; |
391 | break; |
392 | default: // escaped variable |
393 | nonSpace = true; |
394 | callback = sendEscaped; |
395 | } |
396 | |
397 | var end = template.indexOf(closeTag, i); |
398 | |
399 | if (end === -1) { |
400 | throw debug(new Error('Tag "' + openTag + '" was not closed properly'), template, line, options.file); |
401 | } |
402 | |
403 | var source = template.substring(i, end); |
404 | |
405 | if (callback) { |
406 | callback(source); |
407 | } |
408 | |
409 | // Maintain line count for \n in source. |
410 | var n = 0; |
411 | while (~(n = source.indexOf("\n", n))) { |
412 | line++; |
413 | n++; |
414 | } |
415 | |
416 | i = end + closeTag.length - 1; |
417 | openTag = nextOpenTag; |
418 | closeTag = nextCloseTag; |
419 | } else { |
420 | c = template.substr(i, 1); |
421 | |
422 | switch (c) { |
423 | case '"': |
424 | case "\\": |
425 | nonSpace = true; |
426 | code.push("\\" + c); |
427 | break; |
428 | case "\r": |
429 | // Ignore carriage returns. |
430 | break; |
431 | case "\n": |
432 | spaces.push(code.length); |
433 | code.push("\\n"); |
434 | stripSpace(); // Check for whitespace on the current line. |
435 | line++; |
436 | break; |
437 | default: |
438 | if (isWhitespace(c)) { |
439 | spaces.push(code.length); |
440 | } else { |
441 | nonSpace = true; |
442 | } |
443 | |
444 | code.push(c); |
445 | } |
446 | } |
447 | } |
448 | |
449 | if (sectionStack.length != 0) { |
450 | throw debug(new Error('Section "' + sectionStack[sectionStack.length - 1].name + '" was not closed properly'), template, line, options.file); |
451 | } |
452 | |
453 | // Clean up any whitespace from a closing {{tag}} that was at the end |
454 | // of the template without a trailing \n. |
455 | stripSpace(); |
456 | |
457 | code.push( |
458 | '";', |
459 | "\nreturn buffer;", |
460 | "\n} catch (e) { throw {error: e, line: line}; }" |
461 | ); |
462 | |
463 | // Ignore `buffer += "";` statements. |
464 | var body = code.join("").replace(/buffer \+= "";\n/g, ""); |
465 | |
466 | if (options.debug) { |
467 | if (typeof console != "undefined" && console.log) { |
468 | console.log(body); |
469 | } else if (typeof print === "function") { |
470 | print(body); |
471 | } |
472 | } |
473 | |
474 | return body; |
475 | } |
476 | |
477 | /** |
478 | * Used by `compile` to generate a reusable function for the given `template`. |
479 | */ |
480 | function _compile(template, options) { |
481 | var args = "view,partials,stack,lookup,escapeHTML,renderSection,render"; |
482 | var body = parse(template, options); |
483 | var fn = new Function(args, body); |
484 | |
485 | // This anonymous function wraps the generated function so we can do |
486 | // argument coercion, setup some variables, and handle any errors |
487 | // encountered while executing it. |
488 | return function (view, partials) { |
489 | partials = partials || {}; |
490 | |
491 | var stack = [view]; // context stack |
492 | |
493 | try { |
494 | return fn(view, partials, stack, lookup, escapeHTML, renderSection, render); |
495 | } catch (e) { |
496 | throw debug(e.error, template, e.line, options.file); |
497 | } |
498 | }; |
499 | } |
500 | |
501 | // Cache of pre-compiled templates. |
502 | var _cache = {}; |
503 | |
504 | /** |
505 | * Clear the cache of compiled templates. |
506 | */ |
507 | function clearCache() { |
508 | _cache = {}; |
509 | } |
510 | |
511 | /** |
512 | * Compiles the given `template` into a reusable function using the given |
513 | * `options`. In addition to the options accepted by Mustache.parse, |
514 | * recognized options include the following: |
515 | * |
516 | * - cache Set `false` to bypass any pre-compiled version of the given |
517 | * template. Otherwise, a given `template` string will be cached |
518 | * the first time it is parsed |
519 | */ |
520 | function compile(template, options) { |
521 | options = options || {}; |
522 | |
523 | // Use a pre-compiled version from the cache if we have one. |
524 | if (options.cache !== false) { |
525 | if (!_cache[template]) { |
526 | _cache[template] = _compile(template, options); |
527 | } |
528 | |
529 | return _cache[template]; |
530 | } |
531 | |
532 | return _compile(template, options); |
533 | } |
534 | |
535 | /** |
536 | * High-level function that renders the given `template` using the given |
537 | * `view` and `partials`. If you need to use any of the template options (see |
538 | * `compile` above), you must compile in a separate step, and then call that |
539 | * compiled function. |
540 | */ |
541 | function render(template, view, partials) { |
542 | return compile(template)(view, partials); |
543 | } |
544 | |
545 | })(Mustache); |
546 | |
547 | $.mustache = function (template, view, partials) { |
548 | return Mustache.render(template, view, partials); |
549 | }; |
550 | |
551 | $.fn.mustache = function (view, partials) { |
552 | return $(this).map(function (i, elm) { |
553 | var template = $(elm).html().trim(); |
554 | var output = $.mustache(template, view, partials); |
555 | return $(output).get(); |
556 | }); |
557 | }; |
558 | |
559 | })(jQuery); |