You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2180 lines
47 KiB
2180 lines
47 KiB
/*! |
|
* Stylus - Parser |
|
* Copyright (c) Automattic <developer.wordpress.com> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var Lexer = require('./lexer') |
|
, nodes = require('./nodes') |
|
, Token = require('./token') |
|
, units = require('./units') |
|
, errors = require('./errors') |
|
, cache = require('./cache'); |
|
|
|
// debuggers |
|
|
|
var debug = { |
|
lexer: require('debug')('stylus:lexer') |
|
, selector: require('debug')('stylus:parser:selector') |
|
}; |
|
|
|
/** |
|
* Selector composite tokens. |
|
*/ |
|
|
|
var selectorTokens = [ |
|
'ident' |
|
, 'string' |
|
, 'selector' |
|
, 'function' |
|
, 'comment' |
|
, 'boolean' |
|
, 'space' |
|
, 'color' |
|
, 'unit' |
|
, 'for' |
|
, 'in' |
|
, '[' |
|
, ']' |
|
, '(' |
|
, ')' |
|
, '+' |
|
, '-' |
|
, '*' |
|
, '*=' |
|
, '<' |
|
, '>' |
|
, '=' |
|
, ':' |
|
, '&' |
|
, '&&' |
|
, '~' |
|
, '{' |
|
, '}' |
|
, '.' |
|
, '..' |
|
, '/' |
|
]; |
|
|
|
/** |
|
* CSS pseudo-classes and pseudo-elements. |
|
* See http://dev.w3.org/csswg/selectors4/ |
|
*/ |
|
|
|
var pseudoSelectors = [ |
|
// Logical Combinations |
|
'matches' |
|
, 'not' |
|
|
|
// Linguistic Pseudo-classes |
|
, 'dir' |
|
, 'lang' |
|
|
|
// Location Pseudo-classes |
|
, 'any-link' |
|
, 'link' |
|
, 'visited' |
|
, 'local-link' |
|
, 'target' |
|
, 'scope' |
|
|
|
// User Action Pseudo-classes |
|
, 'hover' |
|
, 'active' |
|
, 'focus' |
|
, 'drop' |
|
|
|
// Time-dimensional Pseudo-classes |
|
, 'current' |
|
, 'past' |
|
, 'future' |
|
|
|
// The Input Pseudo-classes |
|
, 'enabled' |
|
, 'disabled' |
|
, 'read-only' |
|
, 'read-write' |
|
, 'placeholder-shown' |
|
, 'checked' |
|
, 'indeterminate' |
|
, 'valid' |
|
, 'invalid' |
|
, 'in-range' |
|
, 'out-of-range' |
|
, 'required' |
|
, 'optional' |
|
, 'user-error' |
|
|
|
// Tree-Structural pseudo-classes |
|
, 'root' |
|
, 'empty' |
|
, 'blank' |
|
, 'nth-child' |
|
, 'nth-last-child' |
|
, 'first-child' |
|
, 'last-child' |
|
, 'only-child' |
|
, 'nth-of-type' |
|
, 'nth-last-of-type' |
|
, 'first-of-type' |
|
, 'last-of-type' |
|
, 'only-of-type' |
|
, 'nth-match' |
|
, 'nth-last-match' |
|
|
|
// Grid-Structural Selectors |
|
, 'nth-column' |
|
, 'nth-last-column' |
|
|
|
// Pseudo-elements |
|
, 'first-line' |
|
, 'first-letter' |
|
, 'before' |
|
, 'after' |
|
|
|
// Non-standard |
|
, 'selection' |
|
]; |
|
|
|
/** |
|
* 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 || {}; |
|
Parser.cache = Parser.cache || Parser.getCache(options); |
|
this.hash = Parser.cache.key(str, options); |
|
this.lexer = {}; |
|
if (!Parser.cache.has(this.hash)) { |
|
this.lexer = new Lexer(str, options); |
|
} |
|
this.prefix = options.prefix || ''; |
|
this.root = options.root || new nodes.Root; |
|
this.state = ['root']; |
|
this.stash = []; |
|
this.parens = 0; |
|
this.css = 0; |
|
this.state.pop = function(){ |
|
self.prevState = [].pop.call(this); |
|
}; |
|
}; |
|
|
|
/** |
|
* Get cache instance. |
|
* |
|
* @param {Object} options |
|
* @return {Object} |
|
* @api private |
|
*/ |
|
|
|
Parser.getCache = function(options) { |
|
return false === options.cache |
|
? cache(false) |
|
: cache(options.cache || 'memory', options); |
|
}; |
|
|
|
/** |
|
* Parser prototype. |
|
*/ |
|
|
|
Parser.prototype = { |
|
|
|
/** |
|
* Constructor. |
|
*/ |
|
|
|
constructor: Parser, |
|
|
|
/** |
|
* Return current state. |
|
* |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
currentState: function() { |
|
return this.state[this.state.length - 1]; |
|
}, |
|
|
|
/** |
|
* Return previous state. |
|
* |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
previousState: function() { |
|
return this.state[this.state.length - 2]; |
|
}, |
|
|
|
/** |
|
* Parse the input, then return the root node. |
|
* |
|
* @return {Node} |
|
* @api private |
|
*/ |
|
|
|
parse: function(){ |
|
var block = this.parent = this.root; |
|
if (Parser.cache.has(this.hash)) { |
|
block = Parser.cache.get(this.hash); |
|
// normalize cached imports |
|
if ('block' == block.nodeName) block.constructor = nodes.Root; |
|
} else { |
|
while ('eos' != this.peek().type) { |
|
this.skipWhitespace(); |
|
if ('eos' == this.peek().type) break; |
|
var stmt = this.statement(); |
|
this.accept(';'); |
|
if (!stmt) this.error('unexpected token {peek}, not allowed at the root level'); |
|
block.push(stmt); |
|
} |
|
Parser.cache.set(this.hash, block); |
|
} |
|
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 errors.ParseError(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) { |
|
this.error('expected "' + type + '", got {peek}'); |
|
} |
|
return this.next(); |
|
}, |
|
|
|
/** |
|
* Get the next token. |
|
* |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
next: function() { |
|
var tok = this.stash.length |
|
? this.stash.pop() |
|
: this.lexer.next() |
|
, line = tok.lineno |
|
, column = tok.column || 1; |
|
|
|
if (tok.val && tok.val.nodeName) { |
|
tok.val.lineno = line; |
|
tok.val.column = column; |
|
} |
|
nodes.lineno = line; |
|
nodes.column = column; |
|
debug.lexer('%s %s', tok.type, tok.val || ''); |
|
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){ |
|
var val = this.lookahead(n).val; |
|
return val && ~pseudoSelectors.indexOf(val.name); |
|
}, |
|
|
|
/** |
|
* Check if the current line contains `type`. |
|
* |
|
* @param {String} type |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
lineContains: function(type){ |
|
var i = 1 |
|
, la; |
|
|
|
while (la = this.lookahead(i++)) { |
|
if (~['indent', 'outdent', 'newline', 'eos'].indexOf(la.type)) return; |
|
if (type == la.type) return true; |
|
} |
|
}, |
|
|
|
/** |
|
* Valid selector tokens. |
|
*/ |
|
|
|
selectorToken: function() { |
|
if (this.isSelectorToken(1)) { |
|
if ('{' == this.peek().type) { |
|
// unclosed, must be a block |
|
if (!this.lineContains('}')) return; |
|
// check if ':' is within the braces. |
|
// though not required by Stylus, chances |
|
// are if someone is using {} they will |
|
// use CSS-style props, helping us with |
|
// the ambiguity in this case |
|
var i = 0 |
|
, la; |
|
while (la = this.lookahead(++i)) { |
|
if ('}' == la.type) { |
|
// Check empty block. |
|
if (i == 2 || (i == 3 && this.lookahead(i - 1).type == 'space')) |
|
return; |
|
break; |
|
} |
|
if (':' == la.type) return; |
|
} |
|
} |
|
return this.next(); |
|
} |
|
}, |
|
|
|
/** |
|
* Skip the given `tokens`. |
|
* |
|
* @param {Array} tokens |
|
* @api private |
|
*/ |
|
|
|
skip: function(tokens) { |
|
while (~tokens.indexOf(this.peek().type)) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume whitespace. |
|
*/ |
|
|
|
skipWhitespace: function() { |
|
this.skip(['space', 'indent', 'outdent', 'newline']); |
|
}, |
|
|
|
/** |
|
* Consume newlines. |
|
*/ |
|
|
|
skipNewlines: function() { |
|
while ('newline' == this.peek().type) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume spaces. |
|
*/ |
|
|
|
skipSpaces: function() { |
|
while ('space' == this.peek().type) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume spaces and comments. |
|
*/ |
|
|
|
skipSpacesAndComments: function() { |
|
while ('space' == this.peek().type |
|
|| 'comment' == 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. |
|
* |
|
* @param {Boolean} [fromProperty] |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
looksLikeSelector: function(fromProperty) { |
|
var i = 1 |
|
, brace; |
|
|
|
// Real property |
|
if (fromProperty && ':' == this.lookahead(i + 1).type |
|
&& (this.lookahead(i + 1).space || 'indent' == this.lookahead(i + 2).type)) |
|
return false; |
|
|
|
// Assume selector when an ident is |
|
// followed by a selector |
|
while ('ident' == this.lookahead(i).type |
|
&& ('newline' == this.lookahead(i + 1).type |
|
|| ',' == this.lookahead(i + 1).type)) i += 2; |
|
|
|
while (this.isSelectorToken(i) |
|
|| ',' == this.lookahead(i).type) { |
|
|
|
if ('selector' == this.lookahead(i).type) |
|
return true; |
|
|
|
if ('&' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
if ('.' == this.lookahead(i).type && 'ident' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
if ('*' == this.lookahead(i).type && 'newline' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// Pseudo-elements |
|
if (':' == this.lookahead(i).type |
|
&& ':' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// #a after an ident and newline |
|
if ('color' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i - 1).type) |
|
return true; |
|
|
|
if (this.looksLikeAttributeSelector(i)) |
|
return true; |
|
|
|
if (('=' == this.lookahead(i).type || 'function' == this.lookahead(i).type) |
|
&& '{' == this.lookahead(i + 1).type) |
|
return false; |
|
|
|
// Hash values inside properties |
|
if (':' == this.lookahead(i).type |
|
&& !this.isPseudoSelector(i + 1) |
|
&& this.lineContains('.')) |
|
return false; |
|
|
|
// the ':' token within braces signifies |
|
// a selector. ex: "foo{bar:'baz'}" |
|
if ('{' == this.lookahead(i).type) brace = true; |
|
else if ('}' == this.lookahead(i).type) brace = false; |
|
if (brace && ':' == this.lookahead(i).type) return true; |
|
|
|
// '{' preceded by a space is considered a selector. |
|
// for example "foo{bar}{baz}" may be a property, |
|
// however "foo{bar} {baz}" is a selector |
|
if ('space' == this.lookahead(i).type |
|
&& '{' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// Assume pseudo selectors are NOT properties |
|
// as 'td:th-child(1)' may look like a property |
|
// and function call to the parser otherwise |
|
if (':' == this.lookahead(i++).type |
|
&& !this.lookahead(i-1).space |
|
&& this.isPseudoSelector(i)) |
|
return true; |
|
|
|
// Trailing space |
|
if ('space' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type |
|
&& '{' == this.lookahead(i + 2).type) |
|
return true; |
|
|
|
if (',' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type) |
|
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).type || |
|
'}' == this.lookahead(i - 1).type) |
|
return false; |
|
} |
|
|
|
// Trailing separators |
|
while (!~[ |
|
'indent' |
|
, 'outdent' |
|
, 'newline' |
|
, 'for' |
|
, 'if' |
|
, ';' |
|
, '}' |
|
, 'eos'].indexOf(this.lookahead(i).type)) |
|
++i; |
|
|
|
if ('indent' == this.lookahead(i).type) |
|
return true; |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms an attribute selector. |
|
*/ |
|
|
|
looksLikeAttributeSelector: function(n) { |
|
var type = this.lookahead(n).type; |
|
if ('=' == type && this.bracketed) return true; |
|
return ('ident' == type || 'string' == type) |
|
&& ']' == this.lookahead(n + 1).type |
|
&& ('newline' == this.lookahead(n + 2).type || this.isSelectorToken(n + 2)) |
|
&& !this.lineContains(':') |
|
&& !this.lineContains('='); |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms a keyframe block. |
|
*/ |
|
|
|
looksLikeKeyframe: function() { |
|
var i = 2 |
|
, type; |
|
switch (this.lookahead(i).type) { |
|
case '{': |
|
case 'indent': |
|
case ',': |
|
return true; |
|
case 'newline': |
|
while ('unit' == this.lookahead(++i).type |
|
|| 'newline' == this.lookahead(i).type) ; |
|
type = this.lookahead(i).type; |
|
return 'indent' == type || '{' == type; |
|
} |
|
}, |
|
|
|
/** |
|
* Check if the current state supports selectors. |
|
*/ |
|
|
|
stateAllowsSelector: function() { |
|
switch (this.currentState()) { |
|
case 'root': |
|
case 'atblock': |
|
case 'selector': |
|
case 'conditional': |
|
case 'function': |
|
case 'atrule': |
|
case 'for': |
|
return true; |
|
} |
|
}, |
|
|
|
/** |
|
* Try to assign @block to the node. |
|
* |
|
* @param {Expression} expr |
|
* @private |
|
*/ |
|
|
|
assignAtblock: function(expr) { |
|
try { |
|
expr.push(this.atblock(expr)); |
|
} catch(err) { |
|
this.error('invalid right-hand side operand in assignment, got {peek}'); |
|
} |
|
}, |
|
|
|
/** |
|
* 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) { |
|
this.allowPostfix = false; |
|
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(this.parent, each); |
|
block.push(stmt); |
|
each.block = block; |
|
stmt = each; |
|
} |
|
} |
|
} |
|
|
|
return stmt; |
|
}, |
|
|
|
/** |
|
* ident |
|
* | selector |
|
* | literal |
|
* | charset |
|
* | namespace |
|
* | import |
|
* | require |
|
* | media |
|
* | atrule |
|
* | scope |
|
* | keyframes |
|
* | mozdocument |
|
* | for |
|
* | if |
|
* | unless |
|
* | comment |
|
* | expression |
|
* | 'return' expression |
|
*/ |
|
|
|
stmt: function() { |
|
var type = this.peek().type; |
|
switch (type) { |
|
case 'keyframes': |
|
return this.keyframes(); |
|
case '-moz-document': |
|
return this.mozdocument(); |
|
case 'comment': |
|
case 'selector': |
|
case 'literal': |
|
case 'charset': |
|
case 'namespace': |
|
case 'import': |
|
case 'require': |
|
case 'extend': |
|
case 'media': |
|
case 'atrule': |
|
case 'ident': |
|
case 'scope': |
|
case 'supports': |
|
case 'unless': |
|
case 'function': |
|
case 'for': |
|
case 'if': |
|
return this[type](); |
|
case 'return': |
|
return this.return(); |
|
case '{': |
|
return this.property(); |
|
default: |
|
// Contextual selectors |
|
if (this.stateAllowsSelector()) { |
|
switch (type) { |
|
case 'color': |
|
case '~': |
|
case '>': |
|
case '<': |
|
case ':': |
|
case '&': |
|
case '&&': |
|
case '[': |
|
case '.': |
|
case '/': |
|
return this.selector(); |
|
// relative reference |
|
case '..': |
|
if ('/' == this.lookahead(2).type) |
|
return this.selector(); |
|
case '+': |
|
return 'function' == this.lookahead(2).type |
|
? this.functionCall() |
|
: this.selector(); |
|
case '*': |
|
return this.property(); |
|
// keyframe blocks (10%, 20% { ... }) |
|
case 'unit': |
|
if (this.looksLikeKeyframe()) return this.selector(); |
|
case '-': |
|
if ('{' == this.lookahead(2).type) |
|
return this.property(); |
|
} |
|
} |
|
|
|
// 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 |
|
, next |
|
, block = this.parent = new nodes.Block(this.parent, node); |
|
|
|
if (false === scope) block.scope = false; |
|
|
|
this.accept('newline'); |
|
|
|
// css-style |
|
if (this.accept('{')) { |
|
this.css++; |
|
delim = '}'; |
|
this.skipWhitespace(); |
|
} else { |
|
delim = 'outdent'; |
|
this.expect('indent'); |
|
} |
|
|
|
while (delim != this.peek().type) { |
|
// css-style |
|
if (this.css) { |
|
if (this.accept('newline') || this.accept('indent')) continue; |
|
stmt = this.statement(); |
|
this.accept(';'); |
|
this.skipWhitespace(); |
|
} else { |
|
if (this.accept('newline')) continue; |
|
// skip useless indents and comments |
|
next = this.lookahead(2).type; |
|
if ('indent' == this.peek().type |
|
&& ~['outdent', 'newline', 'comment'].indexOf(next)) { |
|
this.skip(['indent', 'outdent']); |
|
continue; |
|
} |
|
if ('eos' == this.peek().type) return block; |
|
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; |
|
}, |
|
|
|
/** |
|
* comment space* |
|
*/ |
|
|
|
comment: function(){ |
|
var node = this.next().val; |
|
this.skipSpaces(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* 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'); |
|
this.state.push('for'); |
|
this.cond = true; |
|
var each = new nodes.Each(val, key, this.expression()); |
|
this.cond = false; |
|
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'); |
|
this.state.push('conditional'); |
|
this.cond = true; |
|
var node = new nodes.If(this.expression(), true); |
|
this.cond = false; |
|
node.block = this.block(node, false); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* if expression block (else block)? |
|
*/ |
|
|
|
if: function() { |
|
this.expect('if'); |
|
this.state.push('conditional'); |
|
this.cond = true; |
|
var node = new nodes.If(this.expression()) |
|
, cond |
|
, block; |
|
this.cond = false; |
|
node.block = this.block(node, false); |
|
this.skip(['newline', 'comment']); |
|
while (this.accept('else')) { |
|
if (this.accept('if')) { |
|
this.cond = true; |
|
cond = this.expression(); |
|
this.cond = false; |
|
block = this.block(node, false); |
|
node.elses.push(new nodes.If(cond, block)); |
|
} else { |
|
node.elses.push(this.block(node, false)); |
|
break; |
|
} |
|
this.skip(['newline', 'comment']); |
|
} |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* @block |
|
* |
|
* @param {Expression} [node] |
|
*/ |
|
|
|
atblock: function(node){ |
|
if (!node) this.expect('atblock'); |
|
node = new nodes.Atblock; |
|
this.state.push('atblock'); |
|
node.block = this.block(node, false); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* atrule selector? block? |
|
*/ |
|
|
|
atrule: function(){ |
|
var type = this.expect('atrule').val |
|
, node = new nodes.Atrule(type) |
|
, tok; |
|
this.skipSpacesAndComments(); |
|
node.segments = this.selectorParts(); |
|
this.skipSpacesAndComments(); |
|
tok = this.peek().type; |
|
if ('indent' == tok || '{' == tok || ('newline' == tok |
|
&& '{' == this.lookahead(2).type)) { |
|
this.state.push('atrule'); |
|
node.block = this.block(node); |
|
this.state.pop(); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* scope |
|
*/ |
|
|
|
scope: function(){ |
|
this.expect('scope'); |
|
var selector = this.selectorParts() |
|
.map(function(selector) { return selector.val; }) |
|
.join(''); |
|
this.selectorScope = selector.trim(); |
|
return nodes.null; |
|
}, |
|
|
|
/** |
|
* supports |
|
*/ |
|
|
|
supports: function(){ |
|
this.expect('supports'); |
|
var node = new nodes.Supports(this.supportsCondition()); |
|
this.state.push('atrule'); |
|
node.block = this.block(node); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* supports negation |
|
* | supports op |
|
* | expression |
|
*/ |
|
|
|
supportsCondition: function(){ |
|
var node = this.supportsNegation() |
|
|| this.supportsOp(); |
|
if (!node) { |
|
this.cond = true; |
|
node = this.expression(); |
|
this.cond = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* 'not' supports feature |
|
*/ |
|
|
|
supportsNegation: function(){ |
|
if (this.accept('not')) { |
|
var node = new nodes.Expression; |
|
node.push(new nodes.Literal('not')); |
|
node.push(this.supportsFeature()); |
|
return node; |
|
} |
|
}, |
|
|
|
/** |
|
* supports feature (('and' | 'or') supports feature)+ |
|
*/ |
|
|
|
supportsOp: function(){ |
|
var feature = this.supportsFeature() |
|
, op |
|
, expr; |
|
if (feature) { |
|
expr = new nodes.Expression; |
|
expr.push(feature); |
|
while (op = this.accept('&&') || this.accept('||')) { |
|
expr.push(new nodes.Literal('&&' == op.val ? 'and' : 'or')); |
|
expr.push(this.supportsFeature()); |
|
} |
|
return expr; |
|
} |
|
}, |
|
|
|
/** |
|
* ('(' supports condition ')') |
|
* | feature |
|
*/ |
|
|
|
supportsFeature: function(){ |
|
this.skipSpacesAndComments(); |
|
if ('(' == this.peek().type) { |
|
var la = this.lookahead(2).type; |
|
|
|
if ('ident' == la || '{' == la) { |
|
return this.feature(); |
|
} else { |
|
this.expect('('); |
|
var node = new nodes.Expression; |
|
node.push(new nodes.Literal('(')); |
|
node.push(this.supportsCondition()); |
|
this.expect(')') |
|
node.push(new nodes.Literal(')')); |
|
this.skipSpacesAndComments(); |
|
return node; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* extend |
|
*/ |
|
|
|
extend: function(){ |
|
var tok = this.expect('extend') |
|
, selectors = [] |
|
, sel |
|
, node |
|
, arr; |
|
|
|
do { |
|
arr = this.selectorParts(); |
|
|
|
if (!arr.length) continue; |
|
|
|
sel = new nodes.Selector(arr); |
|
selectors.push(sel); |
|
|
|
if ('!' !== this.peek().type) continue; |
|
|
|
tok = this.lookahead(2); |
|
if ('ident' !== tok.type || 'optional' !== tok.val.name) continue; |
|
|
|
this.skip(['!', 'ident']); |
|
sel.optional = true; |
|
} while(this.accept(',')); |
|
|
|
node = new nodes.Extend(selectors); |
|
node.lineno = tok.lineno; |
|
node.column = tok.column; |
|
return node; |
|
}, |
|
|
|
/** |
|
* media queries |
|
*/ |
|
|
|
media: function() { |
|
this.expect('media'); |
|
this.state.push('atrule'); |
|
var media = new nodes.Media(this.queries()); |
|
media.block = this.block(media); |
|
this.state.pop(); |
|
return media; |
|
}, |
|
|
|
/** |
|
* query (',' query)* |
|
*/ |
|
|
|
queries: function() { |
|
var queries = new nodes.QueryList |
|
, skip = ['comment', 'newline', 'space']; |
|
|
|
do { |
|
this.skip(skip); |
|
queries.push(this.query()); |
|
this.skip(skip); |
|
} while (this.accept(',')); |
|
return queries; |
|
}, |
|
|
|
/** |
|
* expression |
|
* | (ident | 'not')? ident ('and' feature)* |
|
* | feature ('and' feature)* |
|
*/ |
|
|
|
query: function() { |
|
var query = new nodes.Query |
|
, expr |
|
, pred |
|
, id; |
|
|
|
// hash values support |
|
if ('ident' == this.peek().type |
|
&& ('.' == this.lookahead(2).type |
|
|| '[' == this.lookahead(2).type)) { |
|
this.cond = true; |
|
expr = this.expression(); |
|
this.cond = false; |
|
query.push(new nodes.Feature(expr.nodes)); |
|
return query; |
|
} |
|
|
|
if (pred = this.accept('ident') || this.accept('not')) { |
|
pred = new nodes.Literal(pred.val.string || pred.val); |
|
|
|
this.skipSpacesAndComments(); |
|
if (id = this.accept('ident')) { |
|
query.type = id.val; |
|
query.predicate = pred; |
|
} else { |
|
query.type = pred; |
|
} |
|
this.skipSpacesAndComments(); |
|
|
|
if (!this.accept('&&')) return query; |
|
} |
|
|
|
do { |
|
query.push(this.feature()); |
|
} while (this.accept('&&')); |
|
|
|
return query; |
|
}, |
|
|
|
/** |
|
* '(' ident ( ':'? expression )? ')' |
|
*/ |
|
|
|
feature: function() { |
|
this.skipSpacesAndComments(); |
|
this.expect('('); |
|
this.skipSpacesAndComments(); |
|
var node = new nodes.Feature(this.interpolate()); |
|
this.skipSpacesAndComments(); |
|
this.accept(':') |
|
this.skipSpacesAndComments(); |
|
this.inProperty = true; |
|
node.expr = this.list(); |
|
this.inProperty = false; |
|
this.skipSpacesAndComments(); |
|
this.expect(')'); |
|
this.skipSpacesAndComments(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* @-moz-document call (',' call)* block |
|
*/ |
|
|
|
mozdocument: function(){ |
|
this.expect('-moz-document'); |
|
var mozdocument = new nodes.Atrule('-moz-document') |
|
, calls = []; |
|
do { |
|
this.skipSpacesAndComments(); |
|
calls.push(this.functionCall()); |
|
this.skipSpacesAndComments(); |
|
} while (this.accept(',')); |
|
mozdocument.segments = [new nodes.Literal(calls.join(', '))]; |
|
this.state.push('atrule'); |
|
mozdocument.block = this.block(mozdocument, false); |
|
this.state.pop(); |
|
return mozdocument; |
|
}, |
|
|
|
/** |
|
* import expression |
|
*/ |
|
|
|
import: function() { |
|
this.expect('import'); |
|
this.allowPostfix = true; |
|
return new nodes.Import(this.expression(), false); |
|
}, |
|
|
|
/** |
|
* require expression |
|
*/ |
|
|
|
require: function() { |
|
this.expect('require'); |
|
this.allowPostfix = true; |
|
return new nodes.Import(this.expression(), true); |
|
}, |
|
|
|
/** |
|
* charset string |
|
*/ |
|
|
|
charset: function() { |
|
this.expect('charset'); |
|
var str = this.expect('string').val; |
|
this.allowPostfix = true; |
|
return new nodes.Charset(str); |
|
}, |
|
|
|
/** |
|
* namespace ident? (string | url) |
|
*/ |
|
|
|
namespace: function() { |
|
var str |
|
, prefix; |
|
this.expect('namespace'); |
|
|
|
this.skipSpacesAndComments(); |
|
if (prefix = this.accept('ident')) { |
|
prefix = prefix.val; |
|
} |
|
this.skipSpacesAndComments(); |
|
|
|
str = this.accept('string') || this.url(); |
|
this.allowPostfix = true; |
|
return new nodes.Namespace(str, prefix); |
|
}, |
|
|
|
/** |
|
* keyframes name block |
|
*/ |
|
|
|
keyframes: function() { |
|
var tok = this.expect('keyframes') |
|
, keyframes; |
|
|
|
this.skipSpacesAndComments(); |
|
keyframes = new nodes.Keyframes(this.selectorParts(), tok.val); |
|
this.skipSpacesAndComments(); |
|
|
|
// block |
|
this.state.push('atrule'); |
|
keyframes.block = this.block(keyframes); |
|
this.state.pop(); |
|
|
|
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(); |
|
// Member |
|
case '.': |
|
if ('space' == this.lookahead(i - 1).type) return this.selector(); |
|
if (this._ident == this.peek()) return this.id(); |
|
while ('=' != this.lookahead(++i).type |
|
&& !~['[', ',', 'newline', 'indent', 'eos'].indexOf(this.lookahead(i).type)) ; |
|
if ('=' == this.lookahead(i).type) { |
|
this._ident = this.peek(); |
|
return this.expression(); |
|
} else if (this.looksLikeSelector() && this.stateAllowsSelector()) { |
|
return this.selector(); |
|
} |
|
// Assignment []= |
|
case '[': |
|
if (this._ident == this.peek()) return this.id(); |
|
while (']' != this.lookahead(i++).type |
|
&& 'selector' != this.lookahead(i).type |
|
&& 'eos' != this.lookahead(i).type) ; |
|
if ('=' == this.lookahead(i).type) { |
|
this._ident = this.peek(); |
|
return this.expression(); |
|
} else if (this.looksLikeSelector() && this.stateAllowsSelector()) { |
|
return this.selector(); |
|
} |
|
// Operation |
|
case '-': |
|
case '+': |
|
case '/': |
|
case '*': |
|
case '%': |
|
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': |
|
case 'atblock': |
|
case 'atrule': |
|
return '[' == la |
|
? this.subscript() |
|
: this.selector(); |
|
case 'function': |
|
case 'conditional': |
|
return this.looksLikeSelector() |
|
? this.selector() |
|
: this.expression(); |
|
// 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 'selector': |
|
case 'function': |
|
case 'conditional': |
|
case 'atblock': |
|
case 'atrule': |
|
return this.property(); |
|
default: |
|
var id = this.id(); |
|
if ('interpolation' == this.previousState()) id.mixin = true; |
|
return id; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* '*'? (ident | '{' expression '}')+ |
|
*/ |
|
|
|
interpolate: function() { |
|
var node |
|
, segs = [] |
|
, star; |
|
|
|
star = this.accept('*'); |
|
if (star) segs.push(new nodes.Literal('*')); |
|
|
|
while (true) { |
|
if (this.accept('{')) { |
|
this.state.push('interpolation'); |
|
segs.push(this.expression()); |
|
this.expect('}'); |
|
this.state.pop(); |
|
} else if (node = this.accept('-')){ |
|
segs.push(new nodes.Literal('-')); |
|
} 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(true)) return this.selector(); |
|
|
|
// property |
|
var ident = this.interpolate() |
|
, prop = new nodes.Property(ident) |
|
, ret = prop; |
|
|
|
// 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 arr |
|
, group = new nodes.Group |
|
, scope = this.selectorScope |
|
, isRoot = 'root' == this.currentState() |
|
, selector; |
|
|
|
do { |
|
// Clobber newline after , |
|
this.accept('newline'); |
|
|
|
arr = this.selectorParts(); |
|
|
|
// Push the selector |
|
if (isRoot && scope) arr.unshift(new nodes.Literal(scope + ' ')); |
|
if (arr.length) { |
|
selector = new nodes.Selector(arr); |
|
selector.lineno = arr[0].lineno; |
|
selector.column = arr[0].column; |
|
group.push(selector); |
|
} |
|
} while (this.accept(',') || this.accept('newline')); |
|
|
|
if ('selector-parts' == this.currentState()) return group.nodes; |
|
|
|
this.state.push('selector'); |
|
group.block = this.block(group); |
|
this.state.pop(); |
|
|
|
return group; |
|
}, |
|
|
|
selectorParts: function(){ |
|
var tok |
|
, arr = []; |
|
|
|
// Selector candidates, |
|
// stitched together to |
|
// form a selector. |
|
while (tok = this.selectorToken()) { |
|
debug.selector('%s', tok); |
|
// Selector component |
|
switch (tok.type) { |
|
case '{': |
|
this.skipSpaces(); |
|
var expr = this.expression(); |
|
this.skipSpaces(); |
|
this.expect('}'); |
|
arr.push(expr); |
|
break; |
|
case this.prefix && '.': |
|
var literal = new nodes.Literal(tok.val + this.prefix); |
|
literal.prefixed = true; |
|
arr.push(literal); |
|
break; |
|
case 'comment': |
|
// ignore comments |
|
break; |
|
case 'color': |
|
case 'unit': |
|
arr.push(new nodes.Literal(tok.val.raw)); |
|
break; |
|
case 'space': |
|
arr.push(new nodes.Literal(' ')); |
|
break; |
|
case 'function': |
|
arr.push(new nodes.Literal(tok.val.name + '(')); |
|
break; |
|
case 'ident': |
|
arr.push(new nodes.Literal(tok.val.name || tok.val.string)); |
|
break; |
|
default: |
|
arr.push(new nodes.Literal(tok.val)); |
|
if (tok.space) arr.push(new nodes.Literal(' ')); |
|
} |
|
} |
|
|
|
return arr; |
|
}, |
|
|
|
/** |
|
* 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(); |
|
// @block support |
|
if (expr.isEmpty) this.assignAtblock(expr); |
|
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; |
|
break; |
|
case 'eos': |
|
this.error('failed to find closing paren ")"'); |
|
} |
|
} |
|
|
|
// 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 ')' block? |
|
*/ |
|
|
|
functionCall: function() { |
|
var withBlock = this.accept('+'); |
|
if ('url' == this.peek().val.name) return this.url(); |
|
var name = this.expect('function').val.name; |
|
this.state.push('function arguments'); |
|
this.parens++; |
|
var args = this.args(); |
|
this.expect(')'); |
|
this.parens--; |
|
this.state.pop(); |
|
var call = new nodes.Call(name, args); |
|
if (withBlock) { |
|
this.state.push('function'); |
|
call.block = this.block(call); |
|
this.state.pop(); |
|
} |
|
return call; |
|
}, |
|
|
|
/** |
|
* 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; |
|
}, |
|
|
|
/** |
|
* (ident ':')? expression (',' (ident ':')? expression)* |
|
*/ |
|
|
|
args: function() { |
|
var args = new nodes.Arguments |
|
, keyword; |
|
|
|
do { |
|
// keyword |
|
if ('ident' == this.peek().type && ':' == this.lookahead(2).type) { |
|
keyword = this.next().val.string; |
|
this.expect(':'); |
|
args.map[keyword] = this.expression(); |
|
// arg |
|
} else { |
|
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(); |
|
if (expr.nodes.length) { |
|
expr.lineno = expr.nodes[0].lineno; |
|
expr.column = expr.nodes[0].column; |
|
} |
|
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) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
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) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
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) this.error('illegal unary "in", missing left-hand operand'); |
|
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) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
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) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
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) { |
|
this.stash.push(new Token('literal', new nodes.Literal('/'))); |
|
this.operand = false; |
|
return node; |
|
} else { |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
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) this.error('illegal unary "is defined", missing left-hand operand'); |
|
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 = this.unary(); |
|
if (!node) this.error('illegal unary "' + op + '"'); |
|
node = new nodes.UnaryOp(op.type, node); |
|
this.operand = false; |
|
return node; |
|
} |
|
return this.subscript(); |
|
}, |
|
|
|
/** |
|
* member ('[' expression ']')+ '='? |
|
* | member |
|
*/ |
|
|
|
subscript: function() { |
|
var node = this.member() |
|
, id; |
|
while (this.accept('[')) { |
|
node = new nodes.BinOp('[]', node, this.expression()); |
|
this.expect(']'); |
|
} |
|
// TODO: TernaryOp :) |
|
if (this.accept('=')) { |
|
node.op += '='; |
|
node.val = this.list(); |
|
// @block support |
|
if (node.val.isEmpty) this.assignAtblock(node.val); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* primary ('.' id)+ '='? |
|
* | primary |
|
*/ |
|
|
|
member: function() { |
|
var node = this.primary(); |
|
if (node) { |
|
while (this.accept('.')) { |
|
var id = new nodes.Ident(this.expect('ident').val.string); |
|
node = new nodes.Member(node, id); |
|
} |
|
this.skipSpaces(); |
|
if (this.accept('=')) { |
|
node.val = this.list(); |
|
// @block support |
|
if (node.val.isEmpty) this.assignAtblock(node.val); |
|
} |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* '{' '}' |
|
* | '{' pair (ws pair)* '}' |
|
*/ |
|
|
|
object: function(){ |
|
var obj = new nodes.Object |
|
, id, val, comma; |
|
this.expect('{'); |
|
this.skipWhitespace(); |
|
|
|
while (!this.accept('}')) { |
|
if (this.accept('comment') |
|
|| this.accept('newline')) continue; |
|
|
|
if (!comma) this.accept(','); |
|
id = this.accept('ident') || this.accept('string'); |
|
if (!id) this.error('expected "ident" or "string", got {peek}'); |
|
id = id.val.hash; |
|
this.skipSpacesAndComments(); |
|
this.expect(':'); |
|
val = this.expression(); |
|
obj.set(id, val); |
|
comma = this.accept(','); |
|
this.skipWhitespace(); |
|
} |
|
|
|
return obj; |
|
}, |
|
|
|
/** |
|
* unit |
|
* | null |
|
* | color |
|
* | string |
|
* | ident |
|
* | boolean |
|
* | literal |
|
* | object |
|
* | atblock |
|
* | atrule |
|
* | '(' expression ')' '%'? |
|
*/ |
|
|
|
primary: function() { |
|
var tok; |
|
this.skipSpaces(); |
|
|
|
// Parenthesis |
|
if (this.accept('(')) { |
|
++this.parens; |
|
var expr = this.expression() |
|
, paren = this.expect(')'); |
|
--this.parens; |
|
if (this.accept('%')) expr.push(new nodes.Ident('%')); |
|
tok = this.peek(); |
|
// (1 + 2)px, (1 + 2)em, etc. |
|
if (!paren.space |
|
&& 'ident' == tok.type |
|
&& ~units.indexOf(tok.val.string)) { |
|
expr.push(new nodes.Ident(tok.val.string)); |
|
this.next(); |
|
} |
|
return expr; |
|
} |
|
|
|
tok = this.peek(); |
|
|
|
// Primitive |
|
switch (tok.type) { |
|
case 'null': |
|
case 'unit': |
|
case 'color': |
|
case 'string': |
|
case 'literal': |
|
case 'boolean': |
|
case 'comment': |
|
return this.next().val; |
|
case !this.cond && '{': |
|
return this.object(); |
|
case 'atblock': |
|
return this.atblock(); |
|
// property lookup |
|
case 'atrule': |
|
var id = new nodes.Ident(this.next().val); |
|
id.property = true; |
|
return id; |
|
case 'ident': |
|
return this.ident(); |
|
case 'function': |
|
return tok.anonymous |
|
? this.functionDefinition() |
|
: this.functionCall(); |
|
} |
|
} |
|
};
|
|
|