Coverage

98%
1195
1176
19

/lib/dateformatter.js

100%
105
105
0
LineHitsSource
11var utils = require('./utils');
2
31var _months = {
4 full: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
5 abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
6 },
7 _days = {
8 full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
9 abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
10 alt: {'-1': 'Yesterday', 0: 'Today', 1: 'Tomorrow'}
11 };
12
13/*
14DateZ is licensed under the MIT License:
15Copyright (c) 2011 Tomo Universalis (http://tomouniversalis.com)
16Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19*/
201exports.tzOffset = 0;
211exports.DateZ = function () {
2264 var members = {
23 'default': ['getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds', 'toISOString', 'toGMTString', 'toUTCString', 'valueOf', 'getTime'],
24 z: ['getDate', 'getDay', 'getFullYear', 'getHours', 'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds', 'getYear', 'toDateString', 'toLocaleDateString', 'toLocaleTimeString']
25 },
26 d = this;
27
2864 d.date = d.dateZ = (arguments.length > 1) ? new Date(Date.UTC.apply(Date, arguments) + ((new Date()).getTimezoneOffset() * 60000)) : (arguments.length === 1) ? new Date(new Date(arguments['0'])) : new Date();
29
3064 d.timezoneOffset = d.dateZ.getTimezoneOffset();
31
3264 utils.each(members.z, function (name) {
33768 d[name] = function () {
3470 return d.dateZ[name]();
35 };
36 });
3764 utils.each(members['default'], function (name) {
38832 d[name] = function () {
3915 return d.date[name]();
40 };
41 });
42
4364 this.setTimezoneOffset(exports.tzOffset);
44};
451exports.DateZ.prototype = {
46 getTimezoneOffset: function () {
474 return this.timezoneOffset;
48 },
49 setTimezoneOffset: function (offset) {
50127 this.timezoneOffset = offset;
51127 this.dateZ = new Date(this.date.getTime() + this.date.getTimezoneOffset() * 60000 - this.timezoneOffset * 60000);
52127 return this;
53 }
54};
55
56// Day
571exports.d = function (input) {
583 return (input.getDate() < 10 ? '0' : '') + input.getDate();
59};
601exports.D = function (input) {
612 return _days.abbr[input.getDay()];
62};
631exports.j = function (input) {
642 return input.getDate();
65};
661exports.l = function (input) {
671 return _days.full[input.getDay()];
68};
691exports.N = function (input) {
702 var d = input.getDay();
712 return (d >= 1) ? d : 7;
72};
731exports.S = function (input) {
7413 var d = input.getDate();
7513 return (d % 10 === 1 && d !== 11 ? 'st' : (d % 10 === 2 && d !== 12 ? 'nd' : (d % 10 === 3 && d !== 13 ? 'rd' : 'th')));
76};
771exports.w = function (input) {
781 return input.getDay();
79};
801exports.z = function (input, offset, abbr) {
813 var year = input.getFullYear(),
82 e = new exports.DateZ(year, input.getMonth(), input.getDate(), 12, 0, 0),
83 d = new exports.DateZ(year, 0, 1, 12, 0, 0);
84
853 e.setTimezoneOffset(offset, abbr);
863 d.setTimezoneOffset(offset, abbr);
873 return Math.round((e - d) / 86400000);
88};
89
90// Week
911exports.W = function (input) {
921 var target = new Date(input.valueOf()),
93 dayNr = (input.getDay() + 6) % 7,
94 fThurs;
95
961 target.setDate(target.getDate() - dayNr + 3);
971 fThurs = target.valueOf();
981 target.setMonth(0, 1);
991 if (target.getDay() !== 4) {
1001 target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
101 }
102
1031 return 1 + Math.ceil((fThurs - target) / 604800000);
104};
105
106// Month
1071exports.F = function (input) {
1082 return _months.full[input.getMonth()];
109};
1101exports.m = function (input) {
1113 return (input.getMonth() < 9 ? '0' : '') + (input.getMonth() + 1);
112};
1131exports.M = function (input) {
1141 return _months.abbr[input.getMonth()];
115};
1161exports.n = function (input) {
1171 return input.getMonth() + 1;
118};
1191exports.t = function (input) {
1201 return 32 - (new Date(input.getFullYear(), input.getMonth(), 32).getDate());
121};
122
123// Year
1241exports.L = function (input) {
1252 return new Date(input.getFullYear(), 1, 29).getDate() === 29;
126};
1271exports.o = function (input) {
1282 var target = new Date(input.valueOf());
1292 target.setDate(target.getDate() - ((input.getDay() + 6) % 7) + 3);
1302 return target.getFullYear();
131};
1321exports.Y = function (input) {
1333 return input.getFullYear();
134};
1351exports.y = function (input) {
1361 return (input.getFullYear().toString()).substr(2);
137};
138
139// Time
1401exports.a = function (input) {
1412 return input.getHours() < 12 ? 'am' : 'pm';
142};
1431exports.A = function (input) {
1441 return input.getHours() < 12 ? 'AM' : 'PM';
145};
1461exports.B = function (input) {
1471 var hours = input.getUTCHours(), beats;
1481 hours = (hours === 23) ? 0 : hours + 1;
1491 beats = Math.abs(((((hours * 60) + input.getUTCMinutes()) * 60) + input.getUTCSeconds()) / 86.4).toFixed(0);
1501 return ('000'.concat(beats).slice(beats.length));
151};
1521exports.g = function (input) {
1531 var h = input.getHours();
1541 return h === 0 ? 12 : (h > 12 ? h - 12 : h);
155};
1561exports.G = function (input) {
1572 return input.getHours();
158};
1591exports.h = function (input) {
1602 var h = input.getHours();
1612 return ((h < 10 || (12 < h && 22 > h)) ? '0' : '') + ((h < 12) ? h : h - 12);
162};
1631exports.H = function (input) {
1642 var h = input.getHours();
1652 return (h < 10 ? '0' : '') + h;
166};
1671exports.i = function (input) {
1682 var m = input.getMinutes();
1692 return (m < 10 ? '0' : '') + m;
170};
1711exports.s = function (input) {
1721 var s = input.getSeconds();
1731 return (s < 10 ? '0' : '') + s;
174};
175//u = function () { return ''; },
176
177// Timezone
178//e = function () { return ''; },
179//I = function () { return ''; },
1801exports.O = function (input) {
1813 var tz = input.getTimezoneOffset();
1823 return (tz < 0 ? '-' : '+') + (tz / 60 < 10 ? '0' : '') + Math.abs((tz / 60)) + '00';
183};
184//T = function () { return ''; },
1851exports.Z = function (input) {
1861 return input.getTimezoneOffset() * 60;
187};
188
189// Full Date/Time
1901exports.c = function (input) {
1911 return input.toISOString();
192};
1931exports.r = function (input) {
1941 return input.toUTCString();
195};
1961exports.U = function (input) {
1971 return input.getTime() / 1000;
198};
199

/lib/filters.js

99%
159
158
1
LineHitsSource
11var utils = require('./utils'),
2 dateFormatter = require('./dateformatter');
3
4/**
5 * Helper method to recursively run a filter across an object/array and apply it to all of the object/array's values.
6 * @param {*} input
7 * @return {*}
8 * @private
9 */
101function iterateFilter(input) {
11392 var self = this,
12 out = {};
13
14392 if (utils.isArray(input)) {
1523 return utils.map(input, function (value) {
1653 return self.apply(null, arguments);
17 });
18 }
19
20369 if (typeof input === 'object') {
214 utils.each(input, function (value, key) {
225 out[key] = self.apply(null, arguments);
23 });
244 return out;
25 }
26
27365 return;
28}
29
30/**
31 * Backslash-escape characters that need to be escaped.
32 *
33 * @example
34 * {{ "\"quoted string\""|addslashes }}
35 * // => \"quoted string\"
36 *
37 * @param {*} input
38 * @return {*} Backslash-escaped string.
39 */
401exports.addslashes = function (input) {
416 var out = iterateFilter.apply(exports.addslashes, arguments);
426 if (out !== undefined) {
431 return out;
44 }
45
465 return input.replace(/\\/g, '\\\\').replace(/\'/g, "\\'").replace(/\"/g, '\\"');
47};
48
49/**
50 * Upper-case the first letter of the input and lower-case the rest.
51 *
52 * @example
53 * {{ "i like Burritos"|capitalize }}
54 * // => I like burritos
55 *
56 * @param {*} input If given an array or object, each string member will be run through the filter individually.
57 * @return {*} Returns the same type as the input.
58 */
591exports.capitalize = function (input) {
605 var out = iterateFilter.apply(exports.capitalize, arguments);
615 if (out !== undefined) {
621 return out;
63 }
64
654 return input.toString().charAt(0).toUpperCase() + input.toString().substr(1).toLowerCase();
66};
67
68/**
69 * Format a date or Date-compatible string.
70 *
71 * @example
72 * // now = new Date();
73 * {{ now|date('Y-m-d') }}
74 * // => 2013-08-14
75 * @example
76 * // now = new Date();
77 * {{ now|date('jS \o\f F') }}
78 * // => 4th of July
79 *
80 * @param {?(string|date)} input
81 * @param {string} format PHP-style date format compatible string. Escape characters with <code>\</code> for string literals.
82 * @param {number=} offset Timezone offset from GMT in minutes.
83 * @param {string=} abbr Timezone abbreviation. Used for output only.
84 * @return {string} Formatted date string.
85 */
861exports.date = function (input, format, offset, abbr) {
8758 var l = format.length,
88 date = new dateFormatter.DateZ(input),
89 cur,
90 i = 0,
91 out = '';
92
9358 if (offset) {
9457 date.setTimezoneOffset(offset, abbr);
95 }
96
9758 for (i; i < l; i += 1) {
9882 cur = format.charAt(i);
9982 if (cur === '\\') {
1008 i += 1;
1018 out += (i < l) ? format.charAt(i) : cur;
10274 } else if (dateFormatter.hasOwnProperty(cur)) {
10365 out += dateFormatter[cur](date, offset, abbr);
104 } else {
1059 out += cur;
106 }
107 }
10858 return out;
109};
110
111/**
112 * If the input is `undefined`, `null`, or `false`, a default return value can be specified.
113 *
114 * @example
115 * {{ null_value|default('Tacos') }}
116 * // => Tacos
117 *
118 * @example
119 * {{ "Burritos"|default("Tacos") }}
120 * // => Burritos
121 *
122 * @param {*} input
123 * @param {*} def Value to return if `input` is `undefined`, `null`, or `false`.
124 * @return {*} `input` or `def` value.
125 */
1261exports["default"] = function (input, def) {
12721 return (typeof input !== 'undefined' && (input || typeof input === 'number')) ? input : def;
128};
129
130/**
131 * Force escape the output of the variable. Optionally use `e` as a shortcut filter name. This filter will be applied by default if autoescape is turned on.
132 *
133 * @example
134 * {{ "<blah>"|escape }}
135 * // => <blah>
136 *
137 * @example
138 * {{ "<blah>"|e("js") }}
139 * // => \u003Cblah\u003E
140 *
141 * @param {*} input
142 * @param {string} [type='html'] If you pass the string js in as the type, output will be escaped so that it is safe for JavaScript execution.
143 * @return {string} Escaped string.
144 */
1451exports.escape = function (input, type) {
146349 var out = iterateFilter.apply(exports.escape, arguments),
147 inp = input,
148 i = 0,
149 code;
150
151349 if (out !== undefined) {
15217 return out;
153 }
154
155332 if (typeof input !== 'string') {
156106 return input;
157 }
158
159226 out = '';
160
161226 switch (type) {
162 case 'js':
1636 inp = inp.replace(/\\/g, '\\u005C');
1646 for (i; i < inp.length; i += 1) {
165161 code = inp.charCodeAt(i);
166161 if (code < 32) {
1676 code = code.toString(16).toUpperCase();
1686 code = (code.length < 2) ? '0' + code : code;
1696 out += '\\u00' + code;
170 } else {
171155 out += inp[i];
172 }
173 }
1746 return out.replace(/&/g, '\\u0026')
175 .replace(/</g, '\\u003C')
176 .replace(/>/g, '\\u003E')
177 .replace(/\'/g, '\\u0027')
178 .replace(/"/g, '\\u0022')
179 .replace(/\=/g, '\\u003D')
180 .replace(/-/g, '\\u002D')
181 .replace(/;/g, '\\u003B');
182
183 default:
184220 return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
185 .replace(/</g, '<')
186 .replace(/>/g, '>')
187 .replace(/"/g, '"')
188 .replace(/'/g, ''');
189 }
190};
1911exports.e = exports.escape;
192
193/**
194 * Get the first item in an array or character in a string. All other objects will attempt to return the first value available.
195 *
196 * @example
197 * // my_arr = ['a', 'b', 'c']
198 * {{ my_arr|first }}
199 * // => a
200 *
201 * @example
202 * // my_val = 'Tacos'
203 * {{ my_val|first }}
204 * // T
205 *
206 * @param {*} input
207 * @return {*} The first item of the array or first character of the string input.
208 */
2091exports.first = function (input) {
2104 if (typeof input === 'object' && !utils.isArray(input)) {
2111 var keys = utils.keys(input);
2121 return input[keys[0]];
213 }
214
2153 if (typeof input === 'string') {
2161 return input.substr(0, 1);
217 }
218
2192 return input[0];
220};
221
222/**
223 * Group an array of objects by a common key. If an array is not provided, the input value will be returned untouched.
224 *
225 * @example
226 * // people = [{ age: 23, name: 'Paul' }, { age: 26, name: 'Jane' }, { age: 23, name: 'Jim' }];
227 * {% for agegroup in people|groupBy('age') %}
228 * <h2>{{ loop.key }}</h2>
229 * <ul>
230 * {% for person in agegroup %}
231 * <li>{{ person.name }}</li>
232 * {% endfor %}
233 * </ul>
234 * {% endfor %}
235 *
236 * @param {*} input Input object.
237 * @param {string} key Key to group by.
238 * @return {object} Grouped arrays by given key.
239 */
2401exports.groupBy = function (input, key) {
2412 if (!utils.isArray(input)) {
2421 return input;
243 }
244
2451 var out = {};
246
2471 utils.each(input, function (value) {
2483 if (!value.hasOwnProperty(key)) {
2490 return;
250 }
251
2523 var keyname = value[key],
253 newVal = utils.extend({}, value);
2543 delete value[key];
255
2563 if (!out[keyname]) {
2572 out[keyname] = [];
258 }
259
2603 out[keyname].push(value);
261 });
262
2631 return out;
264};
265
266/**
267 * Join the input with a string.
268 *
269 * @example
270 * // my_array = ['foo', 'bar', 'baz']
271 * {{ my_array|join(', ') }}
272 * // => foo, bar, baz
273 *
274 * @example
275 * // my_key_object = { a: 'foo', b: 'bar', c: 'baz' }
276 * {{ my_key_object|join(' and ') }}
277 * // => foo and bar and baz
278 *
279 * @param {*} input
280 * @param {string} glue String value to join items together.
281 * @return {string}
282 */
2831exports.join = function (input, glue) {
28411 if (utils.isArray(input)) {
2857 return input.join(glue);
286 }
287
2884 if (typeof input === 'object') {
2893 var out = [];
2903 utils.each(input, function (value) {
2915 out.push(value);
292 });
2933 return out.join(glue);
294 }
2951 return input;
296};
297
298/**
299 * Return a string representation of an JavaScript object.
300 *
301 * Backwards compatible with swig@0.x.x using `json_encode`.
302 *
303 * @example
304 * // val = { a: 'b' }
305 * {{ val|json }}
306 * // => {"a":"b"}
307 *
308 * @example
309 * // val = { a: 'b' }
310 * {{ val|json(4) }}
311 * // => {
312 * // "a": "b"
313 * // }
314 *
315 * @param {*} input
316 * @param {number} [indent] Number of spaces to indent for pretty-formatting.
317 * @return {string} A valid JSON string.
318 */
3191exports.json = function (input, indent) {
3203 return JSON.stringify(input, null, indent || 0);
321};
3221exports.json_encode = exports.json;
323
324/**
325 * Get the last item in an array or character in a string. All other objects will attempt to return the last value available.
326 *
327 * @example
328 * // my_arr = ['a', 'b', 'c']
329 * {{ my_arr|last }}
330 * // => c
331 *
332 * @example
333 * // my_val = 'Tacos'
334 * {{ my_val|last }}
335 * // s
336 *
337 * @param {*} input
338 * @return {*} The last item of the array or last character of the string.input.
339 */
3401exports.last = function (input) {
3413 if (typeof input === 'object' && !utils.isArray(input)) {
3421 var keys = utils.keys(input);
3431 return input[keys[keys.length - 1]];
344 }
345
3462 if (typeof input === 'string') {
3471 return input.charAt(input.length - 1);
348 }
349
3501 return input[input.length - 1];
351};
352
353/**
354 * Return the input in all lowercase letters.
355 *
356 * @example
357 * {{ "FOOBAR"|lower }}
358 * // => foobar
359 *
360 * @example
361 * // myObj = { a: 'FOO', b: 'BAR' }
362 * {{ myObj|lower|join('') }}
363 * // => foobar
364 *
365 * @param {*} input
366 * @return {*} Returns the same type as the input.
367 */
3681exports.lower = function (input) {
3698 var out = iterateFilter.apply(exports.lower, arguments);
3708 if (out !== undefined) {
3712 return out;
372 }
373
3746 return input.toString().toLowerCase();
375};
376
377/**
378 * Deprecated in favor of <a href="#safe">safe</a>.
379 */
3801exports.raw = function (input) {
3812 return exports.safe(input);
382};
3831exports.raw.safe = true;
384
385/**
386 * Returns a new string with the matched search pattern replaced by the given replacement string. Uses JavaScript's built-in String.replace() method.
387 *
388 * @example
389 * // my_var = 'foobar';
390 * {{ my_var|replace('o', 'e', 'g') }}
391 * // => feebar
392 *
393 * @example
394 * // my_var = "farfegnugen";
395 * {{ my_var|replace('^f', 'p') }}
396 * // => parfegnugen
397 *
398 * @example
399 * // my_var = 'a1b2c3';
400 * {{ my_var|replace('\w', '0', 'g') }}
401 * // => 010203
402 *
403 * @param {string} input
404 * @param {string} search String or pattern to replace from the input.
405 * @param {string} replacement String to replace matched pattern.
406 * @param {string} [flags] Regular Expression flags. 'g': global match, 'i': ignore case, 'm': match over multiple lines
407 * @return {string} Replaced string.
408 */
4091exports.replace = function (input, search, replacement, flags) {
41011 var r = new RegExp(search, flags);
41111 return input.replace(r, replacement);
412};
413
414/**
415 * Reverse sort the input. This is an alias for <code data-language="swig">{{ input|sort(true) }}</code>.
416 *
417 * @example
418 * // val = [1, 2, 3];
419 * {{ val|reverse }}
420 * // => 3,2,1
421 *
422 * @param {array} input
423 * @return {array} Reversed array. The original input object is returned if it was not an array.
424 */
4251exports.reverse = function (input) {
4269 return exports.sort(input, true);
427};
428
429/**
430 * Forces the input to not be auto-escaped. Use this only on content that you know is safe to be rendered on your page.
431 *
432 * @example
433 * // my_var = "<p>Stuff</p>";
434 * {{ my_var|safe }}
435 * // => <p>Stuff</p>
436 *
437 * @param {*} input
438 * @return {*} The input exactly how it was given, regardless of autoescaping status.
439 */
4401exports.safe = function (input) {
441 // This is a magic filter. Its logic is hard-coded into Swig's parser.
4425 return input;
443};
4441exports.safe.safe = true;
445
446/**
447 * Sort the input in an ascending direction.
448 * If given an object, will return the keys as a sorted array.
449 * If given a string, each character will be sorted individually.
450 *
451 * @example
452 * // val = [2, 6, 4];
453 * {{ val|sort }}
454 * // => 2,4,6
455 *
456 * @example
457 * // val = 'zaq';
458 * {{ val|sort }}
459 * // => aqz
460 *
461 * @example
462 * // val = { bar: 1, foo: 2 }
463 * {{ val|sort(true) }}
464 * // => foo,bar
465 *
466 * @param {*} input
467 * @param {boolean} [reverse=false] Output is given reverse-sorted if true.
468 * @return {*} Sorted array;
469 */
4701exports.sort = function (input, reverse) {
47113 var out;
47213 if (utils.isArray(input)) {
4734 out = input.sort();
474 } else {
4759 switch (typeof input) {
476 case 'object':
4772 out = utils.keys(input).sort();
4782 break;
479 case 'string':
4807 out = input.split('');
4817 if (reverse) {
4826 return out.reverse().join('');
483 }
4841 return out.sort().join('');
485 }
486 }
487
4886 if (out && reverse) {
4894 return out.reverse();
490 }
491
4922 return out || input;
493};
494
495/**
496 * Strip HTML tags.
497 *
498 * @example
499 * // stuff = '<p>foobar</p>';
500 * {{ stuff|striptags }}
501 * // => foobar
502 *
503 * @param {*} input
504 * @return {*} Returns the same object as the input, but with all string values stripped of tags.
505 */
5061exports.striptags = function (input) {
5074 var out = iterateFilter.apply(exports.striptags, arguments);
5084 if (out !== undefined) {
5091 return out;
510 }
511
5123 return input.toString().replace(/(<([^>]+)>)/ig, '');
513};
514
515/**
516 * Capitalizes every word given and lower-cases all other letters.
517 *
518 * @example
519 * // my_str = 'this is soMe text';
520 * {{ my_str|title }}
521 * // => This Is Some Text
522 *
523 * @example
524 * // my_arr = ['hi', 'this', 'is', 'an', 'array'];
525 * {{ my_arr|title|join(' ') }}
526 * // => Hi This Is An Array
527 *
528 * @param {*} input
529 * @return {*} Returns the same object as the input, but with all words in strings title-cased.
530 */
5311exports.title = function (input) {
5324 var out = iterateFilter.apply(exports.title, arguments);
5334 if (out !== undefined) {
5341 return out;
535 }
536
5373 return input.toString().replace(/\w\S*/g, function (str) {
5386 return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
539 });
540};
541
542/**
543 * Remove all duplicate items from an array.
544 *
545 * @example
546 * // my_arr = [1, 2, 3, 4, 4, 3, 2, 1];
547 * {{ my_arr|uniq|join(',') }}
548 * // => 1,2,3,4
549 *
550 * @param {array} input
551 * @return {array} Array with unique items. If input was not an array, the original item is returned untouched.
552 */
5531exports.uniq = function (input) {
5542 var result;
555
5562 if (!input || !utils.isArray(input)) {
5571 return '';
558 }
559
5601 result = [];
5611 utils.each(input, function (v) {
5626 if (result.indexOf(v) === -1) {
5634 result.push(v);
564 }
565 });
5661 return result;
567};
568
569/**
570 * Convert the input to all uppercase letters. If an object or array is provided, all values will be uppercased.
571 *
572 * @example
573 * // my_str = 'tacos';
574 * {{ my_str|upper }}
575 * // => TACOS
576 *
577 * @example
578 * // my_arr = ['tacos', 'burritos'];
579 * {{ my_arr|upper|join(' & ') }}
580 * // => TACOS & BURRITOS
581 *
582 * @param {*} input
583 * @return {*} Returns the same type as the input, with all strings upper-cased.
584 */
5851exports.upper = function (input) {
5868 var out = iterateFilter.apply(exports.upper, arguments);
5878 if (out !== undefined) {
5882 return out;
589 }
590
5916 return input.toString().toUpperCase();
592};
593
594/**
595 * URL-encode a string. If an object or array is passed, all values will be URL-encoded.
596 *
597 * @example
598 * // my_str = 'param=1&anotherParam=2';
599 * {{ my_str|url_encode }}
600 * // => param%3D1%26anotherParam%3D2
601 *
602 * @param {*} input
603 * @return {*} URL-encoded string.
604 */
6051exports.url_encode = function (input) {
6064 var out = iterateFilter.apply(exports.url_encode, arguments);
6074 if (out !== undefined) {
6081 return out;
609 }
6103 return encodeURIComponent(input);
611};
612
613/**
614 * URL-decode a string. If an object or array is passed, all values will be URL-decoded.
615 *
616 * @example
617 * // my_str = 'param%3D1%26anotherParam%3D2';
618 * {{ my_str|url_decode }}
619 * // => param=1&anotherParam=2
620 *
621 * @param {*} input
622 * @return {*} URL-decoded string.
623 */
6241exports.url_decode = function (input) {
6254 var out = iterateFilter.apply(exports.url_decode, arguments);
6264 if (out !== undefined) {
6271 return out;
628 }
6293 return decodeURIComponent(input);
630};
631

/lib/lexer.js

96%
25
24
1
LineHitsSource
11var utils = require('./utils');
2
3/**
4 * A lexer token.
5 * @typedef {object} LexerToken
6 * @property {string} match The string that was matched.
7 * @property {number} type Lexer type enum.
8 * @property {number} length Length of the original string processed.
9 */
10
11/**
12 * Enum for token types.
13 * @readonly
14 * @enum {number}
15 */
161var TYPES = {
17 /** Whitespace */
18 WHITESPACE: 0,
19 /** Plain string */
20 STRING: 1,
21 /** Variable filter */
22 FILTER: 2,
23 /** Empty variable filter */
24 FILTEREMPTY: 3,
25 /** Function */
26 FUNCTION: 4,
27 /** Function with no arguments */
28 FUNCTIONEMPTY: 5,
29 /** Open parenthesis */
30 PARENOPEN: 6,
31 /** Close parenthesis */
32 PARENCLOSE: 7,
33 /** Comma */
34 COMMA: 8,
35 /** Variable */
36 VAR: 9,
37 /** Number */
38 NUMBER: 10,
39 /** Math operator */
40 OPERATOR: 11,
41 /** Open square bracket */
42 BRACKETOPEN: 12,
43 /** Close square bracket */
44 BRACKETCLOSE: 13,
45 /** Key on an object using dot-notation */
46 DOTKEY: 14,
47 /** Start of an array */
48 ARRAYOPEN: 15,
49 /** End of an array
50 * Currently unused
51 ARRAYCLOSE: 16, */
52 /** Open curly brace */
53 CURLYOPEN: 17,
54 /** Close curly brace */
55 CURLYCLOSE: 18,
56 /** Colon (:) */
57 COLON: 19,
58 /** JavaScript-valid comparator */
59 COMPARATOR: 20,
60 /** Boolean logic */
61 LOGIC: 21,
62 /** Boolean logic "not" */
63 NOT: 22,
64 /** true or false */
65 BOOL: 23,
66 /** Variable assignment */
67 ASSIGNMENT: 24,
68 /** Start of a method */
69 METHODOPEN: 25,
70 /** End of a method
71 * Currently unused
72 METHODEND: 26, */
73 /** Unknown type */
74 UNKNOWN: 100
75 },
76 rules = [
77 {
78 type: TYPES.WHITESPACE,
79 regex: [
80 /^\s+/
81 ]
82 },
83 {
84 type: TYPES.STRING,
85 regex: [
86 /^""/,
87 /^".*?[^\\]"/,
88 /^''/,
89 /^'.*?[^\\]'/
90 ]
91 },
92 {
93 type: TYPES.FILTER,
94 regex: [
95 /^\|\s*(\w+)\(/
96 ],
97 idx: 1
98 },
99 {
100 type: TYPES.FILTEREMPTY,
101 regex: [
102 /^\|\s*(\w+)/
103 ],
104 idx: 1
105 },
106 {
107 type: TYPES.FUNCTIONEMPTY,
108 regex: [
109 /^\s*(\w+)\(\)/
110 ],
111 idx: 1
112 },
113 {
114 type: TYPES.FUNCTION,
115 regex: [
116 /^\s*(\w+)\(/
117 ],
118 idx: 1
119 },
120 {
121 type: TYPES.PARENOPEN,
122 regex: [
123 /^\(/
124 ]
125 },
126 {
127 type: TYPES.PARENCLOSE,
128 regex: [
129 /^\)/
130 ]
131 },
132 {
133 type: TYPES.COMMA,
134 regex: [
135 /^,/
136 ]
137 },
138 {
139 type: TYPES.LOGIC,
140 regex: [
141 /^(&&|\|\|)\s*/,
142 /^(and|or)\s+/
143 ],
144 idx: 1,
145 replace: {
146 'and': '&&',
147 'or': '||'
148 }
149 },
150 {
151 type: TYPES.COMPARATOR,
152 regex: [
153 /^(===|==|\!==|\!=|<=|<|>=|>|in\s|gte\s|gt\s|lte\s|lt\s)\s*/
154 ],
155 idx: 1,
156 replace: {
157 'gte': '>=',
158 'gt': '>',
159 'lte': '<=',
160 'lt': '<'
161 }
162 },
163 {
164 type: TYPES.ASSIGNMENT,
165 regex: [
166 /^(=|\+=|-=|\*=|\/=)/
167 ]
168 },
169 {
170 type: TYPES.NOT,
171 regex: [
172 /^\!\s*/,
173 /^not\s+/
174 ],
175 replace: {
176 'not': '!'
177 }
178 },
179 {
180 type: TYPES.BOOL,
181 regex: [
182 /^(true|false)\s+/,
183 /^(true|false)$/
184 ],
185 idx: 1
186 },
187 {
188 type: TYPES.VAR,
189 regex: [
190 /^[a-zA-Z_$]\w*((\.\$?\w*)+)?/,
191 /^[a-zA-Z_$]\w*/
192 ]
193 },
194 {
195 type: TYPES.BRACKETOPEN,
196 regex: [
197 /^\[/
198 ]
199 },
200 {
201 type: TYPES.BRACKETCLOSE,
202 regex: [
203 /^\]/
204 ]
205 },
206 {
207 type: TYPES.CURLYOPEN,
208 regex: [
209 /^\{/
210 ]
211 },
212 {
213 type: TYPES.COLON,
214 regex: [
215 /^\:/
216 ]
217 },
218 {
219 type: TYPES.CURLYCLOSE,
220 regex: [
221 /^\}/
222 ]
223 },
224 {
225 type: TYPES.DOTKEY,
226 regex: [
227 /^\.(\w+)/
228 ],
229 idx: 1
230 },
231 {
232 type: TYPES.NUMBER,
233 regex: [
234 /^[+\-]?\d+(\.\d+)?/
235 ]
236 },
237 {
238 type: TYPES.OPERATOR,
239 regex: [
240 /^(\+|\-|\/|\*|%)/
241 ]
242 }
243 ];
244
2451exports.types = TYPES;
246
247/**
248 * Return the token type object for a single chunk of a string.
249 * @param {string} str String chunk.
250 * @return {LexerToken} Defined type, potentially stripped or replaced with more suitable content.
251 * @private
252 */
2531function reader(str) {
2542024 var matched;
255
2562024 utils.some(rules, function (rule) {
25719211 return utils.some(rule.regex, function (regex) {
25826838 var match = str.match(regex),
259 normalized;
260
26126838 if (!match) {
26224814 return;
263 }
264
2652024 normalized = match[rule.idx || 0].replace(/\s*$/, '');
2662024 normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
267
2682024 matched = {
269 match: normalized,
270 type: rule.type,
271 length: match[0].length
272 };
2732024 return true;
274 });
275 });
276
2772024 if (!matched) {
2780 matched = {
279 match: str,
280 type: TYPES.UNKNOWN,
281 length: str.length
282 };
283 }
284
2852024 return matched;
286}
287
288/**
289 * Read a string and break it into separate token types.
290 * @param {string} str
291 * @return {Array.LexerToken} Array of defined types, potentially stripped or replaced with more suitable content.
292 * @private
293 */
2941exports.read = function (str) {
295651 var offset = 0,
296 tokens = [],
297 substr,
298 match;
299651 while (offset < str.length) {
3002024 substr = str.substring(offset);
3012024 match = reader(substr);
3022024 offset += match.length;
3032024 tokens.push(match);
304 }
305651 return tokens;
306};
307

/lib/loaders/filesystem.js

94%
19
18
1
LineHitsSource
11var fs = require('fs'),
2 path = require('path');
3
4/**
5 * Loads templates from the file system.
6 * @alias swig.loaders.fs
7 * @example
8 * swig.setDefaults({ loader: swig.loaders.fs() });
9 * @example
10 * // Load Templates from a specific directory (does not require using relative paths in your templates)
11 * swig.setDefaults({ loader: swig.loaders.fs(__dirname + '/templates' )});
12 * @param {string} [basepath=''] Path to the templates as string. Assigning this value allows you to use semi-absolute paths to templates instead of relative paths.
13 * @param {string} [encoding='utf8'] Template encoding
14 */
151module.exports = function (basepath, encoding) {
163 var ret = {};
17
183 encoding = encoding || 'utf8';
193 basepath = (basepath) ? path.normalize(basepath) : null;
20
21 /**
22 * Resolves <var>to</var> to an absolute path or unique identifier. This is used for building correct, normalized, and absolute paths to a given template.
23 * @alias resolve
24 * @param {string} to Non-absolute identifier or pathname to a file.
25 * @param {string} [from] If given, should attempt to find the <var>to</var> path in relation to this given, known path.
26 * @return {string}
27 */
283 ret.resolve = function (to, from) {
294971 if (basepath) {
304 from = basepath;
31 } else {
324967 from = (from) ? path.dirname(from) : process.cwd();
33 }
344971 return path.resolve(from, to);
35 };
36
37 /**
38 * Loads a single template. Given a unique <var>identifier</var> found by the <var>resolve</var> method this should return the given template.
39 * @alias load
40 * @param {string} identifier Unique identifier of a template (possibly an absolute path).
41 * @param {function} [cb] Asynchronous callback function. If not provided, this method should run synchronously.
42 * @return {string} Template source string.
43 */
443 ret.load = function (identifier, cb) {
4553 if (!fs || (cb && !fs.readFile) || !fs.readFileSync) {
460 throw new Error('Unable to find file ' + identifier + ' because there is no filesystem to read from.');
47 }
48
4953 identifier = ret.resolve(identifier);
50
5153 if (cb) {
525 fs.readFile(identifier, encoding, cb);
535 return;
54 }
5548 return fs.readFileSync(identifier, encoding);
56 };
57
583 return ret;
59};
60

/lib/loaders/index.js

100%
2
2
0
LineHitsSource
1/**
2 * @namespace TemplateLoader
3 * @description Swig is able to accept custom template loaders written by you, so that your templates can come from your favorite storage medium without needing to be part of the core library.
4 * A template loader consists of two methods: <var>resolve</var> and <var>load</var>. Each method is used internally by Swig to find and load the source of the template before attempting to parse and compile it.
5 * @example
6 * // A theoretical memcached loader
7 * var path = require('path'),
8 * Memcached = require('memcached');
9 * function memcachedLoader(locations, options) {
10 * var memcached = new Memcached(locations, options);
11 * return {
12 * resolve: function (to, from) {
13 * return path.resolve(from, to);
14 * },
15 * load: function (identifier, cb) {
16 * memcached.get(identifier, function (err, data) {
17 * // if (!data) { load from filesystem; }
18 * cb(err, data);
19 * });
20 * }
21 * };
22 * };
23 * // Tell swig about the loader:
24 * swig.setDefaults({ loader: memcachedLoader(['192.168.0.2']) });
25 */
26
27/**
28 * @function
29 * @name resolve
30 * @memberof TemplateLoader
31 * @description
32 * Resolves <var>to</var> to an absolute path or unique identifier. This is used for building correct, normalized, and absolute paths to a given template.
33 * @param {string} to Non-absolute identifier or pathname to a file.
34 * @param {string} [from] If given, should attempt to find the <var>to</var> path in relation to this given, known path.
35 * @return {string}
36 */
37
38/**
39 * @function
40 * @name load
41 * @memberof TemplateLoader
42 * @description
43 * Loads a single template. Given a unique <var>identifier</var> found by the <var>resolve</var> method this should return the given template.
44 * @param {string} identifier Unique identifier of a template (possibly an absolute path).
45 * @param {function} [cb] Asynchronous callback function. If not provided, this method should run synchronously.
46 * @return {string} Template source string.
47 */
48
49/**
50 * @private
51 */
521exports.fs = require('./filesystem');
531exports.memory = require('./memory');
54

/lib/loaders/memory.js

100%
20
20
0
LineHitsSource
11var path = require('path'),
2 utils = require('../utils');
3
4/**
5 * Loads templates from a provided object mapping.
6 * @alias swig.loaders.memory
7 * @example
8 * var templates = {
9 * "layout": "{% block content %}{% endblock %}",
10 * "home.html": "{% extends 'layout.html' %}{% block content %}...{% endblock %}"
11 * };
12 * swig.setDefaults({ loader: swig.loaders.memory(templates) });
13 *
14 * @param {object} mapping Hash object with template paths as keys and template sources as values.
15 * @param {string} [basepath] Path to the templates as string. Assigning this value allows you to use semi-absolute paths to templates instead of relative paths.
16 */
171module.exports = function (mapping, basepath) {
187 var ret = {};
19
207 basepath = (basepath) ? path.normalize(basepath) : null;
21
22 /**
23 * Resolves <var>to</var> to an absolute path or unique identifier. This is used for building correct, normalized, and absolute paths to a given template.
24 * @alias resolve
25 * @param {string} to Non-absolute identifier or pathname to a file.
26 * @param {string} [from] If given, should attempt to find the <var>to</var> path in relation to this given, known path.
27 * @return {string}
28 */
297 ret.resolve = function (to, from) {
3011 if (basepath) {
313 from = basepath;
32 } else {
338 from = (from) ? path.dirname(from) : '/';
34 }
3511 return path.resolve(from, to);
36 };
37
38 /**
39 * Loads a single template. Given a unique <var>identifier</var> found by the <var>resolve</var> method this should return the given template.
40 * @alias load
41 * @param {string} identifier Unique identifier of a template (possibly an absolute path).
42 * @param {function} [cb] Asynchronous callback function. If not provided, this method should run synchronously.
43 * @return {string} Template source string.
44 */
457 ret.load = function (pathname, cb) {
4610 var src, paths;
47
4810 paths = [pathname, pathname.replace(/^(\/|\\)/, '')];
49
5010 src = mapping[paths[0]] || mapping[paths[1]];
5110 if (!src) {
521 utils.throwError('Unable to find template "' + pathname + '".');
53 }
54
559 if (cb) {
562 cb(null, src);
572 return;
58 }
597 return src;
60 };
61
627 return ret;
63};
64

/lib/parser.js

99%
275
274
1
LineHitsSource
11var utils = require('./utils'),
2 lexer = require('./lexer');
3
41var _t = lexer.types,
5 _reserved = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with'];
6
7
8/**
9 * Filters are simply functions that perform transformations on their first input argument.
10 * Filters are run at render time, so they may not directly modify the compiled template structure in any way.
11 * All of Swig's built-in filters are written in this same way. For more examples, reference the `filters.js` file in Swig's source.
12 *
13 * To disable auto-escaping on a custom filter, simply add a property to the filter method `safe = true;` and the output from this will not be escaped, no matter what the global settings are for Swig.
14 *
15 * @typedef {function} Filter
16 *
17 * @example
18 * // This filter will return 'bazbop' if the idx on the input is not 'foobar'
19 * swig.setFilter('foobar', function (input, idx) {
20 * return input[idx] === 'foobar' ? input[idx] : 'bazbop';
21 * });
22 * // myvar = ['foo', 'bar', 'baz', 'bop'];
23 * // => {{ myvar|foobar(3) }}
24 * // Since myvar[3] !== 'foobar', we render:
25 * // => bazbop
26 *
27 * @example
28 * // This filter will disable auto-escaping on its output:
29 * function bazbop (input) { return input; }
30 * bazbop.safe = true;
31 * swig.setFilter('bazbop', bazbop);
32 * // => {{ "<p>"|bazbop }}
33 * // => <p>
34 *
35 * @param {*} input Input argument, automatically sent from Swig's built-in parser.
36 * @param {...*} [args] All other arguments are defined by the Filter author.
37 * @return {*}
38 */
39
40/*!
41 * Makes a string safe for a regular expression.
42 * @param {string} str
43 * @return {string}
44 * @private
45 */
461function escapeRegExp(str) {
472730 return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&');
48}
49
50/**
51 * Parse strings of variables and tags into tokens for future compilation.
52 * @class
53 * @param {array} tokens Pre-split tokens read by the Lexer.
54 * @param {object} filters Keyed object of filters that may be applied to variables.
55 * @param {boolean} autoescape Whether or not this should be autoescaped.
56 * @param {number} line Beginning line number for the first token.
57 * @param {string} [filename] Name of the file being parsed.
58 * @private
59 */
601function TokenParser(tokens, filters, autoescape, line, filename) {
61651 this.out = [];
62651 this.state = [];
63651 this.filterApplyIdx = [];
64651 this._parsers = {};
65651 this.line = line;
66651 this.filename = filename;
67651 this.filters = filters;
68651 this.escape = autoescape;
69
70651 this.parse = function () {
71647 var self = this;
72
73647 if (self._parsers.start) {
740 self._parsers.start.call(self);
75 }
76647 utils.each(tokens, function (token, i) {
772002 var prevToken = tokens[i - 1];
782002 self.isLast = (i === tokens.length - 1);
792002 if (prevToken) {
801376 while (prevToken.type === _t.WHITESPACE) {
81287 i -= 1;
82287 prevToken = tokens[i - 1];
83 }
84 }
852002 self.prevToken = prevToken;
862002 self.parseToken(token);
87 });
88589 if (self._parsers.end) {
8918 self._parsers.end.call(self);
90 }
91
92589 if (self.escape) {
93269 self.filterApplyIdx = [0];
94269 if (typeof self.escape === 'string') {
952 self.parseToken({ type: _t.FILTER, match: 'e' });
962 self.parseToken({ type: _t.COMMA, match: ',' });
972 self.parseToken({ type: _t.STRING, match: String(autoescape) });
982 self.parseToken({ type: _t.PARENCLOSE, match: ')'});
99 } else {
100267 self.parseToken({ type: _t.FILTEREMPTY, match: 'e' });
101 }
102 }
103
104589 return self.out;
105 };
106}
107
1081TokenParser.prototype = {
109 /**
110 * Set a custom method to be called when a token type is found.
111 *
112 * @example
113 * parser.on(types.STRING, function (token) {
114 * this.out.push(token.match);
115 * });
116 * @example
117 * parser.on('start', function () {
118 * this.out.push('something at the beginning of your args')
119 * });
120 * parser.on('end', function () {
121 * this.out.push('something at the end of your args');
122 * });
123 *
124 * @param {number} type Token type ID. Found in the Lexer.
125 * @param {Function} fn Callback function. Return true to continue executing the default parsing function.
126 * @return {undefined}
127 */
128 on: function (type, fn) {
129964 this._parsers[type] = fn;
130 },
131
132 /**
133 * Parse a single token.
134 * @param {{match: string, type: number, line: number}} token Lexer token object.
135 * @return {undefined}
136 * @private
137 */
138 parseToken: function (token) {
1392277 var self = this,
140 fn = self._parsers[token.type] || self._parsers['*'],
141 match = token.match,
142 prevToken = self.prevToken,
143 prevTokenType = prevToken ? prevToken.type : null,
144 lastState = (self.state.length) ? self.state[self.state.length - 1] : null,
145 temp;
146
1472277 if (fn && typeof fn === 'function') {
148494 if (!fn.call(this, token)) {
149387 return;
150 }
151 }
152
1531868 if (lastState && prevToken &&
154 lastState === _t.FILTER &&
155 prevTokenType === _t.FILTER &&
156 token.type !== _t.PARENCLOSE &&
157 token.type !== _t.COMMA &&
158 token.type !== _t.OPERATOR &&
159 token.type !== _t.FILTER &&
160 token.type !== _t.FILTEREMPTY) {
161107 self.out.push(', ');
162 }
163
1641868 if (lastState && lastState === _t.METHODOPEN) {
16519 self.state.pop();
16619 if (token.type !== _t.PARENCLOSE) {
16711 self.out.push(', ');
168 }
169 }
170
1711868 switch (token.type) {
172 case _t.WHITESPACE:
173279 break;
174
175 case _t.STRING:
176220 self.filterApplyIdx.push(self.out.length);
177220 self.out.push(match.replace(/\\/g, '\\\\'));
178220 break;
179
180 case _t.NUMBER:
181 case _t.BOOL:
182110 self.filterApplyIdx.push(self.out.length);
183110 self.out.push(match);
184110 break;
185
186 case _t.FILTER:
187111 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1881 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
189 }
190110 self.escape = self.filters[match].safe ? false : self.escape;
191110 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
192110 self.state.push(token.type);
193110 break;
194
195 case _t.FILTEREMPTY:
196320 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1971 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
198 }
199319 self.escape = self.filters[match].safe ? false : self.escape;
200319 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
201319 self.out.push(')');
202319 break;
203
204 case _t.FUNCTION:
205 case _t.FUNCTIONEMPTY:
20628 self.out.push('((typeof _ctx.' + match + ' !== "undefined") ? _ctx.' + match +
207 ' : ((typeof ' + match + ' !== "undefined") ? ' + match +
208 ' : _fn))(');
20928 self.escape = false;
21028 if (token.type === _t.FUNCTIONEMPTY) {
2119 self.out[self.out.length - 1] = self.out[self.out.length - 1] + ')';
212 } else {
21319 self.state.push(token.type);
214 }
21528 self.filterApplyIdx.push(self.out.length - 1);
21628 break;
217
218 case _t.PARENOPEN:
21923 self.state.push(token.type);
22023 if (self.filterApplyIdx.length) {
22121 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '(');
22221 if (prevToken && prevTokenType === _t.VAR) {
22319 temp = prevToken.match.split('.').slice(0, -1);
22419 self.out.push(' || _fn).call(' + self.checkMatch(temp));
22519 self.state.push(_t.METHODOPEN);
22619 self.escape = false;
227 } else {
2282 self.out.push(' || _fn)(');
229 }
23021 self.filterApplyIdx.push(self.out.length - 3);
231 } else {
2322 self.out.push('(');
2332 self.filterApplyIdx.push(self.out.length - 1);
234 }
23523 break;
236
237 case _t.PARENCLOSE:
238156 temp = self.state.pop();
239156 if (temp !== _t.PARENOPEN && temp !== _t.FUNCTION && temp !== _t.FILTER) {
2401 utils.throwError('Mismatched nesting state', self.line, self.filename);
241 }
242155 self.out.push(')');
243 // Once off the previous entry
244155 self.filterApplyIdx.pop();
245155 if (temp !== _t.FILTER) {
246 // Once for the open paren
24745 self.filterApplyIdx.pop();
248 }
249155 break;
250
251 case _t.COMMA:
252103 if (lastState !== _t.FUNCTION &&
253 lastState !== _t.FILTER &&
254 lastState !== _t.ARRAYOPEN &&
255 lastState !== _t.CURLYOPEN &&
256 lastState !== _t.PARENOPEN &&
257 lastState !== _t.COLON) {
2581 utils.throwError('Unexpected comma', self.line, self.filename);
259 }
260102 if (lastState === _t.COLON) {
2615 self.state.pop();
262 }
263102 self.out.push(', ');
264102 self.filterApplyIdx.pop();
265102 break;
266
267 case _t.LOGIC:
268 case _t.COMPARATOR:
2696 if (!prevToken ||
270 prevTokenType === _t.COMMA ||
271 prevTokenType === token.type ||
272 prevTokenType === _t.BRACKETOPEN ||
273 prevTokenType === _t.CURLYOPEN ||
274 prevTokenType === _t.PARENOPEN ||
275 prevTokenType === _t.FUNCTION) {
2761 utils.throwError('Unexpected logic', self.line, self.filename);
277 }
2785 self.out.push(token.match);
2795 break;
280
281 case _t.NOT:
2822 self.out.push(token.match);
2832 break;
284
285 case _t.VAR:
286433 self.parseVar(token, match, lastState);
287406 break;
288
289 case _t.BRACKETOPEN:
29018 if (!prevToken ||
291 (prevTokenType !== _t.VAR &&
292 prevTokenType !== _t.BRACKETCLOSE &&
293 prevTokenType !== _t.PARENCLOSE)) {
2944 self.state.push(_t.ARRAYOPEN);
2954 self.filterApplyIdx.push(self.out.length);
296 } else {
29714 self.state.push(token.type);
298 }
29918 self.out.push('[');
30018 break;
301
302 case _t.BRACKETCLOSE:
30318 temp = self.state.pop();
30418 if (temp !== _t.BRACKETOPEN && temp !== _t.ARRAYOPEN) {
3051 utils.throwError('Unexpected closing square bracket', self.line, self.filename);
306 }
30717 self.out.push(']');
30817 self.filterApplyIdx.pop();
30917 break;
310
311 case _t.CURLYOPEN:
3127 self.state.push(token.type);
3137 self.out.push('{');
3147 self.filterApplyIdx.push(self.out.length - 1);
3157 break;
316
317 case _t.COLON:
31812 if (lastState !== _t.CURLYOPEN) {
3191 utils.throwError('Unexpected colon', self.line, self.filename);
320 }
32111 self.state.push(token.type);
32211 self.out.push(':');
32311 self.filterApplyIdx.pop();
32411 break;
325
326 case _t.CURLYCLOSE:
3277 if (lastState === _t.COLON) {
3286 self.state.pop();
329 }
3307 if (self.state.pop() !== _t.CURLYOPEN) {
3311 utils.throwError('Unexpected closing curly brace', self.line, self.filename);
332 }
3336 self.out.push('}');
334
3356 self.filterApplyIdx.pop();
3366 break;
337
338 case _t.DOTKEY:
3398 if (!prevToken || (
340 prevTokenType !== _t.VAR &&
341 prevTokenType !== _t.BRACKETCLOSE &&
342 prevTokenType !== _t.DOTKEY &&
343 prevTokenType !== _t.PARENCLOSE &&
344 prevTokenType !== _t.FUNCTIONEMPTY &&
345 prevTokenType !== _t.FILTEREMPTY &&
346 prevTokenType !== _t.CURLYCLOSE
347 )) {
3481 utils.throwError('Unexpected key "' + match + '"', self.line, self.filename);
349 }
3507 self.out.push('.' + match);
3517 break;
352
353 case _t.OPERATOR:
3547 self.out.push(' ' + match + ' ');
3557 self.filterApplyIdx.pop();
3567 break;
357 }
358 },
359
360 /**
361 * Parse variable token
362 * @param {{match: string, type: number, line: number}} token Lexer token object.
363 * @param {string} match Shortcut for token.match
364 * @param {number} lastState Lexer token type state.
365 * @return {undefined}
366 * @private
367 */
368 parseVar: function (token, match, lastState) {
369433 var self = this;
370
371433 match = match.split('.');
372
373433 if (_reserved.indexOf(match[0]) !== -1) {
37426 utils.throwError('Reserved keyword "' + match[0] + '" attempted to be used as a variable', self.line, self.filename);
375 }
376
377407 self.filterApplyIdx.push(self.out.length);
378407 if (lastState === _t.CURLYOPEN) {
37910 if (match.length > 1) {
3801 utils.throwError('Unexpected dot', self.line, self.filename);
381 }
3829 self.out.push(match[0]);
3839 return;
384 }
385
386397 self.out.push(self.checkMatch(match));
387 },
388
389 /**
390 * Return contextual dot-check string for a match
391 * @param {string} match Shortcut for token.match
392 * @private
393 */
394 checkMatch: function (match) {
395416 var temp = match[0], result;
396
397416 function checkDot(ctx) {
3981248 var c = ctx + temp,
399 m = match,
400 build = '';
401
4021248 build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
4031248 utils.each(m, function (v, i) {
4041392 if (i === 0) {
4051248 return;
406 }
407144 build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
408144 c += '.' + v;
409 });
4101248 build += ')';
411
4121248 return build;
413 }
414
415416 function buildDot(ctx) {
416832 return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
417 }
418416 result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
419416 return '(' + result + ' !== null ? ' + result + ' : ' + '"" )';
420 }
421};
422
423/**
424 * Parse a source string into tokens that are ready for compilation.
425 *
426 * @example
427 * exports.parse('{{ tacos }}', {}, tags, filters);
428 * // => [{ compile: [Function], ... }]
429 *
430 * @param {string} source Swig template source.
431 * @param {object} opts Swig options object.
432 * @param {object} tags Keyed object of tags that can be parsed and compiled.
433 * @param {object} filters Keyed object of filters that may be applied to variables.
434 * @return {array} List of tokens ready for compilation.
435 */
4361exports.parse = function (source, opts, tags, filters) {
437455 source = source.replace(/\r\n/g, '\n');
438455 var escape = opts.autoescape,
439 tagOpen = opts.tagControls[0],
440 tagClose = opts.tagControls[1],
441 varOpen = opts.varControls[0],
442 varClose = opts.varControls[1],
443 escapedTagOpen = escapeRegExp(tagOpen),
444 escapedTagClose = escapeRegExp(tagClose),
445 escapedVarOpen = escapeRegExp(varOpen),
446 escapedVarClose = escapeRegExp(varClose),
447 tagStrip = new RegExp('^' + escapedTagOpen + '-?\\s*-?|-?\\s*-?' + escapedTagClose + '$', 'g'),
448 tagStripBefore = new RegExp('^' + escapedTagOpen + '-'),
449 tagStripAfter = new RegExp('-' + escapedTagClose + '$'),
450 varStrip = new RegExp('^' + escapedVarOpen + '-?\\s*-?|-?\\s*-?' + escapedVarClose + '$', 'g'),
451 varStripBefore = new RegExp('^' + escapedVarOpen + '-'),
452 varStripAfter = new RegExp('-' + escapedVarClose + '$'),
453 cmtOpen = opts.cmtControls[0],
454 cmtClose = opts.cmtControls[1],
455 anyChar = '[\\s\\S]*?',
456 // Split the template source based on variable, tag, and comment blocks
457 // /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\{#[\s\S]*?#\})/
458 splitter = new RegExp(
459 '(' +
460 escapedTagOpen + anyChar + escapedTagClose + '|' +
461 escapedVarOpen + anyChar + escapedVarClose + '|' +
462 escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) +
463 ')'
464 ),
465 line = 1,
466 stack = [],
467 parent = null,
468 tokens = [],
469 blocks = {},
470 inRaw = false,
471 stripNext;
472
473 /**
474 * Parse a variable.
475 * @param {string} str String contents of the variable, between <i>{{</i> and <i>}}</i>
476 * @param {number} line The line number that this variable starts on.
477 * @return {VarToken} Parsed variable token object.
478 * @private
479 */
480455 function parseVariable(str, line) {
481354 var tokens = lexer.read(utils.strip(str)),
482 parser,
483 out;
484
485354 parser = new TokenParser(tokens, filters, escape, line, opts.filename);
486354 out = parser.parse().join('');
487
488318 if (parser.state.length) {
4892 utils.throwError('Unable to parse "' + str + '"', line, opts.filename);
490 }
491
492 /**
493 * A parsed variable token.
494 * @typedef {object} VarToken
495 * @property {function} compile Method for compiling this token.
496 */
497316 return {
498 compile: function () {
499312 return '_output += ' + out + ';\n';
500 }
501 };
502 }
503455 exports.parseVariable = parseVariable;
504
505 /**
506 * Parse a tag.
507 * @param {string} str String contents of the tag, between <i>{%</i> and <i>%}</i>
508 * @param {number} line The line number that this tag starts on.
509 * @return {TagToken} Parsed token object.
510 * @private
511 */
512455 function parseTag(str, line) {
513478 var tokens, parser, chunks, tagName, tag, args, last;
514
515478 if (utils.startsWith(str, 'end')) {
516178 last = stack[stack.length - 1];
517178 if (last && last.name === str.split(/\s+/)[0].replace(/^end/, '') && last.ends) {
518176 switch (last.name) {
519 case 'autoescape':
5209 escape = opts.autoescape;
5219 break;
522 case 'raw':
5234 inRaw = false;
5244 break;
525 }
526176 stack.pop();
527176 return;
528 }
529
5302 if (!inRaw) {
5311 utils.throwError('Unexpected end of tag "' + str.replace(/^end/, '') + '"', line, opts.filename);
532 }
533 }
534
535301 if (inRaw) {
5363 return;
537 }
538
539298 chunks = str.split(/\s+(.+)?/);
540298 tagName = chunks.shift();
541
542298 if (!tags.hasOwnProperty(tagName)) {
5431 utils.throwError('Unexpected tag "' + str + '"', line, opts.filename);
544 }
545
546297 tokens = lexer.read(utils.strip(chunks.join(' ')));
547297 parser = new TokenParser(tokens, filters, false, line, opts.filename);
548297 tag = tags[tagName];
549
550 /**
551 * Define custom parsing methods for your tag.
552 * @callback parse
553 *
554 * @example
555 * exports.parse = function (str, line, parser, types, options) {
556 * parser.on('start', function () {
557 * // ...
558 * });
559 * parser.on(types.STRING, function (token) {
560 * // ...
561 * });
562 * };
563 *
564 * @param {string} str The full token string of the tag.
565 * @param {number} line The line number that this tag appears on.
566 * @param {TokenParser} parser A TokenParser instance.
567 * @param {TYPES} types Lexer token type enum.
568 * @param {TagToken[]} stack The current stack of open tags.
569 * @param {SwigOpts} options Swig Options Object.
570 */
571297 if (!tag.parse(chunks[1], line, parser, _t, stack, opts)) {
5722 utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename);
573 }
574
575293 parser.parse();
576271 args = parser.out;
577
578271 switch (tagName) {
579 case 'autoescape':
5809 escape = (args[0] !== 'false') ? args[0] : false;
5819 break;
582 case 'raw':
5834 inRaw = true;
5844 break;
585 }
586
587 /**
588 * A parsed tag token.
589 * @typedef {Object} TagToken
590 * @property {compile} [compile] Method for compiling this token.
591 * @property {array} [args] Array of arguments for the tag.
592 * @property {Token[]} [content=[]] An array of tokens that are children of this Token.
593 * @property {boolean} [ends] Whether or not this tag requires an end tag.
594 * @property {string} name The name of this tag.
595 */
596271 return {
597 block: !!tags[tagName].block,
598 compile: tag.compile,
599 args: args,
600 content: [],
601 ends: tag.ends,
602 name: tagName
603 };
604 }
605
606 /**
607 * Strip the whitespace from the previous token, if it is a string.
608 * @param {object} token Parsed token.
609 * @return {object} If the token was a string, trailing whitespace will be stripped.
610 */
611455 function stripPrevToken(token) {
61210 if (typeof token === 'string') {
6138 token = token.replace(/\s*$/, '');
614 }
61510 return token;
616 }
617
618 /*!
619 * Loop over the source, split via the tag/var/comment regular expression splitter.
620 * Send each chunk to the appropriate parser.
621 */
622455 utils.each(source.split(splitter), function (chunk) {
6232071 var token, lines, stripPrev, prevToken, prevChildToken;
624
6252071 if (!chunk) {
626896 return;
627 }
628
629 // Is a variable?
6301175 if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
631354 stripPrev = varStripBefore.test(chunk);
632354 stripNext = varStripAfter.test(chunk);
633354 token = parseVariable(chunk.replace(varStrip, ''), line);
634 // Is a tag?
635821 } else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
636478 stripPrev = tagStripBefore.test(chunk);
637478 stripNext = tagStripAfter.test(chunk);
638478 token = parseTag(chunk.replace(tagStrip, ''), line);
639450 if (token) {
640271 if (token.name === 'extends') {
64125 parent = token.args.join('').replace(/^\'|\'$/g, '').replace(/^\"|\"$/g, '');
642 }
643
644271 if (token.block && (!stack.length || token.name === 'block')) {
645109 blocks[token.args.join('')] = token;
646 }
647 }
648450 if (inRaw && !token) {
6493 token = chunk;
650 }
651 // Is a content string?
652343 } else if (inRaw || (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))) {
653336 token = (stripNext) ? chunk.replace(/^\s*/, '') : chunk;
654336 stripNext = false;
6557 } else if (utils.startsWith(chunk, cmtOpen) && utils.endsWith(chunk, cmtClose)) {
6567 return;
657 }
658
659 // Did this tag ask to strip previous whitespace? <code>{%- ... %}</code> or <code>{{- ... }}</code>
6601102 if (stripPrev && tokens.length) {
66110 prevToken = tokens.pop();
66210 if (typeof prevToken === 'string') {
6634 prevToken = stripPrevToken(prevToken);
6646 } else if (prevToken.content && prevToken.content.length) {
6656 prevChildToken = stripPrevToken(prevToken.content.pop());
6666 prevToken.content.push(prevChildToken);
667 }
66810 tokens.push(prevToken);
669 }
670
671 // This was a comment, so let's just keep going.
6721102 if (!token) {
673182 return;
674 }
675
676 // If there's an open item in the stack, add this to its content.
677920 if (stack.length) {
678267 stack[stack.length - 1].content.push(token);
679 } else {
680653 tokens.push(token);
681 }
682
683 // If the token is a tag that requires an end tag, open it on the stack.
684920 if (token.name && token.ends) {
685179 stack.push(token);
686 }
687
688920 lines = chunk.match(/\n/g);
689920 line += (lines) ? lines.length : 0;
690 });
691
692389 return {
693 name: opts.filename,
694 parent: parent,
695 tokens: tokens,
696 blocks: blocks
697 };
698};
699
700
701/**
702 * Compile an array of tokens.
703 * @param {Token[]} template An array of template tokens.
704 * @param {Templates[]} parents Array of parent templates.
705 * @param {SwigOpts} [options] Swig options object.
706 * @param {string} [blockName] Name of the current block context.
707 * @return {string} Partial for a compiled JavaScript method that will output a rendered template.
708 */
7091exports.compile = function (template, parents, options, blockName) {
710516 var out = '',
711 tokens = utils.isArray(template) ? template : template.tokens;
712
713516 utils.each(tokens, function (token) {
714771 var o;
715771 if (typeof token === 'string') {
716255 out += '_output += "' + token.replace(/\\/g, '\\\\').replace(/\n|\r/g, '\\n').replace(/"/g, '\\"') + '";\n';
717255 return;
718 }
719
720 /**
721 * Compile callback for VarToken and TagToken objects.
722 * @callback compile
723 *
724 * @example
725 * exports.compile = function (compiler, args, content, parents, options, blockName) {
726 * if (args[0] === 'foo') {
727 * return compiler(content, parents, options, blockName) + '\n';
728 * }
729 * return '_output += "fallback";\n';
730 * };
731 *
732 * @param {parserCompiler} compiler
733 * @param {array} [args] Array of parsed arguments on the for the token.
734 * @param {array} [content] Array of content within the token.
735 * @param {array} [parents] Array of parent templates for the current template context.
736 * @param {SwigOpts} [options] Swig Options Object
737 * @param {string} [blockName] Name of the direct block parent, if any.
738 */
739516 o = token.compile(exports.compile, token.args ? token.args.slice(0) : [], token.content ? token.content.slice(0) : [], parents, options, blockName);
740516 out += o || '';
741 });
742
743516 return out;
744};
745

/lib/swig.js

99%
205
204
1
LineHitsSource
11var utils = require('./utils'),
2 _tags = require('./tags'),
3 _filters = require('./filters'),
4 parser = require('./parser'),
5 dateformatter = require('./dateformatter'),
6 loaders = require('./loaders');
7
8/**
9 * Swig version number as a string.
10 * @example
11 * if (swig.version === "1.4.1") { ... }
12 *
13 * @type {String}
14 */
151exports.version = "1.4.1";
16
17/**
18 * Swig Options Object. This object can be passed to many of the API-level Swig methods to control various aspects of the engine. All keys are optional.
19 * @typedef {Object} SwigOpts
20 * @property {boolean} autoescape Controls whether or not variable output will automatically be escaped for safe HTML output. Defaults to <code data-language="js">true</code>. Functions executed in variable statements will not be auto-escaped. Your application/functions should take care of their own auto-escaping.
21 * @property {array} varControls Open and close controls for variables. Defaults to <code data-language="js">['{{', '}}']</code>.
22 * @property {array} tagControls Open and close controls for tags. Defaults to <code data-language="js">['{%', '%}']</code>.
23 * @property {array} cmtControls Open and close controls for comments. Defaults to <code data-language="js">['{#', '#}']</code>.
24 * @property {object} locals Default variable context to be passed to <strong>all</strong> templates.
25 * @property {CacheOptions} cache Cache control for templates. Defaults to saving in <code data-language="js">'memory'</code>. Send <code data-language="js">false</code> to disable. Send an object with <code data-language="js">get</code> and <code data-language="js">set</code> functions to customize.
26 * @property {TemplateLoader} loader The method that Swig will use to load templates. Defaults to <var>swig.loaders.fs</var>.
27 */
281var defaultOptions = {
29 autoescape: true,
30 varControls: ['{{', '}}'],
31 tagControls: ['{%', '%}'],
32 cmtControls: ['{#', '#}'],
33 locals: {},
34 /**
35 * Cache control for templates. Defaults to saving all templates into memory.
36 * @typedef {boolean|string|object} CacheOptions
37 * @example
38 * // Default
39 * swig.setDefaults({ cache: 'memory' });
40 * @example
41 * // Disables caching in Swig.
42 * swig.setDefaults({ cache: false });
43 * @example
44 * // Custom cache storage and retrieval
45 * swig.setDefaults({
46 * cache: {
47 * get: function (key) { ... },
48 * set: function (key, val) { ... }
49 * }
50 * });
51 */
52 cache: 'memory',
53 /**
54 * Configure Swig to use either the <var>swig.loaders.fs</var> or <var>swig.loaders.memory</var> template loader. Or, you can write your own!
55 * For more information, please see the <a href="../loaders/">Template Loaders documentation</a>.
56 * @typedef {class} TemplateLoader
57 * @example
58 * // Default, FileSystem loader
59 * swig.setDefaults({ loader: swig.loaders.fs() });
60 * @example
61 * // FileSystem loader allowing a base path
62 * // With this, you don't use relative URLs in your template references
63 * swig.setDefaults({ loader: swig.loaders.fs(__dirname + '/templates') });
64 * @example
65 * // Memory Loader
66 * swig.setDefaults({ loader: swig.loaders.memory({
67 * layout: '{% block foo %}{% endblock %}',
68 * page1: '{% extends "layout" %}{% block foo %}Tacos!{% endblock %}'
69 * })});
70 */
71 loader: loaders.fs()
72 },
73 defaultInstance;
74
75/**
76 * Empty function, used in templates.
77 * @return {string} Empty string
78 * @private
79 */
802function efn() { return ''; }
81
82/**
83 * Validate the Swig options object.
84 * @param {?SwigOpts} options Swig options object.
85 * @return {undefined} This method will throw errors if anything is wrong.
86 * @private
87 */
881function validateOptions(options) {
89838 if (!options) {
9087 return;
91 }
92
93751 utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
942238 if (!options.hasOwnProperty(key)) {
951217 return;
96 }
971021 if (!utils.isArray(options[key]) || options[key].length !== 2) {
986 throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
99 }
1001015 if (options[key][0] === options[key][1]) {
1013 throw new Error('Option "' + key + '" open and close controls must not be the same.');
102 }
1031012 utils.each(options[key], function (a, i) {
1042021 if (a.length < 2) {
1056 throw new Error('Option "' + key + '" ' + ((i) ? 'open ' : 'close ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
106 }
107 });
108 });
109
110736 if (options.hasOwnProperty('cache')) {
111337 if (options.cache && options.cache !== 'memory') {
1123 if (!options.cache.get || !options.cache.set) {
1132 throw new Error('Invalid cache option ' + JSON.stringify(options.cache) + ' found. Expected "memory" or { get: function (key) { ... }, set: function (key, value) { ... } }.');
114 }
115 }
116 }
117734 if (options.hasOwnProperty('loader')) {
118344 if (options.loader) {
119344 if (!options.loader.load || !options.loader.resolve) {
1203 throw new Error('Invalid loader option ' + JSON.stringify(options.loader) + ' found. Expected { load: function (pathname, cb) { ... }, resolve: function (to, from) { ... } }.');
121 }
122 }
123 }
124
125}
126
127/**
128 * Set defaults for the base and all new Swig environments.
129 *
130 * @example
131 * swig.setDefaults({ cache: false });
132 * // => Disables Cache
133 *
134 * @example
135 * swig.setDefaults({ locals: { now: function () { return new Date(); } }});
136 * // => sets a globally accessible method for all template
137 * // contexts, allowing you to print the current date
138 * // => {{ now()|date('F jS, Y') }}
139 *
140 * @param {SwigOpts} [options={}] Swig options object.
141 * @return {undefined}
142 */
1431exports.setDefaults = function (options) {
144342 validateOptions(options);
145338 defaultInstance.options = utils.extend(defaultInstance.options, options);
146};
147
148/**
149 * Set the default TimeZone offset for date formatting via the date filter. This is a global setting and will affect all Swig environments, old or new.
150 * @param {number} offset Offset from GMT, in minutes.
151 * @return {undefined}
152 */
1531exports.setDefaultTZOffset = function (offset) {
1542 dateformatter.tzOffset = offset;
155};
156
157/**
158 * Create a new, separate Swig compile/render environment.
159 *
160 * @example
161 * var swig = require('swig');
162 * var myswig = new swig.Swig({varControls: ['<%=', '%>']});
163 * myswig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
164 * // => Tacos are delicious!
165 * swig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
166 * // => 'Tacos are <%= tacos =>!'
167 *
168 * @param {SwigOpts} [opts={}] Swig options object.
169 * @return {object} New Swig environment.
170 */
1711exports.Swig = function (opts) {
17226 validateOptions(opts);
17325 this.options = utils.extend({}, defaultOptions, opts || {});
17425 this.cache = {};
17525 this.extensions = {};
17625 var self = this,
177 tags = _tags,
178 filters = _filters;
179
180 /**
181 * Get combined locals context.
182 * @param {?SwigOpts} [options] Swig options object.
183 * @return {object} Locals context.
184 * @private
185 */
18625 function getLocals(options) {
187901 if (!options || !options.locals) {
188332 return self.options.locals;
189 }
190
191569 return utils.extend({}, self.options.locals, options.locals);
192 }
193
194 /**
195 * Get compiled template from the cache.
196 * @param {string} key Name of template.
197 * @return {object|undefined} Template function and tokens.
198 * @private
199 */
20025 function cacheGet(key) {
2014970 if (!self.options.cache) {
2022 return;
203 }
204
2054968 if (self.options.cache === 'memory') {
2064967 return self.cache[key];
207 }
208
2091 return self.options.cache.get(key);
210 }
211
212 /**
213 * Store a template in the cache.
214 * @param {string} key Name of template.
215 * @param {object} val Template function and tokens.
216 * @return {undefined}
217 * @private
218 */
21925 function cacheSet(key, val) {
22037 if (!self.options.cache) {
2212 return;
222 }
223
22435 if (self.options.cache === 'memory') {
22534 self.cache[key] = val;
22634 return;
227 }
228
2291 self.options.cache.set(key, val);
230 }
231
232 /**
233 * Clears the in-memory template cache.
234 *
235 * @example
236 * swig.invalidateCache();
237 *
238 * @return {undefined}
239 */
24025 this.invalidateCache = function () {
241332 if (self.options.cache === 'memory') {
242332 self.cache = {};
243 }
244 };
245
246 /**
247 * Add a custom filter for swig variables.
248 *
249 * @example
250 * function replaceMs(input) { return input.replace(/m/g, 'f'); }
251 * swig.setFilter('replaceMs', replaceMs);
252 * // => {{ "onomatopoeia"|replaceMs }}
253 * // => onofatopeia
254 *
255 * @param {string} name Name of filter, used in templates. <strong>Will</strong> overwrite previously defined filters, if using the same name.
256 * @param {function} method Function that acts against the input. See <a href="/docs/filters/#custom">Custom Filters</a> for more information.
257 * @return {undefined}
258 */
25925 this.setFilter = function (name, method) {
2603 if (typeof method !== "function") {
2611 throw new Error('Filter "' + name + '" is not a valid function.');
262 }
2632 filters[name] = method;
264 };
265
266 /**
267 * Add a custom tag. To expose your own extensions to compiled template code, see <code data-language="js">swig.setExtension</code>.
268 *
269 * For a more in-depth explanation of writing custom tags, see <a href="../extending/#tags">Custom Tags</a>.
270 *
271 * @example
272 * var tacotag = require('./tacotag');
273 * swig.setTag('tacos', tacotag.parse, tacotag.compile, tacotag.ends, tacotag.blockLevel);
274 * // => {% tacos %}Make this be tacos.{% endtacos %}
275 * // => Tacos tacos tacos tacos.
276 *
277 * @param {string} name Tag name.
278 * @param {function} parse Method for parsing tokens.
279 * @param {function} compile Method for compiling renderable output.
280 * @param {boolean} [ends=false] Whether or not this tag requires an <i>end</i> tag.
281 * @param {boolean} [blockLevel=false] If false, this tag will not be compiled outside of <code>block</code> tags when extending a parent template.
282 * @return {undefined}
283 */
28425 this.setTag = function (name, parse, compile, ends, blockLevel) {
2854 if (typeof parse !== 'function') {
2861 throw new Error('Tag "' + name + '" parse method is not a valid function.');
287 }
288
2893 if (typeof compile !== 'function') {
2901 throw new Error('Tag "' + name + '" compile method is not a valid function.');
291 }
292
2932 tags[name] = {
294 parse: parse,
295 compile: compile,
296 ends: ends || false,
297 block: !!blockLevel
298 };
299 };
300
301 /**
302 * Add extensions for custom tags. This allows any custom tag to access a globally available methods via a special globally available object, <var>_ext</var>, in templates.
303 *
304 * @example
305 * swig.setExtension('trans', function (v) { return translate(v); });
306 * function compileTrans(compiler, args, content, parent, options) {
307 * return '_output += _ext.trans(' + args[0] + ');'
308 * };
309 * swig.setTag('trans', parseTrans, compileTrans, true);
310 *
311 * @param {string} name Key name of the extension. Accessed via <code data-language="js">_ext[name]</code>.
312 * @param {*} object The method, value, or object that should be available via the given name.
313 * @return {undefined}
314 */
31525 this.setExtension = function (name, object) {
3161 self.extensions[name] = object;
317 };
318
319 /**
320 * Parse a given source string into tokens.
321 *
322 * @param {string} source Swig template source.
323 * @param {SwigOpts} [options={}] Swig options object.
324 * @return {object} parsed Template tokens object.
325 * @private
326 */
32725 this.parse = function (source, options) {
328470 validateOptions(options);
329
330455 var locals = getLocals(options),
331 opts = {},
332 k;
333
334455 for (k in options) {
335393 if (options.hasOwnProperty(k) && k !== 'locals') {
336110 opts[k] = options[k];
337 }
338 }
339
340455 options = utils.extend({}, self.options, opts);
341455 options.locals = locals;
342
343455 return parser.parse(source, options, tags, filters);
344 };
345
346 /**
347 * Parse a given file into tokens.
348 *
349 * @param {string} pathname Full path to file to parse.
350 * @param {SwigOpts} [options={}] Swig options object.
351 * @return {object} parsed Template tokens object.
352 * @private
353 */
35425 this.parseFile = function (pathname, options) {
35527 var src;
356
35727 if (!options) {
3580 options = {};
359 }
360
36127 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
362
36327 src = self.options.loader.load(pathname);
364
36526 if (!options.filename) {
3664 options = utils.extend({ filename: pathname }, options);
367 }
368
36926 return self.parse(src, options);
370 };
371
372 /**
373 * Re-Map blocks within a list of tokens to the template's block objects.
374 * @param {array} tokens List of tokens for the parent object.
375 * @param {object} template Current template that needs to be mapped to the parent's block and token list.
376 * @return {array}
377 * @private
378 */
37925 function remapBlocks(blocks, tokens) {
38049 return utils.map(tokens, function (token) {
381111 var args = token.args ? token.args.join('') : '';
382111 if (token.name === 'block' && blocks[args]) {
38321 token = blocks[args];
384 }
385111 if (token.content && token.content.length) {
38627 token.content = remapBlocks(blocks, token.content);
387 }
388111 return token;
389 });
390 }
391
392 /**
393 * Import block-level tags to the token list that are not actual block tags.
394 * @param {array} blocks List of block-level tags.
395 * @param {array} tokens List of tokens to render.
396 * @return {undefined}
397 * @private
398 */
39925 function importNonBlocks(blocks, tokens) {
40022 utils.each(blocks, function (block) {
40127 if (block.name !== 'block') {
4023 tokens.unshift(block);
403 }
404 });
405 }
406
407 /**
408 * Recursively compile and get parents of given parsed token object.
409 *
410 * @param {object} tokens Parsed tokens from template.
411 * @param {SwigOpts} [options={}] Swig options object.
412 * @return {object} Parsed tokens from parent templates.
413 * @private
414 */
41525 function getParents(tokens, options) {
416363 var parentName = tokens.parent,
417 parentFiles = [],
418 parents = [],
419 parentFile,
420 parent,
421 l;
422
423363 while (parentName) {
42427 if (!options || !options.filename) {
4251 throw new Error('Cannot extend "' + parentName + '" because current template has no filename.');
426 }
427
42826 parentFile = parentFile || options.filename;
42926 parentFile = self.options.loader.resolve(parentName, parentFile);
43026 parent = cacheGet(parentFile) || self.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
43125 parentName = parent.parent;
432
43325 if (parentFiles.indexOf(parentFile) !== -1) {
4341 throw new Error('Illegal circular extends of "' + parentFile + '".');
435 }
43624 parentFiles.push(parentFile);
437
43824 parents.push(parent);
439 }
440
441 // Remap each parents'(1) blocks onto its own parent(2), receiving the full token list for rendering the original parent(1) on its own.
442360 l = parents.length;
443360 for (l = parents.length - 2; l >= 0; l -= 1) {
4446 parents[l].tokens = remapBlocks(parents[l].blocks, parents[l + 1].tokens);
4456 importNonBlocks(parents[l].blocks, parents[l].tokens);
446 }
447
448360 return parents;
449 }
450
451 /**
452 * Pre-compile a source string into a cache-able template function.
453 *
454 * @example
455 * swig.precompile('{{ tacos }}');
456 * // => {
457 * // tpl: function (_swig, _locals, _filters, _utils, _fn) { ... },
458 * // tokens: {
459 * // name: undefined,
460 * // parent: null,
461 * // tokens: [...],
462 * // blocks: {}
463 * // }
464 * // }
465 *
466 * In order to render a pre-compiled template, you must have access to filters and utils from Swig. <var>efn</var> is simply an empty function that does nothing.
467 *
468 * @param {string} source Swig template source string.
469 * @param {SwigOpts} [options={}] Swig options object.
470 * @return {object} Renderable function and tokens object.
471 */
47225 this.precompile = function (source, options) {
473444 var tokens = self.parse(source, options),
474 parents = getParents(tokens, options),
475 tpl;
476
477360 if (parents.length) {
478 // Remap the templates first-parent's tokens using this template's blocks.
47916 tokens.tokens = remapBlocks(tokens.blocks, parents[0].tokens);
48016 importNonBlocks(tokens.blocks, tokens.tokens);
481 }
482
483360 tpl = new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
484 ' var _ext = _swig.extensions,\n' +
485 ' _output = "";\n' +
486 parser.compile(tokens, parents, options) + '\n' +
487 ' return _output;\n'
488 );
489
490360 return { tpl: tpl, tokens: tokens };
491 };
492
493 /**
494 * Compile and render a template string for final output.
495 *
496 * When rendering a source string, a file path should be specified in the options object in order for <var>extends</var>, <var>include</var>, and <var>import</var> to work properly. Do this by adding <code data-language="js">{ filename: '/absolute/path/to/mytpl.html' }</code> to the options argument.
497 *
498 * @example
499 * swig.render('{{ tacos }}', { locals: { tacos: 'Tacos!!!!' }});
500 * // => Tacos!!!!
501 *
502 * @param {string} source Swig template source string.
503 * @param {SwigOpts} [options={}] Swig options object.
504 * @return {string} Rendered output.
505 */
50625 this.render = function (source, options) {
507381 return self.compile(source, options)();
508 };
509
510 /**
511 * Compile and render a template file for final output. This is most useful for libraries like Express.js.
512 *
513 * @example
514 * swig.renderFile('./template.html', {}, function (err, output) {
515 * if (err) {
516 * throw err;
517 * }
518 * console.log(output);
519 * });
520 *
521 * @example
522 * swig.renderFile('./template.html', {});
523 * // => output
524 *
525 * @param {string} pathName File location.
526 * @param {object} [locals={}] Template variable context.
527 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
528 * @return {string} Rendered output.
529 */
53025 this.renderFile = function (pathName, locals, cb) {
53112 if (cb) {
5325 self.compileFile(pathName, {}, function (err, fn) {
5335 var result;
534
5355 if (err) {
5361 cb(err);
5371 return;
538 }
539
5404 try {
5414 result = fn(locals);
542 } catch (err2) {
5431 cb(err2);
5441 return;
545 }
546
5473 cb(null, result);
548 });
5495 return;
550 }
551
5527 return self.compileFile(pathName)(locals);
553 };
554
555 /**
556 * Compile string source into a renderable template function.
557 *
558 * @example
559 * var tpl = swig.compile('{{ tacos }}');
560 * // => {
561 * // [Function: compiled]
562 * // parent: null,
563 * // tokens: [{ compile: [Function] }],
564 * // blocks: {}
565 * // }
566 * tpl({ tacos: 'Tacos!!!!' });
567 * // => Tacos!!!!
568 *
569 * When compiling a source string, a file path should be specified in the options object in order for <var>extends</var>, <var>include</var>, and <var>import</var> to work properly. Do this by adding <code data-language="js">{ filename: '/absolute/path/to/mytpl.html' }</code> to the options argument.
570 *
571 * @param {string} source Swig template source string.
572 * @param {SwigOpts} [options={}] Swig options object.
573 * @return {function} Renderable function with keys for parent, blocks, and tokens.
574 */
57525 this.compile = function (source, options) {
576442 var key = options ? options.filename : null,
577 cached = key ? cacheGet(key) : null,
578 context,
579 contextLength,
580 pre;
581
582442 if (cached) {
5831 return cached;
584 }
585
586441 context = getLocals(options);
587441 contextLength = utils.keys(context).length;
588441 pre = this.precompile(source, options);
589
590357 function compiled(locals) {
5915191 var lcls;
5925191 if (locals && contextLength) {
5931 lcls = utils.extend({}, context, locals);
5945190 } else if (locals && !contextLength) {
5954869 lcls = locals;
596321 } else if (!locals && contextLength) {
597276 lcls = context;
598 } else {
59945 lcls = {};
600 }
6015191 return pre.tpl(self, lcls, filters, utils, efn);
602 }
603
604357 utils.extend(compiled, pre.tokens);
605
606357 if (key) {
60736 cacheSet(key, compiled);
608 }
609
610357 return compiled;
611 };
612
613 /**
614 * Compile a source file into a renderable template function.
615 *
616 * @example
617 * var tpl = swig.compileFile('./mytpl.html');
618 * // => {
619 * // [Function: compiled]
620 * // parent: null,
621 * // tokens: [{ compile: [Function] }],
622 * // blocks: {}
623 * // }
624 * tpl({ tacos: 'Tacos!!!!' });
625 * // => Tacos!!!!
626 *
627 * @example
628 * swig.compileFile('/myfile.txt', { varControls: ['<%=', '=%>'], tagControls: ['<%', '%>']});
629 * // => will compile 'myfile.txt' using the var and tag controls as specified.
630 *
631 * @param {string} pathname File location.
632 * @param {SwigOpts} [options={}] Swig options object.
633 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
634 * @return {function} Renderable function with keys for parent, blocks, and tokens.
635 */
63625 this.compileFile = function (pathname, options, cb) {
6374876 var src, cached;
638
6394876 if (!options) {
64021 options = {};
641 }
642
6434876 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
6444875 if (!options.filename) {
6454875 options = utils.extend({ filename: pathname }, options);
646 }
6474875 cached = cacheGet(pathname);
648
6494875 if (cached) {
6504840 if (cb) {
6511 cb(null, cached);
6521 return;
653 }
6544839 return cached;
655 }
656
65735 if (cb) {
6587 self.options.loader.load(pathname, function (err, src) {
6597 if (err) {
6601 cb(err);
6611 return;
662 }
6636 var compiled;
664
6656 try {
6666 compiled = self.compile(src, options);
667 } catch (err2) {
6681 cb(err2);
6691 return;
670 }
671
6725 cb(err, compiled);
673 });
6747 return;
675 }
676
67728 src = self.options.loader.load(pathname);
67826 return self.compile(src, options);
679 };
680
681 /**
682 * Run a pre-compiled template function. This is most useful in the browser when you've pre-compiled your templates with the Swig command-line tool.
683 *
684 * @example
685 * $ swig compile ./mytpl.html --wrap-start="var mytpl = " > mytpl.js
686 * @example
687 * <script src="mytpl.js"></script>
688 * <script>
689 * swig.run(mytpl, {});
690 * // => "rendered template..."
691 * </script>
692 *
693 * @param {function} tpl Pre-compiled Swig template function. Use the Swig CLI to compile your templates.
694 * @param {object} [locals={}] Template variable context.
695 * @param {string} [filepath] Filename used for caching the template.
696 * @return {string} Rendered output.
697 */
69825 this.run = function (tpl, locals, filepath) {
6995 var context = getLocals({ locals: locals });
7005 if (filepath) {
7011 cacheSet(filepath, tpl);
702 }
7035 return tpl(self, context, filters, utils, efn);
704 };
705};
706
707/*!
708 * Export methods publicly
709 */
7101defaultInstance = new exports.Swig();
7111exports.setFilter = defaultInstance.setFilter;
7121exports.setTag = defaultInstance.setTag;
7131exports.setExtension = defaultInstance.setExtension;
7141exports.parseFile = defaultInstance.parseFile;
7151exports.precompile = defaultInstance.precompile;
7161exports.compile = defaultInstance.compile;
7171exports.compileFile = defaultInstance.compileFile;
7181exports.render = defaultInstance.render;
7191exports.renderFile = defaultInstance.renderFile;
7201exports.run = defaultInstance.run;
7211exports.invalidateCache = defaultInstance.invalidateCache;
7221exports.loaders = loaders;
723

/lib/tags/autoescape.js

100%
13
13
0
LineHitsSource
11var utils = require('../utils'),
2 strings = ['html', 'js'];
3
4/**
5 * Control auto-escaping of variable output from within your templates.
6 *
7 * @alias autoescape
8 *
9 * @example
10 * // myvar = '<foo>';
11 * {% autoescape true %}{{ myvar }}{% endautoescape %}
12 * // => <foo>
13 * {% autoescape false %}{{ myvar }}{% endautoescape %}
14 * // => <foo>
15 *
16 * @param {boolean|string} control One of `true`, `false`, `"js"` or `"html"`.
17 */
181exports.compile = function (compiler, args, content, parents, options, blockName) {
196 return compiler(content, parents, options, blockName);
20};
211exports.parse = function (str, line, parser, types, stack, opts) {
2211 var matched;
2311 parser.on('*', function (token) {
2412 if (!matched &&
25 (token.type === types.BOOL ||
26 (token.type === types.STRING && strings.indexOf(token.match) === -1))
27 ) {
2810 this.out.push(token.match);
2910 matched = true;
3010 return;
31 }
322 utils.throwError('Unexpected token "' + token.match + '" in autoescape tag', line, opts.filename);
33 });
34
3511 return true;
36};
371exports.ends = true;
38

/lib/tags/block.js

100%
8
8
0
LineHitsSource
1/**
2 * Defines a block in a template that can be overridden by a template extending this one and/or will override the current template's parent template block of the same name.
3 *
4 * See <a href="#inheritance">Template Inheritance</a> for more information.
5 *
6 * @alias block
7 *
8 * @example
9 * {% block body %}...{% endblock %}
10 *
11 * @param {literal} name Name of the block for use in parent and extended templates.
12 */
131exports.compile = function (compiler, args, content, parents, options) {
1423 return compiler(content, parents, options, args.join(''));
15};
16
171exports.parse = function (str, line, parser) {
1840 parser.on('*', function (token) {
1940 this.out.push(token.match);
20 });
2140 return true;
22};
23
241exports.ends = true;
251exports.block = true;
26

/lib/tags/else.js

100%
6
6
0
LineHitsSource
1/**
2 * Used within an <code data-language="swig">{% if %}</code> tag, the code block following this tag up until <code data-language="swig">{% endif %}</code> will be rendered if the <i>if</i> statement returns false.
3 *
4 * @alias else
5 *
6 * @example
7 * {% if false %}
8 * statement1
9 * {% else %}
10 * statement2
11 * {% endif %}
12 * // => statement2
13 *
14 */
151exports.compile = function () {
163 return '} else {\n';
17};
18
191exports.parse = function (str, line, parser, types, stack) {
205 parser.on('*', function (token) {
211 throw new Error('"else" tag does not accept any tokens. Found "' + token.match + '" on line ' + line + '.');
22 });
23
245 return (stack.length && stack[stack.length - 1].name === 'if');
25};
26

/lib/tags/elseif.js

100%
6
6
0
LineHitsSource
11var ifparser = require('./if').parse;
2
3/**
4 * Like <code data-language="swig">{% else %}</code>, except this tag can take more conditional statements.
5 *
6 * @alias elseif
7 * @alias elif
8 *
9 * @example
10 * {% if false %}
11 * Tacos
12 * {% elseif true %}
13 * Burritos
14 * {% else %}
15 * Churros
16 * {% endif %}
17 * // => Burritos
18 *
19 * @param {...mixed} conditional Conditional statement that returns a truthy or falsy value.
20 */
211exports.compile = function (compiler, args) {
225 return '} else if (' + args.join(' ') + ') {\n';
23};
24
251exports.parse = function (str, line, parser, types, stack) {
267 var okay = ifparser(str, line, parser, types, stack);
276 return okay && (stack.length && stack[stack.length - 1].name === 'if');
28};
29

/lib/tags/extends.js

100%
4
4
0
LineHitsSource
1/**
2 * Makes the current template extend a parent template. This tag must be the first item in your template.
3 *
4 * See <a href="#inheritance">Template Inheritance</a> for more information.
5 *
6 * @alias extends
7 *
8 * @example
9 * {% extends "./layout.html" %}
10 *
11 * @param {string} parentFile Relative path to the file that this template extends.
12 */
131exports.compile = function () {};
14
151exports.parse = function () {
1625 return true;
17};
18
191exports.ends = false;
20

/lib/tags/filter.js

100%
29
29
0
LineHitsSource
11var filters = require('../filters');
2
3/**
4 * Apply a filter to an entire block of template.
5 *
6 * @alias filter
7 *
8 * @example
9 * {% filter uppercase %}oh hi, {{ name }}{% endfilter %}
10 * // => OH HI, PAUL
11 *
12 * @example
13 * {% filter replace(".", "!", "g") %}Hi. My name is Paul.{% endfilter %}
14 * // => Hi! My name is Paul!
15 *
16 * @param {function} filter The filter that should be applied to the contents of the tag.
17 */
18
191exports.compile = function (compiler, args, content, parents, options, blockName) {
205 var filter = args.shift().replace(/\($/, ''),
21 val = '(function () {\n' +
22 ' var _output = "";\n' +
23 compiler(content, parents, options, blockName) +
24 ' return _output;\n' +
25 '})()';
26
275 if (args[args.length - 1] === ')') {
284 args.pop();
29 }
30
315 args = (args.length) ? ', ' + args.join('') : '';
325 return '_output += _filters["' + filter + '"](' + val + args + ');\n';
33};
34
351exports.parse = function (str, line, parser, types) {
366 var filter;
37
386 function check(filter) {
396 if (!filters.hasOwnProperty(filter)) {
401 throw new Error('Filter "' + filter + '" does not exist on line ' + line + '.');
41 }
42 }
43
446 parser.on(types.FUNCTION, function (token) {
455 if (!filter) {
464 filter = token.match.replace(/\($/, '');
474 check(filter);
484 this.out.push(token.match);
494 this.state.push(token.type);
504 return;
51 }
521 return true;
53 });
54
556 parser.on(types.VAR, function (token) {
563 if (!filter) {
572 filter = token.match;
582 check(filter);
591 this.out.push(filter);
601 return;
61 }
621 return true;
63 });
64
656 return true;
66};
67
681exports.ends = true;
69

/lib/tags/for.js

100%
34
34
0
LineHitsSource
11var ctx = '_ctx.',
2 ctxloop = ctx + 'loop';
3
4/**
5 * Loop over objects and arrays.
6 *
7 * @alias for
8 *
9 * @example
10 * // obj = { one: 'hi', two: 'bye' };
11 * {% for x in obj %}
12 * {% if loop.first %}<ul>{% endif %}
13 * <li>{{ loop.index }} - {{ loop.key }}: {{ x }}</li>
14 * {% if loop.last %}</ul>{% endif %}
15 * {% endfor %}
16 * // => <ul>
17 * // <li>1 - one: hi</li>
18 * // <li>2 - two: bye</li>
19 * // </ul>
20 *
21 * @example
22 * // arr = [1, 2, 3]
23 * // Reverse the array, shortcut the key/index to `key`
24 * {% for key, val in arr|reverse %}
25 * {{ key }} -- {{ val }}
26 * {% endfor %}
27 * // => 0 -- 3
28 * // 1 -- 2
29 * // 2 -- 1
30 *
31 * @param {literal} [key] A shortcut to the index of the array or current key accessor.
32 * @param {literal} variable The current value will be assigned to this variable name temporarily. The variable will be reset upon ending the for tag.
33 * @param {literal} in Literally, "in". This token is required.
34 * @param {object} object An enumerable object that will be iterated over.
35 *
36 * @return {loop.index} The current iteration of the loop (1-indexed)
37 * @return {loop.index0} The current iteration of the loop (0-indexed)
38 * @return {loop.revindex} The number of iterations from the end of the loop (1-indexed)
39 * @return {loop.revindex0} The number of iterations from the end of the loop (0-indexed)
40 * @return {loop.key} If the iterator is an object, this will be the key of the current item, otherwise it will be the same as the loop.index.
41 * @return {loop.first} True if the current object is the first in the object or array.
42 * @return {loop.last} True if the current object is the last in the object or array.
43 */
441exports.compile = function (compiler, args, content, parents, options, blockName) {
4529 var val = args.shift(),
46 key = '__k',
47 ctxloopcache = (ctx + '__loopcache' + Math.random()).replace(/\./g, ''),
48 last;
49
5029 if (args[0] && args[0] === ',') {
515 args.shift();
525 key = val;
535 val = args.shift();
54 }
55
5629 last = args.join('');
57
5829 return [
59 '(function () {\n',
60 ' var __l = ' + last + ', __len = (_utils.isArray(__l)) ? __l.length : _utils.keys(__l).length;\n',
61 ' if (!__l) { return; }\n',
62 ' ' + ctxloopcache + ' = { loop: ' + ctxloop + ', ' + val + ': ' + ctx + val + ', ' + key + ': ' + ctx + key + ' };\n',
63 ' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n',
64 ' _utils.each(__l, function (' + val + ', ' + key + ') {\n',
65 ' ' + ctx + val + ' = ' + val + ';\n',
66 ' ' + ctx + key + ' = ' + key + ';\n',
67 ' ' + ctxloop + '.key = ' + key + ';\n',
68 ' ' + ctxloop + '.first = (' + ctxloop + '.index0 === 0);\n',
69 ' ' + ctxloop + '.last = (' + ctxloop + '.revindex0 === 0);\n',
70 ' ' + compiler(content, parents, options, blockName),
71 ' ' + ctxloop + '.index += 1; ' + ctxloop + '.index0 += 1; ' + ctxloop + '.revindex -= 1; ' + ctxloop + '.revindex0 -= 1;\n',
72 ' });\n',
73 ' ' + ctxloop + ' = ' + ctxloopcache + '.loop;\n',
74 ' ' + ctx + val + ' = ' + ctxloopcache + '.' + val + ';\n',
75 ' ' + ctx + key + ' = ' + ctxloopcache + '.' + key + ';\n',
76 ' ' + ctxloopcache + ' = undefined;\n',
77 '})();\n'
78 ].join('');
79};
80
811exports.parse = function (str, line, parser, types) {
8231 var firstVar, ready;
83
8431 parser.on(types.NUMBER, function (token) {
854 var lastState = this.state.length ? this.state[this.state.length - 1] : null;
864 if (!ready ||
87 (lastState !== types.ARRAYOPEN &&
88 lastState !== types.CURLYOPEN &&
89 lastState !== types.CURLYCLOSE &&
90 lastState !== types.FUNCTION &&
91 lastState !== types.FILTER)
92 ) {
931 throw new Error('Unexpected number "' + token.match + '" on line ' + line + '.');
94 }
953 return true;
96 });
97
9831 parser.on(types.VAR, function (token) {
9964 if (ready && firstVar) {
10028 return true;
101 }
102
10336 if (!this.out.length) {
10431 firstVar = true;
105 }
106
10736 this.out.push(token.match);
108 });
109
11031 parser.on(types.COMMA, function (token) {
1117 if (firstVar && this.prevToken.type === types.VAR) {
1125 this.out.push(token.match);
1135 return;
114 }
115
1162 return true;
117 });
118
11931 parser.on(types.COMPARATOR, function (token) {
12031 if (token.match !== 'in' || !firstVar) {
1211 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
122 }
12330 ready = true;
12430 this.filterApplyIdx.push(this.out.length);
125 });
126
12731 return true;
128};
129
1301exports.ends = true;
131

/lib/tags/if.js

100%
25
25
0
LineHitsSource
1/**
2 * Used to create conditional statements in templates. Accepts most JavaScript valid comparisons.
3 *
4 * Can be used in conjunction with <a href="#elseif"><code data-language="swig">{% elseif ... %}</code></a> and <a href="#else"><code data-language="swig">{% else %}</code></a> tags.
5 *
6 * @alias if
7 *
8 * @example
9 * {% if x %}{% endif %}
10 * {% if !x %}{% endif %}
11 * {% if not x %}{% endif %}
12 *
13 * @example
14 * {% if x and y %}{% endif %}
15 * {% if x && y %}{% endif %}
16 * {% if x or y %}{% endif %}
17 * {% if x || y %}{% endif %}
18 * {% if x || (y && z) %}{% endif %}
19 *
20 * @example
21 * {% if x [operator] y %}
22 * Operators: ==, !=, <, <=, >, >=, ===, !==
23 * {% endif %}
24 *
25 * @example
26 * {% if x == 'five' %}
27 * The operands can be also be string or number literals
28 * {% endif %}
29 *
30 * @example
31 * {% if x|lower === 'tacos' %}
32 * You can use filters on any operand in the statement.
33 * {% endif %}
34 *
35 * @example
36 * {% if x in y %}
37 * If x is a value that is present in y, this will return true.
38 * {% endif %}
39 *
40 * @param {...mixed} conditional Conditional statement that returns a truthy or falsy value.
41 */
421exports.compile = function (compiler, args, content, parents, options, blockName) {
4352 return 'if (' + args.join(' ') + ') { \n' +
44 compiler(content, parents, options, blockName) + '\n' +
45 '}';
46};
47
481exports.parse = function (str, line, parser, types) {
4968 if (typeof str === "undefined") {
502 throw new Error('No conditional statement provided on line ' + line + '.');
51 }
52
5366 parser.on(types.COMPARATOR, function (token) {
5424 if (this.isLast) {
551 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
56 }
5723 if (this.prevToken.type === types.NOT) {
581 throw new Error('Attempted logic "not ' + token.match + '" on line ' + line + '. Use !(foo ' + token.match + ') instead.');
59 }
6022 this.out.push(token.match);
6122 this.filterApplyIdx.push(this.out.length);
62 });
63
6466 parser.on(types.NOT, function (token) {
657 if (this.isLast) {
661 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
67 }
686 this.out.push(token.match);
69 });
70
7166 parser.on(types.BOOL, function (token) {
7220 this.out.push(token.match);
73 });
74
7566 parser.on(types.LOGIC, function (token) {
766 if (!this.out.length || this.isLast) {
772 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
78 }
794 this.out.push(token.match);
804 this.filterApplyIdx.pop();
81 });
82
8366 return true;
84};
85
861exports.ends = true;
87

/lib/tags/import.js

100%
36
36
0
LineHitsSource
11var utils = require('../utils');
2
3/**
4 * Allows you to import macros from another file directly into your current context.
5 * The import tag is specifically designed for importing macros into your template with a specific context scope. This is very useful for keeping your macros from overriding template context that is being injected by your server-side page generation.
6 *
7 * @alias import
8 *
9 * @example
10 * {% import './formmacros.html' as forms %}
11 * {{ form.input("text", "name") }}
12 * // => <input type="text" name="name">
13 *
14 * @example
15 * {% import "../shared/tags.html" as tags %}
16 * {{ tags.stylesheet('global') }}
17 * // => <link rel="stylesheet" href="/global.css">
18 *
19 * @param {string|var} file Relative path from the current template file to the file to import macros from.
20 * @param {literal} as Literally, "as".
21 * @param {literal} varname Local-accessible object name to assign the macros to.
22 */
231exports.compile = function (compiler, args) {
242 var ctx = args.pop(),
25 out = '_ctx.' + ctx + ' = {};\n var _output = "";\n',
26 replacements = utils.map(args, function (arg) {
278 return {
28 ex: new RegExp('_ctx.' + arg.name, 'g'),
29 re: '_ctx.' + ctx + '.' + arg.name
30 };
31 });
32
33 // Replace all occurrences of all macros in this file with
34 // proper namespaced definitions and calls
352 utils.each(args, function (arg) {
368 var c = arg.compiled;
378 utils.each(replacements, function (re) {
3832 c = c.replace(re.ex, re.re);
39 });
408 out += c;
41 });
42
432 return out;
44};
45
461exports.parse = function (str, line, parser, types, stack, opts) {
475 var parseFile = require('../swig').parseFile,
48 compiler = require('../parser').compile,
49 parseOpts = { resolveFrom: opts.filename },
50 compileOpts = utils.extend({}, opts, parseOpts),
51 tokens,
52 ctx;
53
545 parser.on(types.STRING, function (token) {
555 var self = this;
565 if (!tokens) {
574 tokens = parseFile(token.match.replace(/^("|')|("|')$/g, ''), parseOpts).tokens;
584 utils.each(tokens, function (token) {
5938 var out = '',
60 macroName;
6138 if (!token || token.name !== 'macro' || !token.compile) {
6226 return;
63 }
6412 macroName = token.args[0];
6512 out += token.compile(compiler, token.args, token.content, [], compileOpts) + '\n';
6612 self.out.push({compiled: out, name: macroName});
67 });
684 return;
69 }
70
711 throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
72 });
73
745 parser.on(types.VAR, function (token) {
757 var self = this;
767 if (!tokens || ctx) {
771 throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
78 }
79
806 if (token.match === 'as') {
813 return;
82 }
83
843 ctx = token.match;
853 self.out.push(ctx);
863 return false;
87 });
88
895 return true;
90};
91
921exports.block = true;
93

/lib/tags/include.js

100%
35
35
0
LineHitsSource
11var ignore = 'ignore',
2 missing = 'missing',
3 only = 'only';
4
5/**
6 * Includes a template partial in place. The template is rendered within the current locals variable context.
7 *
8 * @alias include
9 *
10 * @example
11 * // food = 'burritos';
12 * // drink = 'lemonade';
13 * {% include "./partial.html" %}
14 * // => I like burritos and lemonade.
15 *
16 * @example
17 * // my_obj = { food: 'tacos', drink: 'horchata' };
18 * {% include "./partial.html" with my_obj only %}
19 * // => I like tacos and horchata.
20 *
21 * @example
22 * {% include "/this/file/does/not/exist" ignore missing %}
23 * // => (Nothing! empty string)
24 *
25 * @param {string|var} file The path, relative to the template root, to render into the current context.
26 * @param {literal} [with] Literally, "with".
27 * @param {object} [context] Local variable key-value object context to provide to the included file.
28 * @param {literal} [only] Restricts to <strong>only</strong> passing the <code>with context</code> as local variables–the included template will not be aware of any other local variables in the parent template. For best performance, usage of this option is recommended if possible.
29 * @param {literal} [ignore missing] Will output empty string if not found instead of throwing an error.
30 */
311exports.compile = function (compiler, args) {
3211 var file = args.shift(),
33 onlyIdx = args.indexOf(only),
34 onlyCtx = onlyIdx !== -1 ? args.splice(onlyIdx, 1) : false,
35 parentFile = (args.pop() || '').replace(/\\/g, '\\\\'),
36 ignore = args[args.length - 1] === missing ? (args.pop()) : false,
37 w = args.join('');
38
3911 return (ignore ? ' try {\n' : '') +
40 '_output += _swig.compileFile(' + file + ', {' +
41 'resolveFrom: "' + parentFile + '"' +
42 '})(' +
43 ((onlyCtx && w) ? w : (!w ? '_ctx' : '_utils.extend({}, _ctx, ' + w + ')')) +
44 ');\n' +
45 (ignore ? '} catch (e) {}\n' : '');
46};
47
481exports.parse = function (str, line, parser, types, stack, opts) {
4913 var file, w;
5013 parser.on(types.STRING, function (token) {
5116 if (!file) {
5212 file = token.match;
5312 this.out.push(file);
5412 return;
55 }
56
574 return true;
58 });
59
6013 parser.on(types.VAR, function (token) {
6115 if (!file) {
621 file = token.match;
631 return true;
64 }
65
6614 if (!w && token.match === 'with') {
672 w = true;
682 return;
69 }
70
7112 if (w && token.match === only && this.prevToken.match !== 'with') {
721 this.out.push(token.match);
731 return;
74 }
75
7611 if (token.match === ignore) {
773 return false;
78 }
79
808 if (token.match === missing) {
813 if (this.prevToken.match !== ignore) {
821 throw new Error('Unexpected token "' + missing + '" on line ' + line + '.');
83 }
842 this.out.push(token.match);
852 return false;
86 }
87
885 if (this.prevToken.match === ignore) {
891 throw new Error('Expected "' + missing + '" on line ' + line + ' but found "' + token.match + '".');
90 }
91
924 return true;
93 });
94
9513 parser.on('end', function () {
9611 this.out.push(opts.filename || null);
97 });
98
9913 return true;
100};
101

/lib/tags/index.js

100%
16
16
0
LineHitsSource
11exports.autoescape = require('./autoescape');
21exports.block = require('./block');
31exports["else"] = require('./else');
41exports.elseif = require('./elseif');
51exports.elif = exports.elseif;
61exports["extends"] = require('./extends');
71exports.filter = require('./filter');
81exports["for"] = require('./for');
91exports["if"] = require('./if');
101exports["import"] = require('./import');
111exports.include = require('./include');
121exports.macro = require('./macro');
131exports.parent = require('./parent');
141exports.raw = require('./raw');
151exports.set = require('./set');
161exports.spaceless = require('./spaceless');
17

/lib/tags/macro.js

100%
29
29
0
LineHitsSource
1/**
2 * Create custom, reusable snippets within your templates.
3 * Can be imported from one template to another using the <a href="#import"><code data-language="swig">{% import ... %}</code></a> tag.
4 *
5 * @alias macro
6 *
7 * @example
8 * {% macro input(type, name, id, label, value, error) %}
9 * <label for="{{ name }}">{{ label }}</label>
10 * <input type="{{ type }}" name="{{ name }}" id="{{ id }}" value="{{ value }}"{% if error %} class="error"{% endif %}>
11 * {% endmacro %}
12 *
13 * {{ input("text", "fname", "fname", "First Name", fname.value, fname.errors) }}
14 * // => <label for="fname">First Name</label>
15 * // <input type="text" name="fname" id="fname" value="">
16 *
17 * @param {...arguments} arguments User-defined arguments.
18 */
191exports.compile = function (compiler, args, content, parents, options, blockName) {
2031 var fnName = args.shift();
21
2231 return '_ctx.' + fnName + ' = function (' + args.join('') + ') {\n' +
23 ' var _output = "";\n' +
24 ' __ctx = _utils.extend({}, _ctx),\n' +
25 ' _ctx = _utils.extend({}, __ctx);\n' +
26 ' _utils.each(_ctx, function (v, k) {\n' +
27 ' if (["' + args.join('","') + '"].indexOf(k) !== -1) { delete _ctx[k]; }\n' +
28 ' });\n' +
29 compiler(content, parents, options, blockName) + '\n' +
30 ' _ctx = __ctx;\n' +
31 ' return _output;\n' +
32 '};\n' +
33 '_ctx.' + fnName + '.safe = true;\n';
34};
35
361exports.parse = function (str, line, parser, types) {
3733 var name;
38
3933 parser.on(types.VAR, function (token) {
4027 if (token.match.indexOf('.') !== -1) {
411 throw new Error('Unexpected dot in macro argument "' + token.match + '" on line ' + line + '.');
42 }
4326 this.out.push(token.match);
44 });
45
4633 parser.on(types.FUNCTION, function (token) {
4716 if (!name) {
4816 name = token.match;
4916 this.out.push(name);
5016 this.state.push(types.FUNCTION);
51 }
52 });
53
5433 parser.on(types.FUNCTIONEMPTY, function (token) {
5514 if (!name) {
5614 name = token.match;
5714 this.out.push(name);
58 }
59 });
60
6133 parser.on(types.PARENCLOSE, function () {
6215 if (this.isLast) {
6314 return;
64 }
651 throw new Error('Unexpected parenthesis close on line ' + line + '.');
66 });
67
6833 parser.on(types.COMMA, function () {
698 return true;
70 });
71
7233 parser.on('*', function () {
738 return;
74 });
75
7633 return true;
77};
78
791exports.ends = true;
801exports.block = true;
81

/lib/tags/parent.js

94%
17
16
1
LineHitsSource
1/**
2 * Inject the content from the parent template's block of the same name into the current block.
3 *
4 * See <a href="#inheritance">Template Inheritance</a> for more information.
5 *
6 * @alias parent
7 *
8 * @example
9 * {% extends "./foo.html" %}
10 * {% block content %}
11 * My content.
12 * {% parent %}
13 * {% endblock %}
14 *
15 */
161exports.compile = function (compiler, args, content, parents, options, blockName) {
175 if (!parents || !parents.length) {
181 return '';
19 }
20
214 var parentFile = args[0],
22 breaker = true,
23 l = parents.length,
24 i = 0,
25 parent,
26 block;
27
284 for (i; i < l; i += 1) {
295 parent = parents[i];
305 if (!parent.blocks || !parent.blocks.hasOwnProperty(blockName)) {
310 continue;
32 }
33 // Silly JSLint "Strange Loop" requires return to be in a conditional
345 if (breaker && parentFile !== parent.name) {
354 block = parent.blocks[blockName];
364 return block.compile(compiler, [blockName], block.content, parents.slice(i + 1), options) + '\n';
37 }
38 }
39};
40
411exports.parse = function (str, line, parser, types, stack, opts) {
428 parser.on('*', function (token) {
431 throw new Error('Unexpected argument "' + token.match + '" on line ' + line + '.');
44 });
45
468 parser.on('end', function () {
477 this.out.push(opts.filename);
48 });
49
508 return true;
51};
52

/lib/tags/raw.js

100%
7
7
0
LineHitsSource
1// Magic tag, hardcoded into parser
2
3/**
4 * Forces the content to not be auto-escaped. All swig instructions will be ignored and the content will be rendered exactly as it was given.
5 *
6 * @alias raw
7 *
8 * @example
9 * // foobar = '<p>'
10 * {% raw %}{{ foobar }}{% endraw %}
11 * // => {{ foobar }}
12 *
13 */
141exports.compile = function (compiler, args, content, parents, options, blockName) {
154 return compiler(content, parents, options, blockName);
16};
171exports.parse = function (str, line, parser) {
185 parser.on('*', function (token) {
191 throw new Error('Unexpected token "' + token.match + '" in raw tag on line ' + line + '.');
20 });
215 return true;
22};
231exports.ends = true;
24

/lib/tags/set.js

92%
41
38
3
LineHitsSource
1/**
2 * Set a variable for re-use in the current context. This will over-write any value already set to the context for the given <var>varname</var>.
3 *
4 * @alias set
5 *
6 * @example
7 * {% set foo = "anything!" %}
8 * {{ foo }}
9 * // => anything!
10 *
11 * @example
12 * // index = 2;
13 * {% set bar = 1 %}
14 * {% set bar += index|default(3) %}
15 * // => 3
16 *
17 * @example
18 * // foods = {};
19 * // food = 'chili';
20 * {% set foods[food] = "con queso" %}
21 * {{ foods.chili }}
22 * // => con queso
23 *
24 * @example
25 * // foods = { chili: 'chili con queso' }
26 * {% set foods.chili = "guatamalan insanity pepper" %}
27 * {{ foods.chili }}
28 * // => guatamalan insanity pepper
29 *
30 * @param {literal} varname The variable name to assign the value to.
31 * @param {literal} assignement Any valid JavaScript assignement. <code data-language="js">=, +=, *=, /=, -=</code>
32 * @param {*} value Valid variable output.
33 */
341exports.compile = function (compiler, args) {
3537 return args.join(' ') + ';\n';
36};
37
381exports.parse = function (str, line, parser, types) {
3939 var nameSet = '',
40 propertyName;
41
4239 parser.on(types.VAR, function (token) {
4345 if (propertyName) {
44 // Tell the parser where to find the variable
451 propertyName += '_ctx.' + token.match;
461 return;
47 }
48
4944 if (!parser.out.length) {
5038 nameSet += token.match;
5138 return;
52 }
53
546 return true;
55 });
56
5739 parser.on(types.BRACKETOPEN, function (token) {
588 if (!propertyName && !this.out.length) {
598 propertyName = token.match;
608 return;
61 }
62
630 return true;
64 });
65
6639 parser.on(types.STRING, function (token) {
6734 if (propertyName && !this.out.length) {
687 propertyName += token.match;
697 return;
70 }
71
7227 return true;
73 });
74
7539 parser.on(types.BRACKETCLOSE, function (token) {
768 if (propertyName && !this.out.length) {
778 nameSet += propertyName + token.match;
788 propertyName = undefined;
798 return;
80 }
81
820 return true;
83 });
84
8539 parser.on(types.DOTKEY, function (token) {
861 if (!propertyName && !nameSet) {
870 return true;
88 }
891 nameSet += '.' + token.match;
901 return;
91 });
92
9339 parser.on(types.ASSIGNMENT, function (token) {
9440 if (this.out.length || !nameSet) {
952 throw new Error('Unexpected assignment "' + token.match + '" on line ' + line + '.');
96 }
97
9838 this.out.push(
99 // Prevent the set from spilling into global scope
100 '_ctx.' + nameSet
101 );
10238 this.out.push(token.match);
10338 this.filterApplyIdx.push(this.out.length);
104 });
105
10639 return true;
107};
108
1091exports.block = true;
110

/lib/tags/spaceless.js

100%
14
14
0
LineHitsSource
11var utils = require('../utils');
2
3/**
4 * Attempts to remove whitespace between HTML tags. Use at your own risk.
5 *
6 * @alias spaceless
7 *
8 * @example
9 * {% spaceless %}
10 * {% for num in foo %}
11 * <li>{{ loop.index }}</li>
12 * {% endfor %}
13 * {% endspaceless %}
14 * // => <li>1</li><li>2</li><li>3</li>
15 *
16 */
171exports.compile = function (compiler, args, content, parents, options, blockName) {
185 function stripWhitespace(tokens) {
1910 return utils.map(tokens, function (token) {
209 if (token.content || typeof token !== 'string') {
215 token.content = stripWhitespace(token.content);
225 return token;
23 }
24
254 return token.replace(/^\s+/, '')
26 .replace(/>\s+</g, '><')
27 .replace(/\s+$/, '');
28 });
29 }
30
315 return compiler(stripWhitespace(content), parents, options, blockName);
32};
33
341exports.parse = function (str, line, parser) {
356 parser.on('*', function (token) {
361 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
37 });
38
396 return true;
40};
41
421exports.ends = true;
43

/lib/utils.js

84%
65
55
10
LineHitsSource
11var isArray;
2
3/**
4 * Strip leading and trailing whitespace from a string.
5 * @param {string} input
6 * @return {string} Stripped input.
7 */
81exports.strip = function (input) {
9651 return input.replace(/^\s+|\s+$/g, '');
10};
11
12/**
13 * Test if a string starts with a given prefix.
14 * @param {string} str String to test against.
15 * @param {string} prefix Prefix to check for.
16 * @return {boolean}
17 */
181exports.startsWith = function (str, prefix) {
192807 return str.indexOf(prefix) === 0;
20};
21
22/**
23 * Test if a string ends with a given suffix.
24 * @param {string} str String to test against.
25 * @param {string} suffix Suffix to check for.
26 * @return {boolean}
27 */
281exports.endsWith = function (str, suffix) {
291170 return str.indexOf(suffix, str.length - suffix.length) !== -1;
30};
31
32/**
33 * Iterate over an array or object.
34 * @param {array|object} obj Enumerable object.
35 * @param {Function} fn Callback function executed for each item.
36 * @return {array|object} The original input object.
37 */
381exports.each = function (obj, fn) {
394850 var i, l;
40
414850 if (isArray(obj)) {
424795 i = 0;
434795 l = obj.length;
444795 for (i; i < l; i += 1) {
4512244 if (fn(obj[i], i, obj) === false) {
460 break;
47 }
48 }
49 } else {
5055 for (i in obj) {
51130 if (obj.hasOwnProperty(i)) {
52130 if (fn(obj[i], i, obj) === false) {
530 break;
54 }
55 }
56 }
57 }
58
594705 return obj;
60};
61
62/**
63 * Test if an object is an Array.
64 * @param {object} obj
65 * @return {boolean}
66 */
671exports.isArray = isArray = (Array.hasOwnProperty('isArray')) ? Array.isArray : function (obj) {
680 return (obj) ? (typeof obj === 'object' && Object.prototype.toString.call(obj).indexOf() !== -1) : false;
69};
70
71/**
72 * Test if an item in an enumerable matches your conditions.
73 * @param {array|object} obj Enumerable object.
74 * @param {Function} fn Executed for each item. Return true if your condition is met.
75 * @return {boolean}
76 */
771exports.some = function (obj, fn) {
7821235 var i = 0,
79 result,
80 l;
8121235 if (isArray(obj)) {
8221235 l = obj.length;
83
8421235 for (i; i < l; i += 1) {
8546049 result = fn(obj[i], i, obj);
8646049 if (result) {
874048 break;
88 }
89 }
90 } else {
910 exports.each(obj, function (value, index) {
920 result = fn(value, index, obj);
930 return !(result);
94 });
95 }
9621235 return !!result;
97};
98
99/**
100 * Return a new enumerable, mapped by a given iteration function.
101 * @param {object} obj Enumerable object.
102 * @param {Function} fn Executed for each item. Return the item to replace the original item with.
103 * @return {object} New mapped object.
104 */
1051exports.map = function (obj, fn) {
10684 var i = 0,
107 result = [],
108 l;
109
11084 if (isArray(obj)) {
11183 l = obj.length;
11283 for (i; i < l; i += 1) {
113181 result[i] = fn(obj[i], i);
114 }
115 } else {
1161 for (i in obj) {
1170 if (obj.hasOwnProperty(i)) {
1180 result[i] = fn(obj[i], i);
119 }
120 }
121 }
12284 return result;
123};
124
125/**
126 * Copy all of the properties in the source objects over to the destination object, and return the destination object. It's in-order, so the last source will override properties of the same name in previous arguments.
127 * @param {...object} arguments
128 * @return {object}
129 */
1301exports.extend = function () {
1316682 var args = arguments,
132 target = args[0],
133 objs = (args.length > 1) ? Array.prototype.slice.call(args, 1) : [],
134 i = 0,
135 l = objs.length,
136 key,
137 obj;
138
1396682 for (i; i < l; i += 1) {
1407761 obj = objs[i] || {};
1417761 for (key in obj) {
14214322 if (obj.hasOwnProperty(key)) {
14314322 target[key] = obj[key];
144 }
145 }
146 }
1476682 return target;
148};
149
150/**
151 * Get all of the keys on an object.
152 * @param {object} obj
153 * @return {array}
154 */
1551exports.keys = function (obj) {
156459 if (!obj) {
1571 return [];
158 }
159
160458 if (Object.keys) {
161458 return Object.keys(obj);
162 }
163
1640 return exports.map(obj, function (v, k) {
1650 return k;
166 });
167};
168
169/**
170 * Throw an error with possible line number and source file.
171 * @param {string} message Error message
172 * @param {number} [line] Line number in template.
173 * @param {string} [file] Template file the error occured in.
174 * @throws {Error} No seriously, the point is to throw an error.
175 */
1761exports.throwError = function (message, line, file) {
17745 if (line) {
17844 message += ' on line ' + line;
179 }
18045 if (file) {
18128 message += ' in file ' + file;
182 }
18345 throw new Error(message + '.');
184};
185