UNPKG

11.8 kBJavaScriptView Raw
1var parser = require('./parser'),
2 _ = require('underscore'),
3 filters = require('./filters');
4
5// Javascript keywords can't be a name: 'for.is_invalid' as well as 'for' but not 'for_' or '_for'
6var 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// Returns TRUE if the passed string is a valid javascript string literal
9exports.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// Returns TRUE if the passed string is a valid javascript number or string literal
32exports.isLiteral = function (string) {
33 var literal = false;
34
35 // Check if it's a number literal
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// Variable names starting with __ are reserved.
46exports.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// Variable names starting with __ are reserved.
54exports.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// Checks if a name is a vlaid block name
59exports.isValidBlockName = function (string) {
60 return (/^[A-Za-z]+[A-Za-z_0-9]*$/).test(string);
61};
62
63function stripWhitespace(input) {
64 return input.replace(/^\s+|\s+$/g, '');
65}
66exports.stripWhitespace = stripWhitespace;
67
68// the varname is split on (/(\.|\[|\])/) but it may contain keys with dots,
69// e.g. obj['hello.there']
70// this function searches for these and preserves the literal parts
71function 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* Returns a valid javascript code that will
99* check if a variable (or property chain) exists
100* in the evaled context. For example:
101* check('foo.bar.baz')
102* will return the following string:
103* typeof foo !== 'undefined' && typeof foo.bar !== 'undefined' && typeof foo.bar.baz !== 'undefined'
104*/
105function 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}
170exports.check = check;
171
172/**
173* Returns an escaped string (safe for evaling). If context is passed
174* then returns a concatenation of context and the escaped variable name.
175*/
176exports.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
241exports.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
282exports.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
302exports.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
330exports.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
376exports.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};