Coverage

98%
1185
1166
19

/lib/dateformatter.js

100%
105
105
0
LineHitsSource
11var utils = require('./utils');
2
31var _months = {
4 full: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
5 abbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
6 },
7 _days = {
8 full: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
9 abbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
10 alt: {'-1': 'Yesterday', 0: 'Today', 1: 'Tomorrow'}
11 };
12
13/*
14DateZ is licensed under the MIT License:
15Copyright (c) 2011 Tomo Universalis (http://tomouniversalis.com)
16Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19*/
201exports.tzOffset = 0;
211exports.DateZ = function () {
2260 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
2860 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
3060 d.timezoneOffset = d.dateZ.getTimezoneOffset();
31
3260 utils.each(members.z, function (name) {
33720 d[name] = function () {
3466 return d.dateZ[name]();
35 };
36 });
3760 utils.each(members['default'], function (name) {
38780 d[name] = function () {
3915 return d.date[name]();
40 };
41 });
42
4360 this.setTimezoneOffset(exports.tzOffset);
44};
451exports.DateZ.prototype = {
46 getTimezoneOffset: function () {
474 return this.timezoneOffset;
48 },
49 setTimezoneOffset: function (offset) {
50119 this.timezoneOffset = offset;
51119 this.dateZ = new Date(this.date.getTime() + this.date.getTimezoneOffset() * 60000 - this.timezoneOffset * 60000);
52119 return this;
53 }
54};
55
56// Day
571exports.d = function (input) {
583 return (input.getDate() < 10 ? '0' : '') + input.getDate();
59};
601exports.D = function (input) {
611 return _days.abbr[input.getDay()];
62};
631exports.j = function (input) {
641 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) {
7412 var d = input.getDate();
7512 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) {
1081 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%
156
155
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) {
11375 var self = this,
12 out = {};
13
14375 if (utils.isArray(input)) {
1523 return utils.map(input, function (value) {
1653 return self.apply(null, arguments);
17 });
18 }
19
20352 if (typeof input === 'object') {
214 utils.each(input, function (value, key) {
225 out[key] = self.apply(null, arguments);
23 });
244 return out;
25 }
26
27348 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 *
76 * @param {?(string|date)} input
77 * @param {string} format PHP-style date format compatible string.
78 * @param {number=} offset Timezone offset from GMT in minutes.
79 * @param {string=} abbr Timezone abbreviation. Used for output only.
80 * @return {string} Formatted date string.
81 */
821exports.date = function (input, format, offset, abbr) {
8354 var l = format.length,
84 date = new dateFormatter.DateZ(input),
85 cur,
86 i = 0,
87 out = '';
88
8954 if (offset) {
9053 date.setTimezoneOffset(offset, abbr);
91 }
92
9354 for (i; i < l; i += 1) {
9468 cur = format.charAt(i);
9568 if (dateFormatter.hasOwnProperty(cur)) {
9661 out += dateFormatter[cur](date, offset, abbr);
97 } else {
987 out += cur;
99 }
100 }
10154 return out;
102};
103
104/**
105 * If the input is `undefined`, `null`, or `false`, a default return value can be specified.
106 *
107 * @example
108 * {{ null_value|default('Tacos') }}
109 * // => Tacos
110 *
111 * @example
112 * {{ "Burritos"|default("Tacos") }}
113 * // => Burritos
114 *
115 * @param {*} input
116 * @param {*} def Value to return if `input` is `undefined`, `null`, or `false`.
117 * @return {*} `input` or `def` value.
118 */
1191exports["default"] = function (input, def) {
12019 return (typeof input !== 'undefined' && (input || typeof input === 'number')) ? input : def;
121};
122
123/**
124 * 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.
125 *
126 * @example
127 * {{ "<blah>"|escape }}
128 * // => <blah>
129 *
130 * @example
131 * {{ "<blah>"|e("js") }}
132 * // => \u003Cblah\u003E
133 *
134 * @param {*} input
135 * @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.
136 * @return {string} Escaped string.
137 */
1381exports.escape = function (input, type) {
139332 var out = iterateFilter.apply(exports.escape, arguments),
140 inp = input,
141 i = 0,
142 code;
143
144332 if (out !== undefined) {
14517 return out;
146 }
147
148315 if (typeof input !== 'string') {
149100 return input;
150 }
151
152215 out = '';
153
154215 switch (type) {
155 case 'js':
1566 inp = inp.replace(/\\/g, '\\u005C');
1576 for (i; i < inp.length; i += 1) {
158161 code = inp.charCodeAt(i);
159161 if (code < 32) {
1606 code = code.toString(16).toUpperCase();
1616 code = (code.length < 2) ? '0' + code : code;
1626 out += '\\u00' + code;
163 } else {
164155 out += inp[i];
165 }
166 }
1676 return out.replace(/&/g, '\\u0026')
168 .replace(/</g, '\\u003C')
169 .replace(/>/g, '\\u003E')
170 .replace(/\'/g, '\\u0027')
171 .replace(/"/g, '\\u0022')
172 .replace(/\=/g, '\\u003D')
173 .replace(/-/g, '\\u002D')
174 .replace(/;/g, '\\u003B');
175
176 default:
177209 return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
178 .replace(/</g, '<')
179 .replace(/>/g, '>')
180 .replace(/"/g, '"')
181 .replace(/'/g, ''');
182 }
183};
1841exports.e = exports.escape;
185
186/**
187 * Get the first item in an array or character in a string. All other objects will attempt to return the first value available.
188 *
189 * @example
190 * // my_arr = ['a', 'b', 'c']
191 * {{ my_arr|first }}
192 * // => a
193 *
194 * @example
195 * // my_val = 'Tacos'
196 * {{ my_val|first }}
197 * // T
198 *
199 * @param {*} input
200 * @return {*} The first item of the array or first character of the string input.
201 */
2021exports.first = function (input) {
2034 if (typeof input === 'object' && !utils.isArray(input)) {
2041 var keys = utils.keys(input);
2051 return input[keys[0]];
206 }
207
2083 if (typeof input === 'string') {
2091 return input.substr(0, 1);
210 }
211
2122 return input[0];
213};
214
215/**
216 * Group an array of objects by a common key. If an array is not provided, the input value will be returned untouched.
217 *
218 * @example
219 * // people = [{ age: 23, name: 'Paul' }, { age: 26, name: 'Jane' }, { age: 23, name: 'Jim' }];
220 * {% for agegroup in people|groupBy('age') %}
221 * <h2>{{ loop.key }}</h2>
222 * <ul>
223 * {% for person in agegroup %}
224 * <li>{{ person.name }}</li>
225 * {% endfor %}
226 * </ul>
227 * {% endfor %}
228 *
229 * @param {*} input Input object.
230 * @param {string} key Key to group by.
231 * @return {object} Grouped arrays by given key.
232 */
2331exports.groupBy = function (input, key) {
2342 if (!utils.isArray(input)) {
2351 return input;
236 }
237
2381 var out = {};
239
2401 utils.each(input, function (value) {
2413 if (!value.hasOwnProperty(key)) {
2420 return;
243 }
244
2453 var keyname = value[key],
246 newVal = utils.extend({}, value);
2473 delete value[key];
248
2493 if (!out[keyname]) {
2502 out[keyname] = [];
251 }
252
2533 out[keyname].push(value);
254 });
255
2561 return out;
257};
258
259/**
260 * Join the input with a string.
261 *
262 * @example
263 * // my_array = ['foo', 'bar', 'baz']
264 * {{ my_array|join(', ') }}
265 * // => foo, bar, baz
266 *
267 * @example
268 * // my_key_object = { a: 'foo', b: 'bar', c: 'baz' }
269 * {{ my_key_object|join(' and ') }}
270 * // => foo and bar and baz
271 *
272 * @param {*} input
273 * @param {string} glue String value to join items together.
274 * @return {string}
275 */
2761exports.join = function (input, glue) {
2777 if (utils.isArray(input)) {
2783 return input.join(glue);
279 }
280
2814 if (typeof input === 'object') {
2823 var out = [];
2833 utils.each(input, function (value) {
2845 out.push(value);
285 });
2863 return out.join(glue);
287 }
2881 return input;
289};
290
291/**
292 * Return a string representation of an JavaScript object.
293 *
294 * Backwards compatible with swig@0.x.x using `json_encode`.
295 *
296 * @example
297 * // val = { a: 'b' }
298 * {{ val|json }}
299 * // => {"a":"b"}
300 *
301 * @example
302 * // val = { a: 'b' }
303 * {{ val|json(4) }}
304 * // => {
305 * // "a": "b"
306 * // }
307 *
308 * @param {*} input
309 * @param {number} [indent] Number of spaces to indent for pretty-formatting.
310 * @return {string} A valid JSON string.
311 */
3121exports.json = function (input, indent) {
3133 return JSON.stringify(input, null, indent || 0);
314};
3151exports.json_encode = exports.json;
316
317/**
318 * Get the last item in an array or character in a string. All other objects will attempt to return the last value available.
319 *
320 * @example
321 * // my_arr = ['a', 'b', 'c']
322 * {{ my_arr|last }}
323 * // => c
324 *
325 * @example
326 * // my_val = 'Tacos'
327 * {{ my_val|last }}
328 * // s
329 *
330 * @param {*} input
331 * @return {*} The last item of the array or last character of the string.input.
332 */
3331exports.last = function (input) {
3343 if (typeof input === 'object' && !utils.isArray(input)) {
3351 var keys = utils.keys(input);
3361 return input[keys[keys.length - 1]];
337 }
338
3392 if (typeof input === 'string') {
3401 return input.charAt(input.length - 1);
341 }
342
3431 return input[input.length - 1];
344};
345
346/**
347 * Return the input in all lowercase letters.
348 *
349 * @example
350 * {{ "FOOBAR"|lower }}
351 * // => foobar
352 *
353 * @example
354 * // myObj = { a: 'FOO', b: 'BAR' }
355 * {{ myObj|lower|join('') }}
356 * // => foobar
357 *
358 * @param {*} input
359 * @return {*} Returns the same type as the input.
360 */
3611exports.lower = function (input) {
3628 var out = iterateFilter.apply(exports.lower, arguments);
3638 if (out !== undefined) {
3642 return out;
365 }
366
3676 return input.toString().toLowerCase();
368};
369
370/**
371 * Deprecated in favor of <a href="#safe">safe</a>.
372 */
3731exports.raw = function (input) {
3742 return exports.safe(input);
375};
3761exports.raw.safe = true;
377
378/**
379 * Returns a new string with the matched search pattern replaced by the given replacement string. Uses JavaScript's built-in String.replace() method.
380 *
381 * @example
382 * // my_var = 'foobar';
383 * {{ my_var|replace('o', 'e', 'g') }}
384 * // => feebar
385 *
386 * @example
387 * // my_var = "farfegnugen";
388 * {{ my_var|replace('^f', 'p') }}
389 * // => parfegnugen
390 *
391 * @example
392 * // my_var = 'a1b2c3';
393 * {{ my_var|replace('\w', '0', 'g') }}
394 * // => 010203
395 *
396 * @param {string} input
397 * @param {string} search String or pattern to replace from the input.
398 * @param {string} replacement String to replace matched pattern.
399 * @param {string} [flags] Regular Expression flags. 'g': global match, 'i': ignore case, 'm': match over multiple lines
400 * @return {string} Replaced string.
401 */
4021exports.replace = function (input, search, replacement, flags) {
40311 var r = new RegExp(search, flags);
40411 return input.replace(r, replacement);
405};
406
407/**
408 * Reverse sort the input. This is an alias for <code data-language="swig">{{ input|sort(true) }}</code>.
409 *
410 * @example
411 * // val = [1, 2, 3];
412 * {{ val|reverse }}
413 * // => 3,2,1
414 *
415 * @param {array} input
416 * @return {array} Reversed array. The original input object is returned if it was not an array.
417 */
4181exports.reverse = function (input) {
4193 return exports.sort(input, true);
420};
421
422/**
423 * Forces the input to not be auto-escaped. Use this only on content that you know is safe to be rendered on your page.
424 *
425 * @example
426 * // my_var = "<p>Stuff</p>";
427 * {{ my_var|safe }}
428 * // => <p>Stuff</p>
429 *
430 * @param {*} input
431 * @return {*} The input exactly how it was given, regardless of autoescaping status.
432 */
4331exports.safe = function (input) {
434 // This is a magic filter. Its logic is hard-coded into Swig's parser.
4355 return input;
436};
4371exports.safe.safe = true;
438
439/**
440 * Sort the input in an ascending direction.
441 * If given an object, will return the keys as a sorted array.
442 * If given a string, each character will be sorted individually.
443 *
444 * @example
445 * // val = [2, 6, 4];
446 * {{ val|sort }}
447 * // => 2,4,6
448 *
449 * @example
450 * // val = 'zaq';
451 * {{ val|sort }}
452 * // => aqz
453 *
454 * @example
455 * // val = { bar: 1, foo: 2 }
456 * {{ val|sort(true) }}
457 * // => foo,bar
458 *
459 * @param {*} input
460 * @param {boolean} [reverse=false] Output is given reverse-sorted if true.
461 * @return {*} Sorted array;
462 */
4631exports.sort = function (input, reverse) {
4646 var out;
4656 if (utils.isArray(input)) {
4662 out = input.sort();
467 } else {
4684 switch (typeof input) {
469 case 'object':
4702 out = utils.keys(input).sort();
4712 break;
472 case 'string':
4732 out = input.split('');
4742 if (reverse) {
4751 return out.reverse().join('');
476 }
4771 return out.sort().join('');
478 }
479 }
480
4814 if (out && reverse) {
4822 return out.reverse();
483 }
484
4852 return out || input;
486};
487
488/**
489 * Strip HTML tags.
490 *
491 * @example
492 * // stuff = '<p>foobar</p>';
493 * {{ stuff|striptags }}
494 * // => foobar
495 *
496 * @param {*} input
497 * @return {*} Returns the same object as the input, but with all string values stripped of tags.
498 */
4991exports.striptags = function (input) {
5004 var out = iterateFilter.apply(exports.striptags, arguments);
5014 if (out !== undefined) {
5021 return out;
503 }
504
5053 return input.toString().replace(/(<([^>]+)>)/ig, '');
506};
507
508/**
509 * Capitalizes every word given and lower-cases all other letters.
510 *
511 * @example
512 * // my_str = 'this is soMe text';
513 * {{ my_str|title }}
514 * // => This Is Some Text
515 *
516 * @example
517 * // my_arr = ['hi', 'this', 'is', 'an', 'array'];
518 * {{ my_arr|title|join(' ') }}
519 * // => Hi This Is An Array
520 *
521 * @param {*} input
522 * @return {*} Returns the same object as the input, but with all words in strings title-cased.
523 */
5241exports.title = function (input) {
5254 var out = iterateFilter.apply(exports.title, arguments);
5264 if (out !== undefined) {
5271 return out;
528 }
529
5303 return input.toString().replace(/\w\S*/g, function (str) {
5316 return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
532 });
533};
534
535/**
536 * Remove all duplicate items from an array.
537 *
538 * @example
539 * // my_arr = [1, 2, 3, 4, 4, 3, 2, 1];
540 * {{ my_arr|uniq|join(',') }}
541 * // => 1,2,3,4
542 *
543 * @param {array} input
544 * @return {array} Array with unique items. If input was not an array, the original item is returned untouched.
545 */
5461exports.uniq = function (input) {
5472 var result;
548
5492 if (!input || !utils.isArray(input)) {
5501 return '';
551 }
552
5531 result = [];
5541 utils.each(input, function (v) {
5556 if (result.indexOf(v) === -1) {
5564 result.push(v);
557 }
558 });
5591 return result;
560};
561
562/**
563 * Convert the input to all uppercase letters. If an object or array is provided, all values will be uppercased.
564 *
565 * @example
566 * // my_str = 'tacos';
567 * {{ my_str|upper }}
568 * // => TACOS
569 *
570 * @example
571 * // my_arr = ['tacos', 'burritos'];
572 * {{ my_arr|upper|join(' & ') }}
573 * // => TACOS & BURRITOS
574 *
575 * @param {*} input
576 * @return {*} Returns the same type as the input, with all strings upper-cased.
577 */
5781exports.upper = function (input) {
5798 var out = iterateFilter.apply(exports.upper, arguments);
5808 if (out !== undefined) {
5812 return out;
582 }
583
5846 return input.toString().toUpperCase();
585};
586
587/**
588 * URL-encode a string. If an object or array is passed, all values will be URL-encoded.
589 *
590 * @example
591 * // my_str = 'param=1&anotherParam=2';
592 * {{ my_str|url_encode }}
593 * // => param%3D1%26anotherParam%3D2
594 *
595 * @param {*} input
596 * @return {*} URL-encoded string.
597 */
5981exports.url_encode = function (input) {
5994 var out = iterateFilter.apply(exports.url_encode, arguments);
6004 if (out !== undefined) {
6011 return out;
602 }
6033 return encodeURIComponent(input);
604};
605
606/**
607 * URL-decode a string. If an object or array is passed, all values will be URL-decoded.
608 *
609 * @example
610 * // my_str = 'param%3D1%26anotherParam%3D2';
611 * {{ my_str|url_decode }}
612 * // => param=1&anotherParam=2
613 *
614 * @param {*} input
615 * @return {*} URL-decoded string.
616 */
6171exports.url_decode = function (input) {
6184 var out = iterateFilter.apply(exports.url_decode, arguments);
6194 if (out !== undefined) {
6201 return out;
621 }
6223 return decodeURIComponent(input);
623};
624

/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) {
2541872 var matched;
255
2561872 utils.some(rules, function (rule) {
25717911 return utils.some(rule.regex, function (regex) {
25824998 var match = str.match(regex),
259 normalized;
260
26124998 if (!match) {
26223126 return;
263 }
264
2651872 normalized = match[rule.idx || 0].replace(/\s*$/, '');
2661872 normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
267
2681872 matched = {
269 match: normalized,
270 type: rule.type,
271 length: match[0].length
272 };
2731872 return true;
274 });
275 });
276
2771872 if (!matched) {
2780 matched = {
279 match: str,
280 type: TYPES.UNKNOWN,
281 length: str.length
282 };
283 }
284
2851872 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) {
295608 var offset = 0,
296 tokens = [],
297 substr,
298 match;
299608 while (offset < str.length) {
3001872 substr = str.substring(offset);
3011872 match = reader(substr);
3021872 offset += match.length;
3031872 tokens.push(match);
304 }
305608 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) {
294969 if (basepath) {
304 from = basepath;
31 } else {
324965 from = (from) ? path.dirname(from) : '/';
33 }
344969 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) {
4552 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
4952 identifier = ret.resolve(identifier);
50
5152 if (cb) {
525 fs.readFile(identifier, encoding, cb);
535 return;
54 }
5547 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) {
186 var ret = {};
19
206 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 */
296 ret.resolve = function (to, from) {
3010 if (basepath) {
313 from = basepath;
32 } else {
337 from = (from) ? path.dirname(from) : '/';
34 }
3510 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 */
456 ret.load = function (pathname, cb) {
469 var src, paths;
47
489 paths = [pathname, pathname.replace(/^(\/|\\)/, '')];
49
509 src = mapping[paths[0]] || mapping[paths[1]];
519 if (!src) {
521 utils.throwError('Unable to find template "' + pathname + '".');
53 }
54
558 if (cb) {
561 cb(null, src);
571 return;
58 }
597 return src;
60 };
61
626 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) {
472592 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) {
61608 this.out = [];
62608 this.state = [];
63608 this.filterApplyIdx = [];
64608 this._parsers = {};
65608 this.line = line;
66608 this.filename = filename;
67608 this.filters = filters;
68608 this.escape = autoescape;
69
70608 this.parse = function () {
71606 var self = this;
72
73606 if (self._parsers.start) {
740 self._parsers.start.call(self);
75 }
76606 utils.each(tokens, function (token, i) {
771851 var prevToken = tokens[i - 1];
781851 self.isLast = (i === tokens.length - 1);
791851 if (prevToken) {
801266 while (prevToken.type === _t.WHITESPACE) {
81268 i -= 1;
82268 prevToken = tokens[i - 1];
83 }
84 }
851851 self.prevToken = prevToken;
861851 self.parseToken(token);
87 });
88548 if (self._parsers.end) {
8918 self._parsers.end.call(self);
90 }
91
92548 if (self.escape) {
93249 self.filterApplyIdx = [0];
94249 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 {
100247 self.parseToken({ type: _t.FILTEREMPTY, match: 'e' });
101 }
102 }
103
104548 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) {
129883 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) {
1392106 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
1472106 if (fn && typeof fn === 'function') {
148455 if (!fn.call(this, token)) {
149355 return;
150 }
151 }
152
1531729 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) {
16196 self.out.push(', ');
162 }
163
1641729 if (lastState && lastState === _t.METHODOPEN) {
16519 self.state.pop();
16619 if (token.type !== _t.PARENCLOSE) {
16711 self.out.push(', ');
168 }
169 }
170
1711729 switch (token.type) {
172 case _t.WHITESPACE:
173261 break;
174
175 case _t.STRING:
176203 self.filterApplyIdx.push(self.out.length);
177203 self.out.push(match.replace(/\\/g, '\\\\'));
178203 break;
179
180 case _t.NUMBER:
181 case _t.BOOL:
182103 self.filterApplyIdx.push(self.out.length);
183103 self.out.push(match);
184103 break;
185
186 case _t.FILTER:
187100 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1881 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
189 }
19099 self.escape = self.filters[match].safe ? false : self.escape;
19199 temp = self.filterApplyIdx.pop();
19299 self.out.splice(temp, 0, '_filters["' + match + '"](');
19399 self.state.push(token.type);
19499 self.filterApplyIdx.push(temp);
19599 break;
196
197 case _t.FILTEREMPTY:
198294 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
1991 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
200 }
201293 self.escape = self.filters[match].safe ? false : self.escape;
202293 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
203293 self.out.push(')');
204293 break;
205
206 case _t.FUNCTION:
207 case _t.FUNCTIONEMPTY:
20820 self.out.push('((typeof _ctx.' + match + ' !== "undefined") ? _ctx.' + match +
209 ' : ((typeof ' + match + ' !== "undefined") ? ' + match +
210 ' : _fn))(');
21120 self.escape = false;
21220 if (token.type === _t.FUNCTIONEMPTY) {
2138 self.out[self.out.length - 1] = self.out[self.out.length - 1] + ')';
214 } else {
21512 self.state.push(token.type);
216 }
21720 self.filterApplyIdx.push(self.out.length - 1);
21820 break;
219
220 case _t.PARENOPEN:
22123 self.state.push(token.type);
22223 if (self.filterApplyIdx.length) {
22321 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '(');
22421 if (prevToken && prevTokenType === _t.VAR) {
22519 temp = prevToken.match.split('.').slice(0, -1);
22619 self.out.push(' || _fn).call(' + self.checkMatch(temp));
22719 self.state.push(_t.METHODOPEN);
22819 self.escape = false;
229 } else {
2302 self.out.push(' || _fn)(');
231 }
23221 self.filterApplyIdx.push(self.out.length - 3);
233 } else {
2342 self.out.push('(');
2352 self.filterApplyIdx.push(self.out.length - 1);
236 }
23723 break;
238
239 case _t.PARENCLOSE:
240138 temp = self.state.pop();
241138 if (temp !== _t.PARENOPEN && temp !== _t.FUNCTION && temp !== _t.FILTER) {
2421 utils.throwError('Mismatched nesting state', self.line, self.filename);
243 }
244137 self.out.push(')');
245 // Once off the previous entry
246137 self.filterApplyIdx.pop();
247 // Once for the open paren
248137 self.filterApplyIdx.pop();
249137 break;
250
251 case _t.COMMA:
25297 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 }
26096 if (lastState === _t.COLON) {
2615 self.state.pop();
262 }
26396 self.out.push(', ');
26496 self.filterApplyIdx.pop();
26596 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:
286405 self.parseVar(token, match, lastState);
287378 break;
288
289 case _t.BRACKETOPEN:
29018 if (!prevToken ||
291 (prevTokenType !== _t.VAR &&
292 prevTokenType !== _t.BRACKETCLOSE &&
293 prevTokenType !== _t.PARENCLOSE)) {
2944 self.state.push(_t.ARRAYOPEN);
2954 self.filterApplyIdx.push(self.out.length);
296 } else {
29714 self.state.push(token.type);
298 }
29918 self.out.push('[');
30018 break;
301
302 case _t.BRACKETCLOSE:
30318 temp = self.state.pop();
30418 if (temp !== _t.BRACKETOPEN && temp !== _t.ARRAYOPEN) {
3051 utils.throwError('Unexpected closing square bracket', self.line, self.filename);
306 }
30717 self.out.push(']');
30817 self.filterApplyIdx.pop();
30917 break;
310
311 case _t.CURLYOPEN:
3127 self.state.push(token.type);
3137 self.out.push('{');
3147 self.filterApplyIdx.push(self.out.length - 1);
3157 break;
316
317 case _t.COLON:
31812 if (lastState !== _t.CURLYOPEN) {
3191 utils.throwError('Unexpected colon', self.line, self.filename);
320 }
32111 self.state.push(token.type);
32211 self.out.push(':');
32311 self.filterApplyIdx.pop();
32411 break;
325
326 case _t.CURLYCLOSE:
3277 if (lastState === _t.COLON) {
3286 self.state.pop();
329 }
3307 if (self.state.pop() !== _t.CURLYOPEN) {
3311 utils.throwError('Unexpected closing curly brace', self.line, self.filename);
332 }
3336 self.out.push('}');
334
3356 self.filterApplyIdx.pop();
3366 break;
337
338 case _t.DOTKEY:
3398 if (!prevToken || (
340 prevTokenType !== _t.VAR &&
341 prevTokenType !== _t.BRACKETCLOSE &&
342 prevTokenType !== _t.DOTKEY &&
343 prevTokenType !== _t.PARENCLOSE &&
344 prevTokenType !== _t.FUNCTIONEMPTY &&
345 prevTokenType !== _t.FILTEREMPTY &&
346 prevTokenType !== _t.CURLYCLOSE
347 )) {
3481 utils.throwError('Unexpected key "' + match + '"', self.line, self.filename);
349 }
3507 self.out.push('.' + match);
3517 break;
352
353 case _t.OPERATOR:
3547 self.out.push(' ' + match + ' ');
3557 self.filterApplyIdx.pop();
3567 break;
357 }
358 },
359
360 /**
361 * Parse variable token
362 * @param {{match: string, type: number, line: number}} token Lexer token object.
363 * @param {string} match Shortcut for token.match
364 * @param {number} lastState Lexer token type state.
365 * @return {undefined}
366 * @private
367 */
368 parseVar: function (token, match, lastState) {
369405 var self = this;
370
371405 match = match.split('.');
372
373405 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
377379 self.filterApplyIdx.push(self.out.length);
378379 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
386369 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) {
395388 var temp = match[0];
396
397388 function checkDot(ctx) {
3981164 var c = ctx + temp,
399 m = match,
400 build = '';
401
4021164 build = '(typeof ' + c + ' !== "undefined"';
4031164 utils.each(m, function (v, i) {
4041296 if (i === 0) {
4051164 return;
406 }
407132 build += ' && ' + c + '.' + v + ' !== undefined';
408132 c += '.' + v;
409 });
4101164 build += ')';
411
4121164 return build;
413 }
414
415388 function buildDot(ctx) {
416776 return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
417 }
418
419388 return '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
420 }
421};
422
423/**
424 * Parse a source string into tokens that are ready for compilation.
425 *
426 * @example
427 * exports.parse('{{ tacos }}', {}, tags, filters);
428 * // => [{ compile: [Function], ... }]
429 *
430 * @param {string} source Swig template source.
431 * @param {object} opts Swig options object.
432 * @param {object} tags Keyed object of tags that can be parsed and compiled.
433 * @param {object} filters Keyed object of filters that may be applied to variables.
434 * @return {array} List of tokens ready for compilation.
435 */
4361exports.parse = function (source, opts, tags, filters) {
437432 source = source.replace(/\r\n/g, '\n');
438432 var escape = opts.autoescape,
439 tagOpen = opts.tagControls[0],
440 tagClose = opts.tagControls[1],
441 varOpen = opts.varControls[0],
442 varClose = opts.varControls[1],
443 escapedTagOpen = escapeRegExp(tagOpen),
444 escapedTagClose = escapeRegExp(tagClose),
445 escapedVarOpen = escapeRegExp(varOpen),
446 escapedVarClose = escapeRegExp(varClose),
447 tagStrip = new RegExp('^' + escapedTagOpen + '-?\\s*-?|-?\\s*-?' + escapedTagClose + '$', 'g'),
448 tagStripBefore = new RegExp('^' + escapedTagOpen + '-'),
449 tagStripAfter = new RegExp('-' + escapedTagClose + '$'),
450 varStrip = new RegExp('^' + escapedVarOpen + '-?\\s*-?|-?\\s*-?' + escapedVarClose + '$', 'g'),
451 varStripBefore = new RegExp('^' + escapedVarOpen + '-'),
452 varStripAfter = new RegExp('-' + escapedVarClose + '$'),
453 cmtOpen = opts.cmtControls[0],
454 cmtClose = opts.cmtControls[1],
455 anyChar = '[\\s\\S]*?',
456 // Split the template source based on variable, tag, and comment blocks
457 // /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\{#[\s\S]*?#\})/
458 splitter = new RegExp(
459 '(' +
460 escapedTagOpen + anyChar + escapedTagClose + '|' +
461 escapedVarOpen + anyChar + escapedVarClose + '|' +
462 escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) +
463 ')'
464 ),
465 line = 1,
466 stack = [],
467 parent = null,
468 tokens = [],
469 blocks = {},
470 inRaw = false,
471 stripNext;
472
473 /**
474 * Parse a variable.
475 * @param {string} str String contents of the variable, between <i>{{</i> and <i>}}</i>
476 * @param {number} line The line number that this variable starts on.
477 * @return {VarToken} Parsed variable token object.
478 * @private
479 */
480432 function parseVariable(str, line) {
481330 var tokens = lexer.read(utils.strip(str)),
482 parser,
483 out;
484
485330 parser = new TokenParser(tokens, filters, escape, line, opts.filename);
486330 out = parser.parse().join('');
487
488294 if (parser.state.length) {
4892 utils.throwError('Unable to parse "' + str + '"', line, opts.filename);
490 }
491
492 /**
493 * A parsed variable token.
494 * @typedef {object} VarToken
495 * @property {function} compile Method for compiling this token.
496 */
497292 return {
498 compile: function () {
499288 return '_output += ' + out + ';\n';
500 }
501 };
502 }
503432 exports.parseVariable = parseVariable;
504
505 /**
506 * Parse a tag.
507 * @param {string} str String contents of the tag, between <i>{%</i> and <i>%}</i>
508 * @param {number} line The line number that this tag starts on.
509 * @return {TagToken} Parsed token object.
510 * @private
511 */
512432 function parseTag(str, line) {
513446 var tokens, parser, chunks, tagName, tag, args, last;
514
515446 if (utils.startsWith(str, 'end')) {
516165 last = stack[stack.length - 1];
517165 if (last && last.name === str.split(/\s+/)[0].replace(/^end/, '') && last.ends) {
518163 switch (last.name) {
519 case 'autoescape':
5208 escape = opts.autoescape;
5218 break;
522 case 'raw':
5234 inRaw = false;
5244 break;
525 }
526163 stack.pop();
527163 return;
528 }
529
5302 if (!inRaw) {
5311 utils.throwError('Unexpected end of tag "' + str.replace(/^end/, '') + '"', line, opts.filename);
532 }
533 }
534
535282 if (inRaw) {
5363 return;
537 }
538
539279 chunks = str.split(/\s+(.+)?/);
540279 tagName = chunks.shift();
541
542279 if (!tags.hasOwnProperty(tagName)) {
5431 utils.throwError('Unexpected tag "' + str + '"', line, opts.filename);
544 }
545
546278 tokens = lexer.read(utils.strip(chunks.join(' ')));
547278 parser = new TokenParser(tokens, filters, false, line, opts.filename);
548278 tag = tags[tagName];
549
550 /**
551 * Define custom parsing methods for your tag.
552 * @callback parse
553 *
554 * @example
555 * exports.parse = function (str, line, parser, types, options) {
556 * parser.on('start', function () {
557 * // ...
558 * });
559 * parser.on(types.STRING, function (token) {
560 * // ...
561 * });
562 * };
563 *
564 * @param {string} str The full token string of the tag.
565 * @param {number} line The line number that this tag appears on.
566 * @param {TokenParser} parser A TokenParser instance.
567 * @param {TYPES} types Lexer token type enum.
568 * @param {TagToken[]} stack The current stack of open tags.
569 * @param {SwigOpts} options Swig Options Object.
570 */
571278 if (!tag.parse(chunks[1], line, parser, _t, stack, opts)) {
5722 utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename);
573 }
574
575276 parser.parse();
576254 args = parser.out;
577
578254 switch (tagName) {
579 case 'autoescape':
5808 escape = (args[0] !== 'false') ? args[0] : false;
5818 break;
582 case 'raw':
5834 inRaw = true;
5844 break;
585 }
586
587 /**
588 * A parsed tag token.
589 * @typedef {Object} TagToken
590 * @property {compile} [compile] Method for compiling this token.
591 * @property {array} [args] Array of arguments for the tag.
592 * @property {Token[]} [content=[]] An array of tokens that are children of this Token.
593 * @property {boolean} [ends] Whether or not this tag requires an end tag.
594 * @property {string} name The name of this tag.
595 */
596254 return {
597 block: !!tags[tagName].block,
598 compile: tag.compile,
599 args: args,
600 content: [],
601 ends: tag.ends,
602 name: tagName
603 };
604 }
605
606 /**
607 * Strip the whitespace from the previous token, if it is a string.
608 * @param {object} token Parsed token.
609 * @return {object} If the token was a string, trailing whitespace will be stripped.
610 */
611432 function stripPrevToken(token) {
61210 if (typeof token === 'string') {
6138 token = token.replace(/\s*$/, '');
614 }
61510 return token;
616 }
617
618 /*!
619 * Loop over the source, split via the tag/var/comment regular expression splitter.
620 * Send each chunk to the appropriate parser.
621 */
622432 utils.each(source.split(splitter), function (chunk) {
6231938 var token, lines, stripPrev, prevToken, prevChildToken;
624
6251938 if (!chunk) {
626843 return;
627 }
628
629 // Is a variable?
6301095 if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
631330 stripPrev = varStripBefore.test(chunk);
632330 stripNext = varStripAfter.test(chunk);
633330 token = parseVariable(chunk.replace(varStrip, ''), line);
634 // Is a tag?
635765 } else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
636446 stripPrev = tagStripBefore.test(chunk);
637446 stripNext = tagStripAfter.test(chunk);
638446 token = parseTag(chunk.replace(tagStrip, ''), line);
639420 if (token) {
640254 if (token.name === 'extends') {
64125 parent = token.args.join('').replace(/^\'|\'$/g, '').replace(/^\"|\"$/g, '');
642 }
643
644254 if (token.block && (!stack.length || token.name === 'block')) {
645101 blocks[token.args.join('')] = token;
646 }
647 }
648420 if (inRaw && !token) {
6493 token = chunk;
650 }
651 // Is a content string?
652319 } else if (inRaw || (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))) {
653312 token = (stripNext) ? chunk.replace(/^\s*/, '') : chunk;
654312 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>
6601024 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.
6721024 if (!token) {
673169 return;
674 }
675
676 // If there's an open item in the stack, add this to its content.
677855 if (stack.length) {
678244 stack[stack.length - 1].content.push(token);
679 } else {
680611 tokens.push(token);
681 }
682
683 // If the token is a tag that requires an end tag, open it on the stack.
684855 if (token.name && token.ends) {
685165 stack.push(token);
686 }
687
688855 lines = chunk.match(/\n/g);
689855 line += (lines) ? lines.length : 0;
690 });
691
692368 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) {
710482 var out = '',
711 tokens = utils.isArray(template) ? template : template.tokens;
712
713482 utils.each(tokens, function (token) {
714707 var o;
715707 if (typeof token === 'string') {
716231 out += '_output += "' + token.replace(/\\/g, '\\\\').replace(/\n|\r/g, '\\n').replace(/"/g, '\\"') + '";\n';
717231 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 */
739476 o = token.compile(exports.compile, token.args ? token.args.slice(0) : [], token.content ? token.content.slice(0) : [], parents, options, blockName);
740476 out += o || '';
741 });
742
743482 return out;
744};
745

/lib/swig.js

99%
203
202
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.3.2") { ... }
12 *
13 * @type {String}
14 */
151exports.version = "1.3.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) {
89794 if (!options) {
9079 return;
91 }
92
93715 utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
942130 if (!options.hasOwnProperty(key)) {
951163 return;
96 }
97967 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 }
100961 if (options[key][0] === options[key][1]) {
1013 throw new Error('Option "' + key + '" open and close controls must not be the same.');
102 }
103958 utils.each(options[key], function (a, i) {
1041913 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
110700 if (options.hasOwnProperty('cache')) {
111319 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 }
117698 if (options.hasOwnProperty('loader')) {
118325 if (options.loader) {
119325 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) {
144324 validateOptions(options);
145
146320 var locals = utils.extend({}, defaultOptions.locals, options.locals || {});
147
148320 utils.extend(defaultOptions, options);
149320 defaultOptions.locals = locals;
150
151320 defaultInstance.options = utils.extend(defaultInstance.options, options);
152};
153
154/**
155 * 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.
156 * @param {number} offset Offset from GMT, in minutes.
157 * @return {undefined}
158 */
1591exports.setDefaultTZOffset = function (offset) {
1602 dateformatter.tzOffset = offset;
161};
162
163/**
164 * Create a new, separate Swig compile/render environment.
165 *
166 * @example
167 * var swig = require('swig');
168 * var myswig = new swig.Swig({varControls: ['<%=', '%>']});
169 * myswig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
170 * // => Tacos are delicious!
171 * swig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
172 * // => 'Tacos are <%= tacos =>!'
173 *
174 * @param {SwigOpts} [opts={}] Swig options object.
175 * @return {object} New Swig environment.
176 */
1771exports.Swig = function (opts) {
17823 validateOptions(opts);
17922 this.options = utils.extend({}, defaultOptions, opts || {});
18022 this.cache = {};
18122 this.extensions = {};
18222 var self = this,
183 tags = _tags,
184 filters = _filters;
185
186 /**
187 * Get combined locals context.
188 * @param {?SwigOpts} [options] Swig options object.
189 * @return {object} Locals context.
190 * @private
191 */
19222 function getLocals(options) {
193857 if (!options || !options.locals) {
194318 return self.options.locals;
195 }
196
197539 return utils.extend({}, self.options.locals, options.locals);
198 }
199
200 /**
201 * Get compiled template from the cache.
202 * @param {string} key Name of template.
203 * @return {object|undefined} Template function and tokens.
204 * @private
205 */
20622 function cacheGet(key) {
2074966 if (!self.options.cache) {
2082 return;
209 }
210
2114964 if (self.options.cache === 'memory') {
2124963 return self.cache[key];
213 }
214
2151 return self.options.cache.get(key);
216 }
217
218 /**
219 * Store a template in the cache.
220 * @param {string} key Name of template.
221 * @param {object} val Template function and tokens.
222 * @return {undefined}
223 * @private
224 */
22522 function cacheSet(key, val) {
22635 if (!self.options.cache) {
2272 return;
228 }
229
23033 if (self.options.cache === 'memory') {
23132 self.cache[key] = val;
23232 return;
233 }
234
2351 self.options.cache.set(key, val);
236 }
237
238 /**
239 * Clears the in-memory template cache.
240 *
241 * @example
242 * swig.invalidateCache();
243 *
244 * @return {undefined}
245 */
24622 this.invalidateCache = function () {
247314 if (self.options.cache === 'memory') {
248314 self.cache = {};
249 }
250 };
251
252 /**
253 * Add a custom filter for swig variables.
254 *
255 * @example
256 * function replaceMs(input) { return input.replace(/m/g, 'f'); }
257 * swig.setFilter('replaceMs', replaceMs);
258 * // => {{ "onomatopoeia"|replaceMs }}
259 * // => onofatopeia
260 *
261 * @param {string} name Name of filter, used in templates. <strong>Will</strong> overwrite previously defined filters, if using the same name.
262 * @param {function} method Function that acts against the input. See <a href="/docs/filters/#custom">Custom Filters</a> for more information.
263 * @return {undefined}
264 */
26522 this.setFilter = function (name, method) {
2663 if (typeof method !== "function") {
2671 throw new Error('Filter "' + name + '" is not a valid function.');
268 }
2692 filters[name] = method;
270 };
271
272 /**
273 * Add a custom tag. To expose your own extensions to compiled template code, see <code data-language="js">swig.setExtension</code>.
274 *
275 * For a more in-depth explanation of writing custom tags, see <a href="../extending/#tags">Custom Tags</a>.
276 *
277 * @example
278 * var tacotag = require('./tacotag');
279 * swig.setTag('tacos', tacotag.parse, tacotag.compile, tacotag.ends, tacotag.blockLevel);
280 * // => {% tacos %}Make this be tacos.{% endtacos %}
281 * // => Tacos tacos tacos tacos.
282 *
283 * @param {string} name Tag name.
284 * @param {function} parse Method for parsing tokens.
285 * @param {function} compile Method for compiling renderable output.
286 * @param {boolean} [ends=false] Whether or not this tag requires an <i>end</i> tag.
287 * @param {boolean} [blockLevel=false] If false, this tag will not be compiled outside of <code>block</code> tags when extending a parent template.
288 * @return {undefined}
289 */
29022 this.setTag = function (name, parse, compile, ends, blockLevel) {
2914 if (typeof parse !== 'function') {
2921 throw new Error('Tag "' + name + '" parse method is not a valid function.');
293 }
294
2953 if (typeof compile !== 'function') {
2961 throw new Error('Tag "' + name + '" compile method is not a valid function.');
297 }
298
2992 tags[name] = {
300 parse: parse,
301 compile: compile,
302 ends: ends || false,
303 block: !!blockLevel
304 };
305 };
306
307 /**
308 * 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.
309 *
310 * @example
311 * swig.setExtension('trans', function (v) { return translate(v); });
312 * function compileTrans(compiler, args, content, parent, options) {
313 * return '_output += _ext.trans(' + args[0] + ');'
314 * };
315 * swig.setTag('trans', parseTrans, compileTrans, true);
316 *
317 * @param {string} name Key name of the extension. Accessed via <code data-language="js">_ext[name]</code>.
318 * @param {*} object The method, value, or object that should be available via the given name.
319 * @return {undefined}
320 */
32122 this.setExtension = function (name, object) {
3221 self.extensions[name] = object;
323 };
324
325 /**
326 * Parse a given source string into tokens.
327 *
328 * @param {string} source Swig template source.
329 * @param {SwigOpts} [options={}] Swig options object.
330 * @return {object} parsed Template tokens object.
331 * @private
332 */
33322 this.parse = function (source, options) {
334447 validateOptions(options);
335
336432 var locals = getLocals(options),
337 opts = {},
338 k;
339
340432 for (k in options) {
341376 if (options.hasOwnProperty(k) && k !== 'locals') {
342108 opts[k] = options[k];
343 }
344 }
345
346432 options = utils.extend({}, self.options, opts);
347432 options.locals = locals;
348
349432 return parser.parse(source, options, tags, filters);
350 };
351
352 /**
353 * Parse a given file into tokens.
354 *
355 * @param {string} pathname Full path to file to parse.
356 * @param {SwigOpts} [options={}] Swig options object.
357 * @return {object} parsed Template tokens object.
358 * @private
359 */
36022 this.parseFile = function (pathname, options) {
36127 var src;
362
36327 if (!options) {
3640 options = {};
365 }
366
36727 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
368
36927 src = self.options.loader.load(pathname);
370
37126 if (!options.filename) {
3724 options = utils.extend({ filename: pathname }, options);
373 }
374
37526 return self.parse(src, options);
376 };
377
378 /**
379 * Re-Map blocks within a list of tokens to the template's block objects.
380 * @param {array} tokens List of tokens for the parent object.
381 * @param {object} template Current template that needs to be mapped to the parent's block and token list.
382 * @return {array}
383 * @private
384 */
38522 function remapBlocks(blocks, tokens) {
38649 return utils.map(tokens, function (token) {
387111 var args = token.args ? token.args.join('') : '';
388111 if (token.name === 'block' && blocks[args]) {
38921 token = blocks[args];
390 }
391111 if (token.content && token.content.length) {
39227 token.content = remapBlocks(blocks, token.content);
393 }
394111 return token;
395 });
396 }
397
398 /**
399 * Import block-level tags to the token list that are not actual block tags.
400 * @param {array} blocks List of block-level tags.
401 * @param {array} tokens List of tokens to render.
402 * @return {undefined}
403 * @private
404 */
40522 function importNonBlocks(blocks, tokens) {
40622 utils.each(blocks, function (block) {
40727 if (block.name !== 'block') {
4083 tokens.unshift(block);
409 }
410 });
411 }
412
413 /**
414 * Recursively compile and get parents of given parsed token object.
415 *
416 * @param {object} tokens Parsed tokens from template.
417 * @param {SwigOpts} [options={}] Swig options object.
418 * @return {object} Parsed tokens from parent templates.
419 * @private
420 */
42122 function getParents(tokens, options) {
422342 var parentName = tokens.parent,
423 parentFiles = [],
424 parents = [],
425 parentFile,
426 parent,
427 l;
428
429342 while (parentName) {
43027 if (!options || !options.filename) {
4311 throw new Error('Cannot extend "' + parentName + '" because current template has no filename.');
432 }
433
43426 parentFile = parentFile || options.filename;
43526 parentFile = self.options.loader.resolve(parentName, parentFile);
43626 parent = cacheGet(parentFile) || self.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
43725 parentName = parent.parent;
438
43925 if (parentFiles.indexOf(parentFile) !== -1) {
4401 throw new Error('Illegal circular extends of "' + parentFile + '".');
441 }
44224 parentFiles.push(parentFile);
443
44424 parents.push(parent);
445 }
446
447 // Remap each parents'(1) blocks onto its own parent(2), receiving the full token list for rendering the original parent(1) on its own.
448339 l = parents.length;
449339 for (l = parents.length - 2; l >= 0; l -= 1) {
4506 parents[l].tokens = remapBlocks(parents[l].blocks, parents[l + 1].tokens);
4516 importNonBlocks(parents[l].blocks, parents[l].tokens);
452 }
453
454339 return parents;
455 }
456
457 /**
458 * Pre-compile a source string into a cache-able template function.
459 *
460 * @example
461 * swig.precompile('{{ tacos }}');
462 * // => {
463 * // tpl: function (_swig, _locals, _filters, _utils, _fn) { ... },
464 * // tokens: {
465 * // name: undefined,
466 * // parent: null,
467 * // tokens: [...],
468 * // blocks: {}
469 * // }
470 * // }
471 *
472 * 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.
473 *
474 * @param {string} source Swig template source string.
475 * @param {SwigOpts} [options={}] Swig options object.
476 * @return {object} Renderable function and tokens object.
477 */
47822 this.precompile = function (source, options) {
479421 var tokens = self.parse(source, options),
480 parents = getParents(tokens, options),
481 tpl;
482
483339 if (parents.length) {
484 // Remap the templates first-parent's tokens using this template's blocks.
48516 tokens.tokens = remapBlocks(tokens.blocks, parents[0].tokens);
48616 importNonBlocks(tokens.blocks, tokens.tokens);
487 }
488
489339 tpl = new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
490 ' var _ext = _swig.extensions,\n' +
491 ' _output = "";\n' +
492 parser.compile(tokens, parents, options) + '\n' +
493 ' return _output;\n'
494 );
495
496339 return { tpl: tpl, tokens: tokens };
497 };
498
499 /**
500 * Compile and render a template string for final output.
501 *
502 * 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.
503 *
504 * @example
505 * swig.render('{{ tacos }}', { locals: { tacos: 'Tacos!!!!' }});
506 * // => Tacos!!!!
507 *
508 * @param {string} source Swig template source string.
509 * @param {SwigOpts} [options={}] Swig options object.
510 * @return {string} Rendered output.
511 */
51222 this.render = function (source, options) {
513362 return self.compile(source, options)();
514 };
515
516 /**
517 * Compile and render a template file for final output. This is most useful for libraries like Express.js.
518 *
519 * @example
520 * swig.renderFile('./template.html', {}, function (err, output) {
521 * if (err) {
522 * throw err;
523 * }
524 * console.log(output);
525 * });
526 *
527 * @example
528 * swig.renderFile('./template.html', {});
529 * // => output
530 *
531 * @param {string} pathName File location.
532 * @param {object} [locals={}] Template variable context.
533 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
534 * @return {string} Rendered output.
535 */
53622 this.renderFile = function (pathName, locals, cb) {
53710 if (cb) {
5384 self.compileFile(pathName, {}, function (err, fn) {
5394 if (err) {
5401 cb(err);
5411 return;
542 }
5433 cb(null, fn(locals));
544 });
5454 return;
546 }
547
5486 return self.compileFile(pathName)(locals);
549 };
550
551 /**
552 * Compile string source into a renderable template function.
553 *
554 * @example
555 * var tpl = swig.compile('{{ tacos }}');
556 * // => {
557 * // [Function: compiled]
558 * // parent: null,
559 * // tokens: [{ compile: [Function] }],
560 * // blocks: {}
561 * // }
562 * tpl({ tacos: 'Tacos!!!!' });
563 * // => Tacos!!!!
564 *
565 * 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.
566 *
567 * @param {string} source Swig template source string.
568 * @param {SwigOpts} [options={}] Swig options object.
569 * @return {function} Renderable function with keys for parent, blocks, and tokens.
570 */
57122 this.compile = function (source, options) {
572421 var key = options ? options.filename : null,
573 cached = key ? cacheGet(key) : null,
574 context,
575 contextLength,
576 pre;
577
578421 if (cached) {
5791 return cached;
580 }
581
582420 context = getLocals(options);
583420 contextLength = utils.keys(context).length;
584420 pre = this.precompile(source, options);
585
586338 function compiled(locals) {
5875172 var lcls;
5885172 if (locals && contextLength) {
5899 lcls = utils.extend({}, context, locals);
5905163 } else if (locals && !contextLength) {
5914860 lcls = locals;
592303 } else if (!locals && contextLength) {
593265 lcls = context;
594 } else {
59538 lcls = {};
596 }
5975172 return pre.tpl(self, lcls, filters, utils, efn);
598 }
599
600338 utils.extend(compiled, pre.tokens);
601
602338 if (key) {
60334 cacheSet(key, compiled);
604 }
605
606338 return compiled;
607 };
608
609 /**
610 * Compile a source file into a renderable template function.
611 *
612 * @example
613 * var tpl = swig.compileFile('./mytpl.html');
614 * // => {
615 * // [Function: compiled]
616 * // parent: null,
617 * // tokens: [{ compile: [Function] }],
618 * // blocks: {}
619 * // }
620 * tpl({ tacos: 'Tacos!!!!' });
621 * // => Tacos!!!!
622 *
623 * @example
624 * swig.compileFile('/myfile.txt', { varControls: ['<%=', '=%>'], tagControls: ['<%', '%>']});
625 * // => will compile 'myfile.txt' using the var and tag controls as specified.
626 *
627 * @param {string} pathname File location.
628 * @param {SwigOpts} [options={}] Swig options object.
629 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
630 * @return {function} Renderable function with keys for parent, blocks, and tokens.
631 */
63222 this.compileFile = function (pathname, options, cb) {
6334874 var src, cached;
634
6354874 if (!options) {
63620 options = {};
637 }
638
6394874 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
6404873 if (!options.filename) {
6414873 options = utils.extend({ filename: pathname }, options);
642 }
6434873 cached = cacheGet(pathname);
644
6454873 if (cached) {
6464840 if (cb) {
6471 cb(null, cached);
6481 return;
649 }
6504839 return cached;
651 }
652
65333 if (cb) {
6546 self.options.loader.load(pathname, function (err, src) {
6556 if (err) {
6561 cb(err);
6571 return;
658 }
6595 var compiled;
660
6615 try {
6625 compiled = self.compile(src, options);
663 } catch (err2) {
6641 cb(err2);
6651 return;
666 }
667
6684 cb(err, compiled);
669 });
6706 return;
671 }
672
67327 src = self.options.loader.load(pathname);
67425 return self.compile(src, options);
675 };
676
677 /**
678 * 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.
679 *
680 * @example
681 * $ swig compile ./mytpl.html --wrap-start="var mytpl = " > mytpl.js
682 * @example
683 * <script src="mytpl.js"></script>
684 * <script>
685 * swig.run(mytpl, {});
686 * // => "rendered template..."
687 * </script>
688 *
689 * @param {function} tpl Pre-compiled Swig template function. Use the Swig CLI to compile your templates.
690 * @param {object} [locals={}] Template variable context.
691 * @param {string} [filepath] Filename used for caching the template.
692 * @return {string} Rendered output.
693 */
69422 this.run = function (tpl, locals, filepath) {
6955 var context = getLocals({ locals: locals });
6965 if (filepath) {
6971 cacheSet(filepath, tpl);
698 }
6995 return tpl(self, context, filters, utils, efn);
700 };
701};
702
703/*!
704 * Export methods publicly
705 */
7061defaultInstance = new exports.Swig();
7071exports.setFilter = defaultInstance.setFilter;
7081exports.setTag = defaultInstance.setTag;
7091exports.setExtension = defaultInstance.setExtension;
7101exports.parseFile = defaultInstance.parseFile;
7111exports.precompile = defaultInstance.precompile;
7121exports.compile = defaultInstance.compile;
7131exports.compileFile = defaultInstance.compileFile;
7141exports.render = defaultInstance.render;
7151exports.renderFile = defaultInstance.renderFile;
7161exports.run = defaultInstance.run;
7171exports.invalidateCache = defaultInstance.invalidateCache;
7181exports.loaders = loaders;
719

/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) {
195 return compiler(content, parents, options, blockName);
20};
211exports.parse = function (str, line, parser, types, stack, opts) {
2210 var matched;
2310 parser.on('*', function (token) {
2411 if (!matched &&
25 (token.type === types.BOOL ||
26 (token.type === types.STRING && strings.indexOf(token.match) === -1))
27 ) {
289 this.out.push(token.match);
299 matched = true;
309 return;
31 }
322 utils.throwError('Unexpected token "' + token.match + '" in autoescape tag', line, opts.filename);
33 });
34
3510 return true;
36};
371exports.ends = true;
38

/lib/tags/block.js

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

/lib/tags/else.js

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

/lib/tags/elseif.js

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

/lib/tags/extends.js

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

/lib/tags/filter.js

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

/lib/tags/for.js

100%
33
33
0
LineHitsSource
11var ctx = '_ctx.',
2 ctxloop = ctx + 'loop',
3 ctxloopcache = ctx + '___loopcache';
4
5/**
6 * Loop over objects and arrays.
7 *
8 * @alias for
9 *
10 * @example
11 * // obj = { one: 'hi', two: 'bye' };
12 * {% for x in obj %}
13 * {% if loop.first %}<ul>{% endif %}
14 * <li>{{ loop.index }} - {{ loop.key }}: {{ x }}</li>
15 * {% if loop.last %}</ul>{% endif %}
16 * {% endfor %}
17 * // => <ul>
18 * // <li>1 - one: hi</li>
19 * // <li>2 - two: bye</li>
20 * // </ul>
21 *
22 * @example
23 * // arr = [1, 2, 3]
24 * // Reverse the array, shortcut the key/index to `key`
25 * {% for key, val in arr|reverse %}
26 * {{ key }} -- {{ val }}
27 * {% endfor %}
28 * // => 0 -- 3
29 * // 1 -- 2
30 * // 2 -- 1
31 *
32 * @param {literal} [key] A shortcut to the index of the array or current key accessor.
33 * @param {literal} variable The current value will be assigned to this variable name temporarily. The variable will be reset upon ending the for tag.
34 * @param {literal} in Literally, "in". This token is required.
35 * @param {object} object An enumerable object that will be iterated over.
36 *
37 * @return {loop.index} The current iteration of the loop (1-indexed)
38 * @return {loop.index0} The current iteration of the loop (0-indexed)
39 * @return {loop.revindex} The number of iterations from the end of the loop (1-indexed)
40 * @return {loop.revindex0} The number of iterations from the end of the loop (0-indexed)
41 * @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.
42 * @return {loop.first} True if the current object is the first in the object or array.
43 * @return {loop.last} True if the current object is the last in the object or array.
44 */
451exports.compile = function (compiler, args, content, parents, options, blockName) {
4625 var val = args.shift(),
47 key = '__k',
48 last;
49
5025 if (args[0] && args[0] === ',') {
515 args.shift();
525 key = val;
535 val = args.shift();
54 }
55
5625 last = args.join('');
57
5825 return [
59 '(function () {\n',
60 ' var __l = ' + last + ', __len = (_utils.isArray(__l)) ? __l.length : _utils.keys(__l).length;\n',
61 ' if (!__l) { return; }\n',
62 ' ' + ctxloopcache + ' = { loop: ' + ctxloop + ', ' + val + ': ' + ctx + val + ', ' + key + ': ' + ctx + key + ' };\n',
63 ' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n',
64 ' _utils.each(__l, function (' + val + ', ' + key + ') {\n',
65 ' ' + ctx + val + ' = ' + val + ';\n',
66 ' ' + ctx + key + ' = ' + key + ';\n',
67 ' ' + ctxloop + '.key = ' + key + ';\n',
68 ' ' + ctxloop + '.first = (' + ctxloop + '.index0 === 0);\n',
69 ' ' + ctxloop + '.last = (' + ctxloop + '.revindex0 === 0);\n',
70 ' ' + compiler(content, parents, options, blockName),
71 ' ' + ctxloop + '.index += 1; ' + ctxloop + '.index0 += 1; ' + ctxloop + '.revindex -= 1; ' + ctxloop + '.revindex0 -= 1;\n',
72 ' });\n',
73 ' ' + ctxloop + ' = ' + ctxloopcache + '.loop;\n',
74 ' ' + ctx + val + ' = ' + ctxloopcache + '.' + val + ';\n',
75 ' ' + ctx + key + ' = ' + ctxloopcache + '.' + key + ';\n',
76 '})();\n'
77 ].join('');
78};
79
801exports.parse = function (str, line, parser, types) {
8127 var firstVar, ready;
82
8327 parser.on(types.NUMBER, function (token) {
844 var lastState = this.state.length ? this.state[this.state.length - 1] : null;
854 if (!ready ||
86 (lastState !== types.ARRAYOPEN &&
87 lastState !== types.CURLYOPEN &&
88 lastState !== types.CURLYCLOSE &&
89 lastState !== types.FUNCTION &&
90 lastState !== types.FILTER)
91 ) {
921 throw new Error('Unexpected number "' + token.match + '" on line ' + line + '.');
93 }
943 return true;
95 });
96
9727 parser.on(types.VAR, function (token) {
9856 if (ready && firstVar) {
9924 return true;
100 }
101
10232 if (!this.out.length) {
10327 firstVar = true;
104 }
105
10632 this.out.push(token.match);
107 });
108
10927 parser.on(types.COMMA, function (token) {
1107 if (firstVar && this.prevToken.type === types.VAR) {
1115 this.out.push(token.match);
1125 return;
113 }
114
1152 return true;
116 });
117
11827 parser.on(types.COMPARATOR, function (token) {
11927 if (token.match !== 'in' || !firstVar) {
1201 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
121 }
12226 ready = true;
123 });
124
12527 return true;
126};
127
1281exports.ends = true;
129

/lib/tags/if.js

100%
22
22
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) {
4349 return 'if (' + args.join(' ') + ') { \n' +
44 compiler(content, parents, options, blockName) + '\n' +
45 '}';
46};
47
481exports.parse = function (str, line, parser, types) {
4962 parser.on(types.COMPARATOR, function (token) {
5022 if (this.isLast) {
511 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
52 }
5321 if (this.prevToken.type === types.NOT) {
541 throw new Error('Attempted logic "not ' + token.match + '" on line ' + line + '. Use !(foo ' + token.match + ') instead.');
55 }
5620 this.out.push(token.match);
57 });
58
5962 parser.on(types.NOT, function (token) {
607 if (this.isLast) {
611 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
62 }
636 this.out.push(token.match);
64 });
65
6662 parser.on(types.BOOL, function (token) {
6719 this.out.push(token.match);
68 });
69
7062 parser.on(types.LOGIC, function (token) {
715 if (!this.out.length || this.isLast) {
722 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
73 }
743 this.out.push(token.match);
753 this.filterApplyIdx.pop();
76 });
77
7862 return true;
79};
80
811exports.ends = true;
82

/lib/tags/import.js

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

/lib/tags/include.js

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

/lib/tags/index.js

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

/lib/tags/macro.js

100%
29
29
0
LineHitsSource
1/**
2 * Create custom, reusable snippets within your templates.
3 * Can be imported from one template to another using the <a href="#import"><code data-language="swig">{% import ... %}</code></a> tag.
4 *
5 * @alias macro
6 *
7 * @example
8 * {% macro input(type, name, id, label, value, error) %}
9 * <label for="{{ name }}">{{ label }}</label>
10 * <input type="{{ type }}" name="{{ name }}" id="{{ id }}" value="{{ value }}"{% if error %} class="error"{% endif %}>
11 * {% endmacro %}
12 *
13 * {{ input("text", "fname", "fname", "First Name", fname.value, fname.errors) }}
14 * // => <label for="fname">First Name</label>
15 * // <input type="text" name="fname" id="fname" value="">
16 *
17 * @param {...arguments} arguments User-defined arguments.
18 */
191exports.compile = function (compiler, args, content, parents, options, blockName) {
2026 var fnName = args.shift();
21
2226 return '_ctx.' + fnName + ' = function (' + args.join('') + ') {\n' +
23 ' var _output = "";\n' +
24 compiler(content, parents, options, blockName) + '\n' +
25 ' return _output;\n' +
26 '};\n' +
27 '_ctx.' + fnName + '.safe = true;\n';
28};
29
301exports.parse = function (str, line, parser, types) {
3128 var name;
32
3328 parser.on(types.VAR, function (token) {
3423 if (token.match.indexOf('.') !== -1) {
351 throw new Error('Unexpected dot in macro argument "' + token.match + '" on line ' + line + '.');
36 }
3722 this.out.push(token.match);
38 });
39
4028 parser.on(types.FUNCTION, function (token) {
4113 if (!name) {
4213 name = token.match;
4313 this.out.push(name);
4413 this.state.push(types.FUNCTION);
45 }
46 });
47
4828 parser.on(types.FUNCTIONEMPTY, function (token) {
4912 if (!name) {
5012 name = token.match;
5112 this.out.push(name);
52 }
53 });
54
5528 parser.on(types.PARENCLOSE, function () {
5612 if (this.isLast) {
5711 return;
58 }
591 throw new Error('Unexpected parenthesis close on line ' + line + '.');
60 });
61
6228 parser.on(types.COMMA, function () {
637 return true;
64 });
65
6628 parser.on('*', function () {
677 return;
68 });
69
7028 return true;
71};
72
731exports.ends = true;
741exports.block = true;
75

/lib/tags/parent.js

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

/lib/tags/raw.js

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

/lib/tags/set.js

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

/lib/tags/spaceless.js

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

/lib/utils.js

84%
65
55
10
LineHitsSource
11var isArray;
2
3/**
4 * Strip leading and trailing whitespace from a string.
5 * @param {string} input
6 * @return {string} Stripped input.
7 */
81exports.strip = function (input) {
9608 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) {
192615 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) {
291090 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) {
394546 var i, l;
40
414546 if (isArray(obj)) {
424507 i = 0;
434507 l = obj.length;
444507 for (i; i < l; i += 1) {
4511460 if (fn(obj[i], i, obj) === false) {
460 break;
47 }
48 }
49 } else {
5039 for (i in obj) {
5157 if (obj.hasOwnProperty(i)) {
5257 if (fn(obj[i], i, obj) === false) {
530 break;
54 }
55 }
56 }
57 }
58
594403 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) {
7819783 var i = 0,
79 result,
80 l;
8119783 if (isArray(obj)) {
8219783 l = obj.length;
83
8419783 for (i; i < l; i += 1) {
8542909 result = fn(obj[i], i, obj);
8642909 if (result) {
873744 break;
88 }
89 }
90 } else {
910 exports.each(obj, function (value, index) {
920 result = fn(value, index, obj);
930 return !(result);
94 });
95 }
9619783 return !!result;
97};
98
99/**
100 * Return a new enumerable, mapped by a given iteration function.
101 * @param {object} obj Enumerable object.
102 * @param {Function} fn Executed for each item. Return the item to replace the original item with.
103 * @return {object} New mapped object.
104 */
1051exports.map = function (obj, fn) {
10684 var i = 0,
107 result = [],
108 l;
109
11084 if (isArray(obj)) {
11183 l = obj.length;
11283 for (i; i < l; i += 1) {
113181 result[i] = fn(obj[i], i);
114 }
115 } else {
1161 for (i in obj) {
1170 if (obj.hasOwnProperty(i)) {
1180 result[i] = fn(obj[i], i);
119 }
120 }
121 }
12284 return result;
123};
124
125/**
126 * Copy all of the properties in the source objects over to the destination object, and return the destination object. It's in-order, so the last source will override properties of the same name in previous arguments.
127 * @param {...object} arguments
128 * @return {object}
129 */
1301exports.extend = function () {
1317209 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
1397209 for (i; i < l; i += 1) {
1408560 obj = objs[i] || {};
1418560 for (key in obj) {
14216479 if (obj.hasOwnProperty(key)) {
14316479 target[key] = obj[key];
144 }
145 }
146 }
1477209 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) {
156435 if (!obj) {
1571 return [];
158 }
159
160434 if (Object.keys) {
161434 return Object.keys(obj);
162 }
163
1640 return exports.map(obj, function (v, k) {
1650 return k;
166 });
167};
168
169/**
170 * Throw an error with possible line number and source file.
171 * @param {string} message Error message
172 * @param {number} [line] Line number in template.
173 * @param {string} [file] Template file the error occured in.
174 * @throws {Error} No seriously, the point is to throw an error.
175 */
1761exports.throwError = function (message, line, file) {
17745 if (line) {
17844 message += ' on line ' + line;
179 }
18045 if (file) {
18128 message += ' in file ' + file;
182 }
18345 throw new Error(message + '.');
184};
185