Coverage

98%
1206
1188
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%
167
166
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) {
11408 var self = this,
12 out = {};
13
14408 if (utils.isArray(input)) {
1524 return utils.map(input, function (value) {
1657 return self.apply(null, arguments);
17 });
18 }
19
20384 if (typeof input === 'object') {
214 utils.each(input, function (value, key) {
225 out[key] = self.apply(null, arguments);
23 });
244 return out;
25 }
26
27380 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 (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) {
146365 var out = iterateFilter.apply(exports.escape, arguments),
147 inp = input,
148 i = 0,
149 code;
150
151365 if (out !== undefined) {
15218 return out;
153 }
154
155347 if (typeof input !== 'string') {
156113 return input;
157 }
158
159234 out = '';
160
161234 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:
184228 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 newValue = utils.extend({}, value);
2543 delete newValue[key];
255
2563 if (!out[keyname]) {
2572 out[keyname] = [];
258 }
259
2603 out[keyname].push(newValue);
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 * Get the number of items in an array, string, or object.
355 *
356 * @example
357 * // my_arr = ['a', 'b', 'c']
358 * {{ my_arr|length }}
359 * // => 3
360 *
361 * @example
362 * // my_str = 'Tacos'
363 * {{ my_str|length }}
364 * // => 5
365 *
366 * @example
367 * // my_obj = {a: 5, b: 20}
368 * {{ my_obj|length }}
369 * // => 2
370 *
371 * @param {*} input
372 * @return {*} The length of the input
373 */
3741exports.length = function (input) {
3754 if (typeof input === 'object' && !utils.isArray(input)) {
3761 var keys = utils.keys(input);
3771 return keys.length;
378 }
3793 if (input.hasOwnProperty('length')) {
3802 return input.length;
381 }
3821 return '';
383};
384
385/**
386 * Return the input in all lowercase letters.
387 *
388 * @example
389 * {{ "FOOBAR"|lower }}
390 * // => foobar
391 *
392 * @example
393 * // myObj = { a: 'FOO', b: 'BAR' }
394 * {{ myObj|lower|join('') }}
395 * // => foobar
396 *
397 * @param {*} input
398 * @return {*} Returns the same type as the input.
399 */
4001exports.lower = function (input) {
4018 var out = iterateFilter.apply(exports.lower, arguments);
4028 if (out !== undefined) {
4032 return out;
404 }
405
4066 return input.toString().toLowerCase();
407};
408
409/**
410 * Deprecated in favor of <a href="#safe">safe</a>.
411 */
4121exports.raw = function (input) {
4132 return exports.safe(input);
414};
4151exports.raw.safe = true;
416
417/**
418 * Returns a new string with the matched search pattern replaced by the given replacement string. Uses JavaScript's built-in String.replace() method.
419 *
420 * @example
421 * // my_var = 'foobar';
422 * {{ my_var|replace('o', 'e', 'g') }}
423 * // => feebar
424 *
425 * @example
426 * // my_var = "farfegnugen";
427 * {{ my_var|replace('^f', 'p') }}
428 * // => parfegnugen
429 *
430 * @example
431 * // my_var = 'a1b2c3';
432 * {{ my_var|replace('\w', '0', 'g') }}
433 * // => 010203
434 *
435 * @param {string} input
436 * @param {string} search String or pattern to replace from the input.
437 * @param {string} replacement String to replace matched pattern.
438 * @param {string} [flags] Regular Expression flags. 'g': global match, 'i': ignore case, 'm': match over multiple lines
439 * @return {string} Replaced string.
440 */
4411exports.replace = function (input, search, replacement, flags) {
44211 var r = new RegExp(search, flags);
44311 return input.replace(r, replacement);
444};
445
446/**
447 * Reverse sort the input. This is an alias for <code data-language="swig">{{ input|sort(true) }}</code>.
448 *
449 * @example
450 * // val = [1, 2, 3];
451 * {{ val|reverse }}
452 * // => 3,2,1
453 *
454 * @param {array} input
455 * @return {array} Reversed array. The original input object is returned if it was not an array.
456 */
4571exports.reverse = function (input) {
45810 return exports.sort(input, true);
459};
460
461/**
462 * Forces the input to not be auto-escaped. Use this only on content that you know is safe to be rendered on your page.
463 *
464 * @example
465 * // my_var = "<p>Stuff</p>";
466 * {{ my_var|safe }}
467 * // => <p>Stuff</p>
468 *
469 * @param {*} input
470 * @return {*} The input exactly how it was given, regardless of autoescaping status.
471 */
4721exports.safe = function (input) {
473 // This is a magic filter. Its logic is hard-coded into Swig's parser.
4745 return input;
475};
4761exports.safe.safe = true;
477
478/**
479 * Sort the input in an ascending direction.
480 * If given an object, will return the keys as a sorted array.
481 * If given a string, each character will be sorted individually.
482 *
483 * @example
484 * // val = [2, 6, 4];
485 * {{ val|sort }}
486 * // => 2,4,6
487 *
488 * @example
489 * // val = 'zaq';
490 * {{ val|sort }}
491 * // => aqz
492 *
493 * @example
494 * // val = { bar: 1, foo: 2 }
495 * {{ val|sort(true) }}
496 * // => foo,bar
497 *
498 * @param {*} input
499 * @param {boolean} [reverse=false] Output is given reverse-sorted if true.
500 * @return {*} Sorted array;
501 */
5021exports.sort = function (input, reverse) {
50315 var out, clone;
50415 if (utils.isArray(input)) {
5055 clone = utils.extend([], input);
5065 out = clone.sort();
507 } else {
50810 switch (typeof input) {
509 case 'object':
5102 out = utils.keys(input).sort();
5112 break;
512 case 'string':
5138 out = input.split('');
5148 if (reverse) {
5157 return out.reverse().join('');
516 }
5171 return out.sort().join('');
518 }
519 }
520
5217 if (out && reverse) {
5224 return out.reverse();
523 }
524
5253 return out || input;
526};
527
528/**
529 * Strip HTML tags.
530 *
531 * @example
532 * // stuff = '<p>foobar</p>';
533 * {{ stuff|striptags }}
534 * // => foobar
535 *
536 * @param {*} input
537 * @return {*} Returns the same object as the input, but with all string values stripped of tags.
538 */
5391exports.striptags = function (input) {
5404 var out = iterateFilter.apply(exports.striptags, arguments);
5414 if (out !== undefined) {
5421 return out;
543 }
544
5453 return input.toString().replace(/(<([^>]+)>)/ig, '');
546};
547
548/**
549 * Capitalizes every word given and lower-cases all other letters.
550 *
551 * @example
552 * // my_str = 'this is soMe text';
553 * {{ my_str|title }}
554 * // => This Is Some Text
555 *
556 * @example
557 * // my_arr = ['hi', 'this', 'is', 'an', 'array'];
558 * {{ my_arr|title|join(' ') }}
559 * // => Hi This Is An Array
560 *
561 * @param {*} input
562 * @return {*} Returns the same object as the input, but with all words in strings title-cased.
563 */
5641exports.title = function (input) {
5654 var out = iterateFilter.apply(exports.title, arguments);
5664 if (out !== undefined) {
5671 return out;
568 }
569
5703 return input.toString().replace(/\w\S*/g, function (str) {
5716 return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
572 });
573};
574
575/**
576 * Remove all duplicate items from an array.
577 *
578 * @example
579 * // my_arr = [1, 2, 3, 4, 4, 3, 2, 1];
580 * {{ my_arr|uniq|join(',') }}
581 * // => 1,2,3,4
582 *
583 * @param {array} input
584 * @return {array} Array with unique items. If input was not an array, the original item is returned untouched.
585 */
5861exports.uniq = function (input) {
5872 var result;
588
5892 if (!input || !utils.isArray(input)) {
5901 return '';
591 }
592
5931 result = [];
5941 utils.each(input, function (v) {
5956 if (result.indexOf(v) === -1) {
5964 result.push(v);
597 }
598 });
5991 return result;
600};
601
602/**
603 * Convert the input to all uppercase letters. If an object or array is provided, all values will be uppercased.
604 *
605 * @example
606 * // my_str = 'tacos';
607 * {{ my_str|upper }}
608 * // => TACOS
609 *
610 * @example
611 * // my_arr = ['tacos', 'burritos'];
612 * {{ my_arr|upper|join(' & ') }}
613 * // => TACOS & BURRITOS
614 *
615 * @param {*} input
616 * @return {*} Returns the same type as the input, with all strings upper-cased.
617 */
6181exports.upper = function (input) {
6198 var out = iterateFilter.apply(exports.upper, arguments);
6208 if (out !== undefined) {
6212 return out;
622 }
623
6246 return input.toString().toUpperCase();
625};
626
627/**
628 * URL-encode a string. If an object or array is passed, all values will be URL-encoded.
629 *
630 * @example
631 * // my_str = 'param=1&anotherParam=2';
632 * {{ my_str|url_encode }}
633 * // => param%3D1%26anotherParam%3D2
634 *
635 * @param {*} input
636 * @return {*} URL-encoded string.
637 */
6381exports.url_encode = function (input) {
6394 var out = iterateFilter.apply(exports.url_encode, arguments);
6404 if (out !== undefined) {
6411 return out;
642 }
6433 return encodeURIComponent(input);
644};
645
646/**
647 * URL-decode a string. If an object or array is passed, all values will be URL-decoded.
648 *
649 * @example
650 * // my_str = 'param%3D1%26anotherParam%3D2';
651 * {{ my_str|url_decode }}
652 * // => param=1&anotherParam=2
653 *
654 * @param {*} input
655 * @return {*} URL-decoded string.
656 */
6571exports.url_decode = function (input) {
6584 var out = iterateFilter.apply(exports.url_decode, arguments);
6594 if (out !== undefined) {
6601 return out;
661 }
6623 return decodeURIComponent(input);
663};
664

/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) {
2542112 var matched;
255
2562112 utils.some(rules, function (rule) {
25719996 return utils.some(rule.regex, function (regex) {
25827936 var match = str.match(regex),
259 normalized;
260
26127936 if (!match) {
26225824 return;
263 }
264
2652112 normalized = match[rule.idx || 0].replace(/\s*$/, '');
2662112 normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
267
2682112 matched = {
269 match: normalized,
270 type: rule.type,
271 length: match[0].length
272 };
2732112 return true;
274 });
275 });
276
2772112 if (!matched) {
2780 matched = {
279 match: str,
280 type: TYPES.UNKNOWN,
281 length: str.length
282 };
283 }
284
2852112 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) {
295685 var offset = 0,
296 tokens = [],
297 substr,
298 match;
299685 while (offset < str.length) {
3002112 substr = str.substring(offset);
3012112 match = reader(substr);
3022112 offset += match.length;
3032112 tokens.push(match);
304 }
305685 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) {
295611 if (basepath) {
304 from = basepath;
31 } else {
325607 from = from ? path.dirname(from) : process.cwd();
33 }
345610 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) {
472802 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) {
61685 this.out = [];
62685 this.state = [];
63685 this.filterApplyIdx = [];
64685 this._parsers = {};
65685 this.line = line;
66685 this.filename = filename;
67685 this.filters = filters;
68685 this.escape = autoescape;
69
70685 this.parse = function () {
71681 var self = this;
72
73681 if (self._parsers.start) {
740 self._parsers.start.call(self);
75 }
76681 utils.each(tokens, function (token, i) {
772086 var prevToken = tokens[i - 1];
782086 self.isLast = (i === tokens.length - 1);
792086 if (prevToken) {
801426 while (prevToken.type === _t.WHITESPACE) {
81301 i -= 1;
82301 prevToken = tokens[i - 1];
83 }
84 }
852086 self.prevToken = prevToken;
862086 self.parseToken(token);
87 });
88622 if (self._parsers.end) {
8919 self._parsers.end.call(self);
90 }
91
92622 if (self.escape) {
93276 self.filterApplyIdx = [0];
94276 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 {
100274 self.parseToken({ type: _t.FILTEREMPTY, match: 'e' });
101 }
102 }
103
104622 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) {
1291081 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) {
1392368 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
1472368 if (fn && typeof fn === 'function') {
148524 if (!fn.call(this, token)) {
149413 return;
150 }
151 }
152
1531933 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
1641933 if (lastState && lastState === _t.METHODOPEN) {
16523 self.state.pop();
16623 if (token.type !== _t.PARENCLOSE) {
16711 self.out.push(', ');
168 }
169 }
170
1711933 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:
196333 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1971 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
198 }
199332 self.escape = self.filters[match].safe ? false : self.escape;
200332 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
201332 self.out.push(')');
202332 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:
21928 self.state.push(token.type);
22028 if (self.filterApplyIdx.length) {
22126 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '(');
22226 if (prevToken && prevTokenType === _t.VAR) {
22323 temp = prevToken.match.split('.').slice(0, -1);
22423 self.out.push(' || _fn).call(' + self.checkMatch(temp));
22523 self.state.push(_t.METHODOPEN);
22623 self.escape = false;
227 } else {
2283 self.out.push(' || _fn)(');
229 }
23026 self.filterApplyIdx.push(self.out.length - 3);
231 } else {
2322 self.out.push('(');
2332 self.filterApplyIdx.push(self.out.length - 1);
234 }
23528 break;
236
237 case _t.PARENCLOSE:
238161 temp = self.state.pop();
239161 if (temp !== _t.PARENOPEN && temp !== _t.FUNCTION && temp !== _t.FILTER) {
2401 utils.throwError('Mismatched nesting state', self.line, self.filename);
241 }
242160 self.out.push(')');
243 // Once off the previous entry
244160 self.filterApplyIdx.pop();
245160 if (temp !== _t.FILTER) {
246 // Once for the open paren
24750 self.filterApplyIdx.pop();
248 }
249160 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:
286445 self.parseVar(token, match, lastState);
287418 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:
3399 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 )) {
3482 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) {
369445 var self = this;
370
371445 match = match.split('.');
372
373445 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
377419 self.filterApplyIdx.push(self.out.length);
378419 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
386409 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) {
395432 var temp = match[0], result;
396
397432 function checkDot(ctx) {
3981296 var c = ctx + temp,
399 m = match,
400 build = '';
401
4021296 build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
4031296 utils.each(m, function (v, i) {
4041452 if (i === 0) {
4051296 return;
406 }
407156 build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
408156 c += '.' + v;
409 });
4101296 build += ')';
411
4121296 return build;
413 }
414
415432 function buildDot(ctx) {
416864 return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
417 }
418432 result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
419432 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) {
438467 source = source.replace(/\r\n/g, '\n');
439467 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 */
481467 function parseVariable(str, line) {
482365 var lexedTokens = lexer.read(utils.strip(str)),
483 parser,
484 out;
485
486365 parser = new TokenParser(lexedTokens, filters, escape, line, opts.filename);
487365 out = parser.parse().join('');
488
489329 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 */
498327 return {
499 compile: function () {
500321 return '_output += ' + out + ';\n';
501 }
502 };
503 }
504467 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 */
513467 function parseTag(str, line) {
514517 var lexedTokens, parser, chunks, tagName, tag, args, last;
515
516517 if (utils.startsWith(str, 'end')) {
517194 last = stack[stack.length - 1];
518194 if (last && last.name === str.split(/\s+/)[0].replace(/^end/, '') && last.ends) {
519192 switch (last.name) {
520 case 'autoescape':
5219 escape = opts.autoescape;
5229 break;
523 case 'raw':
5244 inRaw = false;
5254 break;
526 }
527192 stack.pop();
528192 return;
529 }
530
5312 if (!inRaw) {
5321 utils.throwError('Unexpected end of tag "' + str.replace(/^end/, '') + '"', line, opts.filename);
533 }
534 }
535
536324 if (inRaw) {
5373 return;
538 }
539
540321 chunks = str.split(/\s+(.+)?/);
541321 tagName = chunks.shift();
542
543321 if (!tags.hasOwnProperty(tagName)) {
5441 utils.throwError('Unexpected tag "' + str + '"', line, opts.filename);
545 }
546
547320 lexedTokens = lexer.read(utils.strip(chunks.join(' ')));
548320 parser = new TokenParser(lexedTokens, filters, false, line, opts.filename);
549320 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 */
573320 if (!tag.parse(chunks[1], line, parser, _t, stack, opts, swig)) {
5742 utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename);
575 }
576
577316 parser.parse();
578293 args = parser.out;
579
580293 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 */
598293 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 */
613467 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 */
624467 utils.each(source.split(splitter), function (chunk) {
6252182 var token, lines, stripPrev, prevToken, prevChildToken;
626
6272182 if (!chunk) {
628917 return;
629 }
630
631 // Is a variable?
6321265 if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
633365 stripPrev = varStripBefore.test(chunk);
634365 stripNext = varStripAfter.test(chunk);
635365 token = parseVariable(chunk.replace(varStrip, ''), line);
636 // Is a tag?
637900 } else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
638517 stripPrev = tagStripBefore.test(chunk);
639517 stripNext = tagStripAfter.test(chunk);
640517 token = parseTag(chunk.replace(tagStrip, ''), line);
641488 if (token) {
642293 if (token.name === 'extends') {
64326 parent = token.args.join('').replace(/^\'|\'$/g, '').replace(/^\"|\"$/g, '');
644267 } else if (token.block && !stack.length) {
645126 blocks[token.args.join('')] = token;
646 }
647 }
648488 if (inRaw && !token) {
6493 token = chunk;
650 }
651 // Is a content string?
652383 } else if (inRaw || (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))) {
653376 token = stripNext ? chunk.replace(/^\s*/, '') : chunk;
654376 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>
6601191 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.
6721191 if (!token) {
673198 return;
674 }
675
676 // If there's an open item in the stack, add this to its content.
677993 if (stack.length) {
678285 stack[stack.length - 1].content.push(token);
679 } else {
680708 tokens.push(token);
681 }
682
683 // If the token is a tag that requires an end tag, open it on the stack.
684993 if (token.name && token.ends) {
685195 stack.push(token);
686 }
687
688993 lines = chunk.match(/\n/g);
689993 line += lines ? lines.length : 0;
690 });
691
692400 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) {
710541 var out = '',
711 tokens = utils.isArray(template) ? template : template.tokens;
712
713541 utils.each(tokens, function (token) {
714821 var o;
715821 if (typeof token === 'string') {
716282 out += '_output += "' + token.replace(/\\/g, '\\\\').replace(/\n|\r/g, '\\n').replace(/"/g, '\\"') + '";\n';
717282 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 */
739539 o = token.compile(exports.compile, token.args ? token.args.slice(0) : [], token.content ? token.content.slice(0) : [], parents, options, blockName);
740539 out += o || '';
741 });
742
743541 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) {
891111 if (!options) {
9090 return;
91 }
92
931021 utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
943048 if (!options.hasOwnProperty(key)) {
951247 return;
96 }
971801 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 }
1001795 if (options[key][0] === options[key][1]) {
1013 throw new Error('Option "' + key + '" open and close controls must not be the same.');
102 }
1031792 utils.each(options[key], function (a, i) {
1043581 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
1101006 if (options.hasOwnProperty('cache')) {
111598 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 }
1171004 if (options.hasOwnProperty('loader')) {
118604 if (options.loader) {
119604 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) {
144602 validateOptions(options);
145598 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) {
187924 if (!options || !options.locals) {
188345 return self.options.locals;
189 }
190
191579 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) {
2015649 options = options || {};
2025649 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) {
2125609 if (shouldCache(options)) {
2133 return;
214 }
215
2165606 if (self.options.cache === 'memory') {
2175605 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 () {
252592 if (self.options.cache === 'memory') {
253592 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) {
339482 validateOptions(options);
340
341467 var locals = getLocals(options),
342 opt = {},
343 k;
344
345467 for (k in options) {
346405 if (options.hasOwnProperty(k) && k !== 'locals') {
347117 opt[k] = options[k];
348 }
349 }
350
351467 options = utils.extend({}, self.options, opt);
352467 options.locals = locals;
353
354467 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) {
429373 var parentName = tokens.parent,
430 parentFiles = [],
431 parents = [],
432 parentFile,
433 parent,
434 l;
435
436373 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.
455370 l = parents.length;
456370 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
461370 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) {
486455 var tokens = self.parse(source, options),
487 parents = getParents(tokens, options),
488 tpl;
489
490370 if (parents.length) {
491 // Remap the templates first-parent's tokens using this template's blocks.
49217 tokens.tokens = remapBlocks(tokens.blocks, parents[0].tokens);
49317 importNonBlocks(tokens.blocks, tokens.tokens);
494 }
495
496370 try {
497370 tpl = new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
498 ' var _ext = _swig.extensions,\n' +
499 ' _output = "";\n' +
500 parser.compile(tokens, parents, options) + '\n' +
501 ' return _output;\n'
502 );
503 } catch (e) {
5041 utils.throwError(e, null, options.filename);
505 }
506
507369 return { tpl: tpl, tokens: tokens };
508 };
509
510 /**
511 * Compile and render a template string for final output.
512 *
513 * 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.
514 *
515 * @example
516 * swig.render('{{ tacos }}', { locals: { tacos: 'Tacos!!!!' }});
517 * // => Tacos!!!!
518 *
519 * @param {string} source Swig template source string.
520 * @param {SwigOpts} [options={}] Swig options object.
521 * @return {string} Rendered output.
522 */
52326 this.render = function (source, options) {
524388 return self.compile(source, options)();
525 };
526
527 /**
528 * Compile and render a template file for final output. This is most useful for libraries like Express.js.
529 *
530 * @example
531 * swig.renderFile('./template.html', {}, function (err, output) {
532 * if (err) {
533 * throw err;
534 * }
535 * console.log(output);
536 * });
537 *
538 * @example
539 * swig.renderFile('./template.html', {});
540 * // => output
541 *
542 * @param {string} pathName File location.
543 * @param {object} [locals={}] Template variable context.
544 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
545 * @return {string} Rendered output.
546 */
54726 this.renderFile = function (pathName, locals, cb) {
54812 if (cb) {
5495 self.compileFile(pathName, {}, function (err, fn) {
5505 var result;
551
5525 if (err) {
5531 cb(err);
5541 return;
555 }
556
5574 try {
5584 result = fn(locals);
559 } catch (err2) {
5601 cb(err2);
5611 return;
562 }
563
5643 cb(null, result);
565 });
5665 return;
567 }
568
5697 return self.compileFile(pathName)(locals);
570 };
571
572 /**
573 * Compile string source into a renderable template function.
574 *
575 * @example
576 * var tpl = swig.compile('{{ tacos }}');
577 * // => {
578 * // [Function: compiled]
579 * // parent: null,
580 * // tokens: [{ compile: [Function] }],
581 * // blocks: {}
582 * // }
583 * tpl({ tacos: 'Tacos!!!!' });
584 * // => Tacos!!!!
585 *
586 * 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.
587 *
588 * @param {string} source Swig template source string.
589 * @param {SwigOpts} [options={}] Swig options object.
590 * @return {function} Renderable function with keys for parent, blocks, and tokens.
591 */
59226 this.compile = function (source, options) {
593453 var key = options ? options.filename : null,
594 cached = key ? cacheGet(key, options) : null,
595 context,
596 contextLength,
597 pre;
598
599453 if (cached) {
6001 return cached;
601 }
602
603452 context = getLocals(options);
604452 contextLength = utils.keys(context).length;
605452 pre = this.precompile(source, options);
606
607366 function compiled(locals) {
6085830 var lcls;
6095830 if (locals && contextLength) {
6101 lcls = utils.extend({}, context, locals);
6115829 } else if (locals && !contextLength) {
6125501 lcls = locals;
613328 } else if (!locals && contextLength) {
614281 lcls = context;
615 } else {
61647 lcls = {};
617 }
6185830 return pre.tpl(self, lcls, filters, utils, efn);
619 }
620
621366 utils.extend(compiled, pre.tokens);
622
623366 if (key) {
62439 cacheSet(key, options, compiled);
625 }
626
627366 return compiled;
628 };
629
630 /**
631 * Compile a source file into a renderable template function.
632 *
633 * @example
634 * var tpl = swig.compileFile('./mytpl.html');
635 * // => {
636 * // [Function: compiled]
637 * // parent: null,
638 * // tokens: [{ compile: [Function] }],
639 * // blocks: {}
640 * // }
641 * tpl({ tacos: 'Tacos!!!!' });
642 * // => Tacos!!!!
643 *
644 * @example
645 * swig.compileFile('/myfile.txt', { varControls: ['<%=', '=%>'], tagControls: ['<%', '%>']});
646 * // => will compile 'myfile.txt' using the var and tag controls as specified.
647 *
648 * @param {string} pathname File location.
649 * @param {SwigOpts} [options={}] Swig options object.
650 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
651 * @return {function} Renderable function with keys for parent, blocks, and tokens.
652 */
65326 this.compileFile = function (pathname, options, cb) {
6545510 var src, cached;
655
6565510 if (!options) {
65723 options = {};
658 }
659
6605510 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
6615509 if (!options.filename) {
6625509 options = utils.extend({ filename: pathname }, options);
663 }
6645509 cached = cacheGet(pathname, options);
665
6665509 if (cached) {
6675471 if (cb) {
6681 cb(null, cached);
6691 return;
670 }
6715470 return cached;
672 }
673
67438 if (cb) {
6757 self.options.loader.load(pathname, function (err, src) {
6767 if (err) {
6771 cb(err);
6781 return;
679 }
6806 var compiled;
681
6826 try {
6836 compiled = self.compile(src, options);
684 } catch (err2) {
6851 cb(err2);
6861 return;
687 }
688
6895 cb(err, compiled);
690 });
6917 return;
692 }
693
69431 src = self.options.loader.load(pathname);
69529 return self.compile(src, options);
696 };
697
698 /**
699 * 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.
700 *
701 * @example
702 * $ swig compile ./mytpl.html --wrap-start="var mytpl = " > mytpl.js
703 * @example
704 * <script src="mytpl.js"></script>
705 * <script>
706 * swig.run(mytpl, {});
707 * // => "rendered template..."
708 * </script>
709 *
710 * @param {function} tpl Pre-compiled Swig template function. Use the Swig CLI to compile your templates.
711 * @param {object} [locals={}] Template variable context.
712 * @param {string} [filepath] Filename used for caching the template.
713 * @return {string} Rendered output.
714 */
71526 this.run = function (tpl, locals, filepath) {
7165 var context = getLocals({ locals: locals });
7175 if (filepath) {
7181 cacheSet(filepath, {}, tpl);
719 }
7205 return tpl(self, context, filters, utils, efn);
721 };
722};
723
724/*!
725 * Export methods publicly
726 */
7271defaultInstance = new exports.Swig();
7281exports.setFilter = defaultInstance.setFilter;
7291exports.setTag = defaultInstance.setTag;
7301exports.setExtension = defaultInstance.setExtension;
7311exports.parseFile = defaultInstance.parseFile;
7321exports.precompile = defaultInstance.precompile;
7331exports.compile = defaultInstance.compile;
7341exports.compileFile = defaultInstance.compileFile;
7351exports.render = defaultInstance.render;
7361exports.renderFile = defaultInstance.renderFile;
7371exports.run = defaultInstance.run;
7381exports.invalidateCache = defaultInstance.invalidateCache;
7391exports.loaders = loaders;
740

/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 () { return; };
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 (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%
37
37
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 form %}
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 allMacros = utils.map(args, function (arg) {
2612 return arg.name;
27 }).join('|'),
28 out = '_ctx.' + ctx + ' = {};\n var _output = "";\n',
29 replacements = utils.map(args, function (arg) {
3012 return {
31 ex: new RegExp('_ctx.' + arg.name + '(\\W)(?!' + allMacros + ')', 'g'),
32 re: '_ctx.' + ctx + '.' + arg.name + '$1'
33 };
34 });
35
36 // Replace all occurrences of all macros in this file with
37 // proper namespaced definitions and calls
382 utils.each(args, function (arg) {
3912 var c = arg.compiled;
4012 utils.each(replacements, function (re) {
4172 c = c.replace(re.ex, re.re);
42 });
4312 out += c;
44 });
45
462 return out;
47};
48
491exports.parse = function (str, line, parser, types, stack, opts, swig) {
505 var compiler = require('../parser').compile,
51 parseOpts = { resolveFrom: opts.filename },
52 compileOpts = utils.extend({}, opts, parseOpts),
53 tokens,
54 ctx;
55
565 parser.on(types.STRING, function (token) {
575 var self = this;
585 if (!tokens) {
594 tokens = swig.parseFile(token.match.replace(/^("|')|("|')$/g, ''), parseOpts).tokens;
604 utils.each(tokens, function (token) {
6154 var out = '',
62 macroName;
6354 if (!token || token.name !== 'macro' || !token.compile) {
6436 return;
65 }
6618 macroName = token.args[0];
6718 out += token.compile(compiler, token.args, token.content, [], compileOpts) + '\n';
6818 self.out.push({compiled: out, name: macroName});
69 });
704 return;
71 }
72
731 throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
74 });
75
765 parser.on(types.VAR, function (token) {
777 var self = this;
787 if (!tokens || ctx) {
791 throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
80 }
81
826 if (token.match === 'as') {
833 return;
84 }
85
863 ctx = token.match;
873 self.out.push(ctx);
883 return false;
89 });
90
915 return true;
92};
93
941exports.block = true;
95

/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) {
2044 var fnName = args.shift();
21
2244 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) {
3646 var name;
37
3846 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
4546 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
5346 parser.on(types.FUNCTIONEMPTY, function (token) {
5427 if (!name) {
5527 name = token.match;
5627 this.out.push(name);
57 }
58 });
59
6046 parser.on(types.PARENCLOSE, function () {
6115 if (this.isLast) {
6214 return;
63 }
641 throw new Error('Unexpected parenthesis close on line ' + line + '.');
65 });
66
6746 parser.on(types.COMMA, function () {
688 return true;
69 });
70
7146 parser.on('*', function () {
728 return;
73 });
74
7546 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

100%
41
41
0
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) {
3944 var nameSet = '',
40 propertyName;
41
4244 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
5744 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
6644 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
7544 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
8544 parser.on(types.DOTKEY, function (token) {
862 if (!propertyName && !nameSet) {
871 return true;
88 }
891 nameSet += '.' + token.match;
901 return;
91 });
92
9344 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
10644 return true;
107};
108
1091exports.block = true;
110

/lib/tags/spaceless.js

100%
9
9
0
LineHitsSource
1/**
2 * Attempts to remove whitespace between HTML tags. Use at your own risk.
3 *
4 * @alias spaceless
5 *
6 * @example
7 * {% spaceless %}
8 * {% for num in foo %}
9 * <li>{{ loop.index }}</li>
10 * {% endfor %}
11 * {% endspaceless %}
12 * // => <li>1</li><li>2</li><li>3</li>
13 *
14 */
151exports.compile = function (compiler, args, content, parents, options, blockName) {
165 var out = compiler(content, parents, options, blockName);
175 out += '_output = _output.replace(/^\\s+/, "")\n' +
18 ' .replace(/>\\s+</g, "><")\n' +
19 ' .replace(/\\s+$/, "");\n';
20
215 return out;
22};
23
241exports.parse = function (str, line, parser) {
256 parser.on('*', function (token) {
261 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
27 });
28
296 return true;
30};
31
321exports.ends = true;
33

/lib/utils.js

81%
65
53
12
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) {
9685 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) {
193055 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) {
291260 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) {
396051 var i, l;
40
416051 if (isArray(obj)) {
425991 i = 0;
435991 l = obj.length;
445991 for (i; i < l; i += 1) {
4515008 if (fn(obj[i], i, obj) === false) {
460 break;
47 }
48 }
49 } else {
5060 for (i in obj) {
51153 if (obj.hasOwnProperty(i)) {
52153 if (fn(obj[i], i, obj) === false) {
530 break;
54 }
55 }
56 }
57 }
58
595904 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) {
7822108 var i = 0,
79 result,
80 l;
8122108 if (isArray(obj)) {
8222108 l = obj.length;
83
8422108 for (i; i < l; i += 1) {
8547932 result = fn(obj[i], i, obj);
8647932 if (result) {
874224 break;
88 }
89 }
90 } else {
910 exports.each(obj, function (value, index) {
920 result = fn(value, index, obj);
930 return !result;
94 });
95 }
9622108 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) {
10679 var i = 0,
107 result = [],
108 l;
109
11079 if (isArray(obj)) {
11179 l = obj.length;
11279 for (i; i < l; i += 1) {
113197 result[i] = fn(obj[i], i);
114 }
115 } else {
1160 for (i in obj) {
1170 if (obj.hasOwnProperty(i)) {
1180 result[i] = fn(obj[i], i);
119 }
120 }
121 }
12279 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 () {
1317620 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
1397620 for (i; i < l; i += 1) {
1408723 obj = objs[i] || {};
1418723 for (key in obj) {
14216968 if (obj.hasOwnProperty(key)) {
14316968 target[key] = obj[key];
144 }
145 }
146 }
1477620 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) {
156470 if (!obj) {
1570 return [];
158 }
159
160470 if (Object.keys) {
161470 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) {
17747 if (line) {
17845 message += ' on line ' + line;
179 }
18047 if (file) {
18129 message += ' in file ' + file;
182 }
18347 throw new Error(message + '.');
184};
185