Coverage

98%
1202
1184
18

/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) {
11404 var self = this,
12 out = {};
13
14404 if (utils.isArray(input)) {
1524 return utils.map(input, function (value) {
1657 return self.apply(null, arguments);
17 });
18 }
19
20380 if (typeof input === 'object') {
214 utils.each(input, function (value, key) {
225 out[key] = self.apply(null, arguments);
23 });
244 return out;
25 }
26
27376 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) {
146361 var out = iterateFilter.apply(exports.escape, arguments),
147 inp = input,
148 i = 0,
149 code;
150
151361 if (out !== undefined) {
15218 return out;
153 }
154
155343 if (typeof input !== 'string') {
156110 return input;
157 }
158
159233 out = '';
160
161233 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:
184227 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) {
42610 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) {
47115 var out;
47215 if (utils.isArray(input)) {
4735 out = input.sort();
474 } else {
47510 switch (typeof input) {
476 case 'object':
4772 out = utils.keys(input).sort();
4782 break;
479 case 'string':
4808 out = input.split('');
4818 if (reverse) {
4827 return out.reverse().join('');
483 }
4841 return out.sort().join('');
485 }
486 }
487
4887 if (out && reverse) {
4894 return out.reverse();
490 }
491
4923 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) {
2542075 var matched;
255
2562075 utils.some(rules, function (rule) {
25719703 return utils.some(rule.regex, function (regex) {
25827511 var match = str.match(regex),
259 normalized;
260
26127511 if (!match) {
26225436 return;
263 }
264
2652075 normalized = match[rule.idx || 0].replace(/\s*$/, '');
2662075 normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
267
2682075 matched = {
269 match: normalized,
270 type: rule.type,
271 length: match[0].length
272 };
2732075 return true;
274 });
275 });
276
2772075 if (!matched) {
2780 matched = {
279 match: str,
280 type: TYPES.UNKNOWN,
281 length: str.length
282 };
283 }
284
2852075 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) {
295664 var offset = 0,
296 tokens = [],
297 substr,
298 match;
299664 while (offset < str.length) {
3002075 substr = str.substring(offset);
3012075 match = reader(substr);
3022075 offset += match.length;
3032075 tokens.push(match);
304 }
305664 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) {
294980 if (basepath) {
304 from = basepath;
31 } else {
324976 from = (from) ? path.dirname(from) : process.cwd();
33 }
344980 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) {
4557 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
4957 identifier = ret.resolve(identifier);
50
5157 if (cb) {
525 fs.readFile(identifier, encoding, cb);
535 return;
54 }
5552 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) {
472772 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) {
61664 this.out = [];
62664 this.state = [];
63664 this.filterApplyIdx = [];
64664 this._parsers = {};
65664 this.line = line;
66664 this.filename = filename;
67664 this.filters = filters;
68664 this.escape = autoescape;
69
70664 this.parse = function () {
71660 var self = this;
72
73660 if (self._parsers.start) {
740 self._parsers.start.call(self);
75 }
76660 utils.each(tokens, function (token, i) {
772053 var prevToken = tokens[i - 1];
782053 self.isLast = (i === tokens.length - 1);
792053 if (prevToken) {
801414 while (prevToken.type === _t.WHITESPACE) {
81301 i -= 1;
82301 prevToken = tokens[i - 1];
83 }
84 }
852053 self.prevToken = prevToken;
862053 self.parseToken(token);
87 });
88602 if (self._parsers.end) {
8919 self._parsers.end.call(self);
90 }
91
92602 if (self.escape) {
93272 self.filterApplyIdx = [0];
94272 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 {
100270 self.parseToken({ type: _t.FILTEREMPTY, match: 'e' });
101 }
102 }
103
104602 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) {
1291003 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) {
1392331 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
1472331 if (fn && typeof fn === 'function') {
148511 if (!fn.call(this, token)) {
149401 return;
150 }
151 }
152
1531908 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
1641908 if (lastState && lastState === _t.METHODOPEN) {
16519 self.state.pop();
16619 if (token.type !== _t.PARENCLOSE) {
16711 self.out.push(', ');
168 }
169 }
170
1711908 switch (token.type) {
172 case _t.WHITESPACE:
173293 break;
174
175 case _t.STRING:
176222 self.filterApplyIdx.push(self.out.length);
177222 self.out.push(match.replace(/\\/g, '\\\\'));
178222 break;
179
180 case _t.NUMBER:
181 case _t.BOOL:
182116 self.filterApplyIdx.push(self.out.length);
183116 self.out.push(match);
184116 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:
196325 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1971 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
198 }
199324 self.escape = self.filters[match].safe ? false : self.escape;
200324 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
201324 self.out.push(')');
202324 break;
203
204 case _t.FUNCTION:
205 case _t.FUNCTIONEMPTY:
20629 self.out.push('((typeof _ctx.' + match + ' !== "undefined") ? _ctx.' + match +
207 ' : ((typeof ' + match + ' !== "undefined") ? ' + match +
208 ' : _fn))(');
20929 self.escape = false;
21029 if (token.type === _t.FUNCTIONEMPTY) {
21110 self.out[self.out.length - 1] = self.out[self.out.length - 1] + ')';
212 } else {
21319 self.state.push(token.type);
214 }
21529 self.filterApplyIdx.push(self.out.length - 1);
21629 break;
217
218 case _t.PARENOPEN:
21924 self.state.push(token.type);
22024 if (self.filterApplyIdx.length) {
22122 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '(');
22222 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 {
2283 self.out.push(' || _fn)(');
229 }
23022 self.filterApplyIdx.push(self.out.length - 3);
231 } else {
2322 self.out.push('(');
2332 self.filterApplyIdx.push(self.out.length - 1);
234 }
23524 break;
236
237 case _t.PARENCLOSE:
238157 temp = self.state.pop();
239157 if (temp !== _t.PARENOPEN && temp !== _t.FUNCTION && temp !== _t.FILTER) {
2401 utils.throwError('Mismatched nesting state', self.line, self.filename);
241 }
242156 self.out.push(')');
243 // Once off the previous entry
244156 self.filterApplyIdx.pop();
245156 if (temp !== _t.FILTER) {
246 // Once for the open paren
24746 self.filterApplyIdx.pop();
248 }
249156 break;
250
251 case _t.COMMA:
252106 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 }
260105 if (lastState === _t.COLON) {
2615 self.state.pop();
262 }
263105 self.out.push(', ');
264105 self.filterApplyIdx.pop();
265105 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:
286437 self.parseVar(token, match, lastState);
287410 break;
288
289 case _t.BRACKETOPEN:
29019 if (!prevToken ||
291 (prevTokenType !== _t.VAR &&
292 prevTokenType !== _t.BRACKETCLOSE &&
293 prevTokenType !== _t.PARENCLOSE)) {
2945 self.state.push(_t.ARRAYOPEN);
2955 self.filterApplyIdx.push(self.out.length);
296 } else {
29714 self.state.push(token.type);
298 }
29919 self.out.push('[');
30019 break;
301
302 case _t.BRACKETCLOSE:
30319 temp = self.state.pop();
30419 if (temp !== _t.BRACKETOPEN && temp !== _t.ARRAYOPEN) {
3051 utils.throwError('Unexpected closing square bracket', self.line, self.filename);
306 }
30718 self.out.push(']');
30818 self.filterApplyIdx.pop();
30918 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:
3548 self.out.push(' ' + match + ' ');
3558 self.filterApplyIdx.pop();
3568 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) {
369437 var self = this;
370
371437 match = match.split('.');
372
373437 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
377411 self.filterApplyIdx.push(self.out.length);
378411 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
386401 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) {
395420 var temp = match[0], result;
396
397420 function checkDot(ctx) {
3981260 var c = ctx + temp,
399 m = match,
400 build = '';
401
4021260 build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
4031260 utils.each(m, function (v, i) {
4041404 if (i === 0) {
4051260 return;
406 }
407144 build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
408144 c += '.' + v;
409 });
4101260 build += ')';
411
4121260 return build;
413 }
414
415420 function buildDot(ctx) {
416840 return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
417 }
418420 result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
419420 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 * @params {object} swig The current Swig instance
431 * @param {string} source Swig template source.
432 * @param {object} opts Swig options object.
433 * @param {object} tags Keyed object of tags that can be parsed and compiled.
434 * @param {object} filters Keyed object of filters that may be applied to variables.
435 * @return {array} List of tokens ready for compilation.
436 */
4371exports.parse = function (swig, source, opts, tags, filters) {
438462 source = source.replace(/\r\n/g, '\n');
439462 var escape = opts.autoescape,
440 tagOpen = opts.tagControls[0],
441 tagClose = opts.tagControls[1],
442 varOpen = opts.varControls[0],
443 varClose = opts.varControls[1],
444 escapedTagOpen = escapeRegExp(tagOpen),
445 escapedTagClose = escapeRegExp(tagClose),
446 escapedVarOpen = escapeRegExp(varOpen),
447 escapedVarClose = escapeRegExp(varClose),
448 tagStrip = new RegExp('^' + escapedTagOpen + '-?\\s*-?|-?\\s*-?' + escapedTagClose + '$', 'g'),
449 tagStripBefore = new RegExp('^' + escapedTagOpen + '-'),
450 tagStripAfter = new RegExp('-' + escapedTagClose + '$'),
451 varStrip = new RegExp('^' + escapedVarOpen + '-?\\s*-?|-?\\s*-?' + escapedVarClose + '$', 'g'),
452 varStripBefore = new RegExp('^' + escapedVarOpen + '-'),
453 varStripAfter = new RegExp('-' + escapedVarClose + '$'),
454 cmtOpen = opts.cmtControls[0],
455 cmtClose = opts.cmtControls[1],
456 anyChar = '[\\s\\S]*?',
457 // Split the template source based on variable, tag, and comment blocks
458 // /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\{#[\s\S]*?#\})/
459 splitter = new RegExp(
460 '(' +
461 escapedTagOpen + anyChar + escapedTagClose + '|' +
462 escapedVarOpen + anyChar + escapedVarClose + '|' +
463 escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) +
464 ')'
465 ),
466 line = 1,
467 stack = [],
468 parent = null,
469 tokens = [],
470 blocks = {},
471 inRaw = false,
472 stripNext;
473
474 /**
475 * Parse a variable.
476 * @param {string} str String contents of the variable, between <i>{{</i> and <i>}}</i>
477 * @param {number} line The line number that this variable starts on.
478 * @return {VarToken} Parsed variable token object.
479 * @private
480 */
481462 function parseVariable(str, line) {
482357 var tokens = lexer.read(utils.strip(str)),
483 parser,
484 out;
485
486357 parser = new TokenParser(tokens, filters, escape, line, opts.filename);
487357 out = parser.parse().join('');
488
489321 if (parser.state.length) {
4902 utils.throwError('Unable to parse "' + str + '"', line, opts.filename);
491 }
492
493 /**
494 * A parsed variable token.
495 * @typedef {object} VarToken
496 * @property {function} compile Method for compiling this token.
497 */
498319 return {
499 compile: function () {
500315 return '_output += ' + out + ';\n';
501 }
502 };
503 }
504462 exports.parseVariable = parseVariable;
505
506 /**
507 * Parse a tag.
508 * @param {string} str String contents of the tag, between <i>{%</i> and <i>%}</i>
509 * @param {number} line The line number that this tag starts on.
510 * @return {TagToken} Parsed token object.
511 * @private
512 */
513462 function parseTag(str, line) {
514492 var tokens, parser, chunks, tagName, tag, args, last;
515
516492 if (utils.startsWith(str, 'end')) {
517182 last = stack[stack.length - 1];
518182 if (last && last.name === str.split(/\s+/)[0].replace(/^end/, '') && last.ends) {
519180 switch (last.name) {
520 case 'autoescape':
5219 escape = opts.autoescape;
5229 break;
523 case 'raw':
5244 inRaw = false;
5254 break;
526 }
527180 stack.pop();
528180 return;
529 }
530
5312 if (!inRaw) {
5321 utils.throwError('Unexpected end of tag "' + str.replace(/^end/, '') + '"', line, opts.filename);
533 }
534 }
535
536311 if (inRaw) {
5373 return;
538 }
539
540308 chunks = str.split(/\s+(.+)?/);
541308 tagName = chunks.shift();
542
543308 if (!tags.hasOwnProperty(tagName)) {
5441 utils.throwError('Unexpected tag "' + str + '"', line, opts.filename);
545 }
546
547307 tokens = lexer.read(utils.strip(chunks.join(' ')));
548307 parser = new TokenParser(tokens, filters, false, line, opts.filename);
549307 tag = tags[tagName];
550
551 /**
552 * Define custom parsing methods for your tag.
553 * @callback parse
554 *
555 * @example
556 * exports.parse = function (str, line, parser, types, options, swig) {
557 * parser.on('start', function () {
558 * // ...
559 * });
560 * parser.on(types.STRING, function (token) {
561 * // ...
562 * });
563 * };
564 *
565 * @param {string} str The full token string of the tag.
566 * @param {number} line The line number that this tag appears on.
567 * @param {TokenParser} parser A TokenParser instance.
568 * @param {TYPES} types Lexer token type enum.
569 * @param {TagToken[]} stack The current stack of open tags.
570 * @param {SwigOpts} options Swig Options Object.
571 * @param {object} swig The Swig instance (gives acces to loaders, parsers, etc)
572 */
573307 if (!tag.parse(chunks[1], line, parser, _t, stack, opts, swig)) {
5742 utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename);
575 }
576
577303 parser.parse();
578281 args = parser.out;
579
580281 switch (tagName) {
581 case 'autoescape':
5829 escape = (args[0] !== 'false') ? args[0] : false;
5839 break;
584 case 'raw':
5854 inRaw = true;
5864 break;
587 }
588
589 /**
590 * A parsed tag token.
591 * @typedef {Object} TagToken
592 * @property {compile} [compile] Method for compiling this token.
593 * @property {array} [args] Array of arguments for the tag.
594 * @property {Token[]} [content=[]] An array of tokens that are children of this Token.
595 * @property {boolean} [ends] Whether or not this tag requires an end tag.
596 * @property {string} name The name of this tag.
597 */
598281 return {
599 block: !!tags[tagName].block,
600 compile: tag.compile,
601 args: args,
602 content: [],
603 ends: tag.ends,
604 name: tagName
605 };
606 }
607
608 /**
609 * Strip the whitespace from the previous token, if it is a string.
610 * @param {object} token Parsed token.
611 * @return {object} If the token was a string, trailing whitespace will be stripped.
612 */
613462 function stripPrevToken(token) {
61410 if (typeof token === 'string') {
6158 token = token.replace(/\s*$/, '');
616 }
61710 return token;
618 }
619
620 /*!
621 * Loop over the source, split via the tag/var/comment regular expression splitter.
622 * Send each chunk to the appropriate parser.
623 */
624462 utils.each(source.split(splitter), function (chunk) {
6252112 var token, lines, stripPrev, prevToken, prevChildToken;
626
6272112 if (!chunk) {
628908 return;
629 }
630
631 // Is a variable?
6321204 if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
633357 stripPrev = varStripBefore.test(chunk);
634357 stripNext = varStripAfter.test(chunk);
635357 token = parseVariable(chunk.replace(varStrip, ''), line);
636 // Is a tag?
637847 } else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
638492 stripPrev = tagStripBefore.test(chunk);
639492 stripNext = tagStripAfter.test(chunk);
640492 token = parseTag(chunk.replace(tagStrip, ''), line);
641464 if (token) {
642281 if (token.name === 'extends') {
64326 parent = token.args.join('').replace(/^\'|\'$/g, '').replace(/^\"|\"$/g, '');
644255 } else if (token.block && !stack.length) {
645114 blocks[token.args.join('')] = token;
646 }
647 }
648464 if (inRaw && !token) {
6493 token = chunk;
650 }
651 // Is a content string?
652355 } else if (inRaw || (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))) {
653348 token = (stripNext) ? chunk.replace(/^\s*/, '') : chunk;
654348 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>
6601131 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.
6721131 if (!token) {
673186 return;
674 }
675
676 // If there's an open item in the stack, add this to its content.
677945 if (stack.length) {
678273 stack[stack.length - 1].content.push(token);
679 } else {
680672 tokens.push(token);
681 }
682
683 // If the token is a tag that requires an end tag, open it on the stack.
684945 if (token.name && token.ends) {
685183 stack.push(token);
686 }
687
688945 lines = chunk.match(/\n/g);
689945 line += (lines) ? lines.length : 0;
690 });
691
692396 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) {
710525 var out = '',
711 tokens = utils.isArray(template) ? template : template.tokens;
712
713525 utils.each(tokens, function (token) {
714789 var o;
715789 if (typeof token === 'string') {
716262 out += '_output += "' + token.replace(/\\/g, '\\\\').replace(/\n|\r/g, '\\n').replace(/"/g, '\\"') + '";\n';
717262 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 */
739527 o = token.compile(exports.compile, token.args ? token.args.slice(0) : [], token.content ? token.content.slice(0) : [], parents, options, blockName);
740527 out += o || '';
741 });
742
743525 return out;
744};
745

/lib/swig.js

99%
212
211
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.2") { ... }
12 *
13 * @type {String}
14 */
151exports.version = "1.4.2";
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) {
89848 if (!options) {
9089 return;
91 }
92
93759 utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
942262 if (!options.hasOwnProperty(key)) {
951235 return;
96 }
971027 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 }
1001021 if (options[key][0] === options[key][1]) {
1013 throw new Error('Option "' + key + '" open and close controls must not be the same.');
102 }
1031018 utils.each(options[key], function (a, i) {
1042033 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
110744 if (options.hasOwnProperty('cache')) {
111340 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 }
117742 if (options.hasOwnProperty('loader')) {
118346 if (options.loader) {
119346 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) {
144344 validateOptions(options);
145340 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) {
17227 validateOptions(opts);
17326 this.options = utils.extend({}, defaultOptions, opts || {});
17426 this.cache = {};
17526 this.extensions = {};
17626 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 */
18626 function getLocals(options) {
187914 if (!options || !options.locals) {
188343 return self.options.locals;
189 }
190
191571 return utils.extend({}, self.options.locals, options.locals);
192 }
193
194 /**
195 * Determine whether caching is enabled via the options provided and/or defaults
196 * @param {SwigOpts} [options={}] Swig Options Object
197 * @return {boolean}
198 * @private
199 */
20026 function shouldCache(options) {
2015018 options = options || {};
2025018 return (options.hasOwnProperty('cache') && !options.cache) || !self.options.cache;
203 }
204
205 /**
206 * Get compiled template from the cache.
207 * @param {string} key Name of template.
208 * @return {object|undefined} Template function and tokens.
209 * @private
210 */
21126 function cacheGet(key, options) {
2124978 if (shouldCache(options)) {
2133 return;
214 }
215
2164975 if (self.options.cache === 'memory') {
2174974 return self.cache[key];
218 }
219
2201 return self.options.cache.get(key);
221 }
222
223 /**
224 * Store a template in the cache.
225 * @param {string} key Name of template.
226 * @param {object} val Template function and tokens.
227 * @return {undefined}
228 * @private
229 */
23026 function cacheSet(key, options, val) {
23140 if (shouldCache(options)) {
2323 return;
233 }
234
23537 if (self.options.cache === 'memory') {
23636 self.cache[key] = val;
23736 return;
238 }
239
2401 self.options.cache.set(key, val);
241 }
242
243 /**
244 * Clears the in-memory template cache.
245 *
246 * @example
247 * swig.invalidateCache();
248 *
249 * @return {undefined}
250 */
25126 this.invalidateCache = function () {
252334 if (self.options.cache === 'memory') {
253334 self.cache = {};
254 }
255 };
256
257 /**
258 * Add a custom filter for swig variables.
259 *
260 * @example
261 * function replaceMs(input) { return input.replace(/m/g, 'f'); }
262 * swig.setFilter('replaceMs', replaceMs);
263 * // => {{ "onomatopoeia"|replaceMs }}
264 * // => onofatopeia
265 *
266 * @param {string} name Name of filter, used in templates. <strong>Will</strong> overwrite previously defined filters, if using the same name.
267 * @param {function} method Function that acts against the input. See <a href="/docs/filters/#custom">Custom Filters</a> for more information.
268 * @return {undefined}
269 */
27026 this.setFilter = function (name, method) {
2713 if (typeof method !== "function") {
2721 throw new Error('Filter "' + name + '" is not a valid function.');
273 }
2742 filters[name] = method;
275 };
276
277 /**
278 * Add a custom tag. To expose your own extensions to compiled template code, see <code data-language="js">swig.setExtension</code>.
279 *
280 * For a more in-depth explanation of writing custom tags, see <a href="../extending/#tags">Custom Tags</a>.
281 *
282 * @example
283 * var tacotag = require('./tacotag');
284 * swig.setTag('tacos', tacotag.parse, tacotag.compile, tacotag.ends, tacotag.blockLevel);
285 * // => {% tacos %}Make this be tacos.{% endtacos %}
286 * // => Tacos tacos tacos tacos.
287 *
288 * @param {string} name Tag name.
289 * @param {function} parse Method for parsing tokens.
290 * @param {function} compile Method for compiling renderable output.
291 * @param {boolean} [ends=false] Whether or not this tag requires an <i>end</i> tag.
292 * @param {boolean} [blockLevel=false] If false, this tag will not be compiled outside of <code>block</code> tags when extending a parent template.
293 * @return {undefined}
294 */
29526 this.setTag = function (name, parse, compile, ends, blockLevel) {
2964 if (typeof parse !== 'function') {
2971 throw new Error('Tag "' + name + '" parse method is not a valid function.');
298 }
299
3003 if (typeof compile !== 'function') {
3011 throw new Error('Tag "' + name + '" compile method is not a valid function.');
302 }
303
3042 tags[name] = {
305 parse: parse,
306 compile: compile,
307 ends: ends || false,
308 block: !!blockLevel
309 };
310 };
311
312 /**
313 * 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.
314 *
315 * @example
316 * swig.setExtension('trans', function (v) { return translate(v); });
317 * function compileTrans(compiler, args, content, parent, options) {
318 * return '_output += _ext.trans(' + args[0] + ');'
319 * };
320 * swig.setTag('trans', parseTrans, compileTrans, true);
321 *
322 * @param {string} name Key name of the extension. Accessed via <code data-language="js">_ext[name]</code>.
323 * @param {*} object The method, value, or object that should be available via the given name.
324 * @return {undefined}
325 */
32626 this.setExtension = function (name, object) {
3271 self.extensions[name] = object;
328 };
329
330 /**
331 * Parse a given source string into tokens.
332 *
333 * @param {string} source Swig template source.
334 * @param {SwigOpts} [options={}] Swig options object.
335 * @return {object} parsed Template tokens object.
336 * @private
337 */
33826 this.parse = function (source, options) {
339477 validateOptions(options);
340
341462 var locals = getLocals(options),
342 opts = {},
343 k;
344
345462 for (k in options) {
346401 if (options.hasOwnProperty(k) && k !== 'locals') {
347117 opts[k] = options[k];
348 }
349 }
350
351462 options = utils.extend({}, self.options, opts);
352462 options.locals = locals;
353
354462 return parser.parse(this, source, options, tags, filters);
355 };
356
357 /**
358 * Parse a given file into tokens.
359 *
360 * @param {string} pathname Full path to file to parse.
361 * @param {SwigOpts} [options={}] Swig options object.
362 * @return {object} parsed Template tokens object.
363 * @private
364 */
36526 this.parseFile = function (pathname, options) {
36628 var src;
367
36828 if (!options) {
3690 options = {};
370 }
371
37228 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
373
37428 src = self.options.loader.load(pathname);
375
37627 if (!options.filename) {
3774 options = utils.extend({ filename: pathname }, options);
378 }
379
38027 return self.parse(src, options);
381 };
382
383 /**
384 * Re-Map blocks within a list of tokens to the template's block objects.
385 * @param {array} tokens List of tokens for the parent object.
386 * @param {object} template Current template that needs to be mapped to the parent's block and token list.
387 * @return {array}
388 * @private
389 */
39026 function remapBlocks(blocks, tokens) {
39151 return utils.map(tokens, function (token) {
392116 var args = token.args ? token.args.join('') : '';
393116 if (token.name === 'block' && blocks[args]) {
39421 token = blocks[args];
395 }
396116 if (token.content && token.content.length) {
39728 token.content = remapBlocks(blocks, token.content);
398 }
399116 return token;
400 });
401 }
402
403 /**
404 * Import block-level tags to the token list that are not actual block tags.
405 * @param {array} blocks List of block-level tags.
406 * @param {array} tokens List of tokens to render.
407 * @return {undefined}
408 * @private
409 */
41026 function importNonBlocks(blocks, tokens) {
41123 var temp = [];
41252 utils.each(blocks, function (block) { temp.push(block); });
41323 utils.each(temp.reverse(), function (block) {
41429 if (block.name !== 'block') {
4155 tokens.unshift(block);
416 }
417 });
418 }
419
420 /**
421 * Recursively compile and get parents of given parsed token object.
422 *
423 * @param {object} tokens Parsed tokens from template.
424 * @param {SwigOpts} [options={}] Swig options object.
425 * @return {object} Parsed tokens from parent templates.
426 * @private
427 */
42826 function getParents(tokens, options) {
429369 var parentName = tokens.parent,
430 parentFiles = [],
431 parents = [],
432 parentFile,
433 parent,
434 l;
435
436369 while (parentName) {
43728 if (!options || !options.filename) {
4381 throw new Error('Cannot extend "' + parentName + '" because current template has no filename.');
439 }
440
44127 parentFile = parentFile || options.filename;
44227 parentFile = self.options.loader.resolve(parentName, parentFile);
44327 parent = cacheGet(parentFile, options) || self.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
44426 parentName = parent.parent;
445
44626 if (parentFiles.indexOf(parentFile) !== -1) {
4471 throw new Error('Illegal circular extends of "' + parentFile + '".');
448 }
44925 parentFiles.push(parentFile);
450
45125 parents.push(parent);
452 }
453
454 // Remap each parents'(1) blocks onto its own parent(2), receiving the full token list for rendering the original parent(1) on its own.
455366 l = parents.length;
456366 for (l = parents.length - 2; l >= 0; l -= 1) {
4576 parents[l].tokens = remapBlocks(parents[l].blocks, parents[l + 1].tokens);
4586 importNonBlocks(parents[l].blocks, parents[l].tokens);
459 }
460
461366 return parents;
462 }
463
464 /**
465 * Pre-compile a source string into a cache-able template function.
466 *
467 * @example
468 * swig.precompile('{{ tacos }}');
469 * // => {
470 * // tpl: function (_swig, _locals, _filters, _utils, _fn) { ... },
471 * // tokens: {
472 * // name: undefined,
473 * // parent: null,
474 * // tokens: [...],
475 * // blocks: {}
476 * // }
477 * // }
478 *
479 * 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.
480 *
481 * @param {string} source Swig template source string.
482 * @param {SwigOpts} [options={}] Swig options object.
483 * @return {object} Renderable function and tokens object.
484 */
48526 this.precompile = function (source, options) {
486450 var tokens = self.parse(source, options),
487 parents = getParents(tokens, options),
488 tpl,
489 err;
490
491366 if (parents.length) {
492 // Remap the templates first-parent's tokens using this template's blocks.
49317 tokens.tokens = remapBlocks(tokens.blocks, parents[0].tokens);
49417 importNonBlocks(tokens.blocks, tokens.tokens);
495 }
496
497366 try {
498366 tpl = new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
499 ' var _ext = _swig.extensions,\n' +
500 ' _output = "";\n' +
501 parser.compile(tokens, parents, options) + '\n' +
502 ' return _output;\n'
503 );
504 } catch (e) {
5051 utils.throwError(e, null, options.filename);
506 }
507
508365 return { tpl: tpl, tokens: tokens };
509 };
510
511 /**
512 * Compile and render a template string for final output.
513 *
514 * 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.
515 *
516 * @example
517 * swig.render('{{ tacos }}', { locals: { tacos: 'Tacos!!!!' }});
518 * // => Tacos!!!!
519 *
520 * @param {string} source Swig template source string.
521 * @param {SwigOpts} [options={}] Swig options object.
522 * @return {string} Rendered output.
523 */
52426 this.render = function (source, options) {
525383 return self.compile(source, options)();
526 };
527
528 /**
529 * Compile and render a template file for final output. This is most useful for libraries like Express.js.
530 *
531 * @example
532 * swig.renderFile('./template.html', {}, function (err, output) {
533 * if (err) {
534 * throw err;
535 * }
536 * console.log(output);
537 * });
538 *
539 * @example
540 * swig.renderFile('./template.html', {});
541 * // => output
542 *
543 * @param {string} pathName File location.
544 * @param {object} [locals={}] Template variable context.
545 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
546 * @return {string} Rendered output.
547 */
54826 this.renderFile = function (pathName, locals, cb) {
54912 if (cb) {
5505 self.compileFile(pathName, {}, function (err, fn) {
5515 var result;
552
5535 if (err) {
5541 cb(err);
5551 return;
556 }
557
5584 try {
5594 result = fn(locals);
560 } catch (err2) {
5611 cb(err2);
5621 return;
563 }
564
5653 cb(null, result);
566 });
5675 return;
568 }
569
5707 return self.compileFile(pathName)(locals);
571 };
572
573 /**
574 * Compile string source into a renderable template function.
575 *
576 * @example
577 * var tpl = swig.compile('{{ tacos }}');
578 * // => {
579 * // [Function: compiled]
580 * // parent: null,
581 * // tokens: [{ compile: [Function] }],
582 * // blocks: {}
583 * // }
584 * tpl({ tacos: 'Tacos!!!!' });
585 * // => Tacos!!!!
586 *
587 * 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.
588 *
589 * @param {string} source Swig template source string.
590 * @param {SwigOpts} [options={}] Swig options object.
591 * @return {function} Renderable function with keys for parent, blocks, and tokens.
592 */
59326 this.compile = function (source, options) {
594448 var key = options ? options.filename : null,
595 cached = key ? cacheGet(key, options) : null,
596 context,
597 contextLength,
598 pre;
599
600448 if (cached) {
6011 return cached;
602 }
603
604447 context = getLocals(options);
605447 contextLength = utils.keys(context).length;
606447 pre = this.precompile(source, options);
607
608362 function compiled(locals) {
6095195 var lcls;
6105195 if (locals && contextLength) {
6111 lcls = utils.extend({}, context, locals);
6125194 } else if (locals && !contextLength) {
6134870 lcls = locals;
614324 } else if (!locals && contextLength) {
615277 lcls = context;
616 } else {
61747 lcls = {};
618 }
6195195 return pre.tpl(self, lcls, filters, utils, efn);
620 }
621
622362 utils.extend(compiled, pre.tokens);
623
624362 if (key) {
62539 cacheSet(key, options, compiled);
626 }
627
628362 return compiled;
629 };
630
631 /**
632 * Compile a source file into a renderable template function.
633 *
634 * @example
635 * var tpl = swig.compileFile('./mytpl.html');
636 * // => {
637 * // [Function: compiled]
638 * // parent: null,
639 * // tokens: [{ compile: [Function] }],
640 * // blocks: {}
641 * // }
642 * tpl({ tacos: 'Tacos!!!!' });
643 * // => Tacos!!!!
644 *
645 * @example
646 * swig.compileFile('/myfile.txt', { varControls: ['<%=', '=%>'], tagControls: ['<%', '%>']});
647 * // => will compile 'myfile.txt' using the var and tag controls as specified.
648 *
649 * @param {string} pathname File location.
650 * @param {SwigOpts} [options={}] Swig options object.
651 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
652 * @return {function} Renderable function with keys for parent, blocks, and tokens.
653 */
65426 this.compileFile = function (pathname, options, cb) {
6554879 var src, cached;
656
6574879 if (!options) {
65823 options = {};
659 }
660
6614879 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
6624878 if (!options.filename) {
6634878 options = utils.extend({ filename: pathname }, options);
664 }
6654878 cached = cacheGet(pathname, options);
666
6674878 if (cached) {
6684840 if (cb) {
6691 cb(null, cached);
6701 return;
671 }
6724839 return cached;
673 }
674
67538 if (cb) {
6767 self.options.loader.load(pathname, function (err, src) {
6777 if (err) {
6781 cb(err);
6791 return;
680 }
6816 var compiled;
682
6836 try {
6846 compiled = self.compile(src, options);
685 } catch (err2) {
6861 cb(err2);
6871 return;
688 }
689
6905 cb(err, compiled);
691 });
6927 return;
693 }
694
69531 src = self.options.loader.load(pathname);
69629 return self.compile(src, options);
697 };
698
699 /**
700 * 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.
701 *
702 * @example
703 * $ swig compile ./mytpl.html --wrap-start="var mytpl = " > mytpl.js
704 * @example
705 * <script src="mytpl.js"></script>
706 * <script>
707 * swig.run(mytpl, {});
708 * // => "rendered template..."
709 * </script>
710 *
711 * @param {function} tpl Pre-compiled Swig template function. Use the Swig CLI to compile your templates.
712 * @param {object} [locals={}] Template variable context.
713 * @param {string} [filepath] Filename used for caching the template.
714 * @return {string} Rendered output.
715 */
71626 this.run = function (tpl, locals, filepath) {
7175 var context = getLocals({ locals: locals });
7185 if (filepath) {
7191 cacheSet(filepath, {}, tpl);
720 }
7215 return tpl(self, context, filters, utils, efn);
722 };
723};
724
725/*!
726 * Export methods publicly
727 */
7281defaultInstance = new exports.Swig();
7291exports.setFilter = defaultInstance.setFilter;
7301exports.setTag = defaultInstance.setTag;
7311exports.setExtension = defaultInstance.setExtension;
7321exports.parseFile = defaultInstance.parseFile;
7331exports.precompile = defaultInstance.precompile;
7341exports.compile = defaultInstance.compile;
7351exports.compileFile = defaultInstance.compileFile;
7361exports.render = defaultInstance.render;
7371exports.renderFile = defaultInstance.renderFile;
7381exports.run = defaultInstance.run;
7391exports.invalidateCache = defaultInstance.invalidateCache;
7401exports.loaders = loaders;
741

/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) {
1424 return compiler(content, parents, options, args.join(''));
15};
16
171exports.parse = function (str, line, parser) {
1842 parser.on('*', function (token) {
1942 this.out.push(token.match);
20 });
2142 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 () {
1626 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) {
4530 var val = args.shift(),
46 key = '__k',
47 ctxloopcache = (ctx + '__loopcache' + Math.random()).replace(/\./g, ''),
48 last;
49
5030 if (args[0] && args[0] === ',') {
515 args.shift();
525 key = val;
535 val = args.shift();
54 }
55
5630 last = args.join('');
57
5830 return [
59 '(function () {\n',
60 ' var __l = ' + last + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n',
61 ' if (!__l) { return; }\n',
62 ' var ' + 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) {
8232 var firstVar, ready;
83
8432 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
9832 parser.on(types.VAR, function (token) {
9965 if (ready && firstVar) {
10028 return true;
101 }
102
10337 if (!this.out.length) {
10432 firstVar = true;
105 }
106
10737 this.out.push(token.match);
108 });
109
11032 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
11932 parser.on(types.COMPARATOR, function (token) {
12032 if (token.match !== 'in' || !firstVar) {
1211 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
122 }
12331 ready = true;
12431 this.filterApplyIdx.push(this.out.length);
125 });
126
12732 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, swig) {
475 var compiler = require('../parser').compile,
48 parseOpts = { resolveFrom: opts.filename },
49 compileOpts = utils.extend({}, opts, parseOpts),
50 tokens,
51 ctx;
52
535 parser.on(types.STRING, function (token) {
545 var self = this;
555 if (!tokens) {
564 tokens = swig.parseFile(token.match.replace(/^("|')|("|')$/g, ''), parseOpts).tokens;
574 utils.each(tokens, function (token) {
5838 var out = '',
59 macroName;
6038 if (!token || token.name !== 'macro' || !token.compile) {
6126 return;
62 }
6312 macroName = token.args[0];
6412 out += token.compile(compiler, token.args, token.content, [], compileOpts) + '\n';
6512 self.out.push({compiled: out, name: macroName});
66 });
674 return;
68 }
69
701 throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
71 });
72
735 parser.on(types.VAR, function (token) {
747 var self = this;
757 if (!tokens || ctx) {
761 throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
77 }
78
796 if (token.match === 'as') {
803 return;
81 }
82
833 ctx = token.match;
843 self.out.push(ctx);
853 return false;
86 });
87
885 return true;
89};
90
911exports.block = true;
92

/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) {
3212 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
3912 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) {
4914 var file, w;
5014 parser.on(types.STRING, function (token) {
5117 if (!file) {
5213 file = token.match;
5313 this.out.push(file);
5413 return;
55 }
56
574 return true;
58 });
59
6014 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
9514 parser.on('end', function () {
9612 this.out.push(opts.filename || null);
97 });
98
9914 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) {
2032 var fnName = args.shift();
21
2232 return '_ctx.' + fnName + ' = function (' + args.join('') + ') {\n' +
23 ' var _output = "",\n' +
24 ' __ctx = _utils.extend({}, _ctx);\n' +
25 ' _utils.each(_ctx, function (v, k) {\n' +
26 ' if (["' + args.join('","') + '"].indexOf(k) !== -1) { delete _ctx[k]; }\n' +
27 ' });\n' +
28 compiler(content, parents, options, blockName) + '\n' +
29 ' _ctx = _utils.extend(_ctx, __ctx);\n' +
30 ' return _output;\n' +
31 '};\n' +
32 '_ctx.' + fnName + '.safe = true;\n';
33};
34
351exports.parse = function (str, line, parser, types) {
3634 var name;
37
3834 parser.on(types.VAR, function (token) {
3927 if (token.match.indexOf('.') !== -1) {
401 throw new Error('Unexpected dot in macro argument "' + token.match + '" on line ' + line + '.');
41 }
4226 this.out.push(token.match);
43 });
44
4534 parser.on(types.FUNCTION, function (token) {
4616 if (!name) {
4716 name = token.match;
4816 this.out.push(name);
4916 this.state.push(types.FUNCTION);
50 }
51 });
52
5334 parser.on(types.FUNCTIONEMPTY, function (token) {
5415 if (!name) {
5515 name = token.match;
5615 this.out.push(name);
57 }
58 });
59
6034 parser.on(types.PARENCLOSE, function () {
6115 if (this.isLast) {
6214 return;
63 }
641 throw new Error('Unexpected parenthesis close on line ' + line + '.');
65 });
66
6734 parser.on(types.COMMA, function () {
688 return true;
69 });
70
7134 parser.on('*', function () {
728 return;
73 });
74
7534 return true;
76};
77
781exports.ends = true;
791exports.block = true;
80

/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

97%
41
40
1
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) {
3541 return args.join(' ') + ';\n';
36};
37
381exports.parse = function (str, line, parser, types) {
3943 var nameSet = '',
40 propertyName;
41
4243 parser.on(types.VAR, function (token) {
4350 if (propertyName) {
44 // Tell the parser where to find the variable
451 propertyName += '_ctx.' + token.match;
461 return;
47 }
48
4949 if (!parser.out.length) {
5042 nameSet += token.match;
5142 return;
52 }
53
547 return true;
55 });
56
5743 parser.on(types.BRACKETOPEN, function (token) {
589 if (!propertyName && !this.out.length) {
598 propertyName = token.match;
608 return;
61 }
62
631 return true;
64 });
65
6643 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
7543 parser.on(types.BRACKETCLOSE, function (token) {
769 if (propertyName && !this.out.length) {
778 nameSet += propertyName + token.match;
788 propertyName = undefined;
798 return;
80 }
81
821 return true;
83 });
84
8543 parser.on(types.DOTKEY, function (token) {
861 if (!propertyName && !nameSet) {
870 return true;
88 }
891 nameSet += '.' + token.match;
901 return;
91 });
92
9343 parser.on(types.ASSIGNMENT, function (token) {
9444 if (this.out.length || !nameSet) {
952 throw new Error('Unexpected assignment "' + token.match + '" on line ' + line + '.');
96 }
97
9842 this.out.push(
99 // Prevent the set from spilling into global scope
100 '_ctx.' + nameSet
101 );
10242 this.out.push(token.match);
10342 this.filterApplyIdx.push(this.out.length);
104 });
105
10643 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

83%
65
54
11
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) {
9664 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) {
192888 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) {
291199 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) {
394931 var i, l;
40
414931 if (isArray(obj)) {
424873 i = 0;
434873 l = obj.length;
444873 for (i; i < l; i += 1) {
4512431 if (fn(obj[i], i, obj) === false) {
460 break;
47 }
48 }
49 } else {
5058 for (i in obj) {
51139 if (obj.hasOwnProperty(i)) {
52139 if (fn(obj[i], i, obj) === false) {
530 break;
54 }
55 }
56 }
57 }
58
594786 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) {
7821778 var i = 0,
79 result,
80 l;
8121778 if (isArray(obj)) {
8221778 l = obj.length;
83
8421778 for (i; i < l; i += 1) {
8547214 result = fn(obj[i], i, obj);
8647214 if (result) {
874150 break;
88 }
89 }
90 } else {
910 exports.each(obj, function (value, index) {
920 result = fn(value, index, obj);
930 return !(result);
94 });
95 }
9621778 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) {
10687 var i = 0,
107 result = [],
108 l;
109
11087 if (isArray(obj)) {
11186 l = obj.length;
11286 for (i; i < l; i += 1) {
113190 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 }
12287 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 () {
1316705 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
1396705 for (i; i < l; i += 1) {
1407795 obj = objs[i] || {};
1417795 for (key in obj) {
14214428 if (obj.hasOwnProperty(key)) {
14314428 target[key] = obj[key];
144 }
145 }
146 }
1476705 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) {
156464 if (!obj) {
1570 return [];
158 }
159
160464 if (Object.keys) {
161464 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) {
17746 if (line) {
17844 message += ' on line ' + line;
179 }
18046 if (file) {
18129 message += ' in file ' + file;
182 }
18346 throw new Error(message + '.');
184};
185