UNPKG

14.5 kBJavaScriptView Raw
1var helpers = require('./helpers'),
2 filters = require('./filters'),
3 _ = require('underscore');
4
5var variableRegexp = /^\{\{[^\r]*?\}\}$/,
6 logicRegexp = /^\{%[^\r]*?%\}$/,
7 commentRegexp = /^\{#[^\r]*?#\}$/,
8
9 TEMPLATE = exports.TEMPLATE = 0,
10 LOGIC_TOKEN = 1,
11 VAR_TOKEN = 2;
12
13exports.TOKEN_TYPES = {
14 TEMPLATE: TEMPLATE,
15 LOGIC: LOGIC_TOKEN,
16 VAR: VAR_TOKEN
17};
18
19function getMethod(input) {
20 return helpers.stripWhitespace(input).match(/^[\w\.]+/)[0];
21}
22
23function doubleEscape(input) {
24 return input.replace(/\\/g, '\\\\');
25}
26
27function getArgs(input) {
28 return doubleEscape(helpers.stripWhitespace(input).replace(/^[\w\.]+\(|\)$/g, ''));
29}
30
31function getContextVar(varName, context) {
32 var a = varName.split(".");
33 while (a.length) {
34 context = context[a.splice(0, 1)[0]];
35 }
36 return context;
37}
38
39function getTokenArgs(token, parts) {
40 parts = _.map(parts, doubleEscape);
41
42 var i = 0,
43 l = parts.length,
44 arg,
45 ender,
46 out = [];
47
48 function concat(from, ending) {
49 var end = new RegExp('\\' + ending + '$'),
50 i = from,
51 out = '';
52
53 while (!(end).test(out) && i < parts.length) {
54 out += ' ' + parts[i];
55 parts[i] = null;
56 i += 1;
57 }
58
59 if (!end.test(out)) {
60 throw new Error('Malformed arguments ' + out + ' sent to tag.');
61 }
62
63 return out.replace(/^ /, '');
64 }
65
66 for (i; i < l; i += 1) {
67 arg = parts[i];
68 if (arg === null || (/^\s+$/).test(arg)) {
69 continue;
70 }
71
72 if (
73 ((/^\"/).test(arg) && !(/\"[\]\}]?$/).test(arg))
74 || ((/^\'/).test(arg) && !(/\'[\]\}]?$/).test(arg))
75 || ((/^\{/).test(arg) && !(/\}$/).test(arg))
76 || ((/^\[/).test(arg) && !(/\]$/).test(arg))
77 ) {
78 switch (arg.substr(0, 1)) {
79 case "'":
80 ender = "'";
81 break;
82 case '"':
83 ender = '"';
84 break;
85 case '[':
86 ender = ']';
87 break;
88 case '{':
89 ender = '}';
90 break;
91 }
92 out.push(concat(i, ender));
93 continue;
94 }
95
96 out.push(arg);
97 }
98
99 return out;
100}
101
102function findSubBlocks(topToken, blocks) {
103 _.each(topToken.tokens, function (token, index) {
104 if (token.name === 'block') {
105 blocks[token.args[0]] = token;
106 findSubBlocks(token, blocks);
107 }
108 });
109}
110
111function getParentBlock(token) {
112 var block;
113
114 if (token.parentBlock) {
115 block = token.parentBlock;
116 } else if (token.parent) {
117 block = getParentBlock(_.last(token.parent));
118 }
119
120 return block;
121}
122
123exports.parseVariable = function (token, escape) {
124 if (!token) {
125 return {
126 type: null,
127 name: '',
128 filters: [],
129 escape: escape
130 };
131 }
132
133 var filters = [],
134 parts = token.replace(/^\{\{\s*|\s*\}\}$/g, '').split('|'),
135 varname = parts.shift(),
136 args = null,
137 part;
138
139 if ((/\(/).test(varname)) {
140 args = getArgs(varname.replace(/^\w+\./, ''));
141 varname = getMethod(varname);
142 }
143
144 _.each(parts, function (part, i) {
145 if (part && ((/^[\w\.]+\(/).test(part) || (/\)$/).test(part)) && !(/^[\w\.]+\([^\)]*\)$/).test(part)) {
146 parts[i] += ((parts[i + 1]) ? '|' + parts[i + 1] : '');
147 parts[i + 1] = false;
148 }
149 });
150 parts = _.without(parts, false);
151
152 _.each(parts, function (part) {
153 var filter_name = getMethod(part);
154 if ((/\(/).test(part)) {
155 filters.push({
156 name: filter_name,
157 args: getArgs(part)
158 });
159 } else {
160 filters.push({ name: filter_name, args: '' });
161 }
162 });
163
164 return {
165 type: VAR_TOKEN,
166 name: varname,
167 args: args,
168 filters: filters,
169 escape: escape
170 };
171};
172
173exports.parse = function (data, tags, autoescape) {
174 var rawtokens = helpers.stripWhitespace(data).split(/(\{%[^\r]*?%\}|\{\{.*?\}\}|\{#[^\r]*?#\})/),
175 escape = !!autoescape,
176 last_escape = escape,
177 stack = [[]],
178 index = 0,
179 i = 0,
180 j = rawtokens.length,
181 token,
182 parts,
183 tagname,
184 lines = 1,
185 curline = 1,
186 newlines = null,
187 lastToken,
188 rawStart = /^\{\% *raw *\%\}/,
189 rawEnd = /\{\% *endraw *\%\}$/,
190 inRaw = false,
191 stripAfter = false,
192 stripBefore = false,
193 stripStart = false,
194 stripEnd = false;
195
196 for (i; i < j; i += 1) {
197 token = rawtokens[i];
198 curline = lines;
199 newlines = token.match(/\n/g);
200 stripAfter = false;
201 stripBefore = false;
202 stripStart = false;
203 stripEnd = false;
204
205 if (newlines) {
206 lines += newlines.length;
207 }
208
209 if (inRaw !== false && !rawEnd.test(token)) {
210 inRaw += token;
211 continue;
212 }
213
214 // Ignore empty strings and comments
215 if (token.length === 0 || commentRegexp.test(token)) {
216 continue;
217 } else if (/^\s+$/.test(token)) {
218 token = token.replace(/ +/, ' ').replace(/\n+/, '\n');
219 } else if (variableRegexp.test(token)) {
220 token = exports.parseVariable(token, escape);
221 } else if (logicRegexp.test(token)) {
222 if (rawEnd.test(token)) {
223 // Don't care about the content in a raw tag, so end tag may not start correctly
224 token = inRaw + token.replace(rawEnd, '');
225 inRaw = false;
226 stack[index].push(token);
227 continue;
228 }
229
230 if (rawStart.test(token)) {
231 // Have to check the whole token directly, not just parts, as the tag may not end correctly while in raw
232 inRaw = token.replace(rawStart, '');
233 continue;
234 }
235
236 parts = token.replace(/^\{%\s*|\s*%\}$/g, '').split(' ');
237 if (parts[0] === '-') {
238 stripBefore = true;
239 parts.shift();
240 }
241 tagname = parts.shift();
242 if (_.last(parts) === '-') {
243 stripAfter = true;
244 parts.pop();
245 }
246
247 if (index > 0 && (/^end/).test(tagname)) {
248 lastToken = _.last(stack[stack.length - 2]);
249 if ('end' + lastToken.name === tagname) {
250 if (lastToken.name === 'autoescape') {
251 escape = last_escape;
252 }
253 lastToken.strip.end = stripBefore;
254 lastToken.strip.after = stripAfter;
255 stack.pop();
256 index -= 1;
257 continue;
258 }
259
260 throw new Error('Expected end tag for "' + lastToken.name + '", but found "' + tagname + '" at line ' + lines + '.');
261 }
262
263 if (!tags.hasOwnProperty(tagname)) {
264 throw new Error('Unknown logic tag at line ' + lines + ': "' + tagname + '".');
265 }
266
267 if (tagname === 'autoescape') {
268 last_escape = escape;
269 escape = (!parts.length || parts[0] === 'true') ? ((parts.length >= 2) ? parts[1] : true) : false;
270 }
271
272 token = {
273 type: LOGIC_TOKEN,
274 line: curline,
275 name: tagname,
276 compile: tags[tagname],
277 parent: _.uniq(stack[stack.length - 2] || []),
278 strip: {
279 before: stripBefore,
280 after: stripAfter,
281 start: false,
282 end: false
283 }
284 };
285 token.args = getTokenArgs(token, parts);
286
287 if (tags[tagname].ends) {
288 token.strip.after = false;
289 token.strip.start = stripAfter;
290 stack[index].push(token);
291 stack.push(token.tokens = []);
292 index += 1;
293 continue;
294 }
295 }
296
297 // Everything else is treated as a string
298 stack[index].push(token);
299 }
300
301 if (inRaw !== false) {
302 throw new Error('Missing expected end tag for "raw" on line ' + curline + '.');
303 }
304
305 if (index !== 0) {
306 lastToken = _.last(stack[stack.length - 2]);
307 throw new Error('Missing end tag for "' + lastToken.name + '" that was opened on line ' + lastToken.line + '.');
308 }
309
310 return stack[index];
311};
312
313function precompile(indent, context) {
314 var filepath,
315 extendsHasVar,
316 preservedTokens = [];
317
318 // Precompile - extract blocks and create hierarchy based on 'extends' tags
319 // TODO: make block and extends tags accept context variables
320
321 // Only precompile at the template level
322 if (this.type === TEMPLATE) {
323
324 _.each(this.tokens, function (token, index) {
325
326 if (!extendsHasVar) {
327 // Load the parent template
328 if (token.name === 'extends') {
329 filepath = token.args[0];
330
331 if (!helpers.isStringLiteral(filepath)) {
332
333 if (!context) {
334 extendsHasVar = true;
335 return;
336 }
337 filepath = "\"" + getContextVar(filepath, context) + "\"";
338 }
339
340 if (!helpers.isStringLiteral(filepath) || token.args.length > 1) {
341 throw new Error('Extends tag on line ' + token.line + ' accepts exactly one string literal as an argument.');
342 }
343 if (index > 0) {
344 throw new Error('Extends tag must be the first tag in the template, but "extends" found on line ' + token.line + '.');
345 }
346 token.template = this.compileFile(filepath.replace(/['"]/g, ''), true);
347 this.parent = token.template;
348
349 // inherit tokens/blocks from parent.
350 this.blocks = _.extend({}, this.parent.blocks, this.blocks);
351
352 } else if (token.name === 'block') { // Make a list of blocks
353 var blockname = token.args[0],
354 parentBlockIndex;
355
356 if (!helpers.isValidBlockName(blockname) || token.args.length !== 1) {
357 throw new Error('Invalid block tag name "' + blockname + '" on line ' + token.line + '.');
358 }
359
360 // store blocks as flat reference list on top-level
361 // template object
362 this.blocks[blockname] = token;
363
364 // child tokens may contain more blocks at this template
365 // level - apply to flat this.blocks object
366 findSubBlocks(token, this.blocks);
367
368 // search parent list for a matching block, replacing the
369 // parent template block tokens with the derived token.
370 if (this.parent) {
371
372 // Store parent token object on a derived block
373 token.parentBlock = this.parent.blocks[blockname];
374
375 // this will return -1 for a nested block
376 parentBlockIndex = _.indexOf(this.parent.tokens,
377 this.parent.blocks[blockname]);
378 if (parentBlockIndex >= 0) {
379 this.parent.tokens[parentBlockIndex] = token;
380 }
381
382 }
383 } else if (token.type === LOGIC_TOKEN) {
384 // Preserve any template logic from the extended template.
385 preservedTokens.push(token);
386 }
387 // else, discard any tokens that are not under a LOGIC_TOKEN
388 // or VAR_TOKEN (for example, static strings).
389
390 }
391 }, this);
392
393
394 // If extendsHasVar == true, then we know {% extends %} is not using a string literal, thus we can't
395 // compile until render is called, so we return false.
396 if (extendsHasVar) {
397 return false;
398 }
399
400 if (this.parent && this.parent.tokens) {
401 this.tokens = preservedTokens.concat(this.parent.tokens);
402 }
403 }
404}
405
406exports.compile = function compile(indent, context, template) {
407 var code = '',
408 wrappedInMethod,
409 blockname,
410 parentBlock;
411
412 indent = indent || '';
413
414 // Template parameter is optional (not used at the top-level), initialize
415 if (this.type === TEMPLATE) {
416 template = this;
417 }
418
419 // Initialize blocks
420 if (!this.blocks) {
421 this.blocks = {};
422 }
423
424 // Precompile step - process block inheritence into true token hierarchy
425 if (precompile.call(this, indent, context) === false) {
426 return false;
427 }
428
429 // If this is not a template then just iterate through its tokens
430 _.each(this.tokens, function (token, index) {
431 var name, key, args, prev, next;
432 if (typeof token === 'string') {
433 prev = this.tokens[index - 1];
434 next = this.tokens[index + 1];
435 if (prev && prev.strip && prev.strip.after) {
436 token = token.replace(/^\s+/, '');
437 }
438 if (next && next.strip && next.strip.before) {
439 token = token.replace(/\s+$/, '');
440 }
441 code += '_output += "' + doubleEscape(token).replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/"/g, '\\"') + '";\n';
442 return code;
443 }
444
445 if (typeof token !== 'object') {
446 return; // Tokens can be either strings or objects
447 }
448
449 if (token.type === VAR_TOKEN) {
450 name = token.name.replace(/\W/g, '_');
451 key = (helpers.isLiteral(name)) ? '["' + name + '"]' : '.' + name;
452 args = (token.args && token.args.length) ? token.args : '';
453
454 code += 'if (typeof _context !== "undefined" && typeof _context' + key + ' === "function") {\n';
455 wrappedInMethod = helpers.wrapMethod('', { name: name, args: args }, '_context');
456 code += ' _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
457 if (helpers.isValidName(name)) {
458 code += '} else if (typeof ' + name + ' === "function") {\n';
459 wrappedInMethod = helpers.wrapMethod('', { name: name, args: args });
460 code += ' _output = (typeof _output === "undefined") ? ' + wrappedInMethod + ': _output + ' + wrappedInMethod + ';\n';
461 }
462 code += '} else {\n';
463 code += helpers.setVar('__' + name, token);
464 code += ' _output = (typeof _output === "undefined") ? __' + name + ': _output + __' + name + ';\n';
465 code += '}\n';
466 }
467
468 if (token.type !== LOGIC_TOKEN) {
469 return; // Tokens can be either VAR_TOKEN or LOGIC_TOKEN
470 }
471
472 if (token.name === 'block') {
473 blockname = token.args[0];
474
475 // Sanity check - the template should be in the flat block list.
476 if (!template.blocks.hasOwnProperty(blockname)) {
477 throw new Error('Unrecognized nested block. Block \"' + blockname +
478 '\" at line ' + token.line + ' of \"' + template.id +
479 '\" is not in template block list.');
480 }
481
482 code += compile.call(template.blocks[token.args[0]], indent + ' ', context, template);
483 } else if (token.name === 'parent') {
484 parentBlock = getParentBlock(token);
485 if (!parentBlock) {
486 throw new Error('No parent block found for parent tag at line ' +
487 token.line + '.');
488 }
489
490 code += compile.call(parentBlock, indent + ' ', context);
491 } else if (token.hasOwnProperty("compile")) {
492 if (token.strip.start && token.tokens.length && typeof token.tokens[0] === 'string') {
493 token.tokens[0] = token.tokens[0].replace(/^\s+/, '');
494 }
495 if (token.strip.end && token.tokens.length && typeof _.last(token.tokens) === 'string') {
496 token.tokens[token.tokens.length - 1] = _.last(token.tokens).replace(/\s+$/, '');
497 }
498 code += token.compile(indent + ' ', exports);
499 } else {
500 code += compile.call(token, indent + ' ', context);
501 }
502
503 }, this);
504
505 return code;
506};
507