1 | var parser = require('./parser'),
|
2 | _ = require('underscore'),
|
3 | filters = require('./filters');
|
4 |
|
5 |
|
6 | var KEYWORDS = /^(Array|ArrayBuffer|Boolean|Date|Error|eval|EvalError|Function|Infinity|Iterator|JSON|Math|Namespace|NaN|Number|Object|QName|RangeError|ReferenceError|RegExp|StopIteration|String|SyntaxError|TypeError|undefined|uneval|URIError|XML|XMLList|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)(?=(\.|$))/;
|
7 |
|
8 |
|
9 | exports.isStringLiteral = function (string) {
|
10 | if (typeof string !== 'string') {
|
11 | return false;
|
12 | }
|
13 |
|
14 | var first = string.substring(0, 1),
|
15 | last = string.charAt(string.length - 1, 1),
|
16 | teststr;
|
17 |
|
18 | if ((first === last) && (first === "'" || first === '"')) {
|
19 | teststr = string.substr(1, string.length - 2).split('').reverse().join('');
|
20 |
|
21 | if ((first === "'" && (/'(?!\\)/).test(teststr)) || (last === '"' && (/"(?!\\)/).test(teststr))) {
|
22 | throw new Error('Invalid string literal. Unescaped quote (' + string[0] + ') found.');
|
23 | }
|
24 |
|
25 | return true;
|
26 | }
|
27 |
|
28 | return false;
|
29 | };
|
30 |
|
31 |
|
32 | exports.isLiteral = function (string) {
|
33 | var literal = false;
|
34 |
|
35 |
|
36 | if ((/^\d+([.]\d+)?$/).test(string)) {
|
37 | literal = true;
|
38 | } else if (exports.isStringLiteral(string)) {
|
39 | literal = true;
|
40 | }
|
41 |
|
42 | return literal;
|
43 | };
|
44 |
|
45 |
|
46 | exports.isValidName = function (string) {
|
47 | return ((typeof string === 'string')
|
48 | && string.substr(0, 2) !== '__'
|
49 | && (/^([$A-Za-z_]+[$A-Za-z_0-9]*)(\.?([$A-Za-z_]+[$A-Za-z_0-9]*))*$/).test(string)
|
50 | && !KEYWORDS.test(string));
|
51 | };
|
52 |
|
53 |
|
54 | exports.isValidShortName = function (string) {
|
55 | return string.substr(0, 2) !== '__' && (/^[$A-Za-z_]+[$A-Za-z_0-9]*$/).test(string) && !KEYWORDS.test(string);
|
56 | };
|
57 |
|
58 |
|
59 | exports.isValidBlockName = function (string) {
|
60 | return (/^[A-Za-z]+[A-Za-z_0-9]*$/).test(string);
|
61 | };
|
62 |
|
63 | function stripWhitespace(input) {
|
64 | return input.replace(/^\s+|\s+$/g, '');
|
65 | }
|
66 | exports.stripWhitespace = stripWhitespace;
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | function filterVariablePath(props) {
|
72 |
|
73 | var filtered = [],
|
74 | literal = '',
|
75 | i = 0;
|
76 | for (i; i < props.length; i += 1) {
|
77 | if (props[i] && props[i].charAt(0) !== props[i].charAt(props[i].length - 1) &&
|
78 | (props[i].indexOf('"') === 0 || props[i].indexOf("'") === 0)) {
|
79 | literal = props[i];
|
80 | continue;
|
81 | }
|
82 | if (props[i] === '.' && literal) {
|
83 | literal += '.';
|
84 | continue;
|
85 | }
|
86 | if (props[i].indexOf('"') === props[i].length - 1 || props[i].indexOf("'") === props[i].length - 1) {
|
87 | literal += props[i];
|
88 | filtered.push(literal);
|
89 | literal = '';
|
90 | } else {
|
91 | filtered.push(props[i]);
|
92 | }
|
93 | }
|
94 | return _.compact(filtered);
|
95 | }
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 | function check(variable, context) {
|
106 | if (_.isArray(variable)) {
|
107 | return '(true)';
|
108 | }
|
109 |
|
110 | variable = variable.replace(/^this/, '_this.__currentContext');
|
111 |
|
112 | if (exports.isLiteral(variable)) {
|
113 | return '(true)';
|
114 | }
|
115 |
|
116 | var props = variable.split(/(\.|\[|\])/),
|
117 | chain = '',
|
118 | output = [],
|
119 | inArr = false,
|
120 | prevDot = false;
|
121 |
|
122 | if (typeof context === 'string' && context.length) {
|
123 | props.unshift(context);
|
124 | }
|
125 |
|
126 | props = _.reject(props, function (val) {
|
127 | return val === '';
|
128 | });
|
129 |
|
130 | props = filterVariablePath(props);
|
131 |
|
132 | _.each(props, function (prop) {
|
133 | if (prop === '.') {
|
134 | prevDot = true;
|
135 | return;
|
136 | }
|
137 |
|
138 | if (prop === '[') {
|
139 | inArr = true;
|
140 | return;
|
141 | }
|
142 |
|
143 | if (prop === ']') {
|
144 | inArr = false;
|
145 | return;
|
146 | }
|
147 |
|
148 | if (!chain) {
|
149 | chain = prop;
|
150 | } else if (inArr) {
|
151 | if (!exports.isStringLiteral(prop)) {
|
152 | if (prevDot) {
|
153 | output[output.length - 1] = _.last(output).replace(/\] !== "undefined"$/, '_' + prop + '] !== "undefined"');
|
154 | chain = chain.replace(/\]$/, '_' + prop + ']');
|
155 | return;
|
156 | }
|
157 | chain += '[___' + prop + ']';
|
158 | } else {
|
159 | chain += '[' + prop + ']';
|
160 | }
|
161 | } else {
|
162 | chain += '.' + prop;
|
163 | }
|
164 | prevDot = false;
|
165 | output.push('typeof ' + chain + ' !== "undefined"');
|
166 | });
|
167 |
|
168 | return '(' + output.join(' && ') + ')';
|
169 | }
|
170 | exports.check = check;
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 | exports.escapeVarName = function (variable, context) {
|
177 | if (_.isArray(variable)) {
|
178 | _.each(variable, function (val, key) {
|
179 | variable[key] = exports.escapeVarName(val, context);
|
180 | });
|
181 | return variable;
|
182 | }
|
183 |
|
184 | variable = variable.replace(/^this/, '_this.__currentContext');
|
185 |
|
186 | if (exports.isLiteral(variable)) {
|
187 | return variable;
|
188 | }
|
189 | if (typeof context === 'string' && context.length) {
|
190 | variable = context + '.' + variable;
|
191 | }
|
192 |
|
193 | var chain = '',
|
194 | props = variable.split(/(\.|\[|\])/),
|
195 | inArr = false,
|
196 | prevDot = false;
|
197 |
|
198 | props = _.reject(props, function (val) {
|
199 | return val === '';
|
200 | });
|
201 |
|
202 | props = filterVariablePath(props);
|
203 |
|
204 | _.each(props, function (prop) {
|
205 | if (prop === '.') {
|
206 | prevDot = true;
|
207 | return;
|
208 | }
|
209 |
|
210 | if (prop === '[') {
|
211 | inArr = true;
|
212 | return;
|
213 | }
|
214 |
|
215 | if (prop === ']') {
|
216 | inArr = false;
|
217 | return;
|
218 | }
|
219 |
|
220 | if (!chain) {
|
221 | chain = prop;
|
222 | } else if (inArr) {
|
223 | if (!exports.isStringLiteral(prop)) {
|
224 | if (prevDot) {
|
225 | chain = chain.replace(/\]$/, '_' + prop + ']');
|
226 | } else {
|
227 | chain += '[___' + prop + ']';
|
228 | }
|
229 | } else {
|
230 | chain += '[' + prop + ']';
|
231 | }
|
232 | } else {
|
233 | chain += '.' + prop;
|
234 | }
|
235 | prevDot = false;
|
236 | });
|
237 |
|
238 | return chain;
|
239 | };
|
240 |
|
241 | exports.wrapMethod = function (variable, filter, context) {
|
242 | var output = '(function () {\n',
|
243 | args;
|
244 |
|
245 | variable = variable || '""';
|
246 |
|
247 | if (!filter) {
|
248 | return variable;
|
249 | }
|
250 |
|
251 | args = filter.args.split(',');
|
252 | args = _.map(args, function (value) {
|
253 | var varname,
|
254 | stripped = value.replace(/^\s+|\s+$/g, '');
|
255 |
|
256 | try {
|
257 | varname = '__' + parser.parseVariable(stripped).name.replace(/\W/g, '_');
|
258 | } catch (e) {
|
259 | return value;
|
260 | }
|
261 |
|
262 | if (exports.isValidName(stripped)) {
|
263 | output += exports.setVar(varname, parser.parseVariable(stripped));
|
264 | return varname;
|
265 | }
|
266 |
|
267 | return value;
|
268 | });
|
269 |
|
270 | args = (args && args.length) ? args.join(',') : '""';
|
271 | output += 'return ';
|
272 | output += (context) ? context + '["' : '';
|
273 | output += filter.name;
|
274 | output += (context) ? '"]' : '';
|
275 | output += '.call(this';
|
276 | output += (args.length) ? ', ' + args : '';
|
277 | output += ');\n';
|
278 |
|
279 | return output + '})()';
|
280 | };
|
281 |
|
282 | exports.wrapFilter = function (variable, filter) {
|
283 | var output = '',
|
284 | args = '';
|
285 |
|
286 | variable = variable || '""';
|
287 |
|
288 | if (!filter) {
|
289 | return variable;
|
290 | }
|
291 |
|
292 | if (filters.hasOwnProperty(filter.name)) {
|
293 | args = (filter.args) ? variable + ', ' + filter.args : variable;
|
294 | output += exports.wrapMethod(variable, { name: filter.name, args: args }, '_filters');
|
295 | } else {
|
296 | throw new Error('Filter "' + filter.name + '" not found');
|
297 | }
|
298 |
|
299 | return output;
|
300 | };
|
301 |
|
302 | exports.wrapFilters = function (variable, filters, context, escape) {
|
303 | var output = exports.escapeVarName(variable, context);
|
304 |
|
305 | if (filters && filters.length > 0) {
|
306 | _.each(filters, function (filter) {
|
307 | switch (filter.name) {
|
308 | case 'raw':
|
309 | escape = false;
|
310 | return;
|
311 | case 'e':
|
312 | case 'escape':
|
313 | escape = filter.args || escape;
|
314 | return;
|
315 | default:
|
316 | output = exports.wrapFilter(output, filter, '_filters');
|
317 | break;
|
318 | }
|
319 | });
|
320 | }
|
321 |
|
322 | output = output || '""';
|
323 | if (escape) {
|
324 | output = '_filters.escape.call(this, ' + output + ', ' + escape + ')';
|
325 | }
|
326 |
|
327 | return output;
|
328 | };
|
329 |
|
330 | exports.setVar = function (varName, argument) {
|
331 | var out = '',
|
332 | props,
|
333 | output,
|
334 | inArr;
|
335 | if ((/\[/).test(argument.name)) {
|
336 | props = argument.name.split(/(\[|\])/);
|
337 | output = [];
|
338 | inArr = false;
|
339 |
|
340 | _.each(props, function (prop) {
|
341 | if (prop === '') {
|
342 | return;
|
343 | }
|
344 |
|
345 | if (prop === '[') {
|
346 | inArr = true;
|
347 | return;
|
348 | }
|
349 |
|
350 | if (prop === ']') {
|
351 | inArr = false;
|
352 | return;
|
353 | }
|
354 |
|
355 | if (inArr && !exports.isStringLiteral(prop)) {
|
356 | out += exports.setVar('___' + prop.replace(/\W/g, '_'), { name: prop, filters: [], escape: true });
|
357 | }
|
358 | });
|
359 | }
|
360 | out += 'var ' + varName + ' = "";\n' +
|
361 | 'if (' + check(argument.name, '_context') + ') {\n' +
|
362 | ' ' + varName + ' = ' + exports.wrapFilters(argument.name, argument.filters, '_context', argument.escape) + ';\n' +
|
363 | '} else if (' + check(argument.name) + ') {\n' +
|
364 | ' ' + varName + ' = ' + exports.wrapFilters(argument.name, argument.filters, null, argument.escape) + ';\n' +
|
365 | '}\n';
|
366 |
|
367 | if (argument.filters.length) {
|
368 | out += ' else if (true) {\n';
|
369 | out += ' ' + varName + ' = ' + exports.wrapFilters('', argument.filters, null, argument.escape) + ';\n';
|
370 | out += '}\n';
|
371 | }
|
372 |
|
373 | return out;
|
374 | };
|
375 |
|
376 | exports.parseIfArgs = function (args, parser) {
|
377 | var operators = ['==', '<', '>', '!=', '<=', '>=', '===', '!==', '&&', '||', 'in', 'and', 'or'],
|
378 | errorString = 'Bad if-syntax in `{% if ' + args.join(' ') + ' %}...',
|
379 | tokens = [],
|
380 | prevType,
|
381 | last,
|
382 | closing = 0;
|
383 |
|
384 | _.each(args, function (value, index) {
|
385 | var endsep = false,
|
386 | operand;
|
387 |
|
388 | if ((/^\(/).test(value)) {
|
389 | closing += 1;
|
390 | value = value.substr(1);
|
391 | tokens.push({ type: 'separator', value: '(' });
|
392 | }
|
393 |
|
394 | if ((/^\![^=]/).test(value) || (value === 'not')) {
|
395 | if (value === 'not') {
|
396 | value = '';
|
397 | } else {
|
398 | value = value.substr(1);
|
399 | }
|
400 | tokens.push({ type: 'operator', value: '!' });
|
401 | }
|
402 |
|
403 | if ((/\)$/).test(value)) {
|
404 | if (!closing) {
|
405 | throw new Error(errorString);
|
406 | }
|
407 | value = value.replace(/\)$/, '');
|
408 | endsep = true;
|
409 | closing -= 1;
|
410 | }
|
411 |
|
412 | if (value === 'in') {
|
413 | last = tokens.pop();
|
414 | prevType = 'inindex';
|
415 | } else if (_.indexOf(operators, value) !== -1) {
|
416 | if (prevType === 'operator') {
|
417 | throw new Error(errorString);
|
418 | }
|
419 | value = value.replace('and', '&&').replace('or', '||');
|
420 | tokens.push({
|
421 | value: value
|
422 | });
|
423 | prevType = 'operator';
|
424 | } else if (value !== '') {
|
425 | if (prevType === 'value') {
|
426 | throw new Error(errorString);
|
427 | }
|
428 | operand = parser.parseVariable(value);
|
429 |
|
430 | if (prevType === 'inindex') {
|
431 | tokens.push({
|
432 | preout: last.preout + exports.setVar('__op' + index, operand),
|
433 | value: '(((_.isArray(__op' + index + ') || typeof __op' + index + ' === "string") && _.indexOf(__op' + index + ', ' + last.value + ') !== -1) || (typeof __op' + index + ' === "object" && ' + last.value + ' in __op' + index + '))'
|
434 | });
|
435 | last = null;
|
436 | } else {
|
437 | tokens.push({
|
438 | preout: exports.setVar('__op' + index, operand),
|
439 | value: '__op' + index
|
440 | });
|
441 | }
|
442 | prevType = 'value';
|
443 | }
|
444 |
|
445 | if (endsep) {
|
446 | tokens.push({ type: 'separator', value: ')' });
|
447 | }
|
448 | });
|
449 |
|
450 | if (closing > 0) {
|
451 | throw new Error(errorString);
|
452 | }
|
453 |
|
454 | return tokens;
|
455 | };
|