aboutsummaryrefslogtreecommitdiff
path: root/node_modules/stylus/lib/parser.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/stylus/lib/parser.js')
-rw-r--r--node_modules/stylus/lib/parser.js2181
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();
+ }
+ }
+};