Fully end to end encrypted anonymous chat program. Server only stores public key lookup for users and the encrypted messages. No credentials are transfered to the server, but kept in local browser storage. This allows 100% safe chatting.
https://safechat.ch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2180 lines
47 KiB
2180 lines
47 KiB
/*! |
|
* Stylus - Parser |
|
* Copyright (c) Automattic <developer.wordpress.com> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var Lexer = require('./lexer') |
|
, nodes = require('./nodes') |
|
, Token = require('./token') |
|
, units = require('./units') |
|
, errors = require('./errors') |
|
, cache = require('./cache'); |
|
|
|
// debuggers |
|
|
|
var debug = { |
|
lexer: require('debug')('stylus:lexer') |
|
, selector: require('debug')('stylus:parser:selector') |
|
}; |
|
|
|
/** |
|
* Selector composite tokens. |
|
*/ |
|
|
|
var selectorTokens = [ |
|
'ident' |
|
, 'string' |
|
, 'selector' |
|
, 'function' |
|
, 'comment' |
|
, 'boolean' |
|
, 'space' |
|
, 'color' |
|
, 'unit' |
|
, 'for' |
|
, 'in' |
|
, '[' |
|
, ']' |
|
, '(' |
|
, ')' |
|
, '+' |
|
, '-' |
|
, '*' |
|
, '*=' |
|
, '<' |
|
, '>' |
|
, '=' |
|
, ':' |
|
, '&' |
|
, '&&' |
|
, '~' |
|
, '{' |
|
, '}' |
|
, '.' |
|
, '..' |
|
, '/' |
|
]; |
|
|
|
/** |
|
* CSS pseudo-classes and pseudo-elements. |
|
* See http://dev.w3.org/csswg/selectors4/ |
|
*/ |
|
|
|
var pseudoSelectors = [ |
|
// Logical Combinations |
|
'matches' |
|
, 'not' |
|
|
|
// Linguistic Pseudo-classes |
|
, 'dir' |
|
, 'lang' |
|
|
|
// Location Pseudo-classes |
|
, 'any-link' |
|
, 'link' |
|
, 'visited' |
|
, 'local-link' |
|
, 'target' |
|
, 'scope' |
|
|
|
// User Action Pseudo-classes |
|
, 'hover' |
|
, 'active' |
|
, 'focus' |
|
, 'drop' |
|
|
|
// Time-dimensional Pseudo-classes |
|
, 'current' |
|
, 'past' |
|
, 'future' |
|
|
|
// The Input Pseudo-classes |
|
, 'enabled' |
|
, 'disabled' |
|
, 'read-only' |
|
, 'read-write' |
|
, 'placeholder-shown' |
|
, 'checked' |
|
, 'indeterminate' |
|
, 'valid' |
|
, 'invalid' |
|
, 'in-range' |
|
, 'out-of-range' |
|
, 'required' |
|
, 'optional' |
|
, 'user-error' |
|
|
|
// Tree-Structural pseudo-classes |
|
, 'root' |
|
, 'empty' |
|
, 'blank' |
|
, 'nth-child' |
|
, 'nth-last-child' |
|
, 'first-child' |
|
, 'last-child' |
|
, 'only-child' |
|
, 'nth-of-type' |
|
, 'nth-last-of-type' |
|
, 'first-of-type' |
|
, 'last-of-type' |
|
, 'only-of-type' |
|
, 'nth-match' |
|
, 'nth-last-match' |
|
|
|
// Grid-Structural Selectors |
|
, 'nth-column' |
|
, 'nth-last-column' |
|
|
|
// Pseudo-elements |
|
, 'first-line' |
|
, 'first-letter' |
|
, 'before' |
|
, 'after' |
|
|
|
// Non-standard |
|
, 'selection' |
|
]; |
|
|
|
/** |
|
* Initialize a new `Parser` with the given `str` and `options`. |
|
* |
|
* @param {String} str |
|
* @param {Object} options |
|
* @api private |
|
*/ |
|
|
|
var Parser = module.exports = function Parser(str, options) { |
|
var self = this; |
|
options = options || {}; |
|
Parser.cache = Parser.cache || Parser.getCache(options); |
|
this.hash = Parser.cache.key(str, options); |
|
this.lexer = {}; |
|
if (!Parser.cache.has(this.hash)) { |
|
this.lexer = new Lexer(str, options); |
|
} |
|
this.prefix = options.prefix || ''; |
|
this.root = options.root || new nodes.Root; |
|
this.state = ['root']; |
|
this.stash = []; |
|
this.parens = 0; |
|
this.css = 0; |
|
this.state.pop = function(){ |
|
self.prevState = [].pop.call(this); |
|
}; |
|
}; |
|
|
|
/** |
|
* Get cache instance. |
|
* |
|
* @param {Object} options |
|
* @return {Object} |
|
* @api private |
|
*/ |
|
|
|
Parser.getCache = function(options) { |
|
return false === options.cache |
|
? cache(false) |
|
: cache(options.cache || 'memory', options); |
|
}; |
|
|
|
/** |
|
* Parser prototype. |
|
*/ |
|
|
|
Parser.prototype = { |
|
|
|
/** |
|
* Constructor. |
|
*/ |
|
|
|
constructor: Parser, |
|
|
|
/** |
|
* Return current state. |
|
* |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
currentState: function() { |
|
return this.state[this.state.length - 1]; |
|
}, |
|
|
|
/** |
|
* Return previous state. |
|
* |
|
* @return {String} |
|
* @api private |
|
*/ |
|
|
|
previousState: function() { |
|
return this.state[this.state.length - 2]; |
|
}, |
|
|
|
/** |
|
* Parse the input, then return the root node. |
|
* |
|
* @return {Node} |
|
* @api private |
|
*/ |
|
|
|
parse: function(){ |
|
var block = this.parent = this.root; |
|
if (Parser.cache.has(this.hash)) { |
|
block = Parser.cache.get(this.hash); |
|
// normalize cached imports |
|
if ('block' == block.nodeName) block.constructor = nodes.Root; |
|
} else { |
|
while ('eos' != this.peek().type) { |
|
this.skipWhitespace(); |
|
if ('eos' == this.peek().type) break; |
|
var stmt = this.statement(); |
|
this.accept(';'); |
|
if (!stmt) this.error('unexpected token {peek}, not allowed at the root level'); |
|
block.push(stmt); |
|
} |
|
Parser.cache.set(this.hash, block); |
|
} |
|
return block; |
|
}, |
|
|
|
/** |
|
* Throw an `Error` with the given `msg`. |
|
* |
|
* @param {String} msg |
|
* @api private |
|
*/ |
|
|
|
error: function(msg){ |
|
var type = this.peek().type |
|
, val = undefined == this.peek().val |
|
? '' |
|
: ' ' + this.peek().toString(); |
|
if (val.trim() == type.trim()) val = ''; |
|
throw new errors.ParseError(msg.replace('{peek}', '"' + type + val + '"')); |
|
}, |
|
|
|
/** |
|
* Accept the given token `type`, and return it, |
|
* otherwise return `undefined`. |
|
* |
|
* @param {String} type |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
accept: function(type){ |
|
if (type == this.peek().type) { |
|
return this.next(); |
|
} |
|
}, |
|
|
|
/** |
|
* Expect token `type` and return it, throw otherwise. |
|
* |
|
* @param {String} type |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
expect: function(type){ |
|
if (type != this.peek().type) { |
|
this.error('expected "' + type + '", got {peek}'); |
|
} |
|
return this.next(); |
|
}, |
|
|
|
/** |
|
* Get the next token. |
|
* |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
next: function() { |
|
var tok = this.stash.length |
|
? this.stash.pop() |
|
: this.lexer.next() |
|
, line = tok.lineno |
|
, column = tok.column || 1; |
|
|
|
if (tok.val && tok.val.nodeName) { |
|
tok.val.lineno = line; |
|
tok.val.column = column; |
|
} |
|
nodes.lineno = line; |
|
nodes.column = column; |
|
debug.lexer('%s %s', tok.type, tok.val || ''); |
|
return tok; |
|
}, |
|
|
|
/** |
|
* Peek with lookahead(1). |
|
* |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
peek: function() { |
|
return this.lexer.peek(); |
|
}, |
|
|
|
/** |
|
* Lookahead `n` tokens. |
|
* |
|
* @param {Number} n |
|
* @return {Token} |
|
* @api private |
|
*/ |
|
|
|
lookahead: function(n){ |
|
return this.lexer.lookahead(n); |
|
}, |
|
|
|
/** |
|
* Check if the token at `n` is a valid selector token. |
|
* |
|
* @param {Number} n |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
isSelectorToken: function(n) { |
|
var la = this.lookahead(n).type; |
|
switch (la) { |
|
case 'for': |
|
return this.bracketed; |
|
case '[': |
|
this.bracketed = true; |
|
return true; |
|
case ']': |
|
this.bracketed = false; |
|
return true; |
|
default: |
|
return ~selectorTokens.indexOf(la); |
|
} |
|
}, |
|
|
|
/** |
|
* Check if the token at `n` is a pseudo selector. |
|
* |
|
* @param {Number} n |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
isPseudoSelector: function(n){ |
|
var val = this.lookahead(n).val; |
|
return val && ~pseudoSelectors.indexOf(val.name); |
|
}, |
|
|
|
/** |
|
* Check if the current line contains `type`. |
|
* |
|
* @param {String} type |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
lineContains: function(type){ |
|
var i = 1 |
|
, la; |
|
|
|
while (la = this.lookahead(i++)) { |
|
if (~['indent', 'outdent', 'newline', 'eos'].indexOf(la.type)) return; |
|
if (type == la.type) return true; |
|
} |
|
}, |
|
|
|
/** |
|
* Valid selector tokens. |
|
*/ |
|
|
|
selectorToken: function() { |
|
if (this.isSelectorToken(1)) { |
|
if ('{' == this.peek().type) { |
|
// unclosed, must be a block |
|
if (!this.lineContains('}')) return; |
|
// check if ':' is within the braces. |
|
// though not required by Stylus, chances |
|
// are if someone is using {} they will |
|
// use CSS-style props, helping us with |
|
// the ambiguity in this case |
|
var i = 0 |
|
, la; |
|
while (la = this.lookahead(++i)) { |
|
if ('}' == la.type) { |
|
// Check empty block. |
|
if (i == 2 || (i == 3 && this.lookahead(i - 1).type == 'space')) |
|
return; |
|
break; |
|
} |
|
if (':' == la.type) return; |
|
} |
|
} |
|
return this.next(); |
|
} |
|
}, |
|
|
|
/** |
|
* Skip the given `tokens`. |
|
* |
|
* @param {Array} tokens |
|
* @api private |
|
*/ |
|
|
|
skip: function(tokens) { |
|
while (~tokens.indexOf(this.peek().type)) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume whitespace. |
|
*/ |
|
|
|
skipWhitespace: function() { |
|
this.skip(['space', 'indent', 'outdent', 'newline']); |
|
}, |
|
|
|
/** |
|
* Consume newlines. |
|
*/ |
|
|
|
skipNewlines: function() { |
|
while ('newline' == this.peek().type) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume spaces. |
|
*/ |
|
|
|
skipSpaces: function() { |
|
while ('space' == this.peek().type) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Consume spaces and comments. |
|
*/ |
|
|
|
skipSpacesAndComments: function() { |
|
while ('space' == this.peek().type |
|
|| 'comment' == this.peek().type) |
|
this.next(); |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms a function definition, ie trailing |
|
* `{` or indentation. |
|
*/ |
|
|
|
looksLikeFunctionDefinition: function(i) { |
|
return 'indent' == this.lookahead(i).type |
|
|| '{' == this.lookahead(i).type; |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms a selector. |
|
* |
|
* @param {Boolean} [fromProperty] |
|
* @return {Boolean} |
|
* @api private |
|
*/ |
|
|
|
looksLikeSelector: function(fromProperty) { |
|
var i = 1 |
|
, brace; |
|
|
|
// Real property |
|
if (fromProperty && ':' == this.lookahead(i + 1).type |
|
&& (this.lookahead(i + 1).space || 'indent' == this.lookahead(i + 2).type)) |
|
return false; |
|
|
|
// Assume selector when an ident is |
|
// followed by a selector |
|
while ('ident' == this.lookahead(i).type |
|
&& ('newline' == this.lookahead(i + 1).type |
|
|| ',' == this.lookahead(i + 1).type)) i += 2; |
|
|
|
while (this.isSelectorToken(i) |
|
|| ',' == this.lookahead(i).type) { |
|
|
|
if ('selector' == this.lookahead(i).type) |
|
return true; |
|
|
|
if ('&' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
if ('.' == this.lookahead(i).type && 'ident' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
if ('*' == this.lookahead(i).type && 'newline' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// Pseudo-elements |
|
if (':' == this.lookahead(i).type |
|
&& ':' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// #a after an ident and newline |
|
if ('color' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i - 1).type) |
|
return true; |
|
|
|
if (this.looksLikeAttributeSelector(i)) |
|
return true; |
|
|
|
if (('=' == this.lookahead(i).type || 'function' == this.lookahead(i).type) |
|
&& '{' == this.lookahead(i + 1).type) |
|
return false; |
|
|
|
// Hash values inside properties |
|
if (':' == this.lookahead(i).type |
|
&& !this.isPseudoSelector(i + 1) |
|
&& this.lineContains('.')) |
|
return false; |
|
|
|
// the ':' token within braces signifies |
|
// a selector. ex: "foo{bar:'baz'}" |
|
if ('{' == this.lookahead(i).type) brace = true; |
|
else if ('}' == this.lookahead(i).type) brace = false; |
|
if (brace && ':' == this.lookahead(i).type) return true; |
|
|
|
// '{' preceded by a space is considered a selector. |
|
// for example "foo{bar}{baz}" may be a property, |
|
// however "foo{bar} {baz}" is a selector |
|
if ('space' == this.lookahead(i).type |
|
&& '{' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// Assume pseudo selectors are NOT properties |
|
// as 'td:th-child(1)' may look like a property |
|
// and function call to the parser otherwise |
|
if (':' == this.lookahead(i++).type |
|
&& !this.lookahead(i-1).space |
|
&& this.isPseudoSelector(i)) |
|
return true; |
|
|
|
// Trailing space |
|
if ('space' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type |
|
&& '{' == this.lookahead(i + 2).type) |
|
return true; |
|
|
|
if (',' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type) |
|
return true; |
|
} |
|
|
|
// Trailing comma |
|
if (',' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// Trailing brace |
|
if ('{' == this.lookahead(i).type |
|
&& 'newline' == this.lookahead(i + 1).type) |
|
return true; |
|
|
|
// css-style mode, false on ; } |
|
if (this.css) { |
|
if (';' == this.lookahead(i).type || |
|
'}' == this.lookahead(i - 1).type) |
|
return false; |
|
} |
|
|
|
// Trailing separators |
|
while (!~[ |
|
'indent' |
|
, 'outdent' |
|
, 'newline' |
|
, 'for' |
|
, 'if' |
|
, ';' |
|
, '}' |
|
, 'eos'].indexOf(this.lookahead(i).type)) |
|
++i; |
|
|
|
if ('indent' == this.lookahead(i).type) |
|
return true; |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms an attribute selector. |
|
*/ |
|
|
|
looksLikeAttributeSelector: function(n) { |
|
var type = this.lookahead(n).type; |
|
if ('=' == type && this.bracketed) return true; |
|
return ('ident' == type || 'string' == type) |
|
&& ']' == this.lookahead(n + 1).type |
|
&& ('newline' == this.lookahead(n + 2).type || this.isSelectorToken(n + 2)) |
|
&& !this.lineContains(':') |
|
&& !this.lineContains('='); |
|
}, |
|
|
|
/** |
|
* Check if the following sequence of tokens |
|
* forms a keyframe block. |
|
*/ |
|
|
|
looksLikeKeyframe: function() { |
|
var i = 2 |
|
, type; |
|
switch (this.lookahead(i).type) { |
|
case '{': |
|
case 'indent': |
|
case ',': |
|
return true; |
|
case 'newline': |
|
while ('unit' == this.lookahead(++i).type |
|
|| 'newline' == this.lookahead(i).type) ; |
|
type = this.lookahead(i).type; |
|
return 'indent' == type || '{' == type; |
|
} |
|
}, |
|
|
|
/** |
|
* Check if the current state supports selectors. |
|
*/ |
|
|
|
stateAllowsSelector: function() { |
|
switch (this.currentState()) { |
|
case 'root': |
|
case 'atblock': |
|
case 'selector': |
|
case 'conditional': |
|
case 'function': |
|
case 'atrule': |
|
case 'for': |
|
return true; |
|
} |
|
}, |
|
|
|
/** |
|
* Try to assign @block to the node. |
|
* |
|
* @param {Expression} expr |
|
* @private |
|
*/ |
|
|
|
assignAtblock: function(expr) { |
|
try { |
|
expr.push(this.atblock(expr)); |
|
} catch(err) { |
|
this.error('invalid right-hand side operand in assignment, got {peek}'); |
|
} |
|
}, |
|
|
|
/** |
|
* statement |
|
* | statement 'if' expression |
|
* | statement 'unless' expression |
|
*/ |
|
|
|
statement: function() { |
|
var stmt = this.stmt() |
|
, state = this.prevState |
|
, block |
|
, op; |
|
|
|
// special-case statements since it |
|
// is not an expression. We could |
|
// implement postfix conditionals at |
|
// the expression level, however they |
|
// would then fail to enclose properties |
|
if (this.allowPostfix) { |
|
this.allowPostfix = false; |
|
state = 'expression'; |
|
} |
|
|
|
switch (state) { |
|
case 'assignment': |
|
case 'expression': |
|
case 'function arguments': |
|
while (op = |
|
this.accept('if') |
|
|| this.accept('unless') |
|
|| this.accept('for')) { |
|
switch (op.type) { |
|
case 'if': |
|
case 'unless': |
|
stmt = new nodes.If(this.expression(), stmt); |
|
stmt.postfix = true; |
|
stmt.negate = 'unless' == op.type; |
|
this.accept(';'); |
|
break; |
|
case 'for': |
|
var key |
|
, val = this.id().name; |
|
if (this.accept(',')) key = this.id().name; |
|
this.expect('in'); |
|
var each = new nodes.Each(val, key, this.expression()); |
|
block = new nodes.Block(this.parent, each); |
|
block.push(stmt); |
|
each.block = block; |
|
stmt = each; |
|
} |
|
} |
|
} |
|
|
|
return stmt; |
|
}, |
|
|
|
/** |
|
* ident |
|
* | selector |
|
* | literal |
|
* | charset |
|
* | namespace |
|
* | import |
|
* | require |
|
* | media |
|
* | atrule |
|
* | scope |
|
* | keyframes |
|
* | mozdocument |
|
* | for |
|
* | if |
|
* | unless |
|
* | comment |
|
* | expression |
|
* | 'return' expression |
|
*/ |
|
|
|
stmt: function() { |
|
var type = this.peek().type; |
|
switch (type) { |
|
case 'keyframes': |
|
return this.keyframes(); |
|
case '-moz-document': |
|
return this.mozdocument(); |
|
case 'comment': |
|
case 'selector': |
|
case 'literal': |
|
case 'charset': |
|
case 'namespace': |
|
case 'import': |
|
case 'require': |
|
case 'extend': |
|
case 'media': |
|
case 'atrule': |
|
case 'ident': |
|
case 'scope': |
|
case 'supports': |
|
case 'unless': |
|
case 'function': |
|
case 'for': |
|
case 'if': |
|
return this[type](); |
|
case 'return': |
|
return this.return(); |
|
case '{': |
|
return this.property(); |
|
default: |
|
// Contextual selectors |
|
if (this.stateAllowsSelector()) { |
|
switch (type) { |
|
case 'color': |
|
case '~': |
|
case '>': |
|
case '<': |
|
case ':': |
|
case '&': |
|
case '&&': |
|
case '[': |
|
case '.': |
|
case '/': |
|
return this.selector(); |
|
// relative reference |
|
case '..': |
|
if ('/' == this.lookahead(2).type) |
|
return this.selector(); |
|
case '+': |
|
return 'function' == this.lookahead(2).type |
|
? this.functionCall() |
|
: this.selector(); |
|
case '*': |
|
return this.property(); |
|
// keyframe blocks (10%, 20% { ... }) |
|
case 'unit': |
|
if (this.looksLikeKeyframe()) return this.selector(); |
|
case '-': |
|
if ('{' == this.lookahead(2).type) |
|
return this.property(); |
|
} |
|
} |
|
|
|
// Expression fallback |
|
var expr = this.expression(); |
|
if (expr.isEmpty) this.error('unexpected {peek}'); |
|
return expr; |
|
} |
|
}, |
|
|
|
/** |
|
* indent (!outdent)+ outdent |
|
*/ |
|
|
|
block: function(node, scope) { |
|
var delim |
|
, stmt |
|
, next |
|
, block = this.parent = new nodes.Block(this.parent, node); |
|
|
|
if (false === scope) block.scope = false; |
|
|
|
this.accept('newline'); |
|
|
|
// css-style |
|
if (this.accept('{')) { |
|
this.css++; |
|
delim = '}'; |
|
this.skipWhitespace(); |
|
} else { |
|
delim = 'outdent'; |
|
this.expect('indent'); |
|
} |
|
|
|
while (delim != this.peek().type) { |
|
// css-style |
|
if (this.css) { |
|
if (this.accept('newline') || this.accept('indent')) continue; |
|
stmt = this.statement(); |
|
this.accept(';'); |
|
this.skipWhitespace(); |
|
} else { |
|
if (this.accept('newline')) continue; |
|
// skip useless indents and comments |
|
next = this.lookahead(2).type; |
|
if ('indent' == this.peek().type |
|
&& ~['outdent', 'newline', 'comment'].indexOf(next)) { |
|
this.skip(['indent', 'outdent']); |
|
continue; |
|
} |
|
if ('eos' == this.peek().type) return block; |
|
stmt = this.statement(); |
|
this.accept(';'); |
|
} |
|
if (!stmt) this.error('unexpected token {peek} in block'); |
|
block.push(stmt); |
|
} |
|
|
|
// css-style |
|
if (this.css) { |
|
this.skipWhitespace(); |
|
this.expect('}'); |
|
this.skipSpaces(); |
|
this.css--; |
|
} else { |
|
this.expect('outdent'); |
|
} |
|
|
|
this.parent = block.parent; |
|
return block; |
|
}, |
|
|
|
/** |
|
* comment space* |
|
*/ |
|
|
|
comment: function(){ |
|
var node = this.next().val; |
|
this.skipSpaces(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* for val (',' key) in expr |
|
*/ |
|
|
|
for: function() { |
|
this.expect('for'); |
|
var key |
|
, val = this.id().name; |
|
if (this.accept(',')) key = this.id().name; |
|
this.expect('in'); |
|
this.state.push('for'); |
|
this.cond = true; |
|
var each = new nodes.Each(val, key, this.expression()); |
|
this.cond = false; |
|
each.block = this.block(each, false); |
|
this.state.pop(); |
|
return each; |
|
}, |
|
|
|
/** |
|
* return expression |
|
*/ |
|
|
|
return: function() { |
|
this.expect('return'); |
|
var expr = this.expression(); |
|
return expr.isEmpty |
|
? new nodes.Return |
|
: new nodes.Return(expr); |
|
}, |
|
|
|
/** |
|
* unless expression block |
|
*/ |
|
|
|
unless: function() { |
|
this.expect('unless'); |
|
this.state.push('conditional'); |
|
this.cond = true; |
|
var node = new nodes.If(this.expression(), true); |
|
this.cond = false; |
|
node.block = this.block(node, false); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* if expression block (else block)? |
|
*/ |
|
|
|
if: function() { |
|
this.expect('if'); |
|
this.state.push('conditional'); |
|
this.cond = true; |
|
var node = new nodes.If(this.expression()) |
|
, cond |
|
, block; |
|
this.cond = false; |
|
node.block = this.block(node, false); |
|
this.skip(['newline', 'comment']); |
|
while (this.accept('else')) { |
|
if (this.accept('if')) { |
|
this.cond = true; |
|
cond = this.expression(); |
|
this.cond = false; |
|
block = this.block(node, false); |
|
node.elses.push(new nodes.If(cond, block)); |
|
} else { |
|
node.elses.push(this.block(node, false)); |
|
break; |
|
} |
|
this.skip(['newline', 'comment']); |
|
} |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* @block |
|
* |
|
* @param {Expression} [node] |
|
*/ |
|
|
|
atblock: function(node){ |
|
if (!node) this.expect('atblock'); |
|
node = new nodes.Atblock; |
|
this.state.push('atblock'); |
|
node.block = this.block(node, false); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* atrule selector? block? |
|
*/ |
|
|
|
atrule: function(){ |
|
var type = this.expect('atrule').val |
|
, node = new nodes.Atrule(type) |
|
, tok; |
|
this.skipSpacesAndComments(); |
|
node.segments = this.selectorParts(); |
|
this.skipSpacesAndComments(); |
|
tok = this.peek().type; |
|
if ('indent' == tok || '{' == tok || ('newline' == tok |
|
&& '{' == this.lookahead(2).type)) { |
|
this.state.push('atrule'); |
|
node.block = this.block(node); |
|
this.state.pop(); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* scope |
|
*/ |
|
|
|
scope: function(){ |
|
this.expect('scope'); |
|
var selector = this.selectorParts() |
|
.map(function(selector) { return selector.val; }) |
|
.join(''); |
|
this.selectorScope = selector.trim(); |
|
return nodes.null; |
|
}, |
|
|
|
/** |
|
* supports |
|
*/ |
|
|
|
supports: function(){ |
|
this.expect('supports'); |
|
var node = new nodes.Supports(this.supportsCondition()); |
|
this.state.push('atrule'); |
|
node.block = this.block(node); |
|
this.state.pop(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* supports negation |
|
* | supports op |
|
* | expression |
|
*/ |
|
|
|
supportsCondition: function(){ |
|
var node = this.supportsNegation() |
|
|| this.supportsOp(); |
|
if (!node) { |
|
this.cond = true; |
|
node = this.expression(); |
|
this.cond = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* 'not' supports feature |
|
*/ |
|
|
|
supportsNegation: function(){ |
|
if (this.accept('not')) { |
|
var node = new nodes.Expression; |
|
node.push(new nodes.Literal('not')); |
|
node.push(this.supportsFeature()); |
|
return node; |
|
} |
|
}, |
|
|
|
/** |
|
* supports feature (('and' | 'or') supports feature)+ |
|
*/ |
|
|
|
supportsOp: function(){ |
|
var feature = this.supportsFeature() |
|
, op |
|
, expr; |
|
if (feature) { |
|
expr = new nodes.Expression; |
|
expr.push(feature); |
|
while (op = this.accept('&&') || this.accept('||')) { |
|
expr.push(new nodes.Literal('&&' == op.val ? 'and' : 'or')); |
|
expr.push(this.supportsFeature()); |
|
} |
|
return expr; |
|
} |
|
}, |
|
|
|
/** |
|
* ('(' supports condition ')') |
|
* | feature |
|
*/ |
|
|
|
supportsFeature: function(){ |
|
this.skipSpacesAndComments(); |
|
if ('(' == this.peek().type) { |
|
var la = this.lookahead(2).type; |
|
|
|
if ('ident' == la || '{' == la) { |
|
return this.feature(); |
|
} else { |
|
this.expect('('); |
|
var node = new nodes.Expression; |
|
node.push(new nodes.Literal('(')); |
|
node.push(this.supportsCondition()); |
|
this.expect(')') |
|
node.push(new nodes.Literal(')')); |
|
this.skipSpacesAndComments(); |
|
return node; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* extend |
|
*/ |
|
|
|
extend: function(){ |
|
var tok = this.expect('extend') |
|
, selectors = [] |
|
, sel |
|
, node |
|
, arr; |
|
|
|
do { |
|
arr = this.selectorParts(); |
|
|
|
if (!arr.length) continue; |
|
|
|
sel = new nodes.Selector(arr); |
|
selectors.push(sel); |
|
|
|
if ('!' !== this.peek().type) continue; |
|
|
|
tok = this.lookahead(2); |
|
if ('ident' !== tok.type || 'optional' !== tok.val.name) continue; |
|
|
|
this.skip(['!', 'ident']); |
|
sel.optional = true; |
|
} while(this.accept(',')); |
|
|
|
node = new nodes.Extend(selectors); |
|
node.lineno = tok.lineno; |
|
node.column = tok.column; |
|
return node; |
|
}, |
|
|
|
/** |
|
* media queries |
|
*/ |
|
|
|
media: function() { |
|
this.expect('media'); |
|
this.state.push('atrule'); |
|
var media = new nodes.Media(this.queries()); |
|
media.block = this.block(media); |
|
this.state.pop(); |
|
return media; |
|
}, |
|
|
|
/** |
|
* query (',' query)* |
|
*/ |
|
|
|
queries: function() { |
|
var queries = new nodes.QueryList |
|
, skip = ['comment', 'newline', 'space']; |
|
|
|
do { |
|
this.skip(skip); |
|
queries.push(this.query()); |
|
this.skip(skip); |
|
} while (this.accept(',')); |
|
return queries; |
|
}, |
|
|
|
/** |
|
* expression |
|
* | (ident | 'not')? ident ('and' feature)* |
|
* | feature ('and' feature)* |
|
*/ |
|
|
|
query: function() { |
|
var query = new nodes.Query |
|
, expr |
|
, pred |
|
, id; |
|
|
|
// hash values support |
|
if ('ident' == this.peek().type |
|
&& ('.' == this.lookahead(2).type |
|
|| '[' == this.lookahead(2).type)) { |
|
this.cond = true; |
|
expr = this.expression(); |
|
this.cond = false; |
|
query.push(new nodes.Feature(expr.nodes)); |
|
return query; |
|
} |
|
|
|
if (pred = this.accept('ident') || this.accept('not')) { |
|
pred = new nodes.Literal(pred.val.string || pred.val); |
|
|
|
this.skipSpacesAndComments(); |
|
if (id = this.accept('ident')) { |
|
query.type = id.val; |
|
query.predicate = pred; |
|
} else { |
|
query.type = pred; |
|
} |
|
this.skipSpacesAndComments(); |
|
|
|
if (!this.accept('&&')) return query; |
|
} |
|
|
|
do { |
|
query.push(this.feature()); |
|
} while (this.accept('&&')); |
|
|
|
return query; |
|
}, |
|
|
|
/** |
|
* '(' ident ( ':'? expression )? ')' |
|
*/ |
|
|
|
feature: function() { |
|
this.skipSpacesAndComments(); |
|
this.expect('('); |
|
this.skipSpacesAndComments(); |
|
var node = new nodes.Feature(this.interpolate()); |
|
this.skipSpacesAndComments(); |
|
this.accept(':') |
|
this.skipSpacesAndComments(); |
|
this.inProperty = true; |
|
node.expr = this.list(); |
|
this.inProperty = false; |
|
this.skipSpacesAndComments(); |
|
this.expect(')'); |
|
this.skipSpacesAndComments(); |
|
return node; |
|
}, |
|
|
|
/** |
|
* @-moz-document call (',' call)* block |
|
*/ |
|
|
|
mozdocument: function(){ |
|
this.expect('-moz-document'); |
|
var mozdocument = new nodes.Atrule('-moz-document') |
|
, calls = []; |
|
do { |
|
this.skipSpacesAndComments(); |
|
calls.push(this.functionCall()); |
|
this.skipSpacesAndComments(); |
|
} while (this.accept(',')); |
|
mozdocument.segments = [new nodes.Literal(calls.join(', '))]; |
|
this.state.push('atrule'); |
|
mozdocument.block = this.block(mozdocument, false); |
|
this.state.pop(); |
|
return mozdocument; |
|
}, |
|
|
|
/** |
|
* import expression |
|
*/ |
|
|
|
import: function() { |
|
this.expect('import'); |
|
this.allowPostfix = true; |
|
return new nodes.Import(this.expression(), false); |
|
}, |
|
|
|
/** |
|
* require expression |
|
*/ |
|
|
|
require: function() { |
|
this.expect('require'); |
|
this.allowPostfix = true; |
|
return new nodes.Import(this.expression(), true); |
|
}, |
|
|
|
/** |
|
* charset string |
|
*/ |
|
|
|
charset: function() { |
|
this.expect('charset'); |
|
var str = this.expect('string').val; |
|
this.allowPostfix = true; |
|
return new nodes.Charset(str); |
|
}, |
|
|
|
/** |
|
* namespace ident? (string | url) |
|
*/ |
|
|
|
namespace: function() { |
|
var str |
|
, prefix; |
|
this.expect('namespace'); |
|
|
|
this.skipSpacesAndComments(); |
|
if (prefix = this.accept('ident')) { |
|
prefix = prefix.val; |
|
} |
|
this.skipSpacesAndComments(); |
|
|
|
str = this.accept('string') || this.url(); |
|
this.allowPostfix = true; |
|
return new nodes.Namespace(str, prefix); |
|
}, |
|
|
|
/** |
|
* keyframes name block |
|
*/ |
|
|
|
keyframes: function() { |
|
var tok = this.expect('keyframes') |
|
, keyframes; |
|
|
|
this.skipSpacesAndComments(); |
|
keyframes = new nodes.Keyframes(this.selectorParts(), tok.val); |
|
this.skipSpacesAndComments(); |
|
|
|
// block |
|
this.state.push('atrule'); |
|
keyframes.block = this.block(keyframes); |
|
this.state.pop(); |
|
|
|
return keyframes; |
|
}, |
|
|
|
/** |
|
* literal |
|
*/ |
|
|
|
literal: function() { |
|
return this.expect('literal').val; |
|
}, |
|
|
|
/** |
|
* ident space? |
|
*/ |
|
|
|
id: function() { |
|
var tok = this.expect('ident'); |
|
this.accept('space'); |
|
return tok.val; |
|
}, |
|
|
|
/** |
|
* ident |
|
* | assignment |
|
* | property |
|
* | selector |
|
*/ |
|
|
|
ident: function() { |
|
var i = 2 |
|
, la = this.lookahead(i).type; |
|
|
|
while ('space' == la) la = this.lookahead(++i).type; |
|
|
|
switch (la) { |
|
// Assignment |
|
case '=': |
|
case '?=': |
|
case '-=': |
|
case '+=': |
|
case '*=': |
|
case '/=': |
|
case '%=': |
|
return this.assignment(); |
|
// Member |
|
case '.': |
|
if ('space' == this.lookahead(i - 1).type) return this.selector(); |
|
if (this._ident == this.peek()) return this.id(); |
|
while ('=' != this.lookahead(++i).type |
|
&& !~['[', ',', 'newline', 'indent', 'eos'].indexOf(this.lookahead(i).type)) ; |
|
if ('=' == this.lookahead(i).type) { |
|
this._ident = this.peek(); |
|
return this.expression(); |
|
} else if (this.looksLikeSelector() && this.stateAllowsSelector()) { |
|
return this.selector(); |
|
} |
|
// Assignment []= |
|
case '[': |
|
if (this._ident == this.peek()) return this.id(); |
|
while (']' != this.lookahead(i++).type |
|
&& 'selector' != this.lookahead(i).type |
|
&& 'eos' != this.lookahead(i).type) ; |
|
if ('=' == this.lookahead(i).type) { |
|
this._ident = this.peek(); |
|
return this.expression(); |
|
} else if (this.looksLikeSelector() && this.stateAllowsSelector()) { |
|
return this.selector(); |
|
} |
|
// Operation |
|
case '-': |
|
case '+': |
|
case '/': |
|
case '*': |
|
case '%': |
|
case '**': |
|
case '&&': |
|
case '||': |
|
case '>': |
|
case '<': |
|
case '>=': |
|
case '<=': |
|
case '!=': |
|
case '==': |
|
case '?': |
|
case 'in': |
|
case 'is a': |
|
case 'is defined': |
|
// Prevent cyclic .ident, return literal |
|
if (this._ident == this.peek()) { |
|
return this.id(); |
|
} else { |
|
this._ident = this.peek(); |
|
switch (this.currentState()) { |
|
// unary op or selector in property / for |
|
case 'for': |
|
case 'selector': |
|
return this.property(); |
|
// Part of a selector |
|
case 'root': |
|
case 'atblock': |
|
case 'atrule': |
|
return '[' == la |
|
? this.subscript() |
|
: this.selector(); |
|
case 'function': |
|
case 'conditional': |
|
return this.looksLikeSelector() |
|
? this.selector() |
|
: this.expression(); |
|
// Do not disrupt the ident when an operand |
|
default: |
|
return this.operand |
|
? this.id() |
|
: this.expression(); |
|
} |
|
} |
|
// Selector or property |
|
default: |
|
switch (this.currentState()) { |
|
case 'root': |
|
return this.selector(); |
|
case 'for': |
|
case 'selector': |
|
case 'function': |
|
case 'conditional': |
|
case 'atblock': |
|
case 'atrule': |
|
return this.property(); |
|
default: |
|
var id = this.id(); |
|
if ('interpolation' == this.previousState()) id.mixin = true; |
|
return id; |
|
} |
|
} |
|
}, |
|
|
|
/** |
|
* '*'? (ident | '{' expression '}')+ |
|
*/ |
|
|
|
interpolate: function() { |
|
var node |
|
, segs = [] |
|
, star; |
|
|
|
star = this.accept('*'); |
|
if (star) segs.push(new nodes.Literal('*')); |
|
|
|
while (true) { |
|
if (this.accept('{')) { |
|
this.state.push('interpolation'); |
|
segs.push(this.expression()); |
|
this.expect('}'); |
|
this.state.pop(); |
|
} else if (node = this.accept('-')){ |
|
segs.push(new nodes.Literal('-')); |
|
} else if (node = this.accept('ident')){ |
|
segs.push(node.val); |
|
} else { |
|
break; |
|
} |
|
} |
|
if (!segs.length) this.expect('ident'); |
|
return segs; |
|
}, |
|
|
|
/** |
|
* property ':'? expression |
|
* | ident |
|
*/ |
|
|
|
property: function() { |
|
if (this.looksLikeSelector(true)) return this.selector(); |
|
|
|
// property |
|
var ident = this.interpolate() |
|
, prop = new nodes.Property(ident) |
|
, ret = prop; |
|
|
|
// optional ':' |
|
this.accept('space'); |
|
if (this.accept(':')) this.accept('space'); |
|
|
|
this.state.push('property'); |
|
this.inProperty = true; |
|
prop.expr = this.list(); |
|
if (prop.expr.isEmpty) ret = ident[0]; |
|
this.inProperty = false; |
|
this.allowPostfix = true; |
|
this.state.pop(); |
|
|
|
// optional ';' |
|
this.accept(';'); |
|
|
|
return ret; |
|
}, |
|
|
|
/** |
|
* selector ',' selector |
|
* | selector newline selector |
|
* | selector block |
|
*/ |
|
|
|
selector: function() { |
|
var arr |
|
, group = new nodes.Group |
|
, scope = this.selectorScope |
|
, isRoot = 'root' == this.currentState() |
|
, selector; |
|
|
|
do { |
|
// Clobber newline after , |
|
this.accept('newline'); |
|
|
|
arr = this.selectorParts(); |
|
|
|
// Push the selector |
|
if (isRoot && scope) arr.unshift(new nodes.Literal(scope + ' ')); |
|
if (arr.length) { |
|
selector = new nodes.Selector(arr); |
|
selector.lineno = arr[0].lineno; |
|
selector.column = arr[0].column; |
|
group.push(selector); |
|
} |
|
} while (this.accept(',') || this.accept('newline')); |
|
|
|
if ('selector-parts' == this.currentState()) return group.nodes; |
|
|
|
this.state.push('selector'); |
|
group.block = this.block(group); |
|
this.state.pop(); |
|
|
|
return group; |
|
}, |
|
|
|
selectorParts: function(){ |
|
var tok |
|
, arr = []; |
|
|
|
// Selector candidates, |
|
// stitched together to |
|
// form a selector. |
|
while (tok = this.selectorToken()) { |
|
debug.selector('%s', tok); |
|
// Selector component |
|
switch (tok.type) { |
|
case '{': |
|
this.skipSpaces(); |
|
var expr = this.expression(); |
|
this.skipSpaces(); |
|
this.expect('}'); |
|
arr.push(expr); |
|
break; |
|
case this.prefix && '.': |
|
var literal = new nodes.Literal(tok.val + this.prefix); |
|
literal.prefixed = true; |
|
arr.push(literal); |
|
break; |
|
case 'comment': |
|
// ignore comments |
|
break; |
|
case 'color': |
|
case 'unit': |
|
arr.push(new nodes.Literal(tok.val.raw)); |
|
break; |
|
case 'space': |
|
arr.push(new nodes.Literal(' ')); |
|
break; |
|
case 'function': |
|
arr.push(new nodes.Literal(tok.val.name + '(')); |
|
break; |
|
case 'ident': |
|
arr.push(new nodes.Literal(tok.val.name || tok.val.string)); |
|
break; |
|
default: |
|
arr.push(new nodes.Literal(tok.val)); |
|
if (tok.space) arr.push(new nodes.Literal(' ')); |
|
} |
|
} |
|
|
|
return arr; |
|
}, |
|
|
|
/** |
|
* ident ('=' | '?=') expression |
|
*/ |
|
|
|
assignment: function() { |
|
var op |
|
, node |
|
, name = this.id().name; |
|
|
|
if (op = |
|
this.accept('=') |
|
|| this.accept('?=') |
|
|| this.accept('+=') |
|
|| this.accept('-=') |
|
|| this.accept('*=') |
|
|| this.accept('/=') |
|
|| this.accept('%=')) { |
|
this.state.push('assignment'); |
|
var expr = this.list(); |
|
// @block support |
|
if (expr.isEmpty) this.assignAtblock(expr); |
|
node = new nodes.Ident(name, expr); |
|
this.state.pop(); |
|
|
|
switch (op.type) { |
|
case '?=': |
|
var defined = new nodes.BinOp('is defined', node) |
|
, lookup = new nodes.Ident(name); |
|
node = new nodes.Ternary(defined, lookup, node); |
|
break; |
|
case '+=': |
|
case '-=': |
|
case '*=': |
|
case '/=': |
|
case '%=': |
|
node.val = new nodes.BinOp(op.type[0], new nodes.Ident(name), expr); |
|
break; |
|
} |
|
} |
|
|
|
return node; |
|
}, |
|
|
|
/** |
|
* definition |
|
* | call |
|
*/ |
|
|
|
function: function() { |
|
var parens = 1 |
|
, i = 2 |
|
, tok; |
|
|
|
// Lookahead and determine if we are dealing |
|
// with a function call or definition. Here |
|
// we pair parens to prevent false negatives |
|
out: |
|
while (tok = this.lookahead(i++)) { |
|
switch (tok.type) { |
|
case 'function': |
|
case '(': |
|
++parens; |
|
break; |
|
case ')': |
|
if (!--parens) break out; |
|
break; |
|
case 'eos': |
|
this.error('failed to find closing paren ")"'); |
|
} |
|
} |
|
|
|
// Definition or call |
|
switch (this.currentState()) { |
|
case 'expression': |
|
return this.functionCall(); |
|
default: |
|
return this.looksLikeFunctionDefinition(i) |
|
? this.functionDefinition() |
|
: this.expression(); |
|
} |
|
}, |
|
|
|
/** |
|
* url '(' (expression | urlchars)+ ')' |
|
*/ |
|
|
|
url: function() { |
|
this.expect('function'); |
|
this.state.push('function arguments'); |
|
var args = this.args(); |
|
this.expect(')'); |
|
this.state.pop(); |
|
return new nodes.Call('url', args); |
|
}, |
|
|
|
/** |
|
* '+'? ident '(' expression ')' block? |
|
*/ |
|
|
|
functionCall: function() { |
|
var withBlock = this.accept('+'); |
|
if ('url' == this.peek().val.name) return this.url(); |
|
var name = this.expect('function').val.name; |
|
this.state.push('function arguments'); |
|
this.parens++; |
|
var args = this.args(); |
|
this.expect(')'); |
|
this.parens--; |
|
this.state.pop(); |
|
var call = new nodes.Call(name, args); |
|
if (withBlock) { |
|
this.state.push('function'); |
|
call.block = this.block(call); |
|
this.state.pop(); |
|
} |
|
return call; |
|
}, |
|
|
|
/** |
|
* ident '(' params ')' block |
|
*/ |
|
|
|
functionDefinition: function() { |
|
var name = this.expect('function').val.name; |
|
|
|
// params |
|
this.state.push('function params'); |
|
this.skipWhitespace(); |
|
var params = this.params(); |
|
this.skipWhitespace(); |
|
this.expect(')'); |
|
this.state.pop(); |
|
|
|
// Body |
|
this.state.push('function'); |
|
var fn = new nodes.Function(name, params); |
|
fn.block = this.block(fn); |
|
this.state.pop(); |
|
return new nodes.Ident(name, fn); |
|
}, |
|
|
|
/** |
|
* ident |
|
* | ident '...' |
|
* | ident '=' expression |
|
* | ident ',' ident |
|
*/ |
|
|
|
params: function() { |
|
var tok |
|
, node |
|
, params = new nodes.Params; |
|
while (tok = this.accept('ident')) { |
|
this.accept('space'); |
|
params.push(node = tok.val); |
|
if (this.accept('...')) { |
|
node.rest = true; |
|
} else if (this.accept('=')) { |
|
node.val = this.expression(); |
|
} |
|
this.skipWhitespace(); |
|
this.accept(','); |
|
this.skipWhitespace(); |
|
} |
|
return params; |
|
}, |
|
|
|
/** |
|
* (ident ':')? expression (',' (ident ':')? expression)* |
|
*/ |
|
|
|
args: function() { |
|
var args = new nodes.Arguments |
|
, keyword; |
|
|
|
do { |
|
// keyword |
|
if ('ident' == this.peek().type && ':' == this.lookahead(2).type) { |
|
keyword = this.next().val.string; |
|
this.expect(':'); |
|
args.map[keyword] = this.expression(); |
|
// arg |
|
} else { |
|
args.push(this.expression()); |
|
} |
|
} while (this.accept(',')); |
|
|
|
return args; |
|
}, |
|
|
|
/** |
|
* expression (',' expression)* |
|
*/ |
|
|
|
list: function() { |
|
var node = this.expression(); |
|
|
|
while (this.accept(',')) { |
|
if (node.isList) { |
|
list.push(this.expression()); |
|
} else { |
|
var list = new nodes.Expression(true); |
|
list.push(node); |
|
list.push(this.expression()); |
|
node = list; |
|
} |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* negation+ |
|
*/ |
|
|
|
expression: function() { |
|
var node |
|
, expr = new nodes.Expression; |
|
this.state.push('expression'); |
|
while (node = this.negation()) { |
|
if (!node) this.error('unexpected token {peek} in expression'); |
|
expr.push(node); |
|
} |
|
this.state.pop(); |
|
if (expr.nodes.length) { |
|
expr.lineno = expr.nodes[0].lineno; |
|
expr.column = expr.nodes[0].column; |
|
} |
|
return expr; |
|
}, |
|
|
|
/** |
|
* 'not' ternary |
|
* | ternary |
|
*/ |
|
|
|
negation: function() { |
|
if (this.accept('not')) { |
|
return new nodes.UnaryOp('!', this.negation()); |
|
} |
|
return this.ternary(); |
|
}, |
|
|
|
/** |
|
* logical ('?' expression ':' expression)? |
|
*/ |
|
|
|
ternary: function() { |
|
var node = this.logical(); |
|
if (this.accept('?')) { |
|
var trueExpr = this.expression(); |
|
this.expect(':'); |
|
var falseExpr = this.expression(); |
|
node = new nodes.Ternary(node, trueExpr, falseExpr); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* typecheck (('&&' | '||') typecheck)* |
|
*/ |
|
|
|
logical: function() { |
|
var op |
|
, node = this.typecheck(); |
|
while (op = this.accept('&&') || this.accept('||')) { |
|
node = new nodes.BinOp(op.type, node, this.typecheck()); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* equality ('is a' equality)* |
|
*/ |
|
|
|
typecheck: function() { |
|
var op |
|
, node = this.equality(); |
|
while (op = this.accept('is a')) { |
|
this.operand = true; |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
node = new nodes.BinOp(op.type, node, this.equality()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* in (('==' | '!=') in)* |
|
*/ |
|
|
|
equality: function() { |
|
var op |
|
, node = this.in(); |
|
while (op = this.accept('==') || this.accept('!=')) { |
|
this.operand = true; |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
node = new nodes.BinOp(op.type, node, this.in()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* relational ('in' relational)* |
|
*/ |
|
|
|
in: function() { |
|
var node = this.relational(); |
|
while (this.accept('in')) { |
|
this.operand = true; |
|
if (!node) this.error('illegal unary "in", missing left-hand operand'); |
|
node = new nodes.BinOp('in', node, this.relational()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* range (('>=' | '<=' | '>' | '<') range)* |
|
*/ |
|
|
|
relational: function() { |
|
var op |
|
, node = this.range(); |
|
while (op = |
|
this.accept('>=') |
|
|| this.accept('<=') |
|
|| this.accept('<') |
|
|| this.accept('>') |
|
) { |
|
this.operand = true; |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
node = new nodes.BinOp(op.type, node, this.range()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* additive (('..' | '...') additive)* |
|
*/ |
|
|
|
range: function() { |
|
var op |
|
, node = this.additive(); |
|
if (op = this.accept('...') || this.accept('..')) { |
|
this.operand = true; |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
node = new nodes.BinOp(op.val, node, this.additive()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* multiplicative (('+' | '-') multiplicative)* |
|
*/ |
|
|
|
additive: function() { |
|
var op |
|
, node = this.multiplicative(); |
|
while (op = this.accept('+') || this.accept('-')) { |
|
this.operand = true; |
|
node = new nodes.BinOp(op.type, node, this.multiplicative()); |
|
this.operand = false; |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* defined (('**' | '*' | '/' | '%') defined)* |
|
*/ |
|
|
|
multiplicative: function() { |
|
var op |
|
, node = this.defined(); |
|
while (op = |
|
this.accept('**') |
|
|| this.accept('*') |
|
|| this.accept('/') |
|
|| this.accept('%')) { |
|
this.operand = true; |
|
if ('/' == op && this.inProperty && !this.parens) { |
|
this.stash.push(new Token('literal', new nodes.Literal('/'))); |
|
this.operand = false; |
|
return node; |
|
} else { |
|
if (!node) this.error('illegal unary "' + op + '", missing left-hand operand'); |
|
node = new nodes.BinOp(op.type, node, this.defined()); |
|
this.operand = false; |
|
} |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* unary 'is defined' |
|
* | unary |
|
*/ |
|
|
|
defined: function() { |
|
var node = this.unary(); |
|
if (this.accept('is defined')) { |
|
if (!node) this.error('illegal unary "is defined", missing left-hand operand'); |
|
node = new nodes.BinOp('is defined', node); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* ('!' | '~' | '+' | '-') unary |
|
* | subscript |
|
*/ |
|
|
|
unary: function() { |
|
var op |
|
, node; |
|
if (op = |
|
this.accept('!') |
|
|| this.accept('~') |
|
|| this.accept('+') |
|
|| this.accept('-')) { |
|
this.operand = true; |
|
node = this.unary(); |
|
if (!node) this.error('illegal unary "' + op + '"'); |
|
node = new nodes.UnaryOp(op.type, node); |
|
this.operand = false; |
|
return node; |
|
} |
|
return this.subscript(); |
|
}, |
|
|
|
/** |
|
* member ('[' expression ']')+ '='? |
|
* | member |
|
*/ |
|
|
|
subscript: function() { |
|
var node = this.member() |
|
, id; |
|
while (this.accept('[')) { |
|
node = new nodes.BinOp('[]', node, this.expression()); |
|
this.expect(']'); |
|
} |
|
// TODO: TernaryOp :) |
|
if (this.accept('=')) { |
|
node.op += '='; |
|
node.val = this.list(); |
|
// @block support |
|
if (node.val.isEmpty) this.assignAtblock(node.val); |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* primary ('.' id)+ '='? |
|
* | primary |
|
*/ |
|
|
|
member: function() { |
|
var node = this.primary(); |
|
if (node) { |
|
while (this.accept('.')) { |
|
var id = new nodes.Ident(this.expect('ident').val.string); |
|
node = new nodes.Member(node, id); |
|
} |
|
this.skipSpaces(); |
|
if (this.accept('=')) { |
|
node.val = this.list(); |
|
// @block support |
|
if (node.val.isEmpty) this.assignAtblock(node.val); |
|
} |
|
} |
|
return node; |
|
}, |
|
|
|
/** |
|
* '{' '}' |
|
* | '{' pair (ws pair)* '}' |
|
*/ |
|
|
|
object: function(){ |
|
var obj = new nodes.Object |
|
, id, val, comma; |
|
this.expect('{'); |
|
this.skipWhitespace(); |
|
|
|
while (!this.accept('}')) { |
|
if (this.accept('comment') |
|
|| this.accept('newline')) continue; |
|
|
|
if (!comma) this.accept(','); |
|
id = this.accept('ident') || this.accept('string'); |
|
if (!id) this.error('expected "ident" or "string", got {peek}'); |
|
id = id.val.hash; |
|
this.skipSpacesAndComments(); |
|
this.expect(':'); |
|
val = this.expression(); |
|
obj.set(id, val); |
|
comma = this.accept(','); |
|
this.skipWhitespace(); |
|
} |
|
|
|
return obj; |
|
}, |
|
|
|
/** |
|
* unit |
|
* | null |
|
* | color |
|
* | string |
|
* | ident |
|
* | boolean |
|
* | literal |
|
* | object |
|
* | atblock |
|
* | atrule |
|
* | '(' expression ')' '%'? |
|
*/ |
|
|
|
primary: function() { |
|
var tok; |
|
this.skipSpaces(); |
|
|
|
// Parenthesis |
|
if (this.accept('(')) { |
|
++this.parens; |
|
var expr = this.expression() |
|
, paren = this.expect(')'); |
|
--this.parens; |
|
if (this.accept('%')) expr.push(new nodes.Ident('%')); |
|
tok = this.peek(); |
|
// (1 + 2)px, (1 + 2)em, etc. |
|
if (!paren.space |
|
&& 'ident' == tok.type |
|
&& ~units.indexOf(tok.val.string)) { |
|
expr.push(new nodes.Ident(tok.val.string)); |
|
this.next(); |
|
} |
|
return expr; |
|
} |
|
|
|
tok = this.peek(); |
|
|
|
// Primitive |
|
switch (tok.type) { |
|
case 'null': |
|
case 'unit': |
|
case 'color': |
|
case 'string': |
|
case 'literal': |
|
case 'boolean': |
|
case 'comment': |
|
return this.next().val; |
|
case !this.cond && '{': |
|
return this.object(); |
|
case 'atblock': |
|
return this.atblock(); |
|
// property lookup |
|
case 'atrule': |
|
var id = new nodes.Ident(this.next().val); |
|
id.property = true; |
|
return id; |
|
case 'ident': |
|
return this.ident(); |
|
case 'function': |
|
return tok.anonymous |
|
? this.functionDefinition() |
|
: this.functionCall(); |
|
} |
|
} |
|
};
|
|
|