/*! * Stylus - Parser * Copyright(c) 2010 LearnBoost * MIT Licensed */ /** * Module dependencies. */ var Lexer = require('./lexer') , nodes = require('./nodes') , Token = require('./token') , inspect = require('util').inspect , errors = require('./errors'); // 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' , '[' , ']' , '(' , ')' , '+' , '-' , '*' , '*=' , '<' , '>' , '=' , ':' , '&' , '~' , '{' , '}' ]; /** * CSS3 pseudo-selectors. */ var pseudoSelectors = [ 'root' , 'nth-child' , 'nth-last-child' , 'nth-of-type' , 'nth-last-of-type' , 'first-child' , 'last-child' , 'first-of-type' , 'last-of-type' , 'only-child' , 'only-of-type' , 'empty' , 'link' , 'visited' , 'active' , 'hover' , 'focus' , 'target' , 'lang' , 'enabled' , 'disabled' , 'checked' , 'not' ]; /** * 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 || {}; this.lexer = new Lexer(str, options); 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); }; }; /** * Parser prototype. */ Parser.prototype = { /** * Constructor. */ constructor: Parser, /** * Return current state. * * @return {String} * @api private */ currentState: function() { return this.state[this.state.length - 1]; }, /** * Parse the input, then return the root node. * * @return {Node} * @api private */ parse: function(){ var block = this.parent = this.root; while ('eos' != this.peek().type) { if (this.accept('newline')) continue; var stmt = this.statement(); this.accept(';'); if (!stmt) this.error('unexpected token {peek}, not allowed at the root level'); block.push(stmt); } 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(); nodes.lineno = tok.lineno; 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){ return ~pseudoSelectors.indexOf(this.lookahead(n).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'].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) break; if (':' == la.type) return; } } return this.next(); } }, /** * Consume whitespace. */ skipWhitespace: function() { while (~['space', 'indent', 'outdent', 'newline'].indexOf(this.peek().type)) this.next(); }, /** * Consume newlines. */ skipNewlines: function() { while ('newline' == this.peek().type) this.next(); }, /** * Consume spaces. */ skipSpaces: function() { while ('space' == 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. */ looksLikeSelector: function() { var i = 1 , brace; // Assume selector when an ident is // followed by a selector while ('ident' == this.lookahead(i).type && 'newline' == this.lookahead(i + 1).type) i += 2; while (this.isSelectorToken(i) || ',' == this.lookahead(i).type) { if ('selector' == this.lookahead(i).type) return true; // 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.isPseudoSelector(i)) 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) || '}' == this.lookahead(i)) 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 current state supports selectors. */ stateAllowsSelector: function() { switch (this.currentState()) { case 'root': case 'selector': case 'conditional': case 'keyframe': case 'function': case 'font-face': case 'media': case 'for': return true; } }, /** * 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) { delete this.allowPostfix; 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; block.push(stmt); each.block = block; stmt = each; } } } return stmt; }, /** * ident * | selector * | literal * | charset * | import * | media * | scope * | keyframes * | page * | for * | if * | unless * | comment * | expression * | 'return' expression */ stmt: function() { var type = this.peek().type; switch (type) { case '-webkit-keyframes': case 'keyframes': return this.keyframes(); case 'font-face': return this.fontface(); case 'comment': case 'selector': case 'literal': case 'charset': case 'import': case 'extend': case 'media': case 'page': case 'ident': case 'scope': 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 '[': return this.selector(); case '*': return this.property(); 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 , block = this.parent = new nodes.Block(this.parent, node); if (false === scope) block.scope = false; // 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')) continue; stmt = this.statement(); this.accept(';'); this.skipWhitespace(); } else { if (this.accept('newline')) continue; 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'); var each = new nodes.Each(val, key, this.expression()); this.state.push('for'); 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'); var node = new nodes.If(this.expression(), true); this.state.push('conditional'); node.block = this.block(node, false); this.state.pop(); return node; }, /** * if expression block (else block)? */ if: function() { this.expect('if'); var node = new nodes.If(this.expression()); this.state.push('conditional'); node.block = this.block(node, false); while (this.accept('else')) { if (this.accept('if')) { var cond = this.expression() , block = this.block(node, false); node.elses.push(new nodes.If(cond, block)); } else { node.elses.push(this.block(node, false)); break; } } this.state.pop(); return node; }, /** * scope */ scope: function(){ var val = this.expect('scope').val; this.selectorScope = val; return nodes.null; }, /** * extend */ extend: function(){ var val = this.expect('extend').val; return new nodes.Extend(val); }, /** * media */ media: function() { var val = this.expect('media').val , media = new nodes.Media(val); this.state.push('media'); media.block = this.block(media); this.state.pop(); return media; }, /** * fontface */ fontface: function() { this.expect('font-face'); var node = new nodes.FontFace; this.state.push('font-face'); node.block = this.block(node); this.state.pop(); return node; }, /** * import expression */ import: function() { this.expect('import'); this.allowPostfix = true; return new nodes.Import(this.expression()); }, /** * charset string */ charset: function() { this.expect('charset'); var str = this.expect('string').val; this.allowPostfix = true; return new nodes.Charset(str); }, /** * page selector? block */ page: function() { var selector; this.expect('page'); if (this.accept(':')) { var str = this.expect('ident').val.name; selector = new nodes.Literal(':' + str); } var page = new nodes.Page(selector); this.skipSpaces(); this.state.push('page'); page.block = this.block(page); this.state.pop(); return page; }, /** * keyframes name ( * (unit | from | to) * (',' (unit | from | to)*) * block)+ */ keyframes: function() { var pos , tok = this.expect('keyframes') , keyframes = new nodes.Keyframes(this.id(), tok.val) , vals = []; // css-style if (this.accept('{')) { this.css++; this.skipWhitespace(); } else { this.expect('indent'); } this.skipNewlines(); while (pos = this.accept('unit') || this.accept('ident')) { // from | to if ('ident' == pos.type) { this.accept('space'); switch (pos.val.name) { case 'from': pos = new nodes.Unit(0, '%'); break; case 'to': pos = new nodes.Unit(100, '%'); break; default: this.error('"' + pos.val.name + '" is invalid, use "from" or "to"'); } } else { pos = pos.val; } vals.push(pos); // ',' if (this.accept(',') || this.accept('newline')) continue; // block this.state.push('keyframe'); var block = this.block(keyframes); keyframes.push(vals, block); vals = []; this.state.pop(); if (this.css) this.skipWhitespace(); this.skipNewlines(); } // css-style if (this.css) { this.skipWhitespace(); this.expect('}'); this.css--; } else { this.expect('outdent'); } 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(); // Assignment []= case '[': if (this._ident == this.peek()) return this.id(); while (']' != this.lookahead(i++).type && 'selector' != 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 'and': case 'or': 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 'media': case 'font-face': return this.selector(); case 'function': 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 'page': case 'media': case 'font-face': case 'selector': case 'function': case 'keyframe': case 'conditional': return this.property(); default: return this.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()) 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 tok , arr , group = new nodes.Group , scope = this.selectorScope , isRoot = 'root' == this.currentState(); do { arr = []; // Clobber newline after , this.accept('newline'); // 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 'comment': arr.push(new nodes.Literal(tok.val.str)); break; case 'color': 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)); break; default: arr.push(new nodes.Literal(tok.val)); if (tok.space) arr.push(new nodes.Literal(' ')); } } // Push the selector if (isRoot && scope) arr.unshift(new nodes.Literal(scope + ' ')); group.push(new nodes.Selector(arr)); } while (this.accept(',') || this.accept('newline')); this.lexer.allowComments = false; this.state.push('selector'); group.block = this.block(group); this.state.pop(); return group; }, /** * 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(); if (expr.isEmpty) this.error('invalid right-hand side operand in assignment, got {peek}') node = new nodes.Ident(name, expr); this.state.pop(); switch (op.type) { case '?=': var defined = new nodes.BinOp('is defined', node) , lookup = new nodes.Ident(name); node = new nodes.Ternary(defined, lookup, node); break; case '+=': case '-=': case '*=': case '/=': case '%=': node.val = new nodes.BinOp(op.type[0], new nodes.Ident(name), expr); break; } } return node; }, /** * definition * | call */ function: function() { var parens = 1 , i = 2 , tok; // Lookahead and determine if we are dealing // with a function call or definition. Here // we pair parens to prevent false negatives out: while (tok = this.lookahead(i++)) { switch (tok.type) { case 'function': case '(': ++parens; break; case ')': if (!--parens) break out; break; case 'eos': this.error('failed to find closing paren ")"'); } } // Definition or call switch (this.currentState()) { case 'expression': return this.functionCall(); default: return this.looksLikeFunctionDefinition(i) ? this.functionDefinition() : this.expression(); } }, /** * url '(' (expression | urlchars)+ ')' */ url: function() { this.expect('function'); this.state.push('function arguments'); var args = this.args(); this.expect(')'); this.state.pop(); return new nodes.Call('url', args); }, /** * ident '(' expression ')' */ functionCall: function() { 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(); return new nodes.Call(name, args); }, /** * 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(); 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 = new nodes.UnaryOp(op.type, this.unary()); this.operand = false; return node; } return this.subscript(); }, /** * primary ('[' expression ']' '='?)+ * | primary */ subscript: function() { var node = this.primary(); while (this.accept('[')) { node = new nodes.BinOp('[]', node, this.expression()); this.expect(']'); // TODO: TernaryOp :) if (this.accept('=')) { node.op += '='; node.val = this.expression(); } } return node; }, /** * unit * | null * | color * | string * | ident * | boolean * | literal * | '(' expression ')' '%'? */ primary: function() { var op , node; // Parenthesis if (this.accept('(')) { ++this.parens; var expr = this.expression(); this.expect(')'); --this.parens; if (this.accept('%')) expr.push(new nodes.Ident('%')); return expr; } // Primitive switch (this.peek().type) { case 'null': case 'unit': case 'color': case 'string': case 'literal': case 'boolean': return this.next().val; case 'ident': return this.ident(); case 'function': return this.functionCall(); } } };