jade
Version:
Jade template engine
1,361 lines (1,189 loc) • 28.3 kB
JavaScript
/*!
* Stylus - Parser
* Copyright(c) 2010 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Lexer = require('./lexer')
, nodes = require('./nodes')
, inspect = require('sys').inspect;
/**
* Selector composite tokens.
*/
var selectorTokens = [
'ident'
, 'string'
, 'selector'
, 'function'
, 'comment'
, 'space'
, 'color'
, 'unit'
, 'for'
, '['
, ']'
, '('
, ')'
, '+'
, '-'
, '*'
, '*='
, '<'
, '>'
, '='
, ':'
, '&'
, '~'
];
/**
* CSS3 pseudo-selectors.
*/
var pseudoSelectors = [
'root'
, 'nth-child'
, 'nth-last-child'
, 'nth-of-type'
, 'nth-last-of-type'
, 'first-child'
, 'last-child'
, 'first-of-type'
, 'last-of-type'
, 'only-child'
, 'only-of-type'
, 'empty'
, 'link'
, 'visited'
, 'active'
, 'hover'
, 'focus'
, 'target'
, 'lang'
, 'enabled'
, 'disabled'
, 'checked'
, 'not'
];
/**
* Initialize a new `Parser` with the given `str` and `options`.
*
* @param {String} str
* @param {Object} options
* @api private
*/
var Parser = module.exports = function Parser(str, options) {
var self = this;
options = options || {};
this.str = nodes.source = str;
this.lexer = new Lexer(str, options);
this.root = options.root || new nodes.Root;
this.state = ['root'];
this.state.pop = function(){
self.prevState = [].pop.call(this);
};
};
/**
* Parser prototype.
*/
Parser.prototype = {
/**
* Constructor.
*/
constructor: Parser,
/**
* Return current state.
*
* @return {String}
* @api private
*/
currentState: function() {
return this.state[this.state.length - 1];
},
/**
* Parse the input, then return the root node.
*
* @return {Node}
* @api private
*/
parse: function(){
var block = this.parent = this.root;
while ('eos' != this.peek().type) {
if (this.accept('newline')) continue;
var stmt = this.statement();
this.accept(';');
if (!stmt) this.error('unexpected token {peek}, not allowed at the root level');
block.push(stmt);
}
return block;
},
/**
* Throw an `Error` with the given `msg`.
*
* @param {String} msg
* @api private
*/
error: function(msg){
var type = this.peek().type
, val = undefined == this.peek().val
? ''
: ' ' + this.peek().toString();
if (val.trim() == type.trim()) val = '';
throw new Error(msg.replace('{peek}', type + val));
},
/**
* Accept the given token `type`, and return it,
* otherwise return `undefined`.
*
* @param {String} type
* @return {Token}
* @api private
*/
accept: function(type){
if (type == this.peek().type) {
return this.next();
}
},
/**
* Expect token `type` and return it, throw otherwise.
*
* @param {String} type
* @return {Token}
* @api private
*/
expect: function(type){
if (type != this.peek().type) {
throw new Error('expected ' + type + ', got ' + this.peek());
}
return this.next();
},
/**
* Get the next token.
*
* @return {Token}
* @api private
*/
next: function() {
var tok = this.lexer.next();
nodes.lineno = tok.lineno;
return tok;
},
/**
* Peek with lookahead(1).
*
* @return {Token}
* @api private
*/
peek: function() {
return this.lexer.peek();
},
/**
* Lookahead `n` tokens.
*
* @param {Number} n
* @return {Token}
* @api private
*/
lookahead: function(n){
return this.lexer.lookahead(n);
},
/**
* Check if the token at `n` is a valid selector token.
*
* @param {Number} n
* @return {Boolean}
* @api private
*/
isSelectorToken: function(n) {
var la = this.lookahead(n).type;
switch (la) {
case 'for':
return this.bracketed;
case '[':
this.bracketed = true;
return true;
case ']':
this.bracketed = false;
return true;
default:
return ~selectorTokens.indexOf(la);
}
},
/**
* Check if the token at `n` is a pseudo selector.
*
* @param {Number} n
* @return {Boolean}
* @api private
*/
isPseudoSelector: function(n){
return ~pseudoSelectors.indexOf(this.lookahead(n).val.name);
},
/**
* Valid selector tokens.
*/
selectorToken: function() {
if (this.isSelectorToken(1)) return this.next();
},
/**
* Consume whitespace.
*/
skipWhitespace: function() {
while (~['space', 'indent', 'outdent', 'newline'].indexOf(this.peek().type))
this.next();
},
/**
* Consume spaces.
*/
skipSpaces: function() {
while ('space' == this.peek().type)
this.next();
},
/**
* Check if the following sequence of tokens
* forms a function definition, ie trailing
* `{` or indentation.
*/
looksLikeFunctionDefinition: function(i) {
return 'indent' == this.lookahead(i).type
|| '{' == this.lookahead(i).type;
},
/**
* Check if the following sequence of tokens
* forms a selector.
*/
looksLikeSelector: function() {
var i = 1;
// Assume selector when an ident is
// followed by a selector
while ('ident' == this.lookahead(i).type
&& 'newline' == this.lookahead(i + 1).type) i += 2;
// Assume pseudo selectors are NOT properties
// as 'td:th-child(1)' may look like a property
// and function call to the parser otherwise
while (this.isSelectorToken(i)) {
if (':' == this.lookahead(i++).type
&& this.isPseudoSelector(i))
return true;
}
// Trailing comma
if (',' == this.lookahead(i).type
&& 'newline' == this.lookahead(i + 1).type)
return true;
// Trailing brace
if ('{' == this.lookahead(i).type
&& 'newline' == this.lookahead(i + 1).type)
return true;
// css-style mode, false on ; }
if (this.css) {
if (';' == this.lookahead(i) ||
'}' == this.lookahead(i))
return false;
}
// Trailing separators
while (!~[
'newline'
, 'indent'
, 'outdent'
, 'for'
, 'if'
, ';'
, '}'].indexOf(this.lookahead(i).type))
++i;
if ('indent' == this.lookahead(i).type)
return true;
},
/**
* statement
* | statement 'if' expression
* | statement 'unless' expression
*/
statement: function() {
var stmt = this.stmt()
, state = this.prevState
, block
, op;
// special-case statements since it
// is not an expression. We could
// implement postfix conditionals at
// the expression level, however they
// would then fail to enclose properties
if (this.allowPostfix) {
delete this.allowPostfix;
state = 'expression';
}
switch (state) {
case 'assignment':
case 'expression':
case 'function arguments':
while (op =
this.accept('if')
|| this.accept('unless')
|| this.accept('for')) {
switch (op.type) {
case 'if':
case 'unless':
stmt = new nodes.If(this.expression(), stmt);
stmt.postfix = true;
stmt.negate = 'unless' == op.type;
this.accept(';');
break;
case 'for':
var key
, val = this.id().name;
if (this.accept(',')) key = this.id().name;
this.expect('in');
var each = new nodes.Each(val, key, this.expression());
block = new nodes.Block;
block.push(stmt);
each.block = block;
stmt = each;
}
}
}
return stmt;
},
/**
* ident
* | selector
* | literal
* | charset
* | import
* | media
* | keyframes
* | page
* | for
* | if
* | unless
* | expression
* | 'return' expression
*/
stmt: function() {
var type = this.peek().type;
switch (type) {
case 'selector':
case 'literal':
case 'keyframes':
case 'charset':
case 'import':
case 'media':
case 'page':
case 'ident':
case 'unless':
case 'function':
case 'for':
case 'if':
return this[type]();
case 'return':
return this.return();
case '{':
return this.property();
default:
// Contextual selectors
switch (this.currentState()) {
case 'root':
case 'selector':
case 'conditional':
case 'keyframe':
case 'function':
case 'media':
case 'for':
switch (type) {
case 'color':
case '~':
case '+':
case '>':
case '<':
case '*':
case ':':
case '&':
case '[':
return this.selector();
}
}
// Expression fallback
var expr = this.expression();
if (expr.isEmpty) this.error('unexpected {peek}');
return expr;
}
},
/**
* indent (!outdent)+ outdent
*/
block: function(node, scope) {
var delim
, stmt
, _ = this.css
, block = this.parent = new nodes.Block(this.parent, node);
if (false === scope) block.scope = false;
// css-style
if (this.css = this.accept('{')) {
delim = '}';
this.skipWhitespace();
} else {
delim = 'outdent';
this.expect('indent');
}
while (delim != this.peek().type) {
// css-style
if (this.css) {
if (this.accept('newline')) continue;
stmt = this.statement();
this.accept(';');
this.skipWhitespace();
} else {
if (this.accept('newline')) continue;
stmt = this.statement();
this.accept(';');
}
if (!stmt) this.error('unexpected token {peek} in block');
block.push(stmt);
}
// css-style
if (this.css) {
this.skipWhitespace();
this.expect('}');
this.skipSpaces();
this.css = _;
} else {
this.expect('outdent');
}
this.parent = block.parent;
return block;
},
/**
* for val (',' key) in expr
*/
for: function() {
this.expect('for');
var key
, val = this.id().name;
if (this.accept(',')) key = this.id().name;
this.expect('in');
var each = new nodes.Each(val, key, this.expression());
this.state.push('for');
each.block = this.block(each, false);
this.state.pop();
return each;
},
/**
* return expression
*/
return: function() {
this.expect('return');
var expr = this.expression();
return expr.isEmpty
? new nodes.Return
: new nodes.Return(expr);
},
/**
* unless expression block
*/
unless: function() {
this.expect('unless');
var node = new nodes.If(this.expression(), true);
this.state.push('conditional');
node.block = this.block(node, false);
this.state.pop();
return node;
},
/**
* if expression block (else block)?
*/
if: function() {
this.expect('if');
var node = new nodes.If(this.expression());
this.state.push('conditional');
node.block = this.block(node, false);
while (this.accept('else')) {
if (this.accept('if')) {
var cond = this.expression()
, block = this.block(node, false);
node.elses.push(new nodes.If(cond, block));
} else {
node.elses.push(this.block(node, false));
break;
}
}
this.state.pop();
return node;
},
/**
* media
*/
media: function() {
var val = this.expect('media').val
, media = new nodes.Media(val);
this.state.push('media');
media.block = this.block(media);
this.state.pop();
return media;
},
/**
* import expression
*/
import: function() {
this.expect('import');
this.allowPostfix = true;
return new nodes.Import(this.expression());
},
/**
* charset string
*/
charset: function() {
this.expect('charset');
var str = this.expect('string').val;
this.allowPostfix = true;
return new nodes.Charset(str);
},
/**
* page selector? block
*/
page: function() {
var selector;
this.expect('page');
if (this.accept(':')) {
var str = this.expect('ident').val.name;
selector = new nodes.Literal(':' + str);
}
var page = new nodes.Page(selector);
this.state.push('page');
page.block = this.block(page);
this.state.pop();
return page;
},
/**
* keyframes name ((unit | from | to) block)+
*/
keyframes: function() {
this.expect('keyframes');
var pos
, _ = this.css
, keyframes = new nodes.Keyframes(this.id());
// css-sty;e
if (this.css = this.accept('{')) {
this.skipWhitespace();
} else {
this.expect('indent');
}
while (pos = this.accept('unit') || this.accept('ident')) {
// from | to
if ('ident' == pos.type) {
this.accept('space');
switch (pos.val.name) {
case 'from':
pos = new nodes.Unit(0, '%');
break;
case 'to':
pos = new nodes.Unit(100, '%');
break;
default:
throw new Error('invalid ident "' + pos.val.name + '" in selector');
}
} else {
pos = pos.val;
}
// block
this.state.push('keyframe');
var block = this.block(keyframes);
keyframes.push(pos, block);
this.state.pop();
if (this.css) this.skipWhitespace();
}
// css-style
if (this.css) {
this.skipWhitespace();
this.expect('}');
this.css = _;
} else {
this.expect('outdent');
}
return keyframes;
},
/**
* literal
*/
literal: function() {
return this.expect('literal').val;
},
/**
* ident space?
*/
id: function() {
var tok = this.expect('ident');
this.accept('space');
return tok.val;
},
/**
* ident
* | assignment
* | property
* | selector
*/
ident: function() {
var i = 2
, la = this.lookahead(i).type;
while ('space' == la) la = this.lookahead(++i).type;
switch (la) {
// Assignment
case '=':
case '?=':
case '-=':
case '+=':
case '*=':
case '/=':
case '%=':
return this.assignment();
// Operation
case '-':
case '+':
case '/':
case '*':
case '%':
case '**':
case 'and':
case 'or':
case '&&':
case '||':
case '>':
case '<':
case '>=':
case '<=':
case '!=':
case '==':
case '[':
case '?':
case 'in':
case 'is a':
case 'is defined':
// Prevent cyclic .ident, return literal
if (this._ident == this.peek()) {
return this.id();
} else {
this._ident = this.peek();
switch (this.currentState()) {
// unary op or selector in property / for
case 'for':
case 'selector':
return this.property();
// Part of a selector
case 'root':
return this.selector();
// Do not disrupt the ident when an operand
default:
return this.operand
? this.id()
: this.expression();
}
}
// Selector or property
default:
switch (this.currentState()) {
case 'root':
return this.selector();
case 'for':
case 'page':
case 'media':
case 'selector':
case 'function':
case 'keyframe':
case 'conditional':
return this.property();
default:
return this.id();
}
}
},
/**
* (ident | '{' expression '}')+
*/
interpolate: function() {
var node
, segs = [];
while (true) {
if (this.accept('{')) {
this.state.push('interpolation');
segs.push(this.expression());
this.expect('}');
this.state.pop();
} else if (node = this.accept('ident')){
segs.push(node.val);
} else {
break;
}
}
if (!segs.length) this.expect('ident');
return segs;
},
/**
* property ':'? expression
* | ident
*/
property: function() {
if (this.looksLikeSelector()) return this.selector();
// property
var ident = this.interpolate()
, ret = prop = new nodes.Property(ident);
// optional ':'
this.accept('space');
if (this.accept(':')) this.accept('space');
this.state.push('property');
this.inProperty = true;
prop.expr = this.list();
if (prop.expr.isEmpty) ret = ident[0];
this.inProperty = false;
this.allowPostfix = true;
this.state.pop();
// optional ';'
this.accept(';');
return ret;
},
/**
* selector ',' selector
* | selector newline selector
* | selector block
*/
selector: function() {
var tok
, arr
, val
, prev
, parent
, group = new nodes.Group;
// Allow comments in selectors
// for hacks
this.lexer.allowComments = true;
do {
val = prev = null;
arr = [];
// Clobber newline after ,
this.accept('newline');
// Selector candidates,
// stitched together to
// form a selector.
while (tok = this.selectorToken()) {
// Selector component
switch (tok.type) {
case 'unit': val = tok.val.val; break;
case 'ident': val = tok.val.name; break;
case 'function': val = tok.val.name + '('; break;
case 'string': val = tok.val.toString(); break;
case 'color': val = tok.val.raw; break;
case 'space': val = ' '; break;
default: val = tok.val;
}
// Whitespace support
if (!prev || prev.space) {
arr.push(val);
} else {
arr[arr.length-1] += val;
}
prev = tok;
}
// Push the selector
group.push(new nodes.Selector(arr.join(' '), parent));
} while (this.accept(',') || this.accept('newline'));
this.lexer.allowComments = false;
this.state.push('selector');
group.block = this.block(group);
this.state.pop();
return group;
},
/**
* ident ('=' | '?=') expression
*/
assignment: function() {
var op
, node
, name = this.id().name;
if (op =
this.accept('=')
|| this.accept('?=')
|| this.accept('+=')
|| this.accept('-=')
|| this.accept('*=')
|| this.accept('/=')
|| this.accept('%=')) {
this.state.push('assignment');
var expr = this.list();
if (expr.isEmpty) this.error('invalid right-hand side operand in assignment, got {peek}')
node = new nodes.Ident(name, expr);
this.state.pop();
switch (op.type) {
case '?=':
var defined = new nodes.BinOp('is defined', node)
, lookup = new nodes.Ident(name);
node = new nodes.Ternary(defined, lookup, node);
break;
case '+=':
case '-=':
case '*=':
case '/=':
case '%=':
node.val = new nodes.BinOp(op.type[0], new nodes.Ident(name), expr);
break;
}
}
return node;
},
/**
* definition
* | call
*/
function: function() {
var parens = 1
, i = 2
, tok;
// Lookahead and determine if we are dealing
// with a function call or definition. Here
// we pair parens to prevent false negatives
out:
while (tok = this.lookahead(i++)) {
switch (tok.type) {
case 'function': case '(': ++parens; break;
case ')': if (!--parens) break out;
}
}
// Definition or call
switch (this.currentState()) {
case 'expression':
return this.functionCall();
default:
return this.looksLikeFunctionDefinition(i)
? this.functionDefinition()
: this.expression();
}
},
/**
* url '(' (expression | urlchars)+ ')'
*/
url: function() {
this.expect('function');
this.state.push('function arguments');
var args = this.args();
this.expect(')');
this.state.pop();
return new nodes.Call('url', args);
},
/**
* ident '(' expression ')'
*/
functionCall: function() {
if ('url' == this.peek().val.name) return this.url();
var name = this.expect('function').val.name;
this.state.push('function arguments');
var args = this.args();
this.expect(')');
this.state.pop();
return new nodes.Call(name, args);
},
/**
* ident '(' params ')' block
*/
functionDefinition: function() {
var name = this.expect('function').val.name;
// params
this.state.push('function params');
this.skipWhitespace();
var params = this.params();
this.skipWhitespace();
this.expect(')');
this.state.pop();
// Body
this.state.push('function');
var fn = new nodes.Function(name, params);
fn.block = this.block(fn);
this.state.pop();
return new nodes.Ident(name, fn);
},
/**
* ident
* | ident '...'
* | ident '=' expression
* | ident ',' ident
*/
params: function() {
var tok
, node
, params = new nodes.Params;
while (tok = this.accept('ident')) {
this.accept('space');
params.push(node = tok.val);
if (this.accept('...')) {
node.rest = true;
} else if (this.accept('=')) {
node.val = this.expression();
}
this.skipWhitespace();
this.accept(',');
this.skipWhitespace();
}
return params;
},
/**
* expression (',' expression)*
*/
args: function() {
var args = new nodes.Expression;
do {
args.push(this.expression());
} while (this.accept(','));
return args;
},
/**
* expression (',' expression)*
*/
list: function() {
var node = this.expression();
while (this.accept(',')) {
if (node.isList) {
list.push(this.expression());
} else {
var list = new nodes.Expression(true);
list.push(node);
list.push(this.expression());
node = list;
}
}
return node;
},
/**
* negation+
*/
expression: function() {
var node
, expr = new nodes.Expression;
this.state.push('expression');
while (node = this.negation()) {
if (!node) this.error('unexpected token {peek} in expression');
expr.push(node);
}
this.state.pop();
return expr;
},
/**
* 'not' ternary
* | ternary
*/
negation: function() {
if (this.accept('not')) {
return new nodes.UnaryOp('!', this.negation());
}
return this.ternary();
},
/**
* logical ('?' expression ':' expression)?
*/
ternary: function() {
var node = this.logical();
if (this.accept('?')) {
var trueExpr = this.expression();
this.expect(':');
var falseExpr = this.expression();
node = new nodes.Ternary(node, trueExpr, falseExpr);
}
return node;
},
/**
* typecheck (('&&' | '||') typecheck)*
*/
logical: function() {
var op
, node = this.typecheck();
while (op = this.accept('&&') || this.accept('||')) {
node = new nodes.BinOp(op.type, node, this.typecheck());
}
return node;
},
/**
* equality ('is a' equality)*
*/
typecheck: function() {
var op
, node = this.equality();
while (op = this.accept('is a')) {
this.operand = true;
if (!node) throw new Error('illegal unary ' + op);
node = new nodes.BinOp(op.type, node, this.equality());
this.operand = false;
}
return node;
},
/**
* in (('==' | '!=') in)*
*/
equality: function() {
var op
, node = this.in();
while (op = this.accept('==') || this.accept('!=')) {
this.operand = true;
if (!node) throw new Error('illegal unary ' + op);
node = new nodes.BinOp(op.type, node, this.in());
this.operand = false;
}
return node;
},
/**
* relational ('in' relational)*
*/
in: function() {
var node = this.relational();
while (this.accept('in')) {
this.operand = true;
if (!node) throw new Error('illegal unary in');
node = new nodes.BinOp('in', node, this.relational());
this.operand = false;
}
return node;
},
/**
* range (('>=' | '<=' | '>' | '<') range)*
*/
relational: function() {
var op
, node = this.range();
while (op =
this.accept('>=')
|| this.accept('<=')
|| this.accept('<')
|| this.accept('>')
) {
this.operand = true;
if (!node) throw new Error('illegal unary ' + op);
node = new nodes.BinOp(op.type, node, this.range());
this.operand = false;
}
return node;
},
/**
* additive (('..' | '...') additive)*
*/
range: function() {
var op
, node = this.additive();
if (op = this.accept('...') || this.accept('..')) {
this.operand = true;
if (!node) throw new Error('illegal unary ' + op);
node = new nodes.BinOp(op.val, node, this.additive());
this.operand = false;
}
return node;
},
/**
* multiplicative (('+' | '-') multiplicative)*
*/
additive: function() {
var op
, node = this.multiplicative();
while (op = this.accept('+') || this.accept('-')) {
this.operand = true;
node = new nodes.BinOp(op.type, node, this.multiplicative());
this.operand = false;
}
return node;
},
/**
* defined (('**' | '*' | '/' | '%') defined)*
*/
multiplicative: function() {
var op
, node = this.defined();
while (op =
this.accept('**')
|| this.accept('*')
|| this.accept('/')
|| this.accept('%')) {
this.operand = true;
if ('/' == op && this.inProperty && !this.parens) {
var expr = new nodes.Expression;
expr.push(node);
expr.push(new nodes.Literal('/'));
return expr;
} else {
if (!node) throw new Error('illegal unary ' + op);
node = new nodes.BinOp(op.type, node, this.defined());
this.operand = false;
}
}
return node;
},
/**
* unary 'is defined'
* | unary
*/
defined: function() {
var node = this.unary();
if (this.accept('is defined')) {
if (!node) throw new Error('illegal use of "is defined"');
node = new nodes.BinOp('is defined', node);
}
return node;
},
/**
* ('!' | '~' | '+' | '-') unary
* | subscript
*/
unary: function() {
var op
, node;
if (op =
this.accept('!')
|| this.accept('~')
|| this.accept('+')
|| this.accept('-')) {
this.operand = true;
node = new nodes.UnaryOp(op.type, this.unary());
this.operand = false;
return node;
}
return this.subscript();
},
/**
* primary ('[' expression ']')+
* | primary
*/
subscript: function() {
var node = this.primary();
while (this.accept('[')) {
node = new nodes.BinOp('[]', node, this.expression());
this.expect(']');
}
return node;
},
/**
* unit
* | null
* | color
* | string
* | ident
* | boolean
* | literal
* | '(' expression ')'
*/
primary: function() {
var op
, node;
// Parenthesis
if (this.accept('(')) {
this.parens = true;
var expr = this.expression();
this.expect(')');
this.parens = false;
return expr;
}
// Primitive
switch (this.peek().type) {
case 'null':
case 'unit':
case 'color':
case 'string':
case 'literal':
case 'boolean':
return this.next().val;
case 'ident':
return this.ident();
case 'function':
return this.functionCall();
}
}
};