pixelnode/node_modules/jade/lib/parser.js

711 lines
14 KiB
JavaScript

/*!
* 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.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;
}
};