UNPKG

jade

Version:

Jade template engine

626 lines (527 loc) 12.9 kB
/*! * Jade - Parser * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> * 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.options = options; this.contexts = [this]; }; /** * Tags that may not contain tags. */ var textOnly = exports.textOnly = ['code', 'script', 'textarea', 'style', 'title']; /** * 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(); 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 * | id * | class */ 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 '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 = name = 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 name = this.expect('block').val.trim(); var block = 'indent' == this.peek().type ? this.block() : new nodes.Block(new nodes.Literal('')); return this.blocks[name] = this.blocks[name] || block; }, /** * include block? */ parseInclude: function(){ var path = require('path') , fs = require('fs') , dirname = path.dirname , basename = path.basename , join = path.join; var path = name = 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); this.context(parser); var ast = parser.parse(); this.context(); ast.filename = path; if ('indent' == this.peek().type) { ast.lastBlock().push(this.block()); } return ast; }, /** * mixin block */ parseMixin: function(){ var tok = this.expect('mixin') , name = tok.val , args = tok.args; var block = 'indent' == this.peek().type ? this.block() : null; return new nodes.Mixin(name, args, block); }, /** * indent (text | newline)* outdent */ parseTextBlock: function(){ var text = new nodes.Text; text.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': text.push('\\n'); this.advance(); break; case 'indent': text.push('\\n'); this.parseTextBlock().nodes.forEach(function(node){ text.push(node); }); text.push('\\n'); break; default: text.push(indent + this.advance().val); } } if (spaces == this._spaces) this._spaces = null; this.expect('outdent'); return text; }, /** * 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; }, /** * 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 name = this.advance().val , tag = new nodes.Tag(name) , 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 obj = this.advance().attrs , names = Object.keys(obj); for (var i = 0, len = names.length; i < len; ++i) { var name = names[i] , val = obj[name]; tag.setAttribute(name, val); } 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.text = this.parseText(); break; case 'code': tag.code = this.parseCode(); break; case ':': this.advance(); tag.block = new nodes.Block; tag.block.push(this.parseTag()); 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; } };