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.

2181 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();
}
}
};