UNPKG

19.8 kBJavaScriptView Raw
1'use strict';
2
3var Lexer = require('./lexer');
4var nodes = require('./nodes');
5var utils = require('./utils');
6var filters = require('./filters');
7var path = require('path');
8var constantinople = require('constantinople');
9var parseJSExpression = require('character-parser').parseMax;
10var extname = path.extname;
11
12/**
13 * Initialize `Parser` with the given input `str` and `filename`.
14 *
15 * @param {String} str
16 * @param {String} filename
17 * @param {Object} options
18 * @api public
19 */
20
21var Parser = exports = module.exports = function Parser(str, filename, options){
22 //Strip any UTF-8 BOM off of the start of `str`, if it exists.
23 this.input = str.replace(/^\uFEFF/, '');
24 this.lexer = new Lexer(this.input, filename);
25 this.filename = filename;
26 this.blocks = {};
27 this.mixins = {};
28 this.options = options;
29 this.contexts = [this];
30 this.inMixin = 0;
31 this.dependencies = [];
32 this.inBlock = 0;
33};
34
35/**
36 * Parser prototype.
37 */
38
39Parser.prototype = {
40
41 /**
42 * Save original constructor
43 */
44
45 constructor: Parser,
46
47 /**
48 * Push `parser` onto the context stack,
49 * or pop and return a `Parser`.
50 */
51
52 context: function(parser){
53 if (parser) {
54 this.contexts.push(parser);
55 } else {
56 return this.contexts.pop();
57 }
58 },
59
60 /**
61 * Return the next token object.
62 *
63 * @return {Object}
64 * @api private
65 */
66
67 advance: function(){
68 return this.lexer.advance();
69 },
70
71 /**
72 * Single token lookahead.
73 *
74 * @return {Object}
75 * @api private
76 */
77
78 peek: function() {
79 return this.lookahead(1);
80 },
81
82 /**
83 * Return lexer lineno.
84 *
85 * @return {Number}
86 * @api private
87 */
88
89 line: function() {
90 return this.lexer.lineno;
91 },
92
93 /**
94 * `n` token lookahead.
95 *
96 * @param {Number} n
97 * @return {Object}
98 * @api private
99 */
100
101 lookahead: function(n){
102 return this.lexer.lookahead(n);
103 },
104
105 /**
106 * Parse input returning a string of js for evaluation.
107 *
108 * @return {String}
109 * @api public
110 */
111
112 parse: function(){
113 var block = new nodes.Block, parser;
114 block.line = 0;
115 block.filename = this.filename;
116
117 while ('eos' != this.peek().type) {
118 if ('newline' == this.peek().type) {
119 this.advance();
120 } else {
121 var next = this.peek();
122 var expr = this.parseExpr();
123 expr.filename = expr.filename || this.filename;
124 expr.line = next.line;
125 block.push(expr);
126 }
127 }
128
129 if (parser = this.extending) {
130 this.context(parser);
131 var ast = parser.parse();
132 this.context();
133
134 // hoist mixins
135 for (var name in this.mixins)
136 ast.unshift(this.mixins[name]);
137 return ast;
138 }
139
140 if (!this.extending && !this.included && Object.keys(this.blocks).length){
141 var blocks = [];
142 utils.walkAST(block, function (node) {
143 if (node.type === 'Block' && node.name) {
144 blocks.push(node.name);
145 }
146 });
147 Object.keys(this.blocks).forEach(function (name) {
148 if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
149 console.warn('Warning: Unexpected block "'
150 + name
151 + '" '
152 + ' on line '
153 + this.blocks[name].line
154 + ' of '
155 + (this.blocks[name].filename)
156 + '. This block is never used. This warning will be an error in v2.0.0');
157 }
158 }.bind(this));
159 }
160
161 return block;
162 },
163
164 /**
165 * Expect the given type, or throw an exception.
166 *
167 * @param {String} type
168 * @api private
169 */
170
171 expect: function(type){
172 if (this.peek().type === type) {
173 return this.advance();
174 } else {
175 throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
176 }
177 },
178
179 /**
180 * Accept the given `type`.
181 *
182 * @param {String} type
183 * @api private
184 */
185
186 accept: function(type){
187 if (this.peek().type === type) {
188 return this.advance();
189 }
190 },
191
192 /**
193 * tag
194 * | doctype
195 * | mixin
196 * | include
197 * | filter
198 * | comment
199 * | text
200 * | each
201 * | code
202 * | yield
203 * | id
204 * | class
205 * | interpolation
206 */
207
208 parseExpr: function(){
209 switch (this.peek().type) {
210 case 'tag':
211 return this.parseTag();
212 case 'mixin':
213 return this.parseMixin();
214 case 'block':
215 return this.parseBlock();
216 case 'mixin-block':
217 return this.parseMixinBlock();
218 case 'case':
219 return this.parseCase();
220 case 'extends':
221 return this.parseExtends();
222 case 'include':
223 return this.parseInclude();
224 case 'doctype':
225 return this.parseDoctype();
226 case 'filter':
227 return this.parseFilter();
228 case 'comment':
229 return this.parseComment();
230 case 'text':
231 return this.parseText();
232 case 'each':
233 return this.parseEach();
234 case 'code':
235 return this.parseCode();
236 case 'blockCode':
237 return this.parseBlockCode();
238 case 'call':
239 return this.parseCall();
240 case 'interpolation':
241 return this.parseInterpolation();
242 case 'yield':
243 this.advance();
244 var block = new nodes.Block;
245 block.yield = true;
246 return block;
247 case 'id':
248 case 'class':
249 var tok = this.advance();
250 this.lexer.defer(this.lexer.tok('tag', 'div'));
251 this.lexer.defer(tok);
252 return this.parseExpr();
253 default:
254 throw new Error('unexpected token "' + this.peek().type + '"');
255 }
256 },
257
258 /**
259 * Text
260 */
261
262 parseText: function(){
263 var tok = this.expect('text');
264 var tokens = this.parseInlineTagsInText(tok.val);
265 if (tokens.length === 1) return tokens[0];
266 var node = new nodes.Block;
267 for (var i = 0; i < tokens.length; i++) {
268 node.push(tokens[i]);
269 };
270 return node;
271 },
272
273 /**
274 * ':' expr
275 * | block
276 */
277
278 parseBlockExpansion: function(){
279 if (':' == this.peek().type) {
280 this.advance();
281 return new nodes.Block(this.parseExpr());
282 } else {
283 return this.block();
284 }
285 },
286
287 /**
288 * case
289 */
290
291 parseCase: function(){
292 var val = this.expect('case').val;
293 var node = new nodes.Case(val);
294 node.line = this.line();
295
296 var block = new nodes.Block;
297 block.line = this.line();
298 block.filename = this.filename;
299 this.expect('indent');
300 while ('outdent' != this.peek().type) {
301 switch (this.peek().type) {
302 case 'comment':
303 case 'newline':
304 this.advance();
305 break;
306 case 'when':
307 block.push(this.parseWhen());
308 break;
309 case 'default':
310 block.push(this.parseDefault());
311 break;
312 default:
313 throw new Error('Unexpected token "' + this.peek().type
314 + '", expected "when", "default" or "newline"');
315 }
316 }
317 this.expect('outdent');
318
319 node.block = block;
320
321 return node;
322 },
323
324 /**
325 * when
326 */
327
328 parseWhen: function(){
329 var val = this.expect('when').val;
330 if (this.peek().type !== 'newline')
331 return new nodes.Case.When(val, this.parseBlockExpansion());
332 else
333 return new nodes.Case.When(val);
334 },
335
336 /**
337 * default
338 */
339
340 parseDefault: function(){
341 this.expect('default');
342 return new nodes.Case.When('default', this.parseBlockExpansion());
343 },
344
345 /**
346 * code
347 */
348
349 parseCode: function(afterIf){
350 var tok = this.expect('code');
351 var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
352 var block;
353 node.line = this.line();
354
355 // throw an error if an else does not have an if
356 if (tok.isElse && !tok.hasIf) {
357 throw new Error('Unexpected else without if');
358 }
359
360 // handle block
361 block = 'indent' == this.peek().type;
362 if (block) {
363 node.block = this.block();
364 }
365
366 // handle missing block
367 if (tok.requiresBlock && !block) {
368 node.block = new nodes.Block();
369 }
370
371 // mark presense of if for future elses
372 if (tok.isIf && this.peek().isElse) {
373 this.peek().hasIf = true;
374 } else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
375 this.lookahead(2).hasIf = true;
376 }
377
378 return node;
379 },
380
381 /**
382 * block code
383 */
384
385 parseBlockCode: function(){
386 var tok = this.expect('blockCode');
387 var node;
388 var body = this.peek();
389 var text;
390 if (body.type === 'pipeless-text') {
391 this.advance();
392 text = body.val.join('\n');
393 } else {
394 text = '';
395 }
396 node = new nodes.Code(text, false, false);
397 return node;
398 },
399
400 /**
401 * comment
402 */
403
404 parseComment: function(){
405 var tok = this.expect('comment');
406 var node;
407
408 var block;
409 if (block = this.parseTextBlock()) {
410 node = new nodes.BlockComment(tok.val, block, tok.buffer);
411 } else {
412 node = new nodes.Comment(tok.val, tok.buffer);
413 }
414
415 node.line = this.line();
416 return node;
417 },
418
419 /**
420 * doctype
421 */
422
423 parseDoctype: function(){
424 var tok = this.expect('doctype');
425 var node = new nodes.Doctype(tok.val);
426 node.line = this.line();
427 return node;
428 },
429
430 /**
431 * filter attrs? text-block
432 */
433
434 parseFilter: function(){
435 var tok = this.expect('filter');
436 var attrs = this.accept('attrs');
437 var block;
438
439 block = this.parseTextBlock() || new nodes.Block();
440
441 var options = {};
442 if (attrs) {
443 attrs.attrs.forEach(function (attribute) {
444 options[attribute.name] = constantinople.toConstant(attribute.val);
445 });
446 }
447
448 var node = new nodes.Filter(tok.val, block, options);
449 node.line = this.line();
450 return node;
451 },
452
453 /**
454 * each block
455 */
456
457 parseEach: function(){
458 var tok = this.expect('each');
459 var node = new nodes.Each(tok.code, tok.val, tok.key);
460 node.line = this.line();
461 node.block = this.block();
462 if (this.peek().type == 'code' && this.peek().val == 'else') {
463 this.advance();
464 node.alternative = this.block();
465 }
466 return node;
467 },
468
469 /**
470 * Resolves a path relative to the template for use in
471 * includes and extends
472 *
473 * @param {String} path
474 * @param {String} purpose Used in error messages.
475 * @return {String}
476 * @api private
477 */
478
479 resolvePath: function (path, purpose) {
480 var p = require('path');
481 var dirname = p.dirname;
482 var basename = p.basename;
483 var join = p.join;
484
485 if (path[0] !== '/' && !this.filename)
486 throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
487
488 if (path[0] === '/' && !this.options.basedir)
489 throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
490
491 path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
492
493 if (basename(path).indexOf('.') === -1) path += '.jade';
494
495 return path;
496 },
497
498 /**
499 * 'extends' name
500 */
501
502 parseExtends: function(){
503 var fs = require('fs');
504
505 var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
506 if ('.jade' != path.substr(-5)) path += '.jade';
507
508 this.dependencies.push(path);
509 var str = fs.readFileSync(path, 'utf8');
510 var parser = new this.constructor(str, path, this.options);
511 parser.dependencies = this.dependencies;
512
513 parser.blocks = this.blocks;
514 parser.included = this.included;
515 parser.contexts = this.contexts;
516 this.extending = parser;
517
518 // TODO: null node
519 return new nodes.Literal('');
520 },
521
522 /**
523 * 'block' name block
524 */
525
526 parseBlock: function(){
527 var block = this.expect('block');
528 var mode = block.mode;
529 var name = block.val.trim();
530
531 var line = block.line;
532
533 this.inBlock++;
534 block = 'indent' == this.peek().type
535 ? this.block()
536 : new nodes.Block(new nodes.Literal(''));
537 this.inBlock--;
538 block.name = name;
539 block.line = line;
540
541 var prev = this.blocks[name] || {prepended: [], appended: []}
542 if (prev.mode === 'replace') return this.blocks[name] = prev;
543
544 var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
545
546 switch (mode) {
547 case 'append':
548 prev.appended = prev.parser === this ?
549 prev.appended.concat(block.nodes) :
550 block.nodes.concat(prev.appended);
551 break;
552 case 'prepend':
553 prev.prepended = prev.parser === this ?
554 block.nodes.concat(prev.prepended) :
555 prev.prepended.concat(block.nodes);
556 break;
557 }
558 block.nodes = allNodes;
559 block.appended = prev.appended;
560 block.prepended = prev.prepended;
561 block.mode = mode;
562 block.parser = this;
563
564 block.isSubBlock = this.inBlock > 0;
565
566 return this.blocks[name] = block;
567 },
568
569 parseMixinBlock: function () {
570 var block = this.expect('mixin-block');
571 if (!this.inMixin) {
572 throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
573 }
574 return new nodes.MixinBlock();
575 },
576
577 /**
578 * include block?
579 */
580
581 parseInclude: function(){
582 var fs = require('fs');
583 var tok = this.expect('include');
584
585 var path = this.resolvePath(tok.val.trim(), 'include');
586 this.dependencies.push(path);
587 // has-filter
588 if (tok.filter) {
589 var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
590 var options = {filename: path};
591 if (tok.attrs) {
592 tok.attrs.attrs.forEach(function (attribute) {
593 options[attribute.name] = constantinople.toConstant(attribute.val);
594 });
595 }
596 str = filters(tok.filter, str, options);
597 return new nodes.Literal(str);
598 }
599
600 // non-jade
601 if ('.jade' != path.substr(-5)) {
602 var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
603 return new nodes.Literal(str);
604 }
605
606 var str = fs.readFileSync(path, 'utf8');
607 var parser = new this.constructor(str, path, this.options);
608 parser.dependencies = this.dependencies;
609
610 parser.blocks = utils.merge({}, this.blocks);
611 parser.included = true;
612
613 parser.mixins = this.mixins;
614
615 this.context(parser);
616 var ast = parser.parse();
617 this.context();
618 ast.filename = path;
619
620 if ('indent' == this.peek().type) {
621 ast.includeBlock().push(this.block());
622 }
623
624 return ast;
625 },
626
627 /**
628 * call ident block
629 */
630
631 parseCall: function(){
632 var tok = this.expect('call');
633 var name = tok.val;
634 var args = tok.args;
635 var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
636
637 this.tag(mixin);
638 if (mixin.code) {
639 mixin.block.push(mixin.code);
640 mixin.code = null;
641 }
642 if (mixin.block.isEmpty()) mixin.block = null;
643 return mixin;
644 },
645
646 /**
647 * mixin block
648 */
649
650 parseMixin: function(){
651 var tok = this.expect('mixin');
652 var name = tok.val;
653 var args = tok.args;
654 var mixin;
655
656 // definition
657 if ('indent' == this.peek().type) {
658 this.inMixin++;
659 mixin = new nodes.Mixin(name, args, this.block(), false);
660 this.mixins[name] = mixin;
661 this.inMixin--;
662 return mixin;
663 // call
664 } else {
665 return new nodes.Mixin(name, args, null, true);
666 }
667 },
668
669 parseInlineTagsInText: function (str) {
670 var line = this.line();
671
672 var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
673 if (match) {
674 if (match[1]) { // escape
675 var text = new nodes.Text(str.substr(0, match.index) + '#[');
676 text.line = line;
677 var rest = this.parseInlineTagsInText(match[2]);
678 if (rest[0].type === 'Text') {
679 text.val += rest[0].val;
680 rest.shift();
681 }
682 return [text].concat(rest);
683 } else {
684 var text = new nodes.Text(str.substr(0, match.index));
685 text.line = line;
686 var buffer = [text];
687 var rest = match[2];
688 var range = parseJSExpression(rest);
689 var inner = new Parser(range.src, this.filename, this.options);
690 buffer.push(inner.parse());
691 return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
692 }
693 } else {
694 var text = new nodes.Text(str);
695 text.line = line;
696 return [text];
697 }
698 },
699
700 /**
701 * indent (text | newline)* outdent
702 */
703
704 parseTextBlock: function(){
705 var block = new nodes.Block;
706 block.line = this.line();
707 var body = this.peek();
708 if (body.type !== 'pipeless-text') return;
709 this.advance();
710 block.nodes = body.val.reduce(function (accumulator, text) {
711 return accumulator.concat(this.parseInlineTagsInText(text));
712 }.bind(this), []);
713 return block;
714 },
715
716 /**
717 * indent expr* outdent
718 */
719
720 block: function(){
721 var block = new nodes.Block;
722 block.line = this.line();
723 block.filename = this.filename;
724 this.expect('indent');
725 while ('outdent' != this.peek().type) {
726 if ('newline' == this.peek().type) {
727 this.advance();
728 } else {
729 var expr = this.parseExpr();
730 expr.filename = this.filename;
731 block.push(expr);
732 }
733 }
734 this.expect('outdent');
735 return block;
736 },
737
738 /**
739 * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
740 */
741
742 parseInterpolation: function(){
743 var tok = this.advance();
744 var tag = new nodes.Tag(tok.val);
745 tag.buffer = true;
746 return this.tag(tag);
747 },
748
749 /**
750 * tag (attrs | class | id)* (text | code | ':')? newline* block?
751 */
752
753 parseTag: function(){
754 var tok = this.advance();
755 var tag = new nodes.Tag(tok.val);
756
757 tag.selfClosing = tok.selfClosing;
758
759 return this.tag(tag);
760 },
761
762 /**
763 * Parse tag.
764 */
765
766 tag: function(tag){
767 tag.line = this.line();
768
769 var seenAttrs = false;
770 // (attrs | class | id)*
771 out:
772 while (true) {
773 switch (this.peek().type) {
774 case 'id':
775 case 'class':
776 var tok = this.advance();
777 tag.setAttribute(tok.type, "'" + tok.val + "'");
778 continue;
779 case 'attrs':
780 if (seenAttrs) {
781 console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
782 }
783 seenAttrs = true;
784 var tok = this.advance();
785 var attrs = tok.attrs;
786
787 if (tok.selfClosing) tag.selfClosing = true;
788
789 for (var i = 0; i < attrs.length; i++) {
790 tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
791 }
792 continue;
793 case '&attributes':
794 var tok = this.advance();
795 tag.addAttributes(tok.val);
796 break;
797 default:
798 break out;
799 }
800 }
801
802 // check immediate '.'
803 if ('dot' == this.peek().type) {
804 tag.textOnly = true;
805 this.advance();
806 }
807
808 // (text | code | ':')?
809 switch (this.peek().type) {
810 case 'text':
811 tag.block.push(this.parseText());
812 break;
813 case 'code':
814 tag.code = this.parseCode();
815 break;
816 case ':':
817 this.advance();
818 tag.block = new nodes.Block;
819 tag.block.push(this.parseExpr());
820 break;
821 case 'newline':
822 case 'indent':
823 case 'outdent':
824 case 'eos':
825 case 'pipeless-text':
826 break;
827 default:
828 throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
829 }
830
831 // newline*
832 while ('newline' == this.peek().type) this.advance();
833
834 // block?
835 if (tag.textOnly) {
836 tag.block = this.parseTextBlock() || new nodes.Block();
837 } else if ('indent' == this.peek().type) {
838 var block = this.block();
839 for (var i = 0, len = block.nodes.length; i < len; ++i) {
840 tag.block.push(block.nodes[i]);
841 }
842 }
843
844 return tag;
845 }
846};