PageRenderTime 40ms CodeModel.GetById 16ms app.highlight 20ms RepoModel.GetById 1ms app.codeStats 0ms

/app/node_modules/jade/lib/parser.js

https://bitbucket.org/jorritposthuma/inventory-counter-mobile
JavaScript | 714 lines | 433 code | 114 blank | 167 comment | 87 complexity | 031ce6015d1d5e53eefc5cbb349ea262 MD5 | raw file
Possible License(s): MIT, Apache-2.0
  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
 12var 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
 24var 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
 38var textOnly = exports.textOnly = ['script', 'style'];
 39
 40/**
 41 * Parser prototype.
 42 */
 43
 44Parser.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   * | interpolation
189   */
190  
191  parseExpr: function(){
192    switch (this.peek().type) {
193      case 'tag':
194        return this.parseTag();
195      case 'mixin':
196        return this.parseMixin();
197      case 'block':
198        return this.parseBlock();
199      case 'case':
200        return this.parseCase();
201      case 'when':
202        return this.parseWhen();
203      case 'default':
204        return this.parseDefault();
205      case 'extends':
206        return this.parseExtends();
207      case 'include':
208        return this.parseInclude();
209      case 'doctype':
210        return this.parseDoctype();
211      case 'filter':
212        return this.parseFilter();
213      case 'comment':
214        return this.parseComment();
215      case 'text':
216        return this.parseText();
217      case 'each':
218        return this.parseEach();
219      case 'code':
220        return this.parseCode();
221      case 'call':
222        return this.parseCall();
223      case 'interpolation':
224        return this.parseInterpolation();
225      case 'yield':
226        this.advance();
227        var block = new nodes.Block;
228        block.yield = true;
229        return block;
230      case 'id':
231      case 'class':
232        var tok = this.advance();
233        this.lexer.defer(this.lexer.tok('tag', 'div'));
234        this.lexer.defer(tok);
235        return this.parseExpr();
236      default:
237        throw new Error('unexpected token "' + this.peek().type + '"');
238    }
239  },
240  
241  /**
242   * Text
243   */
244  
245  parseText: function(){
246    var tok = this.expect('text')
247      , node = new nodes.Text(tok.val);
248    node.line = this.line();
249    return node;
250  },
251
252  /**
253   *   ':' expr
254   * | block
255   */
256
257  parseBlockExpansion: function(){
258    if (':' == this.peek().type) {
259      this.advance();
260      return new nodes.Block(this.parseExpr());
261    } else {
262      return this.block();
263    }
264  },
265
266  /**
267   * case
268   */
269
270  parseCase: function(){
271    var val = this.expect('case').val
272      , node = new nodes.Case(val);
273    node.line = this.line();
274    node.block = this.block();
275    return node;
276  },
277
278  /**
279   * when
280   */
281
282  parseWhen: function(){
283    var val = this.expect('when').val
284    return new nodes.Case.When(val, this.parseBlockExpansion());
285  },
286  
287  /**
288   * default
289   */
290
291  parseDefault: function(){
292    this.expect('default');
293    return new nodes.Case.When('default', this.parseBlockExpansion());
294  },
295
296  /**
297   * code
298   */
299  
300  parseCode: function(){
301    var tok = this.expect('code')
302      , node = new nodes.Code(tok.val, tok.buffer, tok.escape)
303      , block
304      , i = 1;
305    node.line = this.line();
306    while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i;
307    block = 'indent' == this.lookahead(i).type;
308    if (block) {
309      this.skip(i-1);
310      node.block = this.block();
311    }
312    return node;
313  },
314  
315  /**
316   * comment
317   */
318  
319  parseComment: function(){
320    var tok = this.expect('comment')
321      , node;
322
323    if ('indent' == this.peek().type) {
324      node = new nodes.BlockComment(tok.val, this.block(), tok.buffer);
325    } else {
326      node = new nodes.Comment(tok.val, tok.buffer);
327    }
328
329    node.line = this.line();
330    return node;
331  },
332  
333  /**
334   * doctype
335   */
336  
337  parseDoctype: function(){
338    var tok = this.expect('doctype')
339      , node = new nodes.Doctype(tok.val);
340    node.line = this.line();
341    return node;
342  },
343  
344  /**
345   * filter attrs? text-block
346   */
347  
348  parseFilter: function(){
349    var block
350      , tok = this.expect('filter')
351      , attrs = this.accept('attrs');
352
353    this.lexer.pipeless = true;
354    block = this.parseTextBlock();
355    this.lexer.pipeless = false;
356
357    var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
358    node.line = this.line();
359    return node;
360  },
361  
362  /**
363   * tag ':' attrs? block
364   */
365  
366  parseASTFilter: function(){
367    var block
368      , tok = this.expect('tag')
369      , attrs = this.accept('attrs');
370
371    this.expect(':');
372    block = this.block();
373
374    var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
375    node.line = this.line();
376    return node;
377  },
378  
379  /**
380   * each block
381   */
382  
383  parseEach: function(){
384    var tok = this.expect('each')
385      , node = new nodes.Each(tok.code, tok.val, tok.key);
386    node.line = this.line();
387    node.block = this.block();
388    if (this.peek().type == 'code' && this.peek().val == 'else') {
389      this.advance();
390      node.alternative = this.block();
391    }
392    return node;
393  },
394
395  /**
396   * 'extends' name
397   */
398
399  parseExtends: function(){
400    var path = require('path')
401      , fs = require('fs')
402      , dirname = path.dirname
403      , basename = path.basename
404      , join = path.join;
405
406    if (!this.filename)
407      throw new Error('the "filename" option is required to extend templates');
408
409    var path = this.expect('extends').val.trim()
410      , dir = dirname(this.filename);
411
412    var path = join(dir, path + '.jade')
413      , str = fs.readFileSync(path, 'utf8')
414      , parser = new Parser(str, path, this.options);
415
416    parser.blocks = this.blocks;
417    parser.contexts = this.contexts;
418    this.extending = parser;
419
420    // TODO: null node
421    return new nodes.Literal('');
422  },
423
424  /**
425   * 'block' name block
426   */
427
428  parseBlock: function(){
429    var block = this.expect('block')
430      , mode = block.mode
431      , name = block.val.trim();
432
433    block = 'indent' == this.peek().type
434      ? this.block()
435      : new nodes.Block(new nodes.Literal(''));
436
437    var prev = this.blocks[name];
438
439    if (prev) {
440      switch (prev.mode) {
441        case 'append':
442          block.nodes = block.nodes.concat(prev.nodes);
443          prev = block;
444          break;
445        case 'prepend':
446          block.nodes = prev.nodes.concat(block.nodes);
447          prev = block;
448          break;
449      }
450    }
451
452    block.mode = mode;
453    return this.blocks[name] = prev || block;
454  },
455
456  /**
457   * include block?
458   */
459
460  parseInclude: function(){
461    var path = require('path')
462      , fs = require('fs')
463      , dirname = path.dirname
464      , basename = path.basename
465      , join = path.join;
466
467    var path = this.expect('include').val.trim()
468      , dir = dirname(this.filename);
469
470    if (!this.filename)
471      throw new Error('the "filename" option is required to use includes');
472
473    // no extension
474    if (!~basename(path).indexOf('.')) {
475      path += '.jade';
476    }
477
478    // non-jade
479    if ('.jade' != path.substr(-5)) {
480      var path = join(dir, path)
481        , str = fs.readFileSync(path, 'utf8');
482      return new nodes.Literal(str);
483    }
484
485    var path = join(dir, path)
486      , str = fs.readFileSync(path, 'utf8')
487     , parser = new Parser(str, path, this.options);
488    parser.blocks = this.blocks;
489    parser.mixins = this.mixins;
490
491    this.context(parser);
492    var ast = parser.parse();
493    this.context();
494    ast.filename = path;
495
496    if ('indent' == this.peek().type) {
497      ast.includeBlock().push(this.block());
498    }
499
500    return ast;
501  },
502
503  /**
504   * call ident block
505   */
506
507  parseCall: function(){
508    var tok = this.expect('call')
509      , name = tok.val
510      , args = tok.args
511      , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
512
513    this.tag(mixin);
514    if (mixin.block.isEmpty()) mixin.block = null;
515    return mixin;
516  },
517
518  /**
519   * mixin block
520   */
521
522  parseMixin: function(){
523    var tok = this.expect('mixin')
524      , name = tok.val
525      , args = tok.args
526      , mixin;
527
528    // definition
529    if ('indent' == this.peek().type) {
530      mixin = new nodes.Mixin(name, args, this.block(), false);
531      this.mixins[name] = mixin;
532      return mixin;
533    // call
534    } else {
535      return new nodes.Mixin(name, args, null, true);
536    }
537  },
538
539  /**
540   * indent (text | newline)* outdent
541   */
542
543  parseTextBlock: function(){
544    var block = new nodes.Block;
545    block.line = this.line();
546    var spaces = this.expect('indent').val;
547    if (null == this._spaces) this._spaces = spaces;
548    var indent = Array(spaces - this._spaces + 1).join(' ');
549    while ('outdent' != this.peek().type) {
550      switch (this.peek().type) {
551        case 'newline':
552          this.advance();
553          break;
554        case 'indent':
555          this.parseTextBlock().nodes.forEach(function(node){
556            block.push(node);
557          });
558          break;
559        default:
560          var text = new nodes.Text(indent + this.advance().val);
561          text.line = this.line();
562          block.push(text);
563      }
564    }
565
566    if (spaces == this._spaces) this._spaces = null;
567    this.expect('outdent');
568    return block;
569  },
570
571  /**
572   * indent expr* outdent
573   */
574  
575  block: function(){
576    var block = new nodes.Block;
577    block.line = this.line();
578    this.expect('indent');
579    while ('outdent' != this.peek().type) {
580      if ('newline' == this.peek().type) {
581        this.advance();
582      } else {
583        block.push(this.parseExpr());
584      }
585    }
586    this.expect('outdent');
587    return block;
588  },
589
590  /**
591   * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
592   */
593  
594  parseInterpolation: function(){
595    var tok = this.advance();
596    var tag = new nodes.Tag(tok.val);
597    tag.buffer = true;
598    return this.tag(tag);
599  },
600
601  /**
602   * tag (attrs | class | id)* (text | code | ':')? newline* block?
603   */
604  
605  parseTag: function(){
606    // ast-filter look-ahead
607    var i = 2;
608    if ('attrs' == this.lookahead(i).type) ++i;
609    if (':' == this.lookahead(i).type) {
610      if ('indent' == this.lookahead(++i).type) {
611        return this.parseASTFilter();
612      }
613    }
614
615    var tok = this.advance()
616      , tag = new nodes.Tag(tok.val);
617
618    tag.selfClosing = tok.selfClosing;
619
620    return this.tag(tag);
621  },
622
623  /**
624   * Parse tag.
625   */
626
627  tag: function(tag){
628    var dot;
629
630    tag.line = this.line();
631
632    // (attrs | class | id)*
633    out:
634      while (true) {
635        switch (this.peek().type) {
636          case 'id':
637          case 'class':
638            var tok = this.advance();
639            tag.setAttribute(tok.type, "'" + tok.val + "'");
640            continue;
641          case 'attrs':
642            var tok = this.advance()
643              , obj = tok.attrs
644              , escaped = tok.escaped
645              , names = Object.keys(obj);
646
647            if (tok.selfClosing) tag.selfClosing = true;
648
649            for (var i = 0, len = names.length; i < len; ++i) {
650              var name = names[i]
651                , val = obj[name];
652              tag.setAttribute(name, val, escaped[name]);
653            }
654            continue;
655          default:
656            break out;
657        }
658      }
659
660    // check immediate '.'
661    if ('.' == this.peek().val) {
662      dot = tag.textOnly = true;
663      this.advance();
664    }
665
666    // (text | code | ':')?
667    switch (this.peek().type) {
668      case 'text':
669        tag.block.push(this.parseText());
670        break;
671      case 'code':
672        tag.code = this.parseCode();
673        break;
674      case ':':
675        this.advance();
676        tag.block = new nodes.Block;
677        tag.block.push(this.parseExpr());
678        break;
679    }
680
681    // newline*
682    while ('newline' == this.peek().type) this.advance();
683
684    tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);
685
686    // script special-case
687    if ('script' == tag.name) {
688      var type = tag.getAttribute('type');
689      if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
690        tag.textOnly = false;
691      }
692    }
693
694    // block?
695    if ('indent' == this.peek().type) {
696      if (tag.textOnly) {
697        this.lexer.pipeless = true;
698        tag.block = this.parseTextBlock();
699        this.lexer.pipeless = false;
700      } else {
701        var block = this.block();
702        if (tag.block) {
703          for (var i = 0, len = block.nodes.length; i < len; ++i) {
704            tag.block.push(block.nodes[i]);
705          }
706        } else {
707          tag.block = block;
708        }
709      }
710    }
711    
712    return tag;
713  }
714};