Coverage

98%
1206
1188
18

/lib/dateformatter.js

100%
105
105
0
Line Hits Source
1 1 var utils = require('./utils');
2
3 1 var _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 /*
14 DateZ is licensed under the MIT License:
15 Copyright (c) 2011 Tomo Universalis (http://tomouniversalis.com)
16 Permission 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:
17 The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18 THE 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 */
20 1 exports.tzOffset = 0;
21 1 exports.DateZ = function () {
22 64 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
28 64 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
30 64 d.timezoneOffset = d.dateZ.getTimezoneOffset();
31
32 64 utils.each(members.z, function (name) {
33 768 d[name] = function () {
34 70 return d.dateZ[name]();
35 };
36 });
37 64 utils.each(members['default'], function (name) {
38 832 d[name] = function () {
39 15 return d.date[name]();
40 };
41 });
42
43 64 this.setTimezoneOffset(exports.tzOffset);
44 };
45 1 exports.DateZ.prototype = {
46 getTimezoneOffset: function () {
47 4 return this.timezoneOffset;
48 },
49 setTimezoneOffset: function (offset) {
50 127 this.timezoneOffset = offset;
51 127 this.dateZ = new Date(this.date.getTime() + this.date.getTimezoneOffset() * 60000 - this.timezoneOffset * 60000);
52 127 return this;
53 }
54 };
55
56 // Day
57 1 exports.d = function (input) {
58 3 return (input.getDate() < 10 ? '0' : '') + input.getDate();
59 };
60 1 exports.D = function (input) {
61 2 return _days.abbr[input.getDay()];
62 };
63 1 exports.j = function (input) {
64 2 return input.getDate();
65 };
66 1 exports.l = function (input) {
67 1 return _days.full[input.getDay()];
68 };
69 1 exports.N = function (input) {
70 2 var d = input.getDay();
71 2 return (d >= 1) ? d : 7;
72 };
73 1 exports.S = function (input) {
74 13 var d = input.getDate();
75 13 return (d % 10 === 1 && d !== 11 ? 'st' : (d % 10 === 2 && d !== 12 ? 'nd' : (d % 10 === 3 && d !== 13 ? 'rd' : 'th')));
76 };
77 1 exports.w = function (input) {
78 1 return input.getDay();
79 };
80 1 exports.z = function (input, offset, abbr) {
81 3 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
85 3 e.setTimezoneOffset(offset, abbr);
86 3 d.setTimezoneOffset(offset, abbr);
87 3 return Math.round((e - d) / 86400000);
88 };
89
90 // Week
91 1 exports.W = function (input) {
92 1 var target = new Date(input.valueOf()),
93 dayNr = (input.getDay() + 6) % 7,
94 fThurs;
95
96 1 target.setDate(target.getDate() - dayNr + 3);
97 1 fThurs = target.valueOf();
98 1 target.setMonth(0, 1);
99 1 if (target.getDay() !== 4) {
100 1 target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
101 }
102
103 1 return 1 + Math.ceil((fThurs - target) / 604800000);
104 };
105
106 // Month
107 1 exports.F = function (input) {
108 2 return _months.full[input.getMonth()];
109 };
110 1 exports.m = function (input) {
111 3 return (input.getMonth() < 9 ? '0' : '') + (input.getMonth() + 1);
112 };
113 1 exports.M = function (input) {
114 1 return _months.abbr[input.getMonth()];
115 };
116 1 exports.n = function (input) {
117 1 return input.getMonth() + 1;
118 };
119 1 exports.t = function (input) {
120 1 return 32 - (new Date(input.getFullYear(), input.getMonth(), 32).getDate());
121 };
122
123 // Year
124 1 exports.L = function (input) {
125 2 return new Date(input.getFullYear(), 1, 29).getDate() === 29;
126 };
127 1 exports.o = function (input) {
128 2 var target = new Date(input.valueOf());
129 2 target.setDate(target.getDate() - ((input.getDay() + 6) % 7) + 3);
130 2 return target.getFullYear();
131 };
132 1 exports.Y = function (input) {
133 3 return input.getFullYear();
134 };
135 1 exports.y = function (input) {
136 1 return (input.getFullYear().toString()).substr(2);
137 };
138
139 // Time
140 1 exports.a = function (input) {
141 2 return input.getHours() < 12 ? 'am' : 'pm';
142 };
143 1 exports.A = function (input) {
144 1 return input.getHours() < 12 ? 'AM' : 'PM';
145 };
146 1 exports.B = function (input) {
147 1 var hours = input.getUTCHours(), beats;
148 1 hours = (hours === 23) ? 0 : hours + 1;
149 1 beats = Math.abs(((((hours * 60) + input.getUTCMinutes()) * 60) + input.getUTCSeconds()) / 86.4).toFixed(0);
150 1 return ('000'.concat(beats).slice(beats.length));
151 };
152 1 exports.g = function (input) {
153 1 var h = input.getHours();
154 1 return h === 0 ? 12 : (h > 12 ? h - 12 : h);
155 };
156 1 exports.G = function (input) {
157 2 return input.getHours();
158 };
159 1 exports.h = function (input) {
160 2 var h = input.getHours();
161 2 return ((h < 10 || (12 < h && 22 > h)) ? '0' : '') + ((h < 12) ? h : h - 12);
162 };
163 1 exports.H = function (input) {
164 2 var h = input.getHours();
165 2 return (h < 10 ? '0' : '') + h;
166 };
167 1 exports.i = function (input) {
168 2 var m = input.getMinutes();
169 2 return (m < 10 ? '0' : '') + m;
170 };
171 1 exports.s = function (input) {
172 1 var s = input.getSeconds();
173 1 return (s < 10 ? '0' : '') + s;
174 };
175 //u = function () { return ''; },
176
177 // Timezone
178 //e = function () { return ''; },
179 //I = function () { return ''; },
180 1 exports.O = function (input) {
181 3 var tz = input.getTimezoneOffset();
182 3 return (tz < 0 ? '-' : '+') + (tz / 60 < 10 ? '0' : '') + Math.abs((tz / 60)) + '00';
183 };
184 //T = function () { return ''; },
185 1 exports.Z = function (input) {
186 1 return input.getTimezoneOffset() * 60;
187 };
188
189 // Full Date/Time
190 1 exports.c = function (input) {
191 1 return input.toISOString();
192 };
193 1 exports.r = function (input) {
194 1 return input.toUTCString();
195 };
196 1 exports.U = function (input) {
197 1 return input.getTime() / 1000;
198 };
199

/lib/filters.js

99%
167
166
1
Line Hits Source
1 1 var 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 */
10 1 function iterateFilter(input) {
11 408 var self = this,
12 out = {};
13
14 408 if (utils.isArray(input)) {
15 24 return utils.map(input, function (value) {
16 57 return self.apply(null, arguments);
17 });
18 }
19
20 384 if (typeof input === 'object') {
21 4 utils.each(input, function (value, key) {
22 5 out[key] = self.apply(null, arguments);
23 });
24 4 return out;
25 }
26
27 380 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 */
40 1 exports.addslashes = function (input) {
41 6 var out = iterateFilter.apply(exports.addslashes, arguments);
42 6 if (out !== undefined) {
43 1 return out;
44 }
45
46 5 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 */
59 1 exports.capitalize = function (input) {
60 5 var out = iterateFilter.apply(exports.capitalize, arguments);
61 5 if (out !== undefined) {
62 1 return out;
63 }
64
65 4 return input.toString().charAt(0).toUpperCase() + input.toString().substr(1).toLowerCase();
66 };
67
68 /**
69 * Format a date or Date-compatible string.
70 *
71 * @example
72 * // now = new Date();
73 * {{ now|date('Y-m-d') }}
74 * // => 2013-08-14
75 * @example
76 * // now = new Date();
77 * {{ now|date('jS \o\f F') }}
78 * // => 4th of July
79 *
80 * @param {?(string|date)} input
81 * @param {string} format PHP-style date format compatible string. Escape characters with <code>\</code> for string literals.
82 * @param {number=} offset Timezone offset from GMT in minutes.
83 * @param {string=} abbr Timezone abbreviation. Used for output only.
84 * @return {string} Formatted date string.
85 */
86 1 exports.date = function (input, format, offset, abbr) {
87 58 var l = format.length,
88 date = new dateFormatter.DateZ(input),
89 cur,
90 i = 0,
91 out = '';
92
93 58 if (offset) {
94 57 date.setTimezoneOffset(offset, abbr);
95 }
96
97 58 for (i; i < l; i += 1) {
98 82 cur = format.charAt(i);
99 82 if (cur === '\\') {
100 8 i += 1;
101 8 out += (i < l) ? format.charAt(i) : cur;
102 74 } else if (dateFormatter.hasOwnProperty(cur)) {
103 65 out += dateFormatter[cur](date, offset, abbr);
104 } else {
105 9 out += cur;
106 }
107 }
108 58 return out;
109 };
110
111 /**
112 * If the input is `undefined`, `null`, or `false`, a default return value can be specified.
113 *
114 * @example
115 * {{ null_value|default('Tacos') }}
116 * // => Tacos
117 *
118 * @example
119 * {{ "Burritos"|default("Tacos") }}
120 * // => Burritos
121 *
122 * @param {*} input
123 * @param {*} def Value to return if `input` is `undefined`, `null`, or `false`.
124 * @return {*} `input` or `def` value.
125 */
126 1 exports["default"] = function (input, def) {
127 21 return (input !== undefined && (input || typeof input === 'number')) ? input : def;
128 };
129
130 /**
131 * Force escape the output of the variable. Optionally use `e` as a shortcut filter name. This filter will be applied by default if autoescape is turned on.
132 *
133 * @example
134 * {{ "<blah>"|escape }}
135 * // => <blah>
136 *
137 * @example
138 * {{ "<blah>"|e("js") }}
139 * // => \u003Cblah\u003E
140 *
141 * @param {*} input
142 * @param {string} [type='html'] If you pass the string js in as the type, output will be escaped so that it is safe for JavaScript execution.
143 * @return {string} Escaped string.
144 */
145 1 exports.escape = function (input, type) {
146 365 var out = iterateFilter.apply(exports.escape, arguments),
147 inp = input,
148 i = 0,
149 code;
150
151 365 if (out !== undefined) {
152 18 return out;
153 }
154
155 347 if (typeof input !== 'string') {
156 113 return input;
157 }
158
159 234 out = '';
160
161 234 switch (type) {
162 case 'js':
163 6 inp = inp.replace(/\\/g, '\\u005C');
164 6 for (i; i < inp.length; i += 1) {
165 161 code = inp.charCodeAt(i);
166 161 if (code < 32) {
167 6 code = code.toString(16).toUpperCase();
168 6 code = (code.length < 2) ? '0' + code : code;
169 6 out += '\\u00' + code;
170 } else {
171 155 out += inp[i];
172 }
173 }
174 6 return out.replace(/&/g, '\\u0026')
175 .replace(/</g, '\\u003C')
176 .replace(/>/g, '\\u003E')
177 .replace(/\'/g, '\\u0027')
178 .replace(/"/g, '\\u0022')
179 .replace(/\=/g, '\\u003D')
180 .replace(/-/g, '\\u002D')
181 .replace(/;/g, '\\u003B');
182
183 default:
184 228 return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&')
185 .replace(/</g, '<')
186 .replace(/>/g, '>')
187 .replace(/"/g, '"')
188 .replace(/'/g, ''');
189 }
190 };
191 1 exports.e = exports.escape;
192
193 /**
194 * Get the first item in an array or character in a string. All other objects will attempt to return the first value available.
195 *
196 * @example
197 * // my_arr = ['a', 'b', 'c']
198 * {{ my_arr|first }}
199 * // => a
200 *
201 * @example
202 * // my_val = 'Tacos'
203 * {{ my_val|first }}
204 * // T
205 *
206 * @param {*} input
207 * @return {*} The first item of the array or first character of the string input.
208 */
209 1 exports.first = function (input) {
210 4 if (typeof input === 'object' && !utils.isArray(input)) {
211 1 var keys = utils.keys(input);
212 1 return input[keys[0]];
213 }
214
215 3 if (typeof input === 'string') {
216 1 return input.substr(0, 1);
217 }
218
219 2 return input[0];
220 };
221
222 /**
223 * Group an array of objects by a common key. If an array is not provided, the input value will be returned untouched.
224 *
225 * @example
226 * // people = [{ age: 23, name: 'Paul' }, { age: 26, name: 'Jane' }, { age: 23, name: 'Jim' }];
227 * {% for agegroup in people|groupBy('age') %}
228 * <h2>{{ loop.key }}</h2>
229 * <ul>
230 * {% for person in agegroup %}
231 * <li>{{ person.name }}</li>
232 * {% endfor %}
233 * </ul>
234 * {% endfor %}
235 *
236 * @param {*} input Input object.
237 * @param {string} key Key to group by.
238 * @return {object} Grouped arrays by given key.
239 */
240 1 exports.groupBy = function (input, key) {
241 2 if (!utils.isArray(input)) {
242 1 return input;
243 }
244
245 1 var out = {};
246
247 1 utils.each(input, function (value) {
248 3 if (!value.hasOwnProperty(key)) {
249 0 return;
250 }
251
252 3 var keyname = value[key],
253 newValue = utils.extend({}, value);
254 3 delete newValue[key];
255
256 3 if (!out[keyname]) {
257 2 out[keyname] = [];
258 }
259
260 3 out[keyname].push(newValue);
261 });
262
263 1 return out;
264 };
265
266 /**
267 * Join the input with a string.
268 *
269 * @example
270 * // my_array = ['foo', 'bar', 'baz']
271 * {{ my_array|join(', ') }}
272 * // => foo, bar, baz
273 *
274 * @example
275 * // my_key_object = { a: 'foo', b: 'bar', c: 'baz' }
276 * {{ my_key_object|join(' and ') }}
277 * // => foo and bar and baz
278 *
279 * @param {*} input
280 * @param {string} glue String value to join items together.
281 * @return {string}
282 */
283 1 exports.join = function (input, glue) {
284 11 if (utils.isArray(input)) {
285 7 return input.join(glue);
286 }
287
288 4 if (typeof input === 'object') {
289 3 var out = [];
290 3 utils.each(input, function (value) {
291 5 out.push(value);
292 });
293 3 return out.join(glue);
294 }
295 1 return input;
296 };
297
298 /**
299 * Return a string representation of an JavaScript object.
300 *
301 * Backwards compatible with swig@0.x.x using `json_encode`.
302 *
303 * @example
304 * // val = { a: 'b' }
305 * {{ val|json }}
306 * // => {"a":"b"}
307 *
308 * @example
309 * // val = { a: 'b' }
310 * {{ val|json(4) }}
311 * // => {
312 * // "a": "b"
313 * // }
314 *
315 * @param {*} input
316 * @param {number} [indent] Number of spaces to indent for pretty-formatting.
317 * @return {string} A valid JSON string.
318 */
319 1 exports.json = function (input, indent) {
320 3 return JSON.stringify(input, null, indent || 0);
321 };
322 1 exports.json_encode = exports.json;
323
324 /**
325 * Get the last item in an array or character in a string. All other objects will attempt to return the last value available.
326 *
327 * @example
328 * // my_arr = ['a', 'b', 'c']
329 * {{ my_arr|last }}
330 * // => c
331 *
332 * @example
333 * // my_val = 'Tacos'
334 * {{ my_val|last }}
335 * // s
336 *
337 * @param {*} input
338 * @return {*} The last item of the array or last character of the string.input.
339 */
340 1 exports.last = function (input) {
341 3 if (typeof input === 'object' && !utils.isArray(input)) {
342 1 var keys = utils.keys(input);
343 1 return input[keys[keys.length - 1]];
344 }
345
346 2 if (typeof input === 'string') {
347 1 return input.charAt(input.length - 1);
348 }
349
350 1 return input[input.length - 1];
351 };
352
353 /**
354 * Get the number of items in an array, string, or object.
355 *
356 * @example
357 * // my_arr = ['a', 'b', 'c']
358 * {{ my_arr|length }}
359 * // => 3
360 *
361 * @example
362 * // my_str = 'Tacos'
363 * {{ my_str|length }}
364 * // => 5
365 *
366 * @example
367 * // my_obj = {a: 5, b: 20}
368 * {{ my_obj|length }}
369 * // => 2
370 *
371 * @param {*} input
372 * @return {*} The length of the input
373 */
374 1 exports.length = function (input) {
375 4 if (typeof input === 'object' && !utils.isArray(input)) {
376 1 var keys = utils.keys(input);
377 1 return keys.length;
378 }
379 3 if (input.hasOwnProperty('length')) {
380 2 return input.length;
381 }
382 1 return '';
383 };
384
385 /**
386 * Return the input in all lowercase letters.
387 *
388 * @example
389 * {{ "FOOBAR"|lower }}
390 * // => foobar
391 *
392 * @example
393 * // myObj = { a: 'FOO', b: 'BAR' }
394 * {{ myObj|lower|join('') }}
395 * // => foobar
396 *
397 * @param {*} input
398 * @return {*} Returns the same type as the input.
399 */
400 1 exports.lower = function (input) {
401 8 var out = iterateFilter.apply(exports.lower, arguments);
402 8 if (out !== undefined) {
403 2 return out;
404 }
405
406 6 return input.toString().toLowerCase();
407 };
408
409 /**
410 * Deprecated in favor of <a href="#safe">safe</a>.
411 */
412 1 exports.raw = function (input) {
413 2 return exports.safe(input);
414 };
415 1 exports.raw.safe = true;
416
417 /**
418 * Returns a new string with the matched search pattern replaced by the given replacement string. Uses JavaScript's built-in String.replace() method.
419 *
420 * @example
421 * // my_var = 'foobar';
422 * {{ my_var|replace('o', 'e', 'g') }}
423 * // => feebar
424 *
425 * @example
426 * // my_var = "farfegnugen";
427 * {{ my_var|replace('^f', 'p') }}
428 * // => parfegnugen
429 *
430 * @example
431 * // my_var = 'a1b2c3';
432 * {{ my_var|replace('\w', '0', 'g') }}
433 * // => 010203
434 *
435 * @param {string} input
436 * @param {string} search String or pattern to replace from the input.
437 * @param {string} replacement String to replace matched pattern.
438 * @param {string} [flags] Regular Expression flags. 'g': global match, 'i': ignore case, 'm': match over multiple lines
439 * @return {string} Replaced string.
440 */
441 1 exports.replace = function (input, search, replacement, flags) {
442 11 var r = new RegExp(search, flags);
443 11 return input.replace(r, replacement);
444 };
445
446 /**
447 * Reverse sort the input. This is an alias for <code data-language="swig">{{ input|sort(true) }}</code>.
448 *
449 * @example
450 * // val = [1, 2, 3];
451 * {{ val|reverse }}
452 * // => 3,2,1
453 *
454 * @param {array} input
455 * @return {array} Reversed array. The original input object is returned if it was not an array.
456 */
457 1 exports.reverse = function (input) {
458 10 return exports.sort(input, true);
459 };
460
461 /**
462 * Forces the input to not be auto-escaped. Use this only on content that you know is safe to be rendered on your page.
463 *
464 * @example
465 * // my_var = "<p>Stuff</p>";
466 * {{ my_var|safe }}
467 * // => <p>Stuff</p>
468 *
469 * @param {*} input
470 * @return {*} The input exactly how it was given, regardless of autoescaping status.
471 */
472 1 exports.safe = function (input) {
473 // This is a magic filter. Its logic is hard-coded into Swig's parser.
474 5 return input;
475 };
476 1 exports.safe.safe = true;
477
478 /**
479 * Sort the input in an ascending direction.
480 * If given an object, will return the keys as a sorted array.
481 * If given a string, each character will be sorted individually.
482 *
483 * @example
484 * // val = [2, 6, 4];
485 * {{ val|sort }}
486 * // => 2,4,6
487 *
488 * @example
489 * // val = 'zaq';
490 * {{ val|sort }}
491 * // => aqz
492 *
493 * @example
494 * // val = { bar: 1, foo: 2 }
495 * {{ val|sort(true) }}
496 * // => foo,bar
497 *
498 * @param {*} input
499 * @param {boolean} [reverse=false] Output is given reverse-sorted if true.
500 * @return {*} Sorted array;
501 */
502 1 exports.sort = function (input, reverse) {
503 15 var out, clone;
504 15 if (utils.isArray(input)) {
505 5 clone = utils.extend([], input);
506 5 out = clone.sort();
507 } else {
508 10 switch (typeof input) {
509 case 'object':
510 2 out = utils.keys(input).sort();
511 2 break;
512 case 'string':
513 8 out = input.split('');
514 8 if (reverse) {
515 7 return out.reverse().join('');
516 }
517 1 return out.sort().join('');
518 }
519 }
520
521 7 if (out && reverse) {
522 4 return out.reverse();
523 }
524
525 3 return out || input;
526 };
527
528 /**
529 * Strip HTML tags.
530 *
531 * @example
532 * // stuff = '<p>foobar</p>';
533 * {{ stuff|striptags }}
534 * // => foobar
535 *
536 * @param {*} input
537 * @return {*} Returns the same object as the input, but with all string values stripped of tags.
538 */
539 1 exports.striptags = function (input) {
540 4 var out = iterateFilter.apply(exports.striptags, arguments);
541 4 if (out !== undefined) {
542 1 return out;
543 }
544
545 3 return input.toString().replace(/(<([^>]+)>)/ig, '');
546 };
547
548 /**
549 * Capitalizes every word given and lower-cases all other letters.
550 *
551 * @example
552 * // my_str = 'this is soMe text';
553 * {{ my_str|title }}
554 * // => This Is Some Text
555 *
556 * @example
557 * // my_arr = ['hi', 'this', 'is', 'an', 'array'];
558 * {{ my_arr|title|join(' ') }}
559 * // => Hi This Is An Array
560 *
561 * @param {*} input
562 * @return {*} Returns the same object as the input, but with all words in strings title-cased.
563 */
564 1 exports.title = function (input) {
565 4 var out = iterateFilter.apply(exports.title, arguments);
566 4 if (out !== undefined) {
567 1 return out;
568 }
569
570 3 return input.toString().replace(/\w\S*/g, function (str) {
571 6 return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
572 });
573 };
574
575 /**
576 * Remove all duplicate items from an array.
577 *
578 * @example
579 * // my_arr = [1, 2, 3, 4, 4, 3, 2, 1];
580 * {{ my_arr|uniq|join(',') }}
581 * // => 1,2,3,4
582 *
583 * @param {array} input
584 * @return {array} Array with unique items. If input was not an array, the original item is returned untouched.
585 */
586 1 exports.uniq = function (input) {
587 2 var result;
588
589 2 if (!input || !utils.isArray(input)) {
590 1 return '';
591 }
592
593 1 result = [];
594 1 utils.each(input, function (v) {
595 6 if (result.indexOf(v) === -1) {
596 4 result.push(v);
597 }
598 });
599 1 return result;
600 };
601
602 /**
603 * Convert the input to all uppercase letters. If an object or array is provided, all values will be uppercased.
604 *
605 * @example
606 * // my_str = 'tacos';
607 * {{ my_str|upper }}
608 * // => TACOS
609 *
610 * @example
611 * // my_arr = ['tacos', 'burritos'];
612 * {{ my_arr|upper|join(' & ') }}
613 * // => TACOS & BURRITOS
614 *
615 * @param {*} input
616 * @return {*} Returns the same type as the input, with all strings upper-cased.
617 */
618 1 exports.upper = function (input) {
619 8 var out = iterateFilter.apply(exports.upper, arguments);
620 8 if (out !== undefined) {
621 2 return out;
622 }
623
624 6 return input.toString().toUpperCase();
625 };
626
627 /**
628 * URL-encode a string. If an object or array is passed, all values will be URL-encoded.
629 *
630 * @example
631 * // my_str = 'param=1&anotherParam=2';
632 * {{ my_str|url_encode }}
633 * // => param%3D1%26anotherParam%3D2
634 *
635 * @param {*} input
636 * @return {*} URL-encoded string.
637 */
638 1 exports.url_encode = function (input) {
639 4 var out = iterateFilter.apply(exports.url_encode, arguments);
640 4 if (out !== undefined) {
641 1 return out;
642 }
643 3 return encodeURIComponent(input);
644 };
645
646 /**
647 * URL-decode a string. If an object or array is passed, all values will be URL-decoded.
648 *
649 * @example
650 * // my_str = 'param%3D1%26anotherParam%3D2';
651 * {{ my_str|url_decode }}
652 * // => param=1&anotherParam=2
653 *
654 * @param {*} input
655 * @return {*} URL-decoded string.
656 */
657 1 exports.url_decode = function (input) {
658 4 var out = iterateFilter.apply(exports.url_decode, arguments);
659 4 if (out !== undefined) {
660 1 return out;
661 }
662 3 return decodeURIComponent(input);
663 };
664

/lib/lexer.js

96%
25
24
1
Line Hits Source
1 1 var 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 */
16 1 var 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
245 1 exports.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 */
253 1 function reader(str) {
254 2112 var matched;
255
256 2112 utils.some(rules, function (rule) {
257 19996 return utils.some(rule.regex, function (regex) {
258 27936 var match = str.match(regex),
259 normalized;
260
261 27936 if (!match) {
262 25824 return;
263 }
264
265 2112 normalized = match[rule.idx || 0].replace(/\s*$/, '');
266 2112 normalized = (rule.hasOwnProperty('replace') && rule.replace.hasOwnProperty(normalized)) ? rule.replace[normalized] : normalized;
267
268 2112 matched = {
269 match: normalized,
270 type: rule.type,
271 length: match[0].length
272 };
273 2112 return true;
274 });
275 });
276
277 2112 if (!matched) {
278 0 matched = {
279 match: str,
280 type: TYPES.UNKNOWN,
281 length: str.length
282 };
283 }
284
285 2112 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 */
294 1 exports.read = function (str) {
295 685 var offset = 0,
296 tokens = [],
297 substr,
298 match;
299 685 while (offset < str.length) {
300 2112 substr = str.substring(offset);
301 2112 match = reader(substr);
302 2112 offset += match.length;
303 2112 tokens.push(match);
304 }
305 685 return tokens;
306 };
307

/lib/loaders/filesystem.js

94%
19
18
1
Line Hits Source
1 1 var 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 */
15 1 module.exports = function (basepath, encoding) {
16 3 var ret = {};
17
18 3 encoding = encoding || 'utf8';
19 3 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 */
28 3 ret.resolve = function (to, from) {
29 5611 if (basepath) {
30 4 from = basepath;
31 } else {
32 5607 from = from ? path.dirname(from) : process.cwd();
33 }
34 5610 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 */
44 3 ret.load = function (identifier, cb) {
45 57 if (!fs || (cb && !fs.readFile) || !fs.readFileSync) {
46 0 throw new Error('Unable to find file ' + identifier + ' because there is no filesystem to read from.');
47 }
48
49 57 identifier = ret.resolve(identifier);
50
51 57 if (cb) {
52 5 fs.readFile(identifier, encoding, cb);
53 5 return;
54 }
55 52 return fs.readFileSync(identifier, encoding);
56 };
57
58 3 return ret;
59 };
60

/lib/loaders/index.js

100%
2
2
0
Line Hits Source
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 */
52 1 exports.fs = require('./filesystem');
53 1 exports.memory = require('./memory');
54

/lib/loaders/memory.js

100%
20
20
0
Line Hits Source
1 1 var 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 */
17 1 module.exports = function (mapping, basepath) {
18 7 var ret = {};
19
20 7 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 */
29 7 ret.resolve = function (to, from) {
30 11 if (basepath) {
31 3 from = basepath;
32 } else {
33 8 from = from ? path.dirname(from) : '/';
34 }
35 11 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 */
45 7 ret.load = function (pathname, cb) {
46 10 var src, paths;
47
48 10 paths = [pathname, pathname.replace(/^(\/|\\)/, '')];
49
50 10 src = mapping[paths[0]] || mapping[paths[1]];
51 10 if (!src) {
52 1 utils.throwError('Unable to find template "' + pathname + '".');
53 }
54
55 9 if (cb) {
56 2 cb(null, src);
57 2 return;
58 }
59 7 return src;
60 };
61
62 7 return ret;
63 };
64

/lib/parser.js

99%
275
274
1
Line Hits Source
1 1 var utils = require('./utils'),
2 lexer = require('./lexer');
3
4 1 var _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 */
46 1 function escapeRegExp(str) {
47 2802 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 */
60 1 function TokenParser(tokens, filters, autoescape, line, filename) {
61 685 this.out = [];
62 685 this.state = [];
63 685 this.filterApplyIdx = [];
64 685 this._parsers = {};
65 685 this.line = line;
66 685 this.filename = filename;
67 685 this.filters = filters;
68 685 this.escape = autoescape;
69
70 685 this.parse = function () {
71 681 var self = this;
72
73 681 if (self._parsers.start) {
74 0 self._parsers.start.call(self);
75 }
76 681 utils.each(tokens, function (token, i) {
77 2086 var prevToken = tokens[i - 1];
78 2086 self.isLast = (i === tokens.length - 1);
79 2086 if (prevToken) {
80 1426 while (prevToken.type === _t.WHITESPACE) {
81 301 i -= 1;
82 301 prevToken = tokens[i - 1];
83 }
84 }
85 2086 self.prevToken = prevToken;
86 2086 self.parseToken(token);
87 });
88 622 if (self._parsers.end) {
89 19 self._parsers.end.call(self);
90 }
91
92 622 if (self.escape) {
93 276 self.filterApplyIdx = [0];
94 276 if (typeof self.escape === 'string') {
95 2 self.parseToken({ type: _t.FILTER, match: 'e' });
96 2 self.parseToken({ type: _t.COMMA, match: ',' });
97 2 self.parseToken({ type: _t.STRING, match: String(autoescape) });
98 2 self.parseToken({ type: _t.PARENCLOSE, match: ')'});
99 } else {
100 274 self.parseToken({ type: _t.FILTEREMPTY, match: 'e' });
101 }
102 }
103
104 622 return self.out;
105 };
106 }
107
108 1 TokenParser.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) {
129 1081 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) {
139 2368 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
147 2368 if (fn && typeof fn === 'function') {
148 524 if (!fn.call(this, token)) {
149 413 return;
150 }
151 }
152
153 1933 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) {
161 107 self.out.push(', ');
162 }
163
164 1933 if (lastState && lastState === _t.METHODOPEN) {
165 23 self.state.pop();
166 23 if (token.type !== _t.PARENCLOSE) {
167 11 self.out.push(', ');
168 }
169 }
170
171 1933 switch (token.type) {
172 case _t.WHITESPACE:
173 293 break;
174
175 case _t.STRING:
176 222 self.filterApplyIdx.push(self.out.length);
177 222 self.out.push(match.replace(/\\/g, '\\\\'));
178 222 break;
179
180 case _t.NUMBER:
181 case _t.BOOL:
182 116 self.filterApplyIdx.push(self.out.length);
183 116 self.out.push(match);
184 116 break;
185
186 case _t.FILTER:
187 111 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
188 1 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
189 }
190 110 self.escape = self.filters[match].safe ? false : self.escape;
191 110 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
192 110 self.state.push(token.type);
193 110 break;
194
195 case _t.FILTEREMPTY:
196 333 if (!self.filters.hasOwnProperty(match) || typeof self.filters[match] !== "function") {
197 1 utils.throwError('Invalid filter "' + match + '"', self.line, self.filename);
198 }
199 332 self.escape = self.filters[match].safe ? false : self.escape;
200 332 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '_filters["' + match + '"](');
201 332 self.out.push(')');
202 332 break;
203
204 case _t.FUNCTION:
205 case _t.FUNCTIONEMPTY:
206 29 self.out.push('((typeof _ctx.' + match + ' !== "undefined") ? _ctx.' + match +
207 ' : ((typeof ' + match + ' !== "undefined") ? ' + match +
208 ' : _fn))(');
209 29 self.escape = false;
210 29 if (token.type === _t.FUNCTIONEMPTY) {
211 10 self.out[self.out.length - 1] = self.out[self.out.length - 1] + ')';
212 } else {
213 19 self.state.push(token.type);
214 }
215 29 self.filterApplyIdx.push(self.out.length - 1);
216 29 break;
217
218 case _t.PARENOPEN:
219 28 self.state.push(token.type);
220 28 if (self.filterApplyIdx.length) {
221 26 self.out.splice(self.filterApplyIdx[self.filterApplyIdx.length - 1], 0, '(');
222 26 if (prevToken && prevTokenType === _t.VAR) {
223 23 temp = prevToken.match.split('.').slice(0, -1);
224 23 self.out.push(' || _fn).call(' + self.checkMatch(temp));
225 23 self.state.push(_t.METHODOPEN);
226 23 self.escape = false;
227 } else {
228 3 self.out.push(' || _fn)(');
229 }
230 26 self.filterApplyIdx.push(self.out.length - 3);
231 } else {
232 2 self.out.push('(');
233 2 self.filterApplyIdx.push(self.out.length - 1);
234 }
235 28 break;
236
237 case _t.PARENCLOSE:
238 161 temp = self.state.pop();
239 161 if (temp !== _t.PARENOPEN && temp !== _t.FUNCTION && temp !== _t.FILTER) {
240 1 utils.throwError('Mismatched nesting state', self.line, self.filename);
241 }
242 160 self.out.push(')');
243 // Once off the previous entry
244 160 self.filterApplyIdx.pop();
245 160 if (temp !== _t.FILTER) {
246 // Once for the open paren
247 50 self.filterApplyIdx.pop();
248 }
249 160 break;
250
251 case _t.COMMA:
252 106 if (lastState !== _t.FUNCTION &&
253 lastState !== _t.FILTER &&
254 lastState !== _t.ARRAYOPEN &&
255 lastState !== _t.CURLYOPEN &&
256 lastState !== _t.PARENOPEN &&
257 lastState !== _t.COLON) {
258 1 utils.throwError('Unexpected comma', self.line, self.filename);
259 }
260 105 if (lastState === _t.COLON) {
261 5 self.state.pop();
262 }
263 105 self.out.push(', ');
264 105 self.filterApplyIdx.pop();
265 105 break;
266
267 case _t.LOGIC:
268 case _t.COMPARATOR:
269 6 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) {
276 1 utils.throwError('Unexpected logic', self.line, self.filename);
277 }
278 5 self.out.push(token.match);
279 5 break;
280
281 case _t.NOT:
282 2 self.out.push(token.match);
283 2 break;
284
285 case _t.VAR:
286 445 self.parseVar(token, match, lastState);
287 418 break;
288
289 case _t.BRACKETOPEN:
290 19 if (!prevToken ||
291 (prevTokenType !== _t.VAR &&
292 prevTokenType !== _t.BRACKETCLOSE &&
293 prevTokenType !== _t.PARENCLOSE)) {
294 5 self.state.push(_t.ARRAYOPEN);
295 5 self.filterApplyIdx.push(self.out.length);
296 } else {
297 14 self.state.push(token.type);
298 }
299 19 self.out.push('[');
300 19 break;
301
302 case _t.BRACKETCLOSE:
303 19 temp = self.state.pop();
304 19 if (temp !== _t.BRACKETOPEN && temp !== _t.ARRAYOPEN) {
305 1 utils.throwError('Unexpected closing square bracket', self.line, self.filename);
306 }
307 18 self.out.push(']');
308 18 self.filterApplyIdx.pop();
309 18 break;
310
311 case _t.CURLYOPEN:
312 7 self.state.push(token.type);
313 7 self.out.push('{');
314 7 self.filterApplyIdx.push(self.out.length - 1);
315 7 break;
316
317 case _t.COLON:
318 12 if (lastState !== _t.CURLYOPEN) {
319 1 utils.throwError('Unexpected colon', self.line, self.filename);
320 }
321 11 self.state.push(token.type);
322 11 self.out.push(':');
323 11 self.filterApplyIdx.pop();
324 11 break;
325
326 case _t.CURLYCLOSE:
327 7 if (lastState === _t.COLON) {
328 6 self.state.pop();
329 }
330 7 if (self.state.pop() !== _t.CURLYOPEN) {
331 1 utils.throwError('Unexpected closing curly brace', self.line, self.filename);
332 }
333 6 self.out.push('}');
334
335 6 self.filterApplyIdx.pop();
336 6 break;
337
338 case _t.DOTKEY:
339 9 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 )) {
348 2 utils.throwError('Unexpected key "' + match + '"', self.line, self.filename);
349 }
350 7 self.out.push('.' + match);
351 7 break;
352
353 case _t.OPERATOR:
354 8 self.out.push(' ' + match + ' ');
355 8 self.filterApplyIdx.pop();
356 8 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) {
369 445 var self = this;
370
371 445 match = match.split('.');
372
373 445 if (_reserved.indexOf(match[0]) !== -1) {
374 26 utils.throwError('Reserved keyword "' + match[0] + '" attempted to be used as a variable', self.line, self.filename);
375 }
376
377 419 self.filterApplyIdx.push(self.out.length);
378 419 if (lastState === _t.CURLYOPEN) {
379 10 if (match.length > 1) {
380 1 utils.throwError('Unexpected dot', self.line, self.filename);
381 }
382 9 self.out.push(match[0]);
383 9 return;
384 }
385
386 409 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) {
395 432 var temp = match[0], result;
396
397 432 function checkDot(ctx) {
398 1296 var c = ctx + temp,
399 m = match,
400 build = '';
401
402 1296 build = '(typeof ' + c + ' !== "undefined" && ' + c + ' !== null';
403 1296 utils.each(m, function (v, i) {
404 1452 if (i === 0) {
405 1296 return;
406 }
407 156 build += ' && ' + c + '.' + v + ' !== undefined && ' + c + '.' + v + ' !== null';
408 156 c += '.' + v;
409 });
410 1296 build += ')';
411
412 1296 return build;
413 }
414
415 432 function buildDot(ctx) {
416 864 return '(' + checkDot(ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
417 }
418 432 result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
419 432 return '(' + result + ' !== null ? ' + result + ' : ' + '"" )';
420 }
421 };
422
423 /**
424 * Parse a source string into tokens that are ready for compilation.
425 *
426 * @example
427 * exports.parse('{{ tacos }}', {}, tags, filters);
428 * // => [{ compile: [Function], ... }]
429 *
430 * @params {object} swig The current Swig instance
431 * @param {string} source Swig template source.
432 * @param {object} opts Swig options object.
433 * @param {object} tags Keyed object of tags that can be parsed and compiled.
434 * @param {object} filters Keyed object of filters that may be applied to variables.
435 * @return {array} List of tokens ready for compilation.
436 */
437 1 exports.parse = function (swig, source, opts, tags, filters) {
438 467 source = source.replace(/\r\n/g, '\n');
439 467 var escape = opts.autoescape,
440 tagOpen = opts.tagControls[0],
441 tagClose = opts.tagControls[1],
442 varOpen = opts.varControls[0],
443 varClose = opts.varControls[1],
444 escapedTagOpen = escapeRegExp(tagOpen),
445 escapedTagClose = escapeRegExp(tagClose),
446 escapedVarOpen = escapeRegExp(varOpen),
447 escapedVarClose = escapeRegExp(varClose),
448 tagStrip = new RegExp('^' + escapedTagOpen + '-?\\s*-?|-?\\s*-?' + escapedTagClose + '$', 'g'),
449 tagStripBefore = new RegExp('^' + escapedTagOpen + '-'),
450 tagStripAfter = new RegExp('-' + escapedTagClose + '$'),
451 varStrip = new RegExp('^' + escapedVarOpen + '-?\\s*-?|-?\\s*-?' + escapedVarClose + '$', 'g'),
452 varStripBefore = new RegExp('^' + escapedVarOpen + '-'),
453 varStripAfter = new RegExp('-' + escapedVarClose + '$'),
454 cmtOpen = opts.cmtControls[0],
455 cmtClose = opts.cmtControls[1],
456 anyChar = '[\\s\\S]*?',
457 // Split the template source based on variable, tag, and comment blocks
458 // /(\{%[\s\S]*?%\}|\{\{[\s\S]*?\}\}|\{#[\s\S]*?#\})/
459 splitter = new RegExp(
460 '(' +
461 escapedTagOpen + anyChar + escapedTagClose + '|' +
462 escapedVarOpen + anyChar + escapedVarClose + '|' +
463 escapeRegExp(cmtOpen) + anyChar + escapeRegExp(cmtClose) +
464 ')'
465 ),
466 line = 1,
467 stack = [],
468 parent = null,
469 tokens = [],
470 blocks = {},
471 inRaw = false,
472 stripNext;
473
474 /**
475 * Parse a variable.
476 * @param {string} str String contents of the variable, between <i>{{</i> and <i>}}</i>
477 * @param {number} line The line number that this variable starts on.
478 * @return {VarToken} Parsed variable token object.
479 * @private
480 */
481 467 function parseVariable(str, line) {
482 365 var lexedTokens = lexer.read(utils.strip(str)),
483 parser,
484 out;
485
486 365 parser = new TokenParser(lexedTokens, filters, escape, line, opts.filename);
487 365 out = parser.parse().join('');
488
489 329 if (parser.state.length) {
490 2 utils.throwError('Unable to parse "' + str + '"', line, opts.filename);
491 }
492
493 /**
494 * A parsed variable token.
495 * @typedef {object} VarToken
496 * @property {function} compile Method for compiling this token.
497 */
498 327 return {
499 compile: function () {
500 321 return '_output += ' + out + ';\n';
501 }
502 };
503 }
504 467 exports.parseVariable = parseVariable;
505
506 /**
507 * Parse a tag.
508 * @param {string} str String contents of the tag, between <i>{%</i> and <i>%}</i>
509 * @param {number} line The line number that this tag starts on.
510 * @return {TagToken} Parsed token object.
511 * @private
512 */
513 467 function parseTag(str, line) {
514 517 var lexedTokens, parser, chunks, tagName, tag, args, last;
515
516 517 if (utils.startsWith(str, 'end')) {
517 194 last = stack[stack.length - 1];
518 194 if (last && last.name === str.split(/\s+/)[0].replace(/^end/, '') && last.ends) {
519 192 switch (last.name) {
520 case 'autoescape':
521 9 escape = opts.autoescape;
522 9 break;
523 case 'raw':
524 4 inRaw = false;
525 4 break;
526 }
527 192 stack.pop();
528 192 return;
529 }
530
531 2 if (!inRaw) {
532 1 utils.throwError('Unexpected end of tag "' + str.replace(/^end/, '') + '"', line, opts.filename);
533 }
534 }
535
536 324 if (inRaw) {
537 3 return;
538 }
539
540 321 chunks = str.split(/\s+(.+)?/);
541 321 tagName = chunks.shift();
542
543 321 if (!tags.hasOwnProperty(tagName)) {
544 1 utils.throwError('Unexpected tag "' + str + '"', line, opts.filename);
545 }
546
547 320 lexedTokens = lexer.read(utils.strip(chunks.join(' ')));
548 320 parser = new TokenParser(lexedTokens, filters, false, line, opts.filename);
549 320 tag = tags[tagName];
550
551 /**
552 * Define custom parsing methods for your tag.
553 * @callback parse
554 *
555 * @example
556 * exports.parse = function (str, line, parser, types, options, swig) {
557 * parser.on('start', function () {
558 * // ...
559 * });
560 * parser.on(types.STRING, function (token) {
561 * // ...
562 * });
563 * };
564 *
565 * @param {string} str The full token string of the tag.
566 * @param {number} line The line number that this tag appears on.
567 * @param {TokenParser} parser A TokenParser instance.
568 * @param {TYPES} types Lexer token type enum.
569 * @param {TagToken[]} stack The current stack of open tags.
570 * @param {SwigOpts} options Swig Options Object.
571 * @param {object} swig The Swig instance (gives acces to loaders, parsers, etc)
572 */
573 320 if (!tag.parse(chunks[1], line, parser, _t, stack, opts, swig)) {
574 2 utils.throwError('Unexpected tag "' + tagName + '"', line, opts.filename);
575 }
576
577 316 parser.parse();
578 293 args = parser.out;
579
580 293 switch (tagName) {
581 case 'autoescape':
582 9 escape = (args[0] !== 'false') ? args[0] : false;
583 9 break;
584 case 'raw':
585 4 inRaw = true;
586 4 break;
587 }
588
589 /**
590 * A parsed tag token.
591 * @typedef {Object} TagToken
592 * @property {compile} [compile] Method for compiling this token.
593 * @property {array} [args] Array of arguments for the tag.
594 * @property {Token[]} [content=[]] An array of tokens that are children of this Token.
595 * @property {boolean} [ends] Whether or not this tag requires an end tag.
596 * @property {string} name The name of this tag.
597 */
598 293 return {
599 block: !!tags[tagName].block,
600 compile: tag.compile,
601 args: args,
602 content: [],
603 ends: tag.ends,
604 name: tagName
605 };
606 }
607
608 /**
609 * Strip the whitespace from the previous token, if it is a string.
610 * @param {object} token Parsed token.
611 * @return {object} If the token was a string, trailing whitespace will be stripped.
612 */
613 467 function stripPrevToken(token) {
614 10 if (typeof token === 'string') {
615 8 token = token.replace(/\s*$/, '');
616 }
617 10 return token;
618 }
619
620 /*!
621 * Loop over the source, split via the tag/var/comment regular expression splitter.
622 * Send each chunk to the appropriate parser.
623 */
624 467 utils.each(source.split(splitter), function (chunk) {
625 2182 var token, lines, stripPrev, prevToken, prevChildToken;
626
627 2182 if (!chunk) {
628 917 return;
629 }
630
631 // Is a variable?
632 1265 if (!inRaw && utils.startsWith(chunk, varOpen) && utils.endsWith(chunk, varClose)) {
633 365 stripPrev = varStripBefore.test(chunk);
634 365 stripNext = varStripAfter.test(chunk);
635 365 token = parseVariable(chunk.replace(varStrip, ''), line);
636 // Is a tag?
637 900 } else if (utils.startsWith(chunk, tagOpen) && utils.endsWith(chunk, tagClose)) {
638 517 stripPrev = tagStripBefore.test(chunk);
639 517 stripNext = tagStripAfter.test(chunk);
640 517 token = parseTag(chunk.replace(tagStrip, ''), line);
641 488 if (token) {
642 293 if (token.name === 'extends') {
643 26 parent = token.args.join('').replace(/^\'|\'$/g, '').replace(/^\"|\"$/g, '');
644 267 } else if (token.block && !stack.length) {
645 126 blocks[token.args.join('')] = token;
646 }
647 }
648 488 if (inRaw && !token) {
649 3 token = chunk;
650 }
651 // Is a content string?
652 383 } else if (inRaw || (!utils.startsWith(chunk, cmtOpen) && !utils.endsWith(chunk, cmtClose))) {
653 376 token = stripNext ? chunk.replace(/^\s*/, '') : chunk;
654 376 stripNext = false;
655 7 } else if (utils.startsWith(chunk, cmtOpen) && utils.endsWith(chunk, cmtClose)) {
656 7 return;
657 }
658
659 // Did this tag ask to strip previous whitespace? <code>{%- ... %}</code> or <code>{{- ... }}</code>
660 1191 if (stripPrev && tokens.length) {
661 10 prevToken = tokens.pop();
662 10 if (typeof prevToken === 'string') {
663 4 prevToken = stripPrevToken(prevToken);
664 6 } else if (prevToken.content && prevToken.content.length) {
665 6 prevChildToken = stripPrevToken(prevToken.content.pop());
666 6 prevToken.content.push(prevChildToken);
667 }
668 10 tokens.push(prevToken);
669 }
670
671 // This was a comment, so let's just keep going.
672 1191 if (!token) {
673 198 return;
674 }
675
676 // If there's an open item in the stack, add this to its content.
677 993 if (stack.length) {
678 285 stack[stack.length - 1].content.push(token);
679 } else {
680 708 tokens.push(token);
681 }
682
683 // If the token is a tag that requires an end tag, open it on the stack.
684 993 if (token.name && token.ends) {
685 195 stack.push(token);
686 }
687
688 993 lines = chunk.match(/\n/g);
689 993 line += lines ? lines.length : 0;
690 });
691
692 400 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 */
709 1 exports.compile = function (template, parents, options, blockName) {
710 541 var out = '',
711 tokens = utils.isArray(template) ? template : template.tokens;
712
713 541 utils.each(tokens, function (token) {
714 821 var o;
715 821 if (typeof token === 'string') {
716 282 out += '_output += "' + token.replace(/\\/g, '\\\\').replace(/\n|\r/g, '\\n').replace(/"/g, '\\"') + '";\n';
717 282 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 */
739 539 o = token.compile(exports.compile, token.args ? token.args.slice(0) : [], token.content ? token.content.slice(0) : [], parents, options, blockName);
740 539 out += o || '';
741 });
742
743 541 return out;
744 };
745

/lib/swig.js

99%
212
211
1
Line Hits Source
1 1 var utils = require('./utils'),
2 _tags = require('./tags'),
3 _filters = require('./filters'),
4 parser = require('./parser'),
5 dateformatter = require('./dateformatter'),
6 loaders = require('./loaders');
7
8 /**
9 * Swig version number as a string.
10 * @example
11 * if (swig.version === "1.4.2") { ... }
12 *
13 * @type {String}
14 */
15 1 exports.version = "1.4.2";
16
17 /**
18 * Swig Options Object. This object can be passed to many of the API-level Swig methods to control various aspects of the engine. All keys are optional.
19 * @typedef {Object} SwigOpts
20 * @property {boolean} autoescape Controls whether or not variable output will automatically be escaped for safe HTML output. Defaults to <code data-language="js">true</code>. Functions executed in variable statements will not be auto-escaped. Your application/functions should take care of their own auto-escaping.
21 * @property {array} varControls Open and close controls for variables. Defaults to <code data-language="js">['{{', '}}']</code>.
22 * @property {array} tagControls Open and close controls for tags. Defaults to <code data-language="js">['{%', '%}']</code>.
23 * @property {array} cmtControls Open and close controls for comments. Defaults to <code data-language="js">['{#', '#}']</code>.
24 * @property {object} locals Default variable context to be passed to <strong>all</strong> templates.
25 * @property {CacheOptions} cache Cache control for templates. Defaults to saving in <code data-language="js">'memory'</code>. Send <code data-language="js">false</code> to disable. Send an object with <code data-language="js">get</code> and <code data-language="js">set</code> functions to customize.
26 * @property {TemplateLoader} loader The method that Swig will use to load templates. Defaults to <var>swig.loaders.fs</var>.
27 */
28 1 var 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 */
80 2 function 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 */
88 1 function validateOptions(options) {
89 1111 if (!options) {
90 90 return;
91 }
92
93 1021 utils.each(['varControls', 'tagControls', 'cmtControls'], function (key) {
94 3048 if (!options.hasOwnProperty(key)) {
95 1247 return;
96 }
97 1801 if (!utils.isArray(options[key]) || options[key].length !== 2) {
98 6 throw new Error('Option "' + key + '" must be an array containing 2 different control strings.');
99 }
100 1795 if (options[key][0] === options[key][1]) {
101 3 throw new Error('Option "' + key + '" open and close controls must not be the same.');
102 }
103 1792 utils.each(options[key], function (a, i) {
104 3581 if (a.length < 2) {
105 6 throw new Error('Option "' + key + '" ' + (i ? 'open ' : 'close ') + 'control must be at least 2 characters. Saw "' + a + '" instead.');
106 }
107 });
108 });
109
110 1006 if (options.hasOwnProperty('cache')) {
111 598 if (options.cache && options.cache !== 'memory') {
112 3 if (!options.cache.get || !options.cache.set) {
113 2 throw new Error('Invalid cache option ' + JSON.stringify(options.cache) + ' found. Expected "memory" or { get: function (key) { ... }, set: function (key, value) { ... } }.');
114 }
115 }
116 }
117 1004 if (options.hasOwnProperty('loader')) {
118 604 if (options.loader) {
119 604 if (!options.loader.load || !options.loader.resolve) {
120 3 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 */
143 1 exports.setDefaults = function (options) {
144 602 validateOptions(options);
145 598 defaultInstance.options = utils.extend(defaultInstance.options, options);
146 };
147
148 /**
149 * Set the default TimeZone offset for date formatting via the date filter. This is a global setting and will affect all Swig environments, old or new.
150 * @param {number} offset Offset from GMT, in minutes.
151 * @return {undefined}
152 */
153 1 exports.setDefaultTZOffset = function (offset) {
154 2 dateformatter.tzOffset = offset;
155 };
156
157 /**
158 * Create a new, separate Swig compile/render environment.
159 *
160 * @example
161 * var swig = require('swig');
162 * var myswig = new swig.Swig({varControls: ['<%=', '%>']});
163 * myswig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
164 * // => Tacos are delicious!
165 * swig.render('Tacos are <%= tacos =>!', { locals: { tacos: 'delicious' }});
166 * // => 'Tacos are <%= tacos =>!'
167 *
168 * @param {SwigOpts} [opts={}] Swig options object.
169 * @return {object} New Swig environment.
170 */
171 1 exports.Swig = function (opts) {
172 27 validateOptions(opts);
173 26 this.options = utils.extend({}, defaultOptions, opts || {});
174 26 this.cache = {};
175 26 this.extensions = {};
176 26 var self = this,
177 tags = _tags,
178 filters = _filters;
179
180 /**
181 * Get combined locals context.
182 * @param {?SwigOpts} [options] Swig options object.
183 * @return {object} Locals context.
184 * @private
185 */
186 26 function getLocals(options) {
187 924 if (!options || !options.locals) {
188 345 return self.options.locals;
189 }
190
191 579 return utils.extend({}, self.options.locals, options.locals);
192 }
193
194 /**
195 * Determine whether caching is enabled via the options provided and/or defaults
196 * @param {SwigOpts} [options={}] Swig Options Object
197 * @return {boolean}
198 * @private
199 */
200 26 function shouldCache(options) {
201 5649 options = options || {};
202 5649 return (options.hasOwnProperty('cache') && !options.cache) || !self.options.cache;
203 }
204
205 /**
206 * Get compiled template from the cache.
207 * @param {string} key Name of template.
208 * @return {object|undefined} Template function and tokens.
209 * @private
210 */
211 26 function cacheGet(key, options) {
212 5609 if (shouldCache(options)) {
213 3 return;
214 }
215
216 5606 if (self.options.cache === 'memory') {
217 5605 return self.cache[key];
218 }
219
220 1 return self.options.cache.get(key);
221 }
222
223 /**
224 * Store a template in the cache.
225 * @param {string} key Name of template.
226 * @param {object} val Template function and tokens.
227 * @return {undefined}
228 * @private
229 */
230 26 function cacheSet(key, options, val) {
231 40 if (shouldCache(options)) {
232 3 return;
233 }
234
235 37 if (self.options.cache === 'memory') {
236 36 self.cache[key] = val;
237 36 return;
238 }
239
240 1 self.options.cache.set(key, val);
241 }
242
243 /**
244 * Clears the in-memory template cache.
245 *
246 * @example
247 * swig.invalidateCache();
248 *
249 * @return {undefined}
250 */
251 26 this.invalidateCache = function () {
252 592 if (self.options.cache === 'memory') {
253 592 self.cache = {};
254 }
255 };
256
257 /**
258 * Add a custom filter for swig variables.
259 *
260 * @example
261 * function replaceMs(input) { return input.replace(/m/g, 'f'); }
262 * swig.setFilter('replaceMs', replaceMs);
263 * // => {{ "onomatopoeia"|replaceMs }}
264 * // => onofatopeia
265 *
266 * @param {string} name Name of filter, used in templates. <strong>Will</strong> overwrite previously defined filters, if using the same name.
267 * @param {function} method Function that acts against the input. See <a href="/docs/filters/#custom">Custom Filters</a> for more information.
268 * @return {undefined}
269 */
270 26 this.setFilter = function (name, method) {
271 3 if (typeof method !== "function") {
272 1 throw new Error('Filter "' + name + '" is not a valid function.');
273 }
274 2 filters[name] = method;
275 };
276
277 /**
278 * Add a custom tag. To expose your own extensions to compiled template code, see <code data-language="js">swig.setExtension</code>.
279 *
280 * For a more in-depth explanation of writing custom tags, see <a href="../extending/#tags">Custom Tags</a>.
281 *
282 * @example
283 * var tacotag = require('./tacotag');
284 * swig.setTag('tacos', tacotag.parse, tacotag.compile, tacotag.ends, tacotag.blockLevel);
285 * // => {% tacos %}Make this be tacos.{% endtacos %}
286 * // => Tacos tacos tacos tacos.
287 *
288 * @param {string} name Tag name.
289 * @param {function} parse Method for parsing tokens.
290 * @param {function} compile Method for compiling renderable output.
291 * @param {boolean} [ends=false] Whether or not this tag requires an <i>end</i> tag.
292 * @param {boolean} [blockLevel=false] If false, this tag will not be compiled outside of <code>block</code> tags when extending a parent template.
293 * @return {undefined}
294 */
295 26 this.setTag = function (name, parse, compile, ends, blockLevel) {
296 4 if (typeof parse !== 'function') {
297 1 throw new Error('Tag "' + name + '" parse method is not a valid function.');
298 }
299
300 3 if (typeof compile !== 'function') {
301 1 throw new Error('Tag "' + name + '" compile method is not a valid function.');
302 }
303
304 2 tags[name] = {
305 parse: parse,
306 compile: compile,
307 ends: ends || false,
308 block: !!blockLevel
309 };
310 };
311
312 /**
313 * Add extensions for custom tags. This allows any custom tag to access a globally available methods via a special globally available object, <var>_ext</var>, in templates.
314 *
315 * @example
316 * swig.setExtension('trans', function (v) { return translate(v); });
317 * function compileTrans(compiler, args, content, parent, options) {
318 * return '_output += _ext.trans(' + args[0] + ');'
319 * };
320 * swig.setTag('trans', parseTrans, compileTrans, true);
321 *
322 * @param {string} name Key name of the extension. Accessed via <code data-language="js">_ext[name]</code>.
323 * @param {*} object The method, value, or object that should be available via the given name.
324 * @return {undefined}
325 */
326 26 this.setExtension = function (name, object) {
327 1 self.extensions[name] = object;
328 };
329
330 /**
331 * Parse a given source string into tokens.
332 *
333 * @param {string} source Swig template source.
334 * @param {SwigOpts} [options={}] Swig options object.
335 * @return {object} parsed Template tokens object.
336 * @private
337 */
338 26 this.parse = function (source, options) {
339 482 validateOptions(options);
340
341 467 var locals = getLocals(options),
342 opt = {},
343 k;
344
345 467 for (k in options) {
346 405 if (options.hasOwnProperty(k) && k !== 'locals') {
347 117 opt[k] = options[k];
348 }
349 }
350
351 467 options = utils.extend({}, self.options, opt);
352 467 options.locals = locals;
353
354 467 return parser.parse(this, source, options, tags, filters);
355 };
356
357 /**
358 * Parse a given file into tokens.
359 *
360 * @param {string} pathname Full path to file to parse.
361 * @param {SwigOpts} [options={}] Swig options object.
362 * @return {object} parsed Template tokens object.
363 * @private
364 */
365 26 this.parseFile = function (pathname, options) {
366 28 var src;
367
368 28 if (!options) {
369 0 options = {};
370 }
371
372 28 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
373
374 28 src = self.options.loader.load(pathname);
375
376 27 if (!options.filename) {
377 4 options = utils.extend({ filename: pathname }, options);
378 }
379
380 27 return self.parse(src, options);
381 };
382
383 /**
384 * Re-Map blocks within a list of tokens to the template's block objects.
385 * @param {array} tokens List of tokens for the parent object.
386 * @param {object} template Current template that needs to be mapped to the parent's block and token list.
387 * @return {array}
388 * @private
389 */
390 26 function remapBlocks(blocks, tokens) {
391 51 return utils.map(tokens, function (token) {
392 116 var args = token.args ? token.args.join('') : '';
393 116 if (token.name === 'block' && blocks[args]) {
394 21 token = blocks[args];
395 }
396 116 if (token.content && token.content.length) {
397 28 token.content = remapBlocks(blocks, token.content);
398 }
399 116 return token;
400 });
401 }
402
403 /**
404 * Import block-level tags to the token list that are not actual block tags.
405 * @param {array} blocks List of block-level tags.
406 * @param {array} tokens List of tokens to render.
407 * @return {undefined}
408 * @private
409 */
410 26 function importNonBlocks(blocks, tokens) {
411 23 var temp = [];
412 52 utils.each(blocks, function (block) { temp.push(block); });
413 23 utils.each(temp.reverse(), function (block) {
414 29 if (block.name !== 'block') {
415 5 tokens.unshift(block);
416 }
417 });
418 }
419
420 /**
421 * Recursively compile and get parents of given parsed token object.
422 *
423 * @param {object} tokens Parsed tokens from template.
424 * @param {SwigOpts} [options={}] Swig options object.
425 * @return {object} Parsed tokens from parent templates.
426 * @private
427 */
428 26 function getParents(tokens, options) {
429 373 var parentName = tokens.parent,
430 parentFiles = [],
431 parents = [],
432 parentFile,
433 parent,
434 l;
435
436 373 while (parentName) {
437 28 if (!options || !options.filename) {
438 1 throw new Error('Cannot extend "' + parentName + '" because current template has no filename.');
439 }
440
441 27 parentFile = parentFile || options.filename;
442 27 parentFile = self.options.loader.resolve(parentName, parentFile);
443 27 parent = cacheGet(parentFile, options) || self.parseFile(parentFile, utils.extend({}, options, { filename: parentFile }));
444 26 parentName = parent.parent;
445
446 26 if (parentFiles.indexOf(parentFile) !== -1) {
447 1 throw new Error('Illegal circular extends of "' + parentFile + '".');
448 }
449 25 parentFiles.push(parentFile);
450
451 25 parents.push(parent);
452 }
453
454 // Remap each parents'(1) blocks onto its own parent(2), receiving the full token list for rendering the original parent(1) on its own.
455 370 l = parents.length;
456 370 for (l = parents.length - 2; l >= 0; l -= 1) {
457 6 parents[l].tokens = remapBlocks(parents[l].blocks, parents[l + 1].tokens);
458 6 importNonBlocks(parents[l].blocks, parents[l].tokens);
459 }
460
461 370 return parents;
462 }
463
464 /**
465 * Pre-compile a source string into a cache-able template function.
466 *
467 * @example
468 * swig.precompile('{{ tacos }}');
469 * // => {
470 * // tpl: function (_swig, _locals, _filters, _utils, _fn) { ... },
471 * // tokens: {
472 * // name: undefined,
473 * // parent: null,
474 * // tokens: [...],
475 * // blocks: {}
476 * // }
477 * // }
478 *
479 * In order to render a pre-compiled template, you must have access to filters and utils from Swig. <var>efn</var> is simply an empty function that does nothing.
480 *
481 * @param {string} source Swig template source string.
482 * @param {SwigOpts} [options={}] Swig options object.
483 * @return {object} Renderable function and tokens object.
484 */
485 26 this.precompile = function (source, options) {
486 455 var tokens = self.parse(source, options),
487 parents = getParents(tokens, options),
488 tpl;
489
490 370 if (parents.length) {
491 // Remap the templates first-parent's tokens using this template's blocks.
492 17 tokens.tokens = remapBlocks(tokens.blocks, parents[0].tokens);
493 17 importNonBlocks(tokens.blocks, tokens.tokens);
494 }
495
496 370 try {
497 370 tpl = new Function('_swig', '_ctx', '_filters', '_utils', '_fn',
498 ' var _ext = _swig.extensions,\n' +
499 ' _output = "";\n' +
500 parser.compile(tokens, parents, options) + '\n' +
501 ' return _output;\n'
502 );
503 } catch (e) {
504 1 utils.throwError(e, null, options.filename);
505 }
506
507 369 return { tpl: tpl, tokens: tokens };
508 };
509
510 /**
511 * Compile and render a template string for final output.
512 *
513 * When rendering a source string, a file path should be specified in the options object in order for <var>extends</var>, <var>include</var>, and <var>import</var> to work properly. Do this by adding <code data-language="js">{ filename: '/absolute/path/to/mytpl.html' }</code> to the options argument.
514 *
515 * @example
516 * swig.render('{{ tacos }}', { locals: { tacos: 'Tacos!!!!' }});
517 * // => Tacos!!!!
518 *
519 * @param {string} source Swig template source string.
520 * @param {SwigOpts} [options={}] Swig options object.
521 * @return {string} Rendered output.
522 */
523 26 this.render = function (source, options) {
524 388 return self.compile(source, options)();
525 };
526
527 /**
528 * Compile and render a template file for final output. This is most useful for libraries like Express.js.
529 *
530 * @example
531 * swig.renderFile('./template.html', {}, function (err, output) {
532 * if (err) {
533 * throw err;
534 * }
535 * console.log(output);
536 * });
537 *
538 * @example
539 * swig.renderFile('./template.html', {});
540 * // => output
541 *
542 * @param {string} pathName File location.
543 * @param {object} [locals={}] Template variable context.
544 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
545 * @return {string} Rendered output.
546 */
547 26 this.renderFile = function (pathName, locals, cb) {
548 12 if (cb) {
549 5 self.compileFile(pathName, {}, function (err, fn) {
550 5 var result;
551
552 5 if (err) {
553 1 cb(err);
554 1 return;
555 }
556
557 4 try {
558 4 result = fn(locals);
559 } catch (err2) {
560 1 cb(err2);
561 1 return;
562 }
563
564 3 cb(null, result);
565 });
566 5 return;
567 }
568
569 7 return self.compileFile(pathName)(locals);
570 };
571
572 /**
573 * Compile string source into a renderable template function.
574 *
575 * @example
576 * var tpl = swig.compile('{{ tacos }}');
577 * // => {
578 * // [Function: compiled]
579 * // parent: null,
580 * // tokens: [{ compile: [Function] }],
581 * // blocks: {}
582 * // }
583 * tpl({ tacos: 'Tacos!!!!' });
584 * // => Tacos!!!!
585 *
586 * When compiling a source string, a file path should be specified in the options object in order for <var>extends</var>, <var>include</var>, and <var>import</var> to work properly. Do this by adding <code data-language="js">{ filename: '/absolute/path/to/mytpl.html' }</code> to the options argument.
587 *
588 * @param {string} source Swig template source string.
589 * @param {SwigOpts} [options={}] Swig options object.
590 * @return {function} Renderable function with keys for parent, blocks, and tokens.
591 */
592 26 this.compile = function (source, options) {
593 453 var key = options ? options.filename : null,
594 cached = key ? cacheGet(key, options) : null,
595 context,
596 contextLength,
597 pre;
598
599 453 if (cached) {
600 1 return cached;
601 }
602
603 452 context = getLocals(options);
604 452 contextLength = utils.keys(context).length;
605 452 pre = this.precompile(source, options);
606
607 366 function compiled(locals) {
608 5830 var lcls;
609 5830 if (locals && contextLength) {
610 1 lcls = utils.extend({}, context, locals);
611 5829 } else if (locals && !contextLength) {
612 5501 lcls = locals;
613 328 } else if (!locals && contextLength) {
614 281 lcls = context;
615 } else {
616 47 lcls = {};
617 }
618 5830 return pre.tpl(self, lcls, filters, utils, efn);
619 }
620
621 366 utils.extend(compiled, pre.tokens);
622
623 366 if (key) {
624 39 cacheSet(key, options, compiled);
625 }
626
627 366 return compiled;
628 };
629
630 /**
631 * Compile a source file into a renderable template function.
632 *
633 * @example
634 * var tpl = swig.compileFile('./mytpl.html');
635 * // => {
636 * // [Function: compiled]
637 * // parent: null,
638 * // tokens: [{ compile: [Function] }],
639 * // blocks: {}
640 * // }
641 * tpl({ tacos: 'Tacos!!!!' });
642 * // => Tacos!!!!
643 *
644 * @example
645 * swig.compileFile('/myfile.txt', { varControls: ['<%=', '=%>'], tagControls: ['<%', '%>']});
646 * // => will compile 'myfile.txt' using the var and tag controls as specified.
647 *
648 * @param {string} pathname File location.
649 * @param {SwigOpts} [options={}] Swig options object.
650 * @param {Function} [cb] Asyncronous callback function. If not provided, <var>compileFile</var> will run syncronously.
651 * @return {function} Renderable function with keys for parent, blocks, and tokens.
652 */
653 26 this.compileFile = function (pathname, options, cb) {
654 5510 var src, cached;
655
656 5510 if (!options) {
657 23 options = {};
658 }
659
660 5510 pathname = self.options.loader.resolve(pathname, options.resolveFrom);
661 5509 if (!options.filename) {
662 5509 options = utils.extend({ filename: pathname }, options);
663 }
664 5509 cached = cacheGet(pathname, options);
665
666 5509 if (cached) {
667 5471 if (cb) {
668 1 cb(null, cached);
669 1 return;
670 }
671 5470 return cached;
672 }
673
674 38 if (cb) {
675 7 self.options.loader.load(pathname, function (err, src) {
676 7 if (err) {
677 1 cb(err);
678 1 return;
679 }
680 6 var compiled;
681
682 6 try {
683 6 compiled = self.compile(src, options);
684 } catch (err2) {
685 1 cb(err2);
686 1 return;
687 }
688
689 5 cb(err, compiled);
690 });
691 7 return;
692 }
693
694 31 src = self.options.loader.load(pathname);
695 29 return self.compile(src, options);
696 };
697
698 /**
699 * Run a pre-compiled template function. This is most useful in the browser when you've pre-compiled your templates with the Swig command-line tool.
700 *
701 * @example
702 * $ swig compile ./mytpl.html --wrap-start="var mytpl = " > mytpl.js
703 * @example
704 * <script src="mytpl.js"></script>
705 * <script>
706 * swig.run(mytpl, {});
707 * // => "rendered template..."
708 * </script>
709 *
710 * @param {function} tpl Pre-compiled Swig template function. Use the Swig CLI to compile your templates.
711 * @param {object} [locals={}] Template variable context.
712 * @param {string} [filepath] Filename used for caching the template.
713 * @return {string} Rendered output.
714 */
715 26 this.run = function (tpl, locals, filepath) {
716 5 var context = getLocals({ locals: locals });
717 5 if (filepath) {
718 1 cacheSet(filepath, {}, tpl);
719 }
720 5 return tpl(self, context, filters, utils, efn);
721 };
722 };
723
724 /*!
725 * Export methods publicly
726 */
727 1 defaultInstance = new exports.Swig();
728 1 exports.setFilter = defaultInstance.setFilter;
729 1 exports.setTag = defaultInstance.setTag;
730 1 exports.setExtension = defaultInstance.setExtension;
731 1 exports.parseFile = defaultInstance.parseFile;
732 1 exports.precompile = defaultInstance.precompile;
733 1 exports.compile = defaultInstance.compile;
734 1 exports.compileFile = defaultInstance.compileFile;
735 1 exports.render = defaultInstance.render;
736 1 exports.renderFile = defaultInstance.renderFile;
737 1 exports.run = defaultInstance.run;
738 1 exports.invalidateCache = defaultInstance.invalidateCache;
739 1 exports.loaders = loaders;
740

/lib/tags/autoescape.js

100%
13
13
0
Line Hits Source
1 1 var 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 */
18 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
19 6 return compiler(content, parents, options, blockName);
20 };
21 1 exports.parse = function (str, line, parser, types, stack, opts) {
22 11 var matched;
23 11 parser.on('*', function (token) {
24 12 if (!matched &&
25 (token.type === types.BOOL ||
26 (token.type === types.STRING && strings.indexOf(token.match) === -1))
27 ) {
28 10 this.out.push(token.match);
29 10 matched = true;
30 10 return;
31 }
32 2 utils.throwError('Unexpected token "' + token.match + '" in autoescape tag', line, opts.filename);
33 });
34
35 11 return true;
36 };
37 1 exports.ends = true;
38

/lib/tags/block.js

100%
8
8
0
Line Hits Source
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 */
13 1 exports.compile = function (compiler, args, content, parents, options) {
14 24 return compiler(content, parents, options, args.join(''));
15 };
16
17 1 exports.parse = function (str, line, parser) {
18 42 parser.on('*', function (token) {
19 42 this.out.push(token.match);
20 });
21 42 return true;
22 };
23
24 1 exports.ends = true;
25 1 exports.block = true;
26

/lib/tags/else.js

100%
6
6
0
Line Hits Source
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 */
15 1 exports.compile = function () {
16 3 return '} else {\n';
17 };
18
19 1 exports.parse = function (str, line, parser, types, stack) {
20 5 parser.on('*', function (token) {
21 1 throw new Error('"else" tag does not accept any tokens. Found "' + token.match + '" on line ' + line + '.');
22 });
23
24 5 return (stack.length && stack[stack.length - 1].name === 'if');
25 };
26

/lib/tags/elseif.js

100%
6
6
0
Line Hits Source
1 1 var 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 */
21 1 exports.compile = function (compiler, args) {
22 5 return '} else if (' + args.join(' ') + ') {\n';
23 };
24
25 1 exports.parse = function (str, line, parser, types, stack) {
26 7 var okay = ifparser(str, line, parser, types, stack);
27 6 return okay && (stack.length && stack[stack.length - 1].name === 'if');
28 };
29

/lib/tags/extends.js

100%
4
4
0
Line Hits Source
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 */
13 1 exports.compile = function () { return; };
14
15 1 exports.parse = function () {
16 26 return true;
17 };
18
19 1 exports.ends = false;
20

/lib/tags/filter.js

100%
29
29
0
Line Hits Source
1 1 var 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
19 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
20 5 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
27 5 if (args[args.length - 1] === ')') {
28 4 args.pop();
29 }
30
31 5 args = (args.length) ? ', ' + args.join('') : '';
32 5 return '_output += _filters["' + filter + '"](' + val + args + ');\n';
33 };
34
35 1 exports.parse = function (str, line, parser, types) {
36 6 var filter;
37
38 6 function check(filter) {
39 6 if (!filters.hasOwnProperty(filter)) {
40 1 throw new Error('Filter "' + filter + '" does not exist on line ' + line + '.');
41 }
42 }
43
44 6 parser.on(types.FUNCTION, function (token) {
45 5 if (!filter) {
46 4 filter = token.match.replace(/\($/, '');
47 4 check(filter);
48 4 this.out.push(token.match);
49 4 this.state.push(token.type);
50 4 return;
51 }
52 1 return true;
53 });
54
55 6 parser.on(types.VAR, function (token) {
56 3 if (!filter) {
57 2 filter = token.match;
58 2 check(filter);
59 1 this.out.push(filter);
60 1 return;
61 }
62 1 return true;
63 });
64
65 6 return true;
66 };
67
68 1 exports.ends = true;
69

/lib/tags/for.js

100%
34
34
0
Line Hits Source
1 1 var ctx = '_ctx.',
2 ctxloop = ctx + 'loop';
3
4 /**
5 * Loop over objects and arrays.
6 *
7 * @alias for
8 *
9 * @example
10 * // obj = { one: 'hi', two: 'bye' };
11 * {% for x in obj %}
12 * {% if loop.first %}<ul>{% endif %}
13 * <li>{{ loop.index }} - {{ loop.key }}: {{ x }}</li>
14 * {% if loop.last %}</ul>{% endif %}
15 * {% endfor %}
16 * // => <ul>
17 * // <li>1 - one: hi</li>
18 * // <li>2 - two: bye</li>
19 * // </ul>
20 *
21 * @example
22 * // arr = [1, 2, 3]
23 * // Reverse the array, shortcut the key/index to `key`
24 * {% for key, val in arr|reverse %}
25 * {{ key }} -- {{ val }}
26 * {% endfor %}
27 * // => 0 -- 3
28 * // 1 -- 2
29 * // 2 -- 1
30 *
31 * @param {literal} [key] A shortcut to the index of the array or current key accessor.
32 * @param {literal} variable The current value will be assigned to this variable name temporarily. The variable will be reset upon ending the for tag.
33 * @param {literal} in Literally, "in". This token is required.
34 * @param {object} object An enumerable object that will be iterated over.
35 *
36 * @return {loop.index} The current iteration of the loop (1-indexed)
37 * @return {loop.index0} The current iteration of the loop (0-indexed)
38 * @return {loop.revindex} The number of iterations from the end of the loop (1-indexed)
39 * @return {loop.revindex0} The number of iterations from the end of the loop (0-indexed)
40 * @return {loop.key} If the iterator is an object, this will be the key of the current item, otherwise it will be the same as the loop.index.
41 * @return {loop.first} True if the current object is the first in the object or array.
42 * @return {loop.last} True if the current object is the last in the object or array.
43 */
44 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
45 30 var val = args.shift(),
46 key = '__k',
47 ctxloopcache = (ctx + '__loopcache' + Math.random()).replace(/\./g, ''),
48 last;
49
50 30 if (args[0] && args[0] === ',') {
51 5 args.shift();
52 5 key = val;
53 5 val = args.shift();
54 }
55
56 30 last = args.join('');
57
58 30 return [
59 '(function () {\n',
60 ' var __l = ' + last + ', __len = (_utils.isArray(__l) || typeof __l === "string") ? __l.length : _utils.keys(__l).length;\n',
61 ' if (!__l) { return; }\n',
62 ' var ' + ctxloopcache + ' = { loop: ' + ctxloop + ', ' + val + ': ' + ctx + val + ', ' + key + ': ' + ctx + key + ' };\n',
63 ' ' + ctxloop + ' = { first: false, index: 1, index0: 0, revindex: __len, revindex0: __len - 1, length: __len, last: false };\n',
64 ' _utils.each(__l, function (' + val + ', ' + key + ') {\n',
65 ' ' + ctx + val + ' = ' + val + ';\n',
66 ' ' + ctx + key + ' = ' + key + ';\n',
67 ' ' + ctxloop + '.key = ' + key + ';\n',
68 ' ' + ctxloop + '.first = (' + ctxloop + '.index0 === 0);\n',
69 ' ' + ctxloop + '.last = (' + ctxloop + '.revindex0 === 0);\n',
70 ' ' + compiler(content, parents, options, blockName),
71 ' ' + ctxloop + '.index += 1; ' + ctxloop + '.index0 += 1; ' + ctxloop + '.revindex -= 1; ' + ctxloop + '.revindex0 -= 1;\n',
72 ' });\n',
73 ' ' + ctxloop + ' = ' + ctxloopcache + '.loop;\n',
74 ' ' + ctx + val + ' = ' + ctxloopcache + '.' + val + ';\n',
75 ' ' + ctx + key + ' = ' + ctxloopcache + '.' + key + ';\n',
76 ' ' + ctxloopcache + ' = undefined;\n',
77 '})();\n'
78 ].join('');
79 };
80
81 1 exports.parse = function (str, line, parser, types) {
82 32 var firstVar, ready;
83
84 32 parser.on(types.NUMBER, function (token) {
85 4 var lastState = this.state.length ? this.state[this.state.length - 1] : null;
86 4 if (!ready ||
87 (lastState !== types.ARRAYOPEN &&
88 lastState !== types.CURLYOPEN &&
89 lastState !== types.CURLYCLOSE &&
90 lastState !== types.FUNCTION &&
91 lastState !== types.FILTER)
92 ) {
93 1 throw new Error('Unexpected number "' + token.match + '" on line ' + line + '.');
94 }
95 3 return true;
96 });
97
98 32 parser.on(types.VAR, function (token) {
99 65 if (ready && firstVar) {
100 28 return true;
101 }
102
103 37 if (!this.out.length) {
104 32 firstVar = true;
105 }
106
107 37 this.out.push(token.match);
108 });
109
110 32 parser.on(types.COMMA, function (token) {
111 7 if (firstVar && this.prevToken.type === types.VAR) {
112 5 this.out.push(token.match);
113 5 return;
114 }
115
116 2 return true;
117 });
118
119 32 parser.on(types.COMPARATOR, function (token) {
120 32 if (token.match !== 'in' || !firstVar) {
121 1 throw new Error('Unexpected token "' + token.match + '" on line ' + line + '.');
122 }
123 31 ready = true;
124 31 this.filterApplyIdx.push(this.out.length);
125 });
126
127 32 return true;
128 };
129
130 1 exports.ends = true;
131

/lib/tags/if.js

100%
25
25
0
Line Hits Source
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 */
42 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
43 52 return 'if (' + args.join(' ') + ') { \n' +
44 compiler(content, parents, options, blockName) + '\n' +
45 '}';
46 };
47
48 1 exports.parse = function (str, line, parser, types) {
49 68 if (str === undefined) {
50 2 throw new Error('No conditional statement provided on line ' + line + '.');
51 }
52
53 66 parser.on(types.COMPARATOR, function (token) {
54 24 if (this.isLast) {
55 1 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
56 }
57 23 if (this.prevToken.type === types.NOT) {
58 1 throw new Error('Attempted logic "not ' + token.match + '" on line ' + line + '. Use !(foo ' + token.match + ') instead.');
59 }
60 22 this.out.push(token.match);
61 22 this.filterApplyIdx.push(this.out.length);
62 });
63
64 66 parser.on(types.NOT, function (token) {
65 7 if (this.isLast) {
66 1 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
67 }
68 6 this.out.push(token.match);
69 });
70
71 66 parser.on(types.BOOL, function (token) {
72 20 this.out.push(token.match);
73 });
74
75 66 parser.on(types.LOGIC, function (token) {
76 6 if (!this.out.length || this.isLast) {
77 2 throw new Error('Unexpected logic "' + token.match + '" on line ' + line + '.');
78 }
79 4 this.out.push(token.match);
80 4 this.filterApplyIdx.pop();
81 });
82
83 66 return true;
84 };
85
86 1 exports.ends = true;
87

/lib/tags/import.js

100%
37
37
0
Line Hits Source
1 1 var utils = require('../utils');
2
3 /**
4 * Allows you to import macros from another file directly into your current context.
5 * The import tag is specifically designed for importing macros into your template with a specific context scope. This is very useful for keeping your macros from overriding template context that is being injected by your server-side page generation.
6 *
7 * @alias import
8 *
9 * @example
10 * {% import './formmacros.html' as form %}
11 * {{ form.input("text", "name") }}
12 * // => <input type="text" name="name">
13 *
14 * @example
15 * {% import "../shared/tags.html" as tags %}
16 * {{ tags.stylesheet('global') }}
17 * // => <link rel="stylesheet" href="/global.css">
18 *
19 * @param {string|var} file Relative path from the current template file to the file to import macros from.
20 * @param {literal} as Literally, "as".
21 * @param {literal} varname Local-accessible object name to assign the macros to.
22 */
23 1 exports.compile = function (compiler, args) {
24 2 var ctx = args.pop(),
25 allMacros = utils.map(args, function (arg) {
26 12 return arg.name;
27 }).join('|'),
28 out = '_ctx.' + ctx + ' = {};\n var _output = "";\n',
29 replacements = utils.map(args, function (arg) {
30 12 return {
31 ex: new RegExp('_ctx.' + arg.name + '(\\W)(?!' + allMacros + ')', 'g'),
32 re: '_ctx.' + ctx + '.' + arg.name + '$1'
33 };
34 });
35
36 // Replace all occurrences of all macros in this file with
37 // proper namespaced definitions and calls
38 2 utils.each(args, function (arg) {
39 12 var c = arg.compiled;
40 12 utils.each(replacements, function (re) {
41 72 c = c.replace(re.ex, re.re);
42 });
43 12 out += c;
44 });
45
46 2 return out;
47 };
48
49 1 exports.parse = function (str, line, parser, types, stack, opts, swig) {
50 5 var compiler = require('../parser').compile,
51 parseOpts = { resolveFrom: opts.filename },
52 compileOpts = utils.extend({}, opts, parseOpts),
53 tokens,
54 ctx;
55
56 5 parser.on(types.STRING, function (token) {
57 5 var self = this;
58 5 if (!tokens) {
59 4 tokens = swig.parseFile(token.match.replace(/^("|')|("|')$/g, ''), parseOpts).tokens;
60 4 utils.each(tokens, function (token) {
61 54 var out = '',
62 macroName;
63 54 if (!token || token.name !== 'macro' || !token.compile) {
64 36 return;
65 }
66 18 macroName = token.args[0];
67 18 out += token.compile(compiler, token.args, token.content, [], compileOpts) + '\n';
68 18 self.out.push({compiled: out, name: macroName});
69 });
70 4 return;
71 }
72
73 1 throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
74 });
75
76 5 parser.on(types.VAR, function (token) {
77 7 var self = this;
78 7 if (!tokens || ctx) {
79 1 throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
80 }
81
82 6 if (token.match === 'as') {
83 3 return;
84 }
85
86 3 ctx = token.match;
87 3 self.out.push(ctx);
88 3 return false;
89 });
90
91 5 return true;
92 };
93
94 1 exports.block = true;
95

/lib/tags/include.js

100%
35
35
0
Line Hits Source
1 1 var 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 */
31 1 exports.compile = function (compiler, args) {
32 12 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
39 12 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
48 1 exports.parse = function (str, line, parser, types, stack, opts) {
49 14 var file, w;
50 14 parser.on(types.STRING, function (token) {
51 17 if (!file) {
52 13 file = token.match;
53 13 this.out.push(file);
54 13 return;
55 }
56
57 4 return true;
58 });
59
60 14 parser.on(types.VAR, function (token) {
61 15 if (!file) {
62 1 file = token.match;
63 1 return true;
64 }
65
66 14 if (!w && token.match === 'with') {
67 2 w = true;
68 2 return;
69 }
70
71 12 if (w && token.match === only && this.prevToken.match !== 'with') {
72 1 this.out.push(token.match);
73 1 return;
74 }
75
76 11 if (token.match === ignore) {
77 3 return false;
78 }
79
80 8 if (token.match === missing) {
81 3 if (this.prevToken.match !== ignore) {
82 1 throw new Error('Unexpected token "' + missing + '" on line ' + line + '.');
83 }
84 2 this.out.push(token.match);
85 2 return false;
86 }
87
88 5 if (this.prevToken.match === ignore) {
89 1 throw new Error('Expected "' + missing + '" on line ' + line + ' but found "' + token.match + '".');
90 }
91
92 4 return true;
93 });
94
95 14 parser.on('end', function () {
96 12 this.out.push(opts.filename || null);
97 });
98
99 14 return true;
100 };
101

/lib/tags/index.js

100%
16
16
0
Line Hits Source
1 1 exports.autoescape = require('./autoescape');
2 1 exports.block = require('./block');
3 1 exports["else"] = require('./else');
4 1 exports.elseif = require('./elseif');
5 1 exports.elif = exports.elseif;
6 1 exports["extends"] = require('./extends');
7 1 exports.filter = require('./filter');
8 1 exports["for"] = require('./for');
9 1 exports["if"] = require('./if');
10 1 exports["import"] = require('./import');
11 1 exports.include = require('./include');
12 1 exports.macro = require('./macro');
13 1 exports.parent = require('./parent');
14 1 exports.raw = require('./raw');
15 1 exports.set = require('./set');
16 1 exports.spaceless = require('./spaceless');
17

/lib/tags/macro.js

100%
29
29
0
Line Hits Source
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 */
19 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
20 44 var fnName = args.shift();
21
22 44 return '_ctx.' + fnName + ' = function (' + args.join('') + ') {\n' +
23 ' var _output = "",\n' +
24 ' __ctx = _utils.extend({}, _ctx);\n' +
25 ' _utils.each(_ctx, function (v, k) {\n' +
26 ' if (["' + args.join('","') + '"].indexOf(k) !== -1) { delete _ctx[k]; }\n' +
27 ' });\n' +
28 compiler(content, parents, options, blockName) + '\n' +
29 ' _ctx = _utils.extend(_ctx, __ctx);\n' +
30 ' return _output;\n' +
31 '};\n' +
32 '_ctx.' + fnName + '.safe = true;\n';
33 };
34
35 1 exports.parse = function (str, line, parser, types) {
36 46 var name;
37
38 46 parser.on(types.VAR, function (token) {
39 27 if (token.match.indexOf('.') !== -1) {
40 1 throw new Error('Unexpected dot in macro argument "' + token.match + '" on line ' + line + '.');
41 }
42 26 this.out.push(token.match);
43 });
44
45 46 parser.on(types.FUNCTION, function (token) {
46 16 if (!name) {
47 16 name = token.match;
48 16 this.out.push(name);
49 16 this.state.push(types.FUNCTION);
50 }
51 });
52
53 46 parser.on(types.FUNCTIONEMPTY, function (token) {
54 27 if (!name) {
55 27 name = token.match;
56 27 this.out.push(name);
57 }
58 });
59
60 46 parser.on(types.PARENCLOSE, function () {
61 15 if (this.isLast) {
62 14 return;
63 }
64 1 throw new Error('Unexpected parenthesis close on line ' + line + '.');
65 });
66
67 46 parser.on(types.COMMA, function () {
68 8 return true;
69 });
70
71 46 parser.on('*', function () {
72 8 return;
73 });
74
75 46 return true;
76 };
77
78 1 exports.ends = true;
79 1 exports.block = true;
80

/lib/tags/parent.js

94%
17
16
1
Line Hits Source
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 */
16 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
17 5 if (!parents || !parents.length) {
18 1 return '';
19 }
20
21 4 var parentFile = args[0],
22 breaker = true,
23 l = parents.length,
24 i = 0,
25 parent,
26 block;
27
28 4 for (i; i < l; i += 1) {
29 5 parent = parents[i];
30 5 if (!parent.blocks || !parent.blocks.hasOwnProperty(blockName)) {
31 0 continue;
32 }
33 // Silly JSLint "Strange Loop" requires return to be in a conditional
34 5 if (breaker && parentFile !== parent.name) {
35 4 block = parent.blocks[blockName];
36 4 return block.compile(compiler, [blockName], block.content, parents.slice(i + 1), options) + '\n';
37 }
38 }
39 };
40
41 1 exports.parse = function (str, line, parser, types, stack, opts) {
42 8 parser.on('*', function (token) {
43 1 throw new Error('Unexpected argument "' + token.match + '" on line ' + line + '.');
44 });
45
46 8 parser.on('end', function () {
47 7 this.out.push(opts.filename);
48 });
49
50 8 return true;
51 };
52

/lib/tags/raw.js

100%
7
7
0
Line Hits Source
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 */
14 1 exports.compile = function (compiler, args, content, parents, options, blockName) {
15 4 return compiler(content, parents, options, blockName);
16 };
17 1 exports.parse = function (str, line, parser) {
18 5 parser.on('*', function (token) {
19 1 throw new Error('Unexpected token "' + token.match + '" in raw tag on line ' + line + '.');
20 });
21 5 return true;
22 };
23 1 exports.ends = true;
24

/lib/tags/set.js

100%
41
41
0
Line Hits Source
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 */
34 1 exports.compile = function (compiler, args) {
35 41 return args.join(' ') + ';\n';
36 };
37
38 1 exports.parse = function (str, line, parser, types) {
39 44 var nameSet = '',
40 propertyName;
41
42 44 parser.on(types.VAR, function (token) {
43 50 if (propertyName) {
44 // Tell the parser where to find the variable
45 1 propertyName += '_ctx.' + token.match;
46 1 return;
47 }
48
49 49 if (!parser.out.length) {
50 42 nameSet += token.match;
51 42 return;
52 }
53
54 7 return true;
55 });
56
57 44 parser.on(types.BRACKETOPEN, function (token) {
58 9 if (!propertyName && !this.out.length) {
59 8 propertyName = token.match;
60 8 return;
61 }
62
63 1 return true;
64 });
65
66 44 parser.on(types.STRING, function (token) {
67 34 if (propertyName && !this.out.length) {
68 7 propertyName += token.match;
69 7 return;
70 }
71
72 27 return true;
73 });
74
75 44 parser.on(types.BRACKETCLOSE, function (token) {
76 9 if (propertyName && !this.out.length) {
77 8 nameSet += propertyName + token.match;
78 8 propertyName = undefined;
79 8 return;
80 }
81
82 1 return true;
83 });
84
85 44 parser.on(types.DOTKEY, function (token) {
86 2 if (!propertyName && !nameSet) {
87 1 return true;
88 }
89 1 nameSet += '.' + token.match;
90 1 return;
91 });
92
93 44 parser.on(types.ASSIGNMENT, function (token) {
94 44 if (this.out.length || !nameSet) {
95 2 throw new Error('Unexpected assignment "' + token.match + '" on line ' + line + '.');
96 }
97
98 42 this.out.push(
99 // Prevent the set from spilling into global scope
100 '_ctx.' + nameSet
101 );
102 42 this.out.push(token.match);
103 42 this.filterApplyIdx.push(this.out.length);
104 });
105
106 44 return true;
107 };
108
109 1 exports.block = true;
110

/lib/tags/spaceless.js

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

/lib/utils.js

81%
65
53
12
Line Hits Source
1 1 var isArray;
2
3 /**
4 * Strip leading and trailing whitespace from a string.
5 * @param {string} input
6 * @return {string} Stripped input.
7 */
8 1 exports.strip = function (input) {
9 685 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 */
18 1 exports.startsWith = function (str, prefix) {
19 3055 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 */
28 1 exports.endsWith = function (str, suffix) {
29 1260 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 */
38 1 exports.each = function (obj, fn) {
39 6051 var i, l;
40
41 6051 if (isArray(obj)) {
42 5991 i = 0;
43 5991 l = obj.length;
44 5991 for (i; i < l; i += 1) {
45 15008 if (fn(obj[i], i, obj) === false) {
46 0 break;
47 }
48 }
49 } else {
50 60 for (i in obj) {
51 153 if (obj.hasOwnProperty(i)) {
52 153 if (fn(obj[i], i, obj) === false) {
53 0 break;
54 }
55 }
56 }
57 }
58
59 5904 return obj;
60 };
61
62 /**
63 * Test if an object is an Array.
64 * @param {object} obj
65 * @return {boolean}
66 */
67 1 exports.isArray = isArray = (Array.hasOwnProperty('isArray')) ? Array.isArray : function (obj) {
68 0 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 */
77 1 exports.some = function (obj, fn) {
78 22108 var i = 0,
79 result,
80 l;
81 22108 if (isArray(obj)) {
82 22108 l = obj.length;
83
84 22108 for (i; i < l; i += 1) {
85 47932 result = fn(obj[i], i, obj);
86 47932 if (result) {
87 4224 break;
88 }
89 }
90 } else {
91 0 exports.each(obj, function (value, index) {
92 0 result = fn(value, index, obj);
93 0 return !result;
94 });
95 }
96 22108 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 */
105 1 exports.map = function (obj, fn) {
106 79 var i = 0,
107 result = [],
108 l;
109
110 79 if (isArray(obj)) {
111 79 l = obj.length;
112 79 for (i; i < l; i += 1) {
113 197 result[i] = fn(obj[i], i);
114 }
115 } else {
116 0 for (i in obj) {
117 0 if (obj.hasOwnProperty(i)) {
118 0 result[i] = fn(obj[i], i);
119 }
120 }
121 }
122 79 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 */
130 1 exports.extend = function () {
131 7620 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
139 7620 for (i; i < l; i += 1) {
140 8723 obj = objs[i] || {};
141 8723 for (key in obj) {
142 16968 if (obj.hasOwnProperty(key)) {
143 16968 target[key] = obj[key];
144 }
145 }
146 }
147 7620 return target;
148 };
149
150 /**
151 * Get all of the keys on an object.
152 * @param {object} obj
153 * @return {array}
154 */
155 1 exports.keys = function (obj) {
156 470 if (!obj) {
157 0 return [];
158 }
159
160 470 if (Object.keys) {
161 470 return Object.keys(obj);
162 }
163
164 0 return exports.map(obj, function (v, k) {
165 0 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 */
176 1 exports.throwError = function (message, line, file) {
177 47 if (line) {
178 45 message += ' on line ' + line;
179 }
180 47 if (file) {
181 29 message += ' in file ' + file;
182 }
183 47 throw new Error(message + '.');
184 };
185