complete redesign: use nodejs on server instead of php - documentation to be updated
This commit is contained in:
2180
nodejs/node_modules/stylus/lib/parser.js
generated
vendored
Normal file
2180
nodejs/node_modules/stylus/lib/parser.js
generated
vendored
Normal file
@@ -0,0 +1,2180 @@
|
||||
/*!
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user