1 | (function() {
|
2 | var expect, lib, nodes, parser;
|
3 |
|
4 | if(typeof require != 'undefined') {
|
5 | expect = require('expect.js');
|
6 | lib = require('../src/lib');
|
7 | nodes = require('../src/nodes');
|
8 | parser = require('../src/parser');
|
9 | }
|
10 | else {
|
11 | expect = window.expect;
|
12 | lib = nunjucks.require('lib');
|
13 | nodes = nunjucks.require('nodes');
|
14 | parser = nunjucks.require('parser');
|
15 | }
|
16 |
|
17 | function _isAST(node1, node2) {
|
18 |
|
19 |
|
20 |
|
21 | expect(node1.typename).to.be(node2.typename);
|
22 |
|
23 | var children1 = (node1.children && node1.children.length) || 'null';
|
24 | var children2 = (node2.children && node2.children.length) || 'null';
|
25 |
|
26 | if(node2 instanceof nodes.NodeList) {
|
27 | var lit = ': num-children: ';
|
28 | var sig2 = (node2.typename + lit + node2.children.length);
|
29 |
|
30 | expect(node1.children).to.be.ok();
|
31 | var sig1 = (node1.typename + lit + node1.children.length);
|
32 |
|
33 | expect(sig1).to.be(sig2);
|
34 |
|
35 | for(var i=0, l=node2.children.length; i<l; i++) {
|
36 | _isAST(node1.children[i], node2.children[i]);
|
37 | }
|
38 | }
|
39 | else {
|
40 | node2.iterFields(function(value, field) {
|
41 | var ofield = node1[field];
|
42 |
|
43 | if(value instanceof nodes.Node) {
|
44 | _isAST(ofield, value);
|
45 | }
|
46 | else if(lib.isArray(ofield) && lib.isArray(value)) {
|
47 | expect('num-children: ' + ofield.length).to.be('num-children: ' + value.length);
|
48 |
|
49 | lib.each(ofield, function(v, i) {
|
50 | if(ofield[i] instanceof nodes.Node) {
|
51 | _isAST(ofield[i], value[i]);
|
52 | }
|
53 | else if(ofield[i] !== null && value[i] !== null) {
|
54 | expect(ofield[i]).to.be(value[i]);
|
55 | }
|
56 | });
|
57 | }
|
58 | else if((ofield !== null || value !== null) &&
|
59 | (ofield !== undefined || value !== undefined)) {
|
60 | if(ofield === null) {
|
61 | throw new Error(value + ' expected for "' + field +
|
62 | '", null found');
|
63 | }
|
64 |
|
65 | if(value === null) {
|
66 | throw new Error(ofield + ' expected to be null for "' +
|
67 | field + '"');
|
68 | }
|
69 |
|
70 |
|
71 |
|
72 | if(!ofield) {
|
73 | expect(value).to.be(ofield);
|
74 | }
|
75 | else {
|
76 | expect(ofield).to.be(value);
|
77 | }
|
78 | }
|
79 | });
|
80 | }
|
81 | }
|
82 |
|
83 | function isAST(node1, ast) {
|
84 |
|
85 |
|
86 | return _isAST(node1, toNodes(ast));
|
87 | }
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | function toNodes(ast) {
|
93 | if(!(ast && lib.isArray(ast))) {
|
94 | return ast;
|
95 | }
|
96 |
|
97 | var type = ast[0];
|
98 | var F = function() {};
|
99 | F.prototype = type.prototype;
|
100 |
|
101 | var dummy = new F();
|
102 |
|
103 | if(dummy instanceof nodes.NodeList) {
|
104 | return new type(0, 0, lib.map(ast.slice(1), toNodes));
|
105 | }
|
106 | else if(dummy instanceof nodes.CallExtension) {
|
107 | return new type(ast[1], ast[2], ast[3] ? toNodes(ast[3]) : ast[3],
|
108 | lib.isArray(ast[4]) ? lib.map(ast[4], toNodes) : ast[4]);
|
109 | }
|
110 | else {
|
111 | return new type(0, 0,
|
112 | toNodes(ast[1]),
|
113 | toNodes(ast[2]),
|
114 | toNodes(ast[3]),
|
115 | toNodes(ast[4]),
|
116 | toNodes(ast[5]));
|
117 | }
|
118 | }
|
119 |
|
120 | describe('parser', function() {
|
121 | it('should parse basic types', function() {
|
122 | isAST(parser.parse('{{ 1 }}'),
|
123 | [nodes.Root,
|
124 | [nodes.Output,
|
125 | [nodes.Literal, 1]]]);
|
126 |
|
127 | isAST(parser.parse('{{ 4.567 }}'),
|
128 | [nodes.Root,
|
129 | [nodes.Output,
|
130 | [nodes.Literal, 4.567]]]);
|
131 |
|
132 | isAST(parser.parse('{{ "foo" }}'),
|
133 | [nodes.Root,
|
134 | [nodes.Output,
|
135 | [nodes.Literal, 'foo']]]);
|
136 |
|
137 | isAST(parser.parse("{{ 'foo' }}"),
|
138 | [nodes.Root,
|
139 | [nodes.Output,
|
140 | [nodes.Literal, 'foo']]]);
|
141 |
|
142 | isAST(parser.parse("{{ true }}"),
|
143 | [nodes.Root,
|
144 | [nodes.Output,
|
145 | [nodes.Literal, true]]]);
|
146 |
|
147 | isAST(parser.parse("{{ false }}"),
|
148 | [nodes.Root,
|
149 | [nodes.Output,
|
150 | [nodes.Literal, false]]]);
|
151 |
|
152 | isAST(parser.parse("{{ foo }}"),
|
153 | [nodes.Root,
|
154 | [nodes.Output,
|
155 | [nodes.Symbol, 'foo']]]);
|
156 | });
|
157 |
|
158 | it('should parse aggregate types', function() {
|
159 | isAST(parser.parse("{{ [1,2,3] }}"),
|
160 | [nodes.Root,
|
161 | [nodes.Output,
|
162 | [nodes.Array,
|
163 | [nodes.Literal, 1],
|
164 | [nodes.Literal, 2],
|
165 | [nodes.Literal, 3]]]]);
|
166 |
|
167 | isAST(parser.parse("{{ (1,2,3) }}"),
|
168 | [nodes.Root,
|
169 | [nodes.Output,
|
170 | [nodes.Group,
|
171 | [nodes.Literal, 1],
|
172 | [nodes.Literal, 2],
|
173 | [nodes.Literal, 3]]]]);
|
174 |
|
175 | isAST(parser.parse("{{ {foo: 1, 'two': 2} }}"),
|
176 | [nodes.Root,
|
177 | [nodes.Output,
|
178 | [nodes.Dict,
|
179 | [nodes.Pair,
|
180 | [nodes.Symbol, 'foo'],
|
181 | [nodes.Literal, 1]],
|
182 | [nodes.Pair,
|
183 | [nodes.Literal, 'two'],
|
184 | [nodes.Literal, 2]]]]]);
|
185 | });
|
186 |
|
187 | it('should parse variables', function() {
|
188 | isAST(parser.parse('hello {{ foo }}, how are you'),
|
189 | [nodes.Root,
|
190 | [nodes.Output, [nodes.TemplateData, 'hello ']],
|
191 | [nodes.Output, [nodes.Symbol, 'foo']],
|
192 | [nodes.Output, [nodes.TemplateData, ', how are you']]]);
|
193 | });
|
194 |
|
195 | it('should parse blocks', function() {
|
196 | var n = parser.parse('want some {% if hungry %}pizza{% else %}' +
|
197 | 'water{% endif %}?');
|
198 | expect(n.children[1].typename).to.be('If');
|
199 |
|
200 | n = parser.parse('{% block foo %}stuff{% endblock %}');
|
201 | expect(n.children[0].typename).to.be('Block');
|
202 |
|
203 | n = parser.parse('{% extends "test.html" %}stuff');
|
204 | expect(n.children[0].typename).to.be('Extends');
|
205 |
|
206 | n = parser.parse('{% include "test.html" %}');
|
207 | expect(n.children[0].typename).to.be('Include');
|
208 | });
|
209 |
|
210 | it('should parse filters', function() {
|
211 | isAST(parser.parse('{{ foo | bar }}'),
|
212 | [nodes.Root,
|
213 | [nodes.Output,
|
214 | [nodes.Filter,
|
215 | [nodes.Symbol, 'bar'],
|
216 | [nodes.NodeList,
|
217 | [nodes.Symbol, 'foo']]]]]);
|
218 |
|
219 | isAST(parser.parse('{{ foo | bar | baz }}'),
|
220 | [nodes.Root,
|
221 | [nodes.Output,
|
222 | [nodes.Filter,
|
223 | [nodes.Symbol, 'baz'],
|
224 | [nodes.NodeList,
|
225 | [nodes.Filter,
|
226 | [nodes.Symbol, 'bar'],
|
227 | [nodes.NodeList,
|
228 | [nodes.Symbol, 'foo']]]]]]]);
|
229 |
|
230 | isAST(parser.parse('{{ foo | bar(3) }}'),
|
231 | [nodes.Root,
|
232 | [nodes.Output,
|
233 | [nodes.Filter,
|
234 | [nodes.Symbol, 'bar'],
|
235 | [nodes.NodeList,
|
236 | [nodes.Symbol, 'foo'],
|
237 | [nodes.Literal, 3]]]]]);
|
238 | });
|
239 |
|
240 | it('should parse macro definitions', function() {
|
241 | var ast = parser.parse('{% macro foo(bar, baz="foobar") %}' +
|
242 | 'This is a macro' +
|
243 | '{% endmacro %}');
|
244 | isAST(ast,
|
245 | [nodes.Root,
|
246 | [nodes.Macro,
|
247 | [nodes.Symbol, 'foo'],
|
248 | [nodes.NodeList,
|
249 | [nodes.Symbol, 'bar'],
|
250 | [nodes.KeywordArgs,
|
251 | [nodes.Pair,
|
252 | [nodes.Symbol, 'baz'], [nodes.Literal, 'foobar']]]],
|
253 | [nodes.NodeList,
|
254 | [nodes.Output,
|
255 | [nodes.TemplateData, 'This is a macro']]]]]);
|
256 | });
|
257 |
|
258 | it('should parse raw', function() {
|
259 | isAST(parser.parse('{% raw %}hello {{ {% %} }}{% endraw %}'),
|
260 | [nodes.Root,
|
261 | [nodes.Output,
|
262 | [nodes.TemplateData, 'hello {{ {% %} }}']]]);
|
263 | });
|
264 |
|
265 | it('should parse keyword and non-keyword arguments', function() {
|
266 | isAST(parser.parse('{{ foo("bar", falalalala, baz="foobar") }}'),
|
267 | [nodes.Root,
|
268 | [nodes.Output,
|
269 | [nodes.FunCall,
|
270 | [nodes.Symbol, 'foo'],
|
271 | [nodes.NodeList,
|
272 | [nodes.Literal, 'bar'],
|
273 | [nodes.Symbol, 'falalalala'],
|
274 | [nodes.KeywordArgs,
|
275 | [nodes.Pair,
|
276 | [nodes.Symbol, 'baz'],
|
277 | [nodes.Literal, 'foobar']]]]]]]);
|
278 | });
|
279 |
|
280 | it('should parse imports', function() {
|
281 | isAST(parser.parse('{% import "foo/bar.html" as baz %}'),
|
282 | [nodes.Root,
|
283 | [nodes.Import,
|
284 | [nodes.Literal, 'foo/bar.html'],
|
285 | [nodes.Symbol, 'baz']]]);
|
286 |
|
287 | isAST(parser.parse('{% from "foo/bar.html" import baz, ' +
|
288 | ' foobar as foobarbaz %}'),
|
289 | [nodes.Root,
|
290 | [nodes.FromImport,
|
291 | [nodes.Literal, "foo/bar.html"],
|
292 | [nodes.NodeList,
|
293 | [nodes.Symbol, 'baz'],
|
294 | [nodes.Pair,
|
295 | [nodes.Symbol, 'foobar'],
|
296 | [nodes.Symbol, 'foobarbaz']]]]]);
|
297 | });
|
298 |
|
299 | it('should parse whitespace control', function() {
|
300 |
|
301 |
|
302 |
|
303 | isAST(parser.parse('{% if x %}\n hi \n{% endif %}'),
|
304 | [nodes.Root,
|
305 | [nodes.If,
|
306 | [nodes.Symbol, 'x'],
|
307 | [nodes.NodeList,
|
308 | [nodes.Output,
|
309 | [nodes.TemplateData, '\n hi \n']]]]]);
|
310 |
|
311 | isAST(parser.parse('{% if x -%}\n hi \n{% endif %}'),
|
312 | [nodes.Root,
|
313 | [nodes.If,
|
314 | [nodes.Symbol, 'x'],
|
315 | [nodes.NodeList,
|
316 | [nodes.Output,
|
317 | [nodes.TemplateData, 'hi \n']]]]]);
|
318 |
|
319 | isAST(parser.parse('{% if x %}\n hi \n{%- endif %}'),
|
320 | [nodes.Root,
|
321 | [nodes.If,
|
322 | [nodes.Symbol, 'x'],
|
323 | [nodes.NodeList,
|
324 | [nodes.Output,
|
325 | [nodes.TemplateData, '\n hi']]]]]);
|
326 |
|
327 | isAST(parser.parse('{% if x -%}\n hi \n{%- endif %}'),
|
328 | [nodes.Root,
|
329 | [nodes.If,
|
330 | [nodes.Symbol, 'x'],
|
331 | [nodes.NodeList,
|
332 | [nodes.Output,
|
333 | [nodes.TemplateData, 'hi']]]]]);
|
334 |
|
335 | isAST(parser.parse('poop \n{%- if x -%}\n hi \n{%- endif %}'),
|
336 | [nodes.Root,
|
337 | [nodes.Output,
|
338 | [nodes.TemplateData, 'poop']],
|
339 | [nodes.If,
|
340 | [nodes.Symbol, 'x'],
|
341 | [nodes.NodeList,
|
342 | [nodes.Output,
|
343 | [nodes.TemplateData, 'hi']]]]]);
|
344 |
|
345 |
|
346 |
|
347 | isAST(parser.parse('{% from x import y %}\n hi \n'),
|
348 | [nodes.Root,
|
349 | [nodes.FromImport,
|
350 | [nodes.Symbol, 'x'],
|
351 | [nodes.NodeList,
|
352 | [nodes.Symbol, 'y']]],
|
353 | [nodes.Output,
|
354 | [nodes.TemplateData, '\n hi \n']]]);
|
355 |
|
356 | isAST(parser.parse('{% from x import y -%}\n hi \n'),
|
357 | [nodes.Root,
|
358 | [nodes.FromImport,
|
359 | [nodes.Symbol, 'x'],
|
360 | [nodes.NodeList,
|
361 | [nodes.Symbol, 'y']]],
|
362 | [nodes.Output,
|
363 | [nodes.TemplateData, 'hi \n']]]);
|
364 | });
|
365 |
|
366 | it('should throw errors', function() {
|
367 | expect(function() {
|
368 | parser.parse('hello {{ foo');
|
369 | }).to.throwException(/expected variable end/);
|
370 |
|
371 | expect(function() {
|
372 | parser.parse('hello {% if');
|
373 | }).to.throwException(/expected expression/);
|
374 |
|
375 | expect(function() {
|
376 | parser.parse('hello {% if sdf zxc');
|
377 | }).to.throwException(/expected block end/);
|
378 |
|
379 | expect(function() {
|
380 | parser.parse('{% include "foo %}');
|
381 | }).to.throwException(/expected block end/);
|
382 |
|
383 | expect(function() {
|
384 | parser.parse('hello {% if sdf %} data');
|
385 | }).to.throwException(/expected endif, else, or endif/);
|
386 |
|
387 | expect(function() {
|
388 | parser.parse('hello {% block sdf %} data');
|
389 | }).to.throwException(/expected endblock/);
|
390 |
|
391 | expect(function() {
|
392 | parser.parse('hello {% bar %} dsfsdf');
|
393 | }).to.throwException(/unknown block tag/);
|
394 |
|
395 | expect(function() {
|
396 | parser.parse('{{ foo(bar baz) }}');
|
397 | }).to.throwException(/expected comma after expression/);
|
398 |
|
399 | expect(function() {
|
400 | parser.parse('{% import "foo" %}');
|
401 | }).to.throwException(/expected "as" keyword/);
|
402 |
|
403 | expect(function() {
|
404 | parser.parse('{% from "foo" %}');
|
405 | }).to.throwException(/expected import/);
|
406 |
|
407 | expect(function() {
|
408 | parser.parse('{% from "foo" import bar baz %}');
|
409 | }).to.throwException(/expected comma/);
|
410 |
|
411 | expect(function() {
|
412 | parser.parse('{% from "foo" import _bar %}');
|
413 | }).to.throwException(/names starting with an underscore cannot be imported/);
|
414 | });
|
415 |
|
416 | it('should parse custom tags', function() {
|
417 |
|
418 | function testtagExtension() {
|
419 | this.tags = ['testtag'];
|
420 |
|
421 |
|
422 | this._name = 'testtagExtension';
|
423 |
|
424 | this.parse = function(parser, nodes) {
|
425 | var begun = parser.peekToken();
|
426 | parser.advanceAfterBlockEnd();
|
427 | return new nodes.CallExtension(this, 'foo');
|
428 | };
|
429 | }
|
430 |
|
431 | function testblocktagExtension() {
|
432 | this.tags = ['testblocktag'];
|
433 | this._name = 'testblocktagExtension';
|
434 |
|
435 | this.parse = function(parser, nodes) {
|
436 | var begun = parser.peekToken();
|
437 | parser.advanceAfterBlockEnd();
|
438 |
|
439 | var content = parser.parseUntilBlocks('endtestblocktag');
|
440 | var tag = new nodes.CallExtension(this, 'bar', null, [1, content]);
|
441 | parser.advanceAfterBlockEnd();
|
442 |
|
443 | return tag;
|
444 | };
|
445 | }
|
446 |
|
447 | function testargsExtension() {
|
448 | this.tags = ['testargs'];
|
449 | this._name = 'testargsExtension';
|
450 |
|
451 | this.parse = function(parser, nodes, tokens) {
|
452 | var begun = parser.peekToken();
|
453 | var args = null;
|
454 |
|
455 |
|
456 | parser.nextToken();
|
457 |
|
458 | args = parser.parseSignature(true);
|
459 | parser.advanceAfterBlockEnd(begun.value);
|
460 |
|
461 | return new nodes.CallExtension(this, 'biz', args);
|
462 | };
|
463 | }
|
464 |
|
465 | var extensions = [new testtagExtension(),
|
466 | new testblocktagExtension(),
|
467 | new testargsExtension()];
|
468 |
|
469 | isAST(parser.parse('{% testtag %}', extensions),
|
470 | [nodes.Root,
|
471 | [nodes.CallExtension, extensions[0], 'foo', undefined, undefined]]);
|
472 |
|
473 | isAST(parser.parse('{% testblocktag %}sdfd{% endtestblocktag %}',
|
474 | extensions),
|
475 | [nodes.Root,
|
476 | [nodes.CallExtension, extensions[1], 'bar', null,
|
477 | [1, [nodes.NodeList,
|
478 | [nodes.Output,
|
479 | [nodes.TemplateData, "sdfd"]]]]]]);
|
480 |
|
481 | isAST(parser.parse('{% testblocktag %}{{ 123 }}{% endtestblocktag %}',
|
482 | extensions),
|
483 | [nodes.Root,
|
484 | [nodes.CallExtension, extensions[1], 'bar', null,
|
485 | [1, [nodes.NodeList,
|
486 | [nodes.Output,
|
487 | [nodes.Literal, 123]]]]]]);
|
488 |
|
489 | isAST(parser.parse('{% testargs(123, "abc", foo="bar") %}', extensions),
|
490 | [nodes.Root,
|
491 | [nodes.CallExtension, extensions[2], 'biz',
|
492 |
|
493 |
|
494 |
|
495 | [nodes.NodeList,
|
496 | [nodes.Literal, 123],
|
497 | [nodes.Literal, "abc"],
|
498 | [nodes.KeywordArgs,
|
499 | [nodes.Pair,
|
500 | [nodes.Symbol, "foo"],
|
501 | [nodes.Literal, "bar"]]]]]]);
|
502 |
|
503 | isAST(parser.parse('{% testargs %}', extensions),
|
504 | [nodes.Root,
|
505 | [nodes.CallExtension, extensions[2], 'biz', null]]);
|
506 | });
|
507 | });
|
508 | })();
|