hexo/node_modules/stylus/lib/parser.js

2295 lines
50 KiB
JavaScript
Raw Normal View History

2023-09-25 15:58:56 +08:00
/*!
* 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
, node
, 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;
// Hash values inside properties
if (
i > 1 &&
'ident' === this.lookahead(i - 1).type &&
'.' === this.lookahead(i).type &&
'ident' === this.lookahead(i + 1).type
) {
while ((node = this.lookahead(i + 2))) {
if ([
'indent',
'outdent',
'{',
';',
'eos',
'selector',
'media',
'if',
'atrule',
')',
'}',
'unit',
'[',
'for',
'function'
].indexOf(node.type) !== -1) {
if (node.type === '[') {
while ((node = this.lookahead(i + 3)) && node.type !== ']') {
if (~['.', 'unit'].indexOf(node.type)) {
return false;
}
i += 1
}
} else {
if (this.isPseudoSelector(i + 2)) {
return true;
}
if (node.type === ')' && this.lookahead(i + 3) && this.lookahead(i + 3).type === '}') {
break;
}
return [
'outdent',
';',
'eos',
'media',
'if',
'atrule',
')',
'}',
'unit',
'for',
'function'
].indexOf(node.type) === -1;
}
}
i += 1
}
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 tok = this.peek(), selector;
switch (tok.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[tok.type]();
case 'return':
return this.return();
case '{':
return this.property();
default:
// Contextual selectors
if (this.stateAllowsSelector()) {
switch (tok.type) {
case 'color':
case '~':
case '>':
case '<':
case ':':
case '&':
case '&&':
case '[':
case '.':
case '/':
selector = this.selector();
selector.column = tok.column;
selector.lineno = tok.lineno;
return 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()) {
selector = this.selector();
selector.column = tok.column;
selector.lineno = tok.lineno;
return 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() {
var token = this.expect('if');
this.state.push('conditional');
this.cond = true;
var node = new nodes.If(this.expression())
, cond
, block
, item;
node.column = token.column;
this.cond = false;
node.block = this.block(node, false);
this.skip(['newline', 'comment']);
while (this.accept('else')) {
token = this.accept('if');
if (token) {
this.cond = true;
cond = this.expression();
this.cond = false;
block = this.block(node, false);
item = new nodes.If(cond, block);
item.column = token.column;
node.elses.push(item);
} 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);
keyframes.column = tok.column;
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,
ident = this.id(),
name = ident.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);
node.lineno = ident.lineno;
node.column = ident.column;
this.state.pop();
switch (op.type) {
case '?=':
var defined = new nodes.BinOp('is defined', node)
, lookup = new nodes.Expression;
lookup.push(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 tok = this.expect('function').val;
var name = tok.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);
call.column = tok.column;
call.lineno = tok.lineno;
if (withBlock) {
this.state.push('function');
call.block = this.block(call);
this.state.pop();
}
return call;
},
/**
* ident '(' params ')' block
*/
functionDefinition: function() {
var
tok = this.expect('function'),
name = tok.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.column = tok.column;
fn.lineno = tok.lineno;
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, hash;
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}');
}
hash = id.val.hash;
this.skipSpacesAndComments();
this.expect(':');
val = this.expression();
obj.setValue(hash, val);
obj.setKey(hash, 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();
}
}
};