Initial import
[darksolar] / node_modules / jade / lib / parser.js
1
2 /*!
3  * Jade - Parser
4  * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5  * MIT Licensed
6  */
7
8 /**
9  * Module dependencies.
10  */
11
12 var Lexer = require('./lexer')
13   , nodes = require('./nodes');
14
15 /**
16  * Initialize `Parser` with the given input `str` and `filename`.
17  *
18  * @param {String} str
19  * @param {String} filename
20  * @param {Object} options
21  * @api public
22  */
23
24 var Parser = exports = module.exports = function Parser(str, filename, options){
25   this.input = str;
26   this.lexer = new Lexer(str, options);
27   this.filename = filename;
28   this.blocks = {};
29   this.mixins = {};
30   this.options = options;
31   this.contexts = [this];
32 };
33
34 /**
35  * Tags that may not contain tags.
36  */
37
38 var textOnly = exports.textOnly = ['script', 'style'];
39
40 /**
41  * Parser prototype.
42  */
43
44 Parser.prototype = {
45
46   /**
47    * Push `parser` onto the context stack,
48    * or pop and return a `Parser`.
49    */
50
51   context: function(parser){
52     if (parser) {
53       this.contexts.push(parser);
54     } else {
55       return this.contexts.pop();
56     }
57   },
58
59   /**
60    * Return the next token object.
61    *
62    * @return {Object}
63    * @api private
64    */
65
66   advance: function(){
67     return this.lexer.advance();
68   },
69
70   /**
71    * Skip `n` tokens.
72    *
73    * @param {Number} n
74    * @api private
75    */
76
77   skip: function(n){
78     while (n--) this.advance();
79   },
80   
81   /**
82    * Single token lookahead.
83    *
84    * @return {Object}
85    * @api private
86    */
87   
88   peek: function() {
89     return this.lookahead(1);
90   },
91   
92   /**
93    * Return lexer lineno.
94    *
95    * @return {Number}
96    * @api private
97    */
98   
99   line: function() {
100     return this.lexer.lineno;
101   },
102   
103   /**
104    * `n` token lookahead.
105    *
106    * @param {Number} n
107    * @return {Object}
108    * @api private
109    */
110   
111   lookahead: function(n){
112     return this.lexer.lookahead(n);
113   },
114   
115   /**
116    * Parse input returning a string of js for evaluation.
117    *
118    * @return {String}
119    * @api public
120    */
121   
122   parse: function(){
123     var block = new nodes.Block, parser;
124     block.line = this.line();
125
126     while ('eos' != this.peek().type) {
127       if ('newline' == this.peek().type) {
128         this.advance();
129       } else {
130         block.push(this.parseExpr());
131       }
132     }
133
134     if (parser = this.extending) {
135       this.context(parser);
136       var ast = parser.parse();
137       this.context();
138       // hoist mixins
139       for (var name in this.mixins)
140         ast.unshift(this.mixins[name]);
141       return ast;
142     }
143
144     return block;
145   },
146   
147   /**
148    * Expect the given type, or throw an exception.
149    *
150    * @param {String} type
151    * @api private
152    */
153   
154   expect: function(type){
155     if (this.peek().type === type) {
156       return this.advance();
157     } else {
158       throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
159     }
160   },
161   
162   /**
163    * Accept the given `type`.
164    *
165    * @param {String} type
166    * @api private
167    */
168   
169   accept: function(type){
170     if (this.peek().type === type) {
171       return this.advance();
172     }
173   },
174   
175   /**
176    *   tag
177    * | doctype
178    * | mixin
179    * | include
180    * | filter
181    * | comment
182    * | text
183    * | each
184    * | code
185    * | yield
186    * | id
187    * | class
188    */
189   
190   parseExpr: function(){
191     switch (this.peek().type) {
192       case 'tag':
193         return this.parseTag();
194       case 'mixin':
195         return this.parseMixin();
196       case 'block':
197         return this.parseBlock();
198       case 'case':
199         return this.parseCase();
200       case 'when':
201         return this.parseWhen();
202       case 'default':
203         return this.parseDefault();
204       case 'extends':
205         return this.parseExtends();
206       case 'include':
207         return this.parseInclude();
208       case 'doctype':
209         return this.parseDoctype();
210       case 'filter':
211         return this.parseFilter();
212       case 'comment':
213         return this.parseComment();
214       case 'text':
215         return this.parseText();
216       case 'each':
217         return this.parseEach();
218       case 'code':
219         return this.parseCode();
220       case 'call':
221         return this.parseCall();
222       case 'yield':
223         this.advance();
224         var block = new nodes.Block;
225         block.yield = true;
226         return block;
227       case 'id':
228       case 'class':
229         var tok = this.advance();
230         this.lexer.defer(this.lexer.tok('tag', 'div'));
231         this.lexer.defer(tok);
232         return this.parseExpr();
233       default:
234         throw new Error('unexpected token "' + this.peek().type + '"');
235     }
236   },
237   
238   /**
239    * Text
240    */
241   
242   parseText: function(){
243     var tok = this.expect('text')
244       , node = new nodes.Text(tok.val);
245     node.line = this.line();
246     return node;
247   },
248
249   /**
250    *   ':' expr
251    * | block
252    */
253
254   parseBlockExpansion: function(){
255     if (':' == this.peek().type) {
256       this.advance();
257       return new nodes.Block(this.parseExpr());
258     } else {
259       return this.block();
260     }
261   },
262
263   /**
264    * case
265    */
266
267   parseCase: function(){
268     var val = this.expect('case').val
269       , node = new nodes.Case(val);
270     node.line = this.line();
271     node.block = this.block();
272     return node;
273   },
274
275   /**
276    * when
277    */
278
279   parseWhen: function(){
280     var val = this.expect('when').val
281     return new nodes.Case.When(val, this.parseBlockExpansion());
282   },
283   
284   /**
285    * default
286    */
287
288   parseDefault: function(){
289     this.expect('default');
290     return new nodes.Case.When('default', this.parseBlockExpansion());
291   },
292
293   /**
294    * code
295    */
296   
297   parseCode: function(){
298     var tok = this.expect('code')
299       , node = new nodes.Code(tok.val, tok.buffer, tok.escape)
300       , block
301       , i = 1;
302     node.line = this.line();
303     while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i;
304     block = 'indent' == this.lookahead(i).type;
305     if (block) {
306       this.skip(i-1);
307       node.block = this.block();
308     }
309     return node;
310   },
311   
312   /**
313    * comment
314    */
315   
316   parseComment: function(){
317     var tok = this.expect('comment')
318       , node;
319
320     if ('indent' == this.peek().type) {
321       node = new nodes.BlockComment(tok.val, this.block(), tok.buffer);
322     } else {
323       node = new nodes.Comment(tok.val, tok.buffer);
324     }
325
326     node.line = this.line();
327     return node;
328   },
329   
330   /**
331    * doctype
332    */
333   
334   parseDoctype: function(){
335     var tok = this.expect('doctype')
336       , node = new nodes.Doctype(tok.val);
337     node.line = this.line();
338     return node;
339   },
340   
341   /**
342    * filter attrs? text-block
343    */
344   
345   parseFilter: function(){
346     var block
347       , tok = this.expect('filter')
348       , attrs = this.accept('attrs');
349
350     this.lexer.pipeless = true;
351     block = this.parseTextBlock();
352     this.lexer.pipeless = false;
353
354     var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
355     node.line = this.line();
356     return node;
357   },
358   
359   /**
360    * tag ':' attrs? block
361    */
362   
363   parseASTFilter: function(){
364     var block
365       , tok = this.expect('tag')
366       , attrs = this.accept('attrs');
367
368     this.expect(':');
369     block = this.block();
370
371     var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
372     node.line = this.line();
373     return node;
374   },
375   
376   /**
377    * each block
378    */
379   
380   parseEach: function(){
381     var tok = this.expect('each')
382       , node = new nodes.Each(tok.code, tok.val, tok.key);
383     node.line = this.line();
384     node.block = this.block();
385     return node;
386   },
387
388   /**
389    * 'extends' name
390    */
391
392   parseExtends: function(){
393     var path = require('path')
394       , fs = require('fs')
395       , dirname = path.dirname
396       , basename = path.basename
397       , join = path.join;
398
399     if (!this.filename)
400       throw new Error('the "filename" option is required to extend templates');
401
402     var path = this.expect('extends').val.trim()
403       , dir = dirname(this.filename);
404
405     var path = join(dir, path + '.jade')
406       , str = fs.readFileSync(path, 'utf8')
407       , parser = new Parser(str, path, this.options);
408
409     parser.blocks = this.blocks;
410     parser.contexts = this.contexts;
411     this.extending = parser;
412
413     // TODO: null node
414     return new nodes.Literal('');
415   },
416
417   /**
418    * 'block' name block
419    */
420
421   parseBlock: function(){
422     var block = this.expect('block')
423       , mode = block.mode
424       , name = block.val.trim();
425
426     block = 'indent' == this.peek().type
427       ? this.block()
428       : new nodes.Block(new nodes.Literal(''));
429
430     var prev = this.blocks[name];
431
432     if (prev) {
433       switch (prev.mode) {
434         case 'append':
435           block.nodes = block.nodes.concat(prev.nodes);
436           prev = block;
437           break;
438         case 'prepend':
439           block.nodes = prev.nodes.concat(block.nodes);
440           prev = block;
441           break;
442       }
443     }
444
445     block.mode = mode;
446     return this.blocks[name] = prev || block;
447   },
448
449   /**
450    * include block?
451    */
452
453   parseInclude: function(){
454     var path = require('path')
455       , fs = require('fs')
456       , dirname = path.dirname
457       , basename = path.basename
458       , join = path.join;
459
460     var path = this.expect('include').val.trim()
461       , dir = dirname(this.filename);
462
463     if (!this.filename)
464       throw new Error('the "filename" option is required to use includes');
465
466     // no extension
467     if (!~basename(path).indexOf('.')) {
468       path += '.jade';
469     }
470
471     // non-jade
472     if ('.jade' != path.substr(-5)) {
473       var path = join(dir, path)
474         , str = fs.readFileSync(path, 'utf8');
475       return new nodes.Literal(str);
476     }
477
478     var path = join(dir, path)
479       , str = fs.readFileSync(path, 'utf8')
480      , parser = new Parser(str, path, this.options);
481     parser.blocks = this.blocks;
482     parser.mixins = this.mixins;
483
484     this.context(parser);
485     var ast = parser.parse();
486     this.context();
487     ast.filename = path;
488
489     if ('indent' == this.peek().type) {
490       ast.includeBlock().push(this.block());
491     }
492
493     return ast;
494   },
495
496   /**
497    * call ident block
498    */
499
500   parseCall: function(){
501     var tok = this.expect('call')
502       , name = tok.val
503       , args = tok.args
504       , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
505
506     this.tag(mixin);
507     if (mixin.block.isEmpty()) mixin.block = null;
508     return mixin;
509   },
510
511   /**
512    * mixin block
513    */
514
515   parseMixin: function(){
516     var tok = this.expect('mixin')
517       , name = tok.val
518       , args = tok.args
519       , mixin;
520
521     // definition
522     if ('indent' == this.peek().type) {
523       mixin = new nodes.Mixin(name, args, this.block(), false);
524       this.mixins[name] = mixin;
525       return mixin;
526     // call
527     } else {
528       return new nodes.Mixin(name, args, null, true);
529     }
530   },
531
532   /**
533    * indent (text | newline)* outdent
534    */
535
536   parseTextBlock: function(){
537     var block = new nodes.Block;
538     block.line = this.line();
539     var spaces = this.expect('indent').val;
540     if (null == this._spaces) this._spaces = spaces;
541     var indent = Array(spaces - this._spaces + 1).join(' ');
542     while ('outdent' != this.peek().type) {
543       switch (this.peek().type) {
544         case 'newline':
545           this.advance();
546           break;
547         case 'indent':
548           this.parseTextBlock().nodes.forEach(function(node){
549             block.push(node);
550           });
551           break;
552         default:
553           var text = new nodes.Text(indent + this.advance().val);
554           text.line = this.line();
555           block.push(text);
556       }
557     }
558
559     if (spaces == this._spaces) this._spaces = null;
560     this.expect('outdent');
561     return block;
562   },
563
564   /**
565    * indent expr* outdent
566    */
567   
568   block: function(){
569     var block = new nodes.Block;
570     block.line = this.line();
571     this.expect('indent');
572     while ('outdent' != this.peek().type) {
573       if ('newline' == this.peek().type) {
574         this.advance();
575       } else {
576         block.push(this.parseExpr());
577       }
578     }
579     this.expect('outdent');
580     return block;
581   },
582
583   /**
584    * tag (attrs | class | id)* (text | code | ':')? newline* block?
585    */
586   
587   parseTag: function(){
588     // ast-filter look-ahead
589     var i = 2;
590     if ('attrs' == this.lookahead(i).type) ++i;
591     if (':' == this.lookahead(i).type) {
592       if ('indent' == this.lookahead(++i).type) {
593         return this.parseASTFilter();
594       }
595     }
596
597     var name = this.advance().val
598       , tag = new nodes.Tag(name);
599     
600     // (attrs | class | id)*
601     out:
602       while (true) {
603         switch (this.peek().type) {
604           case 'id':
605           case 'class':
606             var tok = this.advance();
607             tag.setAttribute(tok.type, "'" + tok.val + "'");
608             continue;
609           case 'attrs':
610             var tok = this.advance()
611               , obj = tok.attrs
612               , escaped = tok.escaped
613               , names = Object.keys(obj);
614
615             for (var i = 0, len = names.length; i < len; ++i) {
616               var name = names[i]
617                 , val = obj[name];
618               tag.setAttribute(name, val, escaped[name]);
619             }
620             continue;
621           default:
622             break out;
623         }
624       }
625
626     // self-closing
627     if ('/' == this.peek().val) {
628       this.advance();
629       tag.selfClosing = true;
630     }
631
632     return this.tag(tag);
633   },
634   
635   tag: function(tag){
636     var dot;
637
638     tag.line = this.line();
639
640     // check immediate '.'
641     if ('.' == this.peek().val) {
642       dot = tag.textOnly = true;
643       this.advance();
644     }
645
646     // (text | code | ':')?
647     switch (this.peek().type) {
648       case 'text':
649         tag.block.push(this.parseText());
650         break;
651       case 'code':
652         tag.code = this.parseCode();
653         break;
654       case ':':
655         this.advance();
656         tag.block = new nodes.Block;
657         tag.block.push(this.parseExpr());
658         break;
659     }
660
661     // newline*
662     while ('newline' == this.peek().type) this.advance();
663
664     tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);
665
666     // script special-case
667     if ('script' == tag.name && tag.getAttribute) {
668       var type = tag.getAttribute('type');
669       if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
670         tag.textOnly = false;
671       }
672     }
673
674     // block?
675     if ('indent' == this.peek().type) {
676       if (tag.textOnly) {
677         this.lexer.pipeless = true;
678         tag.block = this.parseTextBlock();
679         this.lexer.pipeless = false;
680       } else {
681         var block = this.block();
682         if (tag.block) {
683           for (var i = 0, len = block.nodes.length; i < len; ++i) {
684             tag.block.push(block.nodes[i]);
685           }
686         } else {
687           tag.block = block;
688         }
689       }
690     }
691     
692     return tag;
693   }
694 };