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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |