1 | var helpers = require('./helpers'),
|
2 | filters = require('./filters'),
|
3 | _ = require('underscore');
|
4 |
|
5 | var variableRegexp = /^\{\{[^\r]*?\}\}$/,
|
6 | logicRegexp = /^\{%[^\r]*?%\}$/,
|
7 | commentRegexp = /^\{#[^\r]*?#\}$/,
|
8 |
|
9 | TEMPLATE = exports.TEMPLATE = 0,
|
10 | LOGIC_TOKEN = 1,
|
11 | VAR_TOKEN = 2;
|
12 |
|
13 | exports.TOKEN_TYPES = {
|
14 | TEMPLATE: TEMPLATE,
|
15 | LOGIC: LOGIC_TOKEN,
|
16 | VAR: VAR_TOKEN
|
17 | };
|
18 |
|
19 | function getMethod(input) {
|
20 | return helpers.stripWhitespace(input).match(/^[\w\.]+/)[0];
|
21 | }
|
22 |
|
23 | function doubleEscape(input) {
|
24 | return input.replace(/\\/g, '\\\\');
|
25 | }
|
26 |
|
27 | function getArgs(input) {
|
28 | return doubleEscape(helpers.stripWhitespace(input).replace(/^[\w\.]+\(|\)$/g, ''));
|
29 | }
|
30 |
|
31 | function 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 |
|
39 | function 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 |
|
102 | function 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 |
|
111 | function 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 |
|
123 | exports.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 |
|
173 | exports.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 |
|
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 |
|
224 | token = inRaw + token.replace(rawEnd, '');
|
225 | inRaw = false;
|
226 | stack[index].push(token);
|
227 | continue;
|
228 | }
|
229 |
|
230 | if (rawStart.test(token)) {
|
231 |
|
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 |
|
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 |
|
313 | function precompile(indent, context) {
|
314 | var filepath,
|
315 | extendsHasVar,
|
316 | preservedTokens = [];
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 | if (this.type === TEMPLATE) {
|
323 |
|
324 | _.each(this.tokens, function (token, index) {
|
325 |
|
326 | if (!extendsHasVar) {
|
327 |
|
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 |
|
350 | this.blocks = _.extend({}, this.parent.blocks, this.blocks);
|
351 |
|
352 | } else if (token.name === 'block') {
|
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 |
|
361 |
|
362 | this.blocks[blockname] = token;
|
363 |
|
364 |
|
365 |
|
366 | findSubBlocks(token, this.blocks);
|
367 |
|
368 |
|
369 |
|
370 | if (this.parent) {
|
371 |
|
372 |
|
373 | token.parentBlock = this.parent.blocks[blockname];
|
374 |
|
375 |
|
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 |
|
385 | preservedTokens.push(token);
|
386 | }
|
387 |
|
388 |
|
389 |
|
390 | }
|
391 | }, this);
|
392 |
|
393 |
|
394 |
|
395 |
|
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 |
|
406 | exports.compile = function compile(indent, context, template) {
|
407 | var code = '',
|
408 | wrappedInMethod,
|
409 | blockname,
|
410 | parentBlock;
|
411 |
|
412 | indent = indent || '';
|
413 |
|
414 |
|
415 | if (this.type === TEMPLATE) {
|
416 | template = this;
|
417 | }
|
418 |
|
419 |
|
420 | if (!this.blocks) {
|
421 | this.blocks = {};
|
422 | }
|
423 |
|
424 |
|
425 | if (precompile.call(this, indent, context) === false) {
|
426 | return false;
|
427 | }
|
428 |
|
429 |
|
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;
|
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;
|
470 | }
|
471 |
|
472 | if (token.name === 'block') {
|
473 | blockname = token.args[0];
|
474 |
|
475 |
|
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 |
|