diff options
Diffstat (limited to 'node_modules/stylus/lib/parser.js')
| -rw-r--r-- | node_modules/stylus/lib/parser.js | 2181 |
1 files changed, 2181 insertions, 0 deletions
diff --git a/node_modules/stylus/lib/parser.js b/node_modules/stylus/lib/parser.js new file mode 100644 index 00000000..392d27e4 --- /dev/null +++ b/node_modules/stylus/lib/parser.js @@ -0,0 +1,2181 @@ +/*! + * 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.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 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(); + } + } +}; |
