/*! * Jade - Parser * Copyright(c) 2010 TJ Holowaychuk * MIT Licensed */ /** * Module dependencies. */ var Lexer = require('./lexer') , nodes = require('./nodes'); /** * Initialize `Parser` with the given input `str` and `filename`. * * @param {String} str * @param {String} filename * @param {Object} options * @api public */ var Parser = exports = module.exports = function Parser(str, filename, options){ this.input = str; this.lexer = new Lexer(str, options); this.filename = filename; this.blocks = {}; this.mixins = {}; this.options = options; this.contexts = [this]; }; /** * Tags that may not contain tags. */ var textOnly = exports.textOnly = ['script', 'style']; /** * Parser prototype. */ Parser.prototype = { /** * Push `parser` onto the context stack, * or pop and return a `Parser`. */ context: function(parser){ if (parser) { this.contexts.push(parser); } else { return this.contexts.pop(); } }, /** * Return the next token object. * * @return {Object} * @api private */ advance: function(){ return this.lexer.advance(); }, /** * Skip `n` tokens. * * @param {Number} n * @api private */ skip: function(n){ while (n--) this.advance(); }, /** * Single token lookahead. * * @return {Object} * @api private */ peek: function() { return this.lookahead(1); }, /** * Return lexer lineno. * * @return {Number} * @api private */ line: function() { return this.lexer.lineno; }, /** * `n` token lookahead. * * @param {Number} n * @return {Object} * @api private */ lookahead: function(n){ return this.lexer.lookahead(n); }, /** * Parse input returning a string of js for evaluation. * * @return {String} * @api public */ parse: function(){ var block = new nodes.Block, parser; block.line = this.line(); while ('eos' != this.peek().type) { if ('newline' == this.peek().type) { this.advance(); } else { block.push(this.parseExpr()); } } if (parser = this.extending) { this.context(parser); var ast = parser.parse(); this.context(); // hoist mixins for (var name in this.mixins) ast.unshift(this.mixins[name]); return ast; } return block; }, /** * Expect the given type, or throw an exception. * * @param {String} type * @api private */ expect: function(type){ if (this.peek().type === type) { return this.advance(); } else { throw new Error('expected "' + type + '", but got "' + this.peek().type + '"'); } }, /** * Accept the given `type`. * * @param {String} type * @api private */ accept: function(type){ if (this.peek().type === type) { return this.advance(); } }, /** * tag * | doctype * | mixin * | include * | filter * | comment * | text * | each * | code * | yield * | id * | class * | interpolation */ parseExpr: function(){ switch (this.peek().type) { case 'tag': return this.parseTag(); case 'mixin': return this.parseMixin(); case 'block': return this.parseBlock(); case 'case': return this.parseCase(); case 'when': return this.parseWhen(); case 'default': return this.parseDefault(); case 'extends': return this.parseExtends(); case 'include': return this.parseInclude(); case 'doctype': return this.parseDoctype(); case 'filter': return this.parseFilter(); case 'comment': return this.parseComment(); case 'text': return this.parseText(); case 'each': return this.parseEach(); case 'code': return this.parseCode(); case 'call': return this.parseCall(); case 'interpolation': return this.parseInterpolation(); case 'yield': this.advance(); var block = new nodes.Block; block.yield = true; return block; case 'id': case 'class': var tok = this.advance(); this.lexer.defer(this.lexer.tok('tag', 'div')); this.lexer.defer(tok); return this.parseExpr(); default: throw new Error('unexpected token "' + this.peek().type + '"'); } }, /** * Text */ parseText: function(){ var tok = this.expect('text') , node = new nodes.Text(tok.val); node.line = this.line(); return node; }, /** * ':' expr * | block */ parseBlockExpansion: function(){ if (':' == this.peek().type) { this.advance(); return new nodes.Block(this.parseExpr()); } else { return this.block(); } }, /** * case */ parseCase: function(){ var val = this.expect('case').val , node = new nodes.Case(val); node.line = this.line(); node.block = this.block(); return node; }, /** * when */ parseWhen: function(){ var val = this.expect('when').val return new nodes.Case.When(val, this.parseBlockExpansion()); }, /** * default */ parseDefault: function(){ this.expect('default'); return new nodes.Case.When('default', this.parseBlockExpansion()); }, /** * code */ parseCode: function(){ var tok = this.expect('code') , node = new nodes.Code(tok.val, tok.buffer, tok.escape) , block , i = 1; node.line = this.line(); while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i; block = 'indent' == this.lookahead(i).type; if (block) { this.skip(i-1); node.block = this.block(); } return node; }, /** * comment */ parseComment: function(){ var tok = this.expect('comment') , node; if ('indent' == this.peek().type) { node = new nodes.BlockComment(tok.val, this.block(), tok.buffer); } else { node = new nodes.Comment(tok.val, tok.buffer); } node.line = this.line(); return node; }, /** * doctype */ parseDoctype: function(){ var tok = this.expect('doctype') , node = new nodes.Doctype(tok.val); node.line = this.line(); return node; }, /** * filter attrs? text-block */ parseFilter: function(){ var block , tok = this.expect('filter') , attrs = this.accept('attrs'); this.lexer.pipeless = true; block = this.parseTextBlock(); this.lexer.pipeless = false; var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); node.line = this.line(); return node; }, /** * tag ':' attrs? block */ parseASTFilter: function(){ var block , tok = this.expect('tag') , attrs = this.accept('attrs'); this.expect(':'); block = this.block(); var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); node.line = this.line(); return node; }, /** * each block */ parseEach: function(){ var tok = this.expect('each') , node = new nodes.Each(tok.code, tok.val, tok.key); node.line = this.line(); node.block = this.block(); return node; }, /** * 'extends' name */ parseExtends: function(){ var path = require('path') , fs = require('fs') , dirname = path.dirname , basename = path.basename , join = path.join; if (!this.filename) throw new Error('the "filename" option is required to extend templates'); var path = this.expect('extends').val.trim() , dir = dirname(this.filename); var path = join(dir, path + '.jade') , str = fs.readFileSync(path, 'utf8') , parser = new Parser(str, path, this.options); parser.blocks = this.blocks; parser.contexts = this.contexts; this.extending = parser; // TODO: null node return new nodes.Literal(''); }, /** * 'block' name block */ parseBlock: function(){ var block = this.expect('block') , mode = block.mode , name = block.val.trim(); block = 'indent' == this.peek().type ? this.block() : new nodes.Block(new nodes.Literal('')); var prev = this.blocks[name]; if (prev) { switch (prev.mode) { case 'append': block.nodes = block.nodes.concat(prev.nodes); prev = block; break; case 'prepend': block.nodes = prev.nodes.concat(block.nodes); prev = block; break; } } block.mode = mode; return this.blocks[name] = prev || block; }, /** * include block? */ parseInclude: function(){ var path = require('path') , fs = require('fs') , dirname = path.dirname , basename = path.basename , join = path.join; var path = this.expect('include').val.trim() , dir = dirname(this.filename); if (!this.filename) throw new Error('the "filename" option is required to use includes'); // no extension if (!~basename(path).indexOf('.')) { path += '.jade'; } // non-jade if ('.jade' != path.substr(-5)) { var path = join(dir, path) , str = fs.readFileSync(path, 'utf8'); return new nodes.Literal(str); } var path = join(dir, path) , str = fs.readFileSync(path, 'utf8') , parser = new Parser(str, path, this.options); parser.blocks = this.blocks; parser.mixins = this.mixins; this.context(parser); var ast = parser.parse(); this.context(); ast.filename = path; if ('indent' == this.peek().type) { ast.includeBlock().push(this.block()); } return ast; }, /** * call ident block */ parseCall: function(){ var tok = this.expect('call') , name = tok.val , args = tok.args , mixin = new nodes.Mixin(name, args, new nodes.Block, true); this.tag(mixin); if (mixin.block.isEmpty()) mixin.block = null; return mixin; }, /** * mixin block */ parseMixin: function(){ var tok = this.expect('mixin') , name = tok.val , args = tok.args , mixin; // definition if ('indent' == this.peek().type) { mixin = new nodes.Mixin(name, args, this.block(), false); this.mixins[name] = mixin; return mixin; // call } else { return new nodes.Mixin(name, args, null, true); } }, /** * indent (text | newline)* outdent */ parseTextBlock: function(){ var block = new nodes.Block; block.line = this.line(); var spaces = this.expect('indent').val; if (null == this._spaces) this._spaces = spaces; var indent = Array(spaces - this._spaces + 1).join(' '); while ('outdent' != this.peek().type) { switch (this.peek().type) { case 'newline': this.advance(); break; case 'indent': this.parseTextBlock().nodes.forEach(function(node){ block.push(node); }); break; default: var text = new nodes.Text(indent + this.advance().val); text.line = this.line(); block.push(text); } } if (spaces == this._spaces) this._spaces = null; this.expect('outdent'); return block; }, /** * indent expr* outdent */ block: function(){ var block = new nodes.Block; block.line = this.line(); this.expect('indent'); while ('outdent' != this.peek().type) { if ('newline' == this.peek().type) { this.advance(); } else { block.push(this.parseExpr()); } } this.expect('outdent'); return block; }, /** * interpolation (attrs | class | id)* (text | code | ':')? newline* block? */ parseInterpolation: function(){ var tok = this.advance(); var tag = new nodes.Tag(tok.val); tag.buffer = true; return this.tag(tag); }, /** * tag (attrs | class | id)* (text | code | ':')? newline* block? */ parseTag: function(){ // ast-filter look-ahead var i = 2; if ('attrs' == this.lookahead(i).type) ++i; if (':' == this.lookahead(i).type) { if ('indent' == this.lookahead(++i).type) { return this.parseASTFilter(); } } var tok = this.advance() , tag = new nodes.Tag(tok.val); tag.selfClosing = tok.selfClosing; return this.tag(tag); }, /** * Parse tag. */ tag: function(tag){ var dot; tag.line = this.line(); // (attrs | class | id)* out: while (true) { switch (this.peek().type) { case 'id': case 'class': var tok = this.advance(); tag.setAttribute(tok.type, "'" + tok.val + "'"); continue; case 'attrs': var tok = this.advance() , obj = tok.attrs , escaped = tok.escaped , names = Object.keys(obj); if (tok.selfClosing) tag.selfClosing = true; for (var i = 0, len = names.length; i < len; ++i) { var name = names[i] , val = obj[name]; tag.setAttribute(name, val, escaped[name]); } continue; default: break out; } } // check immediate '.' if ('.' == this.peek().val) { dot = tag.textOnly = true; this.advance(); } // (text | code | ':')? switch (this.peek().type) { case 'text': tag.block.push(this.parseText()); break; case 'code': tag.code = this.parseCode(); break; case ':': this.advance(); tag.block = new nodes.Block; tag.block.push(this.parseExpr()); break; } // newline* while ('newline' == this.peek().type) this.advance(); tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name); // script special-case if ('script' == tag.name) { var type = tag.getAttribute('type'); if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) { tag.textOnly = false; } } // block? if ('indent' == this.peek().type) { if (tag.textOnly) { this.lexer.pipeless = true; tag.block = this.parseTextBlock(); this.lexer.pipeless = false; } else { var block = this.block(); if (tag.block) { for (var i = 0, len = block.nodes.length; i < len; ++i) { tag.block.push(block.nodes[i]); } } else { tag.block = block; } } } return tag; } };