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.

1611 lines
35 KiB

/*!
* Stylus - Evaluator
* Copyright (c) Automattic <developer.wordpress.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Visitor = require('./')
, units = require('../units')
, nodes = require('../nodes')
, Stack = require('../stack')
, Frame = require('../stack/frame')
, utils = require('../utils')
, bifs = require('../functions')
, dirname = require('path').dirname
, colors = require('../colors')
, debug = require('debug')('stylus:evaluator')
, fs = require('fs');
/**
* Import `file` and return Block node.
*
* @api private
*/
function importFile(node, file, literal) {
var importStack = this.importStack
, Parser = require('../parser')
, stat;
// Handling the `require`
if (node.once) {
if (this.requireHistory[file]) return nodes.null;
this.requireHistory[file] = true;
if (literal && !this.includeCSS) {
return node;
}
}
// Avoid overflows from importing the same file over again
if (~importStack.indexOf(file))
throw new Error('import loop has been found');
var str = fs.readFileSync(file, 'utf8');
// shortcut for empty files
if (!str.trim()) return nodes.null;
// Expose imports
node.path = file;
node.dirname = dirname(file);
// Store the modified time
stat = fs.statSync(file);
node.mtime = stat.mtime;
this.paths.push(node.dirname);
if (this.options._imports) this.options._imports.push(node.clone());
// Parse the file
importStack.push(file);
nodes.filename = file;
if (literal) {
literal = new nodes.Literal(str.replace(/\r\n?/g, '\n'));
literal.lineno = literal.column = 1;
if (!this.resolveURL) return literal;
}
// parse
var block = new nodes.Block
, parser = new Parser(str, utils.merge({ root: block }, this.options));
try {
block = parser.parse();
} catch (err) {
var line = parser.lexer.lineno
, column = parser.lexer.column;
if (literal && this.includeCSS && this.resolveURL) {
this.warn('ParseError: ' + file + ':' + line + ':' + column + '. This file included as-is');
return literal;
} else {
err.filename = file;
err.lineno = line;
err.column = column;
err.input = str;
throw err;
}
}
// Evaluate imported "root"
block = block.clone(this.currentBlock);
block.parent = this.currentBlock;
block.scope = false;
var ret = this.visit(block);
importStack.pop();
if (!this.resolveURL || this.resolveURL.nocheck) this.paths.pop();
return ret;
}
/**
* Initialize a new `Evaluator` with the given `root` Node
* and the following `options`.
*
* Options:
*
* - `compress` Compress the css output, defaults to false
* - `warn` Warn the user of duplicate function definitions etc
*
* @param {Node} root
* @api private
*/
var Evaluator = module.exports = function Evaluator(root, options) {
options = options || {};
Visitor.call(this, root);
var functions = this.functions = options.functions || {};
this.stack = new Stack;
this.imports = options.imports || [];
this.globals = options.globals || {};
this.paths = options.paths || [];
this.prefix = options.prefix || '';
this.filename = options.filename;
this.includeCSS = options['include css'];
this.resolveURL = functions.url
&& 'resolver' == functions.url.name
&& functions.url.options;
this.paths.push(dirname(options.filename || '.'));
this.stack.push(this.global = new Frame(root));
this.warnings = options.warn;
this.options = options;
this.calling = []; // TODO: remove, use stack
this.importStack = [];
this.requireHistory = {};
this.return = 0;
};
/**
* Inherit from `Visitor.prototype`.
*/
Evaluator.prototype.__proto__ = Visitor.prototype;
/**
* Proxy visit to expose node line numbers.
*
* @param {Node} node
* @return {Node}
* @api private
*/
var visit = Visitor.prototype.visit;
Evaluator.prototype.visit = function(node){
try {
return visit.call(this, node);
} catch (err) {
if (err.filename) throw err;
err.lineno = node.lineno;
err.column = node.column;
err.filename = node.filename;
err.stylusStack = this.stack.toString();
try {
err.input = fs.readFileSync(err.filename, 'utf8');
} catch (err) {
// ignore
}
throw err;
}
};
/**
* Perform evaluation setup:
*
* - populate global scope
* - iterate imports
*
* @api private
*/
Evaluator.prototype.setup = function(){
var root = this.root;
var imports = [];
this.populateGlobalScope();
this.imports.forEach(function(file){
var expr = new nodes.Expression;
expr.push(new nodes.String(file));
imports.push(new nodes.Import(expr));
}, this);
root.nodes = imports.concat(root.nodes);
};
/**
* Populate the global scope with:
*
* - css colors
* - user-defined globals
*
* @api private
*/
Evaluator.prototype.populateGlobalScope = function(){
var scope = this.global.scope;
// colors
Object.keys(colors).forEach(function(name){
var color = colors[name]
, rgba = new nodes.RGBA(color[0], color[1], color[2], color[3])
, node = new nodes.Ident(name, rgba);
rgba.name = name;
scope.add(node);
});
// expose url function
scope.add(new nodes.Ident(
'embedurl',
new nodes.Function('embedurl', require('../functions/url')({
limit: false
}))
));
// user-defined globals
var globals = this.globals;
Object.keys(globals).forEach(function(name){
var val = globals[name];
if (!val.nodeName) val = new nodes.Literal(val);
scope.add(new nodes.Ident(name, val));
});
};
/**
* Evaluate the tree.
*
* @return {Node}
* @api private
*/
Evaluator.prototype.evaluate = function(){
debug('eval %s', this.filename);
this.setup();
return this.visit(this.root);
};
/**
* Visit Group.
*/
Evaluator.prototype.visitGroup = function(group){
group.nodes = group.nodes.map(function(selector){
selector.val = this.interpolate(selector);
debug('ruleset %s', selector.val);
return selector;
}, this);
group.block = this.visit(group.block);
return group;
};
/**
* Visit Return.
*/
Evaluator.prototype.visitReturn = function(ret){
ret.expr = this.visit(ret.expr);
throw ret;
};
/**
* Visit Media.
*/
Evaluator.prototype.visitMedia = function(media){
media.block = this.visit(media.block);
media.val = this.visit(media.val);
return media;
};
/**
* Visit QueryList.
*/
Evaluator.prototype.visitQueryList = function(queries){
var val, query;
queries.nodes.forEach(this.visit, this);
if (1 == queries.nodes.length) {
query = queries.nodes[0];
if (val = this.lookup(query.type)) {
val = val.first.string;
if (!val) return queries;
var Parser = require('../parser')
, parser = new Parser(val, this.options);
queries = this.visit(parser.queries());
}
}
return queries;
};
/**
* Visit Query.
*/
Evaluator.prototype.visitQuery = function(node){
node.predicate = this.visit(node.predicate);
node.type = this.visit(node.type);
node.nodes.forEach(this.visit, this);
return node;
};
/**
* Visit Feature.
*/
Evaluator.prototype.visitFeature = function(node){
node.name = this.interpolate(node);
if (node.expr) {
this.return++;
node.expr = this.visit(node.expr);
this.return--;
}
return node;
};
/**
* Visit Object.
*/
Evaluator.prototype.visitObject = function(obj){
for (var key in obj.vals) {
obj.vals[key] = this.visit(obj.vals[key]);
}
return obj;
};
/**
* Visit Member.
*/
Evaluator.prototype.visitMember = function(node){
var left = node.left
, right = node.right
, obj = this.visit(left).first;
if ('object' != obj.nodeName) {
throw new Error(left.toString() + ' has no property .' + right);
}
if (node.val) {
this.return++;
obj.set(right.name, this.visit(node.val));
this.return--;
}
return obj.get(right.name);
};
/**
* Visit Keyframes.
*/
Evaluator.prototype.visitKeyframes = function(keyframes){
var val;
if (keyframes.fabricated) return keyframes;
keyframes.val = this.interpolate(keyframes).trim();
if (val = this.lookup(keyframes.val)) {
keyframes.val = val.first.string || val.first.name;
}
keyframes.block = this.visit(keyframes.block);
if ('official' != keyframes.prefix) return keyframes;
this.vendors.forEach(function(prefix){
// IE never had prefixes for keyframes
if ('ms' == prefix) return;
var node = keyframes.clone();
node.val = keyframes.val;
node.prefix = prefix;
node.block = keyframes.block;
node.fabricated = true;
this.currentBlock.push(node);
}, this);
return nodes.null;
};
/**
* Visit Function.
*/
Evaluator.prototype.visitFunction = function(fn){
// check local
var local = this.stack.currentFrame.scope.lookup(fn.name);
if (local) this.warn('local ' + local.nodeName + ' "' + fn.name + '" previously defined in this scope');
// user-defined
var user = this.functions[fn.name];
if (user) this.warn('user-defined function "' + fn.name + '" is already defined');
// BIF
var bif = bifs[fn.name];
if (bif) this.warn('built-in function "' + fn.name + '" is already defined');
return fn;
};
/**
* Visit Each.
*/
Evaluator.prototype.visitEach = function(each){
this.return++;
var expr = utils.unwrap(this.visit(each.expr))
, len = expr.nodes.length
, val = new nodes.Ident(each.val)
, key = new nodes.Ident(each.key || '__index__')
, scope = this.currentScope
, block = this.currentBlock
, vals = []
, self = this
, body
, obj;
this.return--;
each.block.scope = false;
function visitBody(key, val) {
scope.add(val);
scope.add(key);
body = self.visit(each.block.clone());
vals = vals.concat(body.nodes);
}
// for prop in obj
if (1 == len && 'object' == expr.nodes[0].nodeName) {
obj = expr.nodes[0];
for (var prop in obj.vals) {
val.val = new nodes.String(prop);
key.val = obj.get(prop);
visitBody(key, val);
}
} else {
for (var i = 0; i < len; ++i) {
val.val = expr.nodes[i];
key.val = new nodes.Unit(i);
visitBody(key, val);
}
}
this.mixin(vals, block);
return vals[vals.length - 1] || nodes.null;
};
/**
* Visit Call.
*/
Evaluator.prototype.visitCall = function(call){
debug('call %s', call);
var fn = this.lookup(call.name)
, literal
, ret;
// url()
this.ignoreColors = 'url' == call.name;
// Variable function
if (fn && 'expression' == fn.nodeName) {
fn = fn.nodes[0];
}
// Not a function? try user-defined or built-ins
if (fn && 'function' != fn.nodeName) {
fn = this.lookupFunction(call.name);
}
// Undefined function? render literal CSS
if (!fn || fn.nodeName != 'function') {
debug('%s is undefined', call);
// Special case for `calc`
if ('calc' == this.unvendorize(call.name)) {
literal = call.args.nodes && call.args.nodes[0];
if (literal) ret = new nodes.Literal(call.name + literal);
} else {
ret = this.literalCall(call);
}
this.ignoreColors = false;
return ret;
}
this.calling.push(call.name);
// Massive stack
if (this.calling.length > 200) {
throw new RangeError('Maximum stylus call stack size exceeded');
}
// First node in expression
if ('expression' == fn.nodeName) fn = fn.first;
// Evaluate arguments
this.return++;
var args = this.visit(call.args);
for (var key in args.map) {
args.map[key] = this.visit(args.map[key].clone());
}
this.return--;
// Built-in
if (fn.fn) {
debug('%s is built-in', call);
ret = this.invokeBuiltin(fn.fn, args);
// User-defined
} else if ('function' == fn.nodeName) {
debug('%s is user-defined', call);
// Evaluate mixin block
if (call.block) call.block = this.visit(call.block);
ret = this.invokeFunction(fn, args, call.block);
}
this.calling.pop();
this.ignoreColors = false;
return ret;
};
/**
* Visit Ident.
*/
Evaluator.prototype.visitIdent = function(ident){
var prop;
// Property lookup
if (ident.property) {
if (prop = this.lookupProperty(ident.name)) {
return this.visit(prop.expr.clone());
}
return nodes.null;
// Lookup
} else if (ident.val.isNull) {
var val = this.lookup(ident.name);
// Object or Block mixin
if (val && ident.mixin) this.mixinNode(val);
return val ? this.visit(val) : ident;
// Assign
} else {
this.return++;
ident.val = this.visit(ident.val);
this.return--;
this.currentScope.add(ident);
return ident.val;
}
};
/**
* Visit BinOp.
*/
Evaluator.prototype.visitBinOp = function(binop){
// Special-case "is defined" pseudo binop
if ('is defined' == binop.op) return this.isDefined(binop.left);
this.return++;
// Visit operands
var op = binop.op
, left = this.visit(binop.left)
, right = ('||' == op || '&&' == op)
? binop.right : this.visit(binop.right);
// HACK: ternary
var val = binop.val
? this.visit(binop.val)
: null;
this.return--;
// Operate
try {
return this.visit(left.operate(op, right, val));
} catch (err) {
// disregard coercion issues in equality
// checks, and simply return false
if ('CoercionError' == err.name) {
switch (op) {
case '==':
return nodes.false;
case '!=':
return nodes.true;
}
}
throw err;
}
};
/**
* Visit UnaryOp.
*/
Evaluator.prototype.visitUnaryOp = function(unary){
var op = unary.op
, node = this.visit(unary.expr);
if ('!' != op) {
node = node.first.clone();
utils.assertType(node, 'unit');
}
switch (op) {
case '-':
node.val = -node.val;
break;
case '+':
node.val = +node.val;
break;
case '~':
node.val = ~node.val;
break;
case '!':
return node.toBoolean().negate();
}
return node;
};
/**
* Visit TernaryOp.
*/
Evaluator.prototype.visitTernary = function(ternary){
var ok = this.visit(ternary.cond).toBoolean();
return ok.isTrue
? this.visit(ternary.trueExpr)
: this.visit(ternary.falseExpr);
};
/**
* Visit Expression.
*/
Evaluator.prototype.visitExpression = function(expr){
for (var i = 0, len = expr.nodes.length; i < len; ++i) {
expr.nodes[i] = this.visit(expr.nodes[i]);
}
// support (n * 5)px etc
if (this.castable(expr)) expr = this.cast(expr);
return expr;
};
/**
* Visit Arguments.
*/
Evaluator.prototype.visitArguments = Evaluator.prototype.visitExpression;
/**
* Visit Property.
*/
Evaluator.prototype.visitProperty = function(prop){
var name = this.interpolate(prop)
, fn = this.lookup(name)
, call = fn && 'function' == fn.first.nodeName
, literal = ~this.calling.indexOf(name)
, _prop = this.property;
// Function of the same name
if (call && !literal && !prop.literal) {
var args = nodes.Arguments.fromExpression(utils.unwrap(prop.expr.clone()));
prop.name = name;
this.property = prop;
this.return++;
this.property.expr = this.visit(prop.expr);
this.return--;
var ret = this.visit(new nodes.Call(name, args));
this.property = _prop;
return ret;
// Regular property
} else {
this.return++;
prop.name = name;
prop.literal = true;
this.property = prop;
prop.expr = this.visit(prop.expr);
this.property = _prop;
this.return--;
return prop;
}
};
/**
* Visit Root.
*/
Evaluator.prototype.visitRoot = function(block){
// normalize cached imports
if (block != this.root) {
block.constructor = nodes.Block;
return this.visit(block);
}
for (var i = 0; i < block.nodes.length; ++i) {
block.index = i;
block.nodes[i] = this.visit(block.nodes[i]);
}
return block;
};
/**
* Visit Block.
*/
Evaluator.prototype.visitBlock = function(block){
this.stack.push(new Frame(block));
for (block.index = 0; block.index < block.nodes.length; ++block.index) {
try {
block.nodes[block.index] = this.visit(block.nodes[block.index]);
} catch (err) {
if ('return' == err.nodeName) {
if (this.return) {
this.stack.pop();
throw err;
} else {
block.nodes[block.index] = err;
break;
}
} else {
throw err;
}
}
}
this.stack.pop();
return block;
};
/**
* Visit Atblock.
*/
Evaluator.prototype.visitAtblock = function(atblock){
atblock.block = this.visit(atblock.block);
return atblock;
};
/**
* Visit Atrule.
*/
Evaluator.prototype.visitAtrule = function(atrule){
atrule.val = this.interpolate(atrule);
if (atrule.block) atrule.block = this.visit(atrule.block);
return atrule;
};
/**
* Visit Supports.
*/
Evaluator.prototype.visitSupports = function(node){
var condition = node.condition
, val;
this.return++;
node.condition = this.visit(condition);
this.return--;
val = condition.first;
if (1 == condition.nodes.length
&& 'string' == val.nodeName) {
node.condition = val.string;
}
node.block = this.visit(node.block);
return node;
};
/**
* Visit If.
*/
Evaluator.prototype.visitIf = function(node){
var ret
, block = this.currentBlock
, negate = node.negate;
this.return++;
var ok = this.visit(node.cond).first.toBoolean();
this.return--;
node.block.scope = node.block.hasMedia;
// Evaluate body
if (negate) {
// unless
if (ok.isFalse) {
ret = this.visit(node.block);
}
} else {
// if
if (ok.isTrue) {
ret = this.visit(node.block);
// else
} else if (node.elses.length) {
var elses = node.elses
, len = elses.length
, cond;
for (var i = 0; i < len; ++i) {
// else if
if (elses[i].cond) {
elses[i].block.scope = elses[i].block.hasMedia;
this.return++;
cond = this.visit(elses[i].cond).first.toBoolean();
this.return--;
if (cond.isTrue) {
ret = this.visit(elses[i].block);
break;
}
// else
} else {
elses[i].scope = elses[i].hasMedia;
ret = this.visit(elses[i]);
}
}
}
}
// mixin conditional statements within
// a selector group or at-rule
if (ret && !node.postfix && block.node
&& ~['group'
, 'atrule'
, 'media'
, 'supports'
, 'keyframes'].indexOf(block.node.nodeName)) {
this.mixin(ret.nodes, block);
return nodes.null;
}
return ret || nodes.null;
};
/**
* Visit Extend.
*/
Evaluator.prototype.visitExtend = function(extend){
var block = this.currentBlock;
if ('group' != block.node.nodeName) block = this.closestGroup;
extend.selectors.forEach(function(selector){
block.node.extends.push({
// Cloning the selector for when we are in a loop and don't want it to affect
// the selector nodes and cause the values to be different to expected
selector: this.interpolate(selector.clone()).trim(),
optional: selector.optional,
lineno: selector.lineno,
column: selector.column
});
}, this);
return nodes.null;
};
/**
* Visit Import.
*/
Evaluator.prototype.visitImport = function(imported){
this.return++;
var path = this.visit(imported.path).first
, nodeName = imported.once ? 'require' : 'import'
, found
, literal;
this.return--;
debug('import %s', path);
// url() passed
if ('url' == path.name) {
if (imported.once) throw new Error('You cannot @require a url');
return imported;
}
// Ensure string
if (!path.string) throw new Error('@' + nodeName + ' string expected');
var name = path = path.string;
// Absolute URL or hash
if (/(?:url\s*\(\s*)?['"]?(?:#|(?:https?:)?\/\/)/i.test(path)) {
if (imported.once) throw new Error('You cannot @require a url');
return imported;
}
// Literal
if (/\.css(?:"|$)/.test(path)) {
literal = true;
if (!imported.once && !this.includeCSS) {
return imported;
}
}
// support optional .styl
if (!literal && !/\.styl$/i.test(path)) path += '.styl';
// Lookup
found = utils.find(path, this.paths, this.filename);
if (!found) {
found = utils.lookupIndex(name, this.paths, this.filename);
}
// Throw if import failed
if (!found) throw new Error('failed to locate @' + nodeName + ' file ' + path);
var block = new nodes.Block;
for (var i = 0, len = found.length; i < len; ++i) {
block.push(importFile.call(this, imported, found[i], literal));
}
return block;
};
/**
* Invoke `fn` with `args`.
*
* @param {Function} fn
* @param {Array} args
* @return {Node}
* @api private
*/
Evaluator.prototype.invokeFunction = function(fn, args, content){
var block = new nodes.Block(fn.block.parent);
// Clone the function body
// to prevent mutation of subsequent calls
var body = fn.block.clone(block);
// mixin block
var mixinBlock = this.stack.currentFrame.block;
// new block scope
this.stack.push(new Frame(block));
var scope = this.currentScope;
// normalize arguments
if ('arguments' != args.nodeName) {
var expr = new nodes.Expression;
expr.push(args);
args = nodes.Arguments.fromExpression(expr);
}
// arguments local
scope.add(new nodes.Ident('arguments', args));
// mixin scope introspection
scope.add(new nodes.Ident('mixin', this.return
? nodes.false
: new nodes.String(mixinBlock.nodeName)));
// current property
if (this.property) {
var prop = this.propertyExpression(this.property, fn.name);
scope.add(new nodes.Ident('current-property', prop));
} else {
scope.add(new nodes.Ident('current-property', nodes.null));
}
// current call stack
var expr = new nodes.Expression;
for (var i = this.calling.length - 1; i-- ; ) {
expr.push(new nodes.Literal(this.calling[i]));
};
scope.add(new nodes.Ident('called-from', expr));
// inject arguments as locals
var i = 0
, len = args.nodes.length;
fn.params.nodes.forEach(function(node){
// rest param support
if (node.rest) {
node.val = new nodes.Expression;
for (; i < len; ++i) node.val.push(args.nodes[i]);
node.val.preserve = true;
node.val.isList = args.isList;
// argument default support
} else {
var arg = args.map[node.name] || args.nodes[i++];
node = node.clone();
if (arg) {
arg.isEmpty ? args.nodes[i - 1] = this.visit(node) : node.val = arg;
} else {
args.push(node.val);
}
// required argument not satisfied
if (node.val.isNull) {
throw new Error('argument "' + node + '" required for ' + fn);
}
}
scope.add(node);
}, this);
// mixin block
if (content) scope.add(new nodes.Ident('block', content, true));
// invoke
return this.invoke(body, true, fn.filename);
};
/**
* Invoke built-in `fn` with `args`.
*
* @param {Function} fn
* @param {Array} args
* @return {Node}
* @api private
*/
Evaluator.prototype.invokeBuiltin = function(fn, args){
// Map arguments to first node
// providing a nicer js api for
// BIFs. Functions may specify that
// they wish to accept full expressions
// via .raw
if (fn.raw) {
args = args.nodes;
} else {
args = utils.params(fn).reduce(function(ret, param){
var arg = args.map[param] || args.nodes.shift()
if (arg) {
arg = utils.unwrap(arg);
var len = arg.nodes.length;
if (len > 1) {
for (var i = 0; i < len; ++i) {
ret.push(utils.unwrap(arg.nodes[i].first));
}
} else {
ret.push(arg.first);
}
}
return ret;
}, []);
}
// Invoke the BIF
var body = utils.coerce(fn.apply(this, args));
// Always wrapping allows js functions
// to return several values with a single
// Expression node
var expr = new nodes.Expression;
expr.push(body);
body = expr;
// Invoke
return this.invoke(body);
};
/**
* Invoke the given function `body`.
*
* @param {Block} body
* @return {Node}
* @api private
*/
Evaluator.prototype.invoke = function(body, stack, filename){
var self = this
, ret;
if (filename) this.paths.push(dirname(filename));
// Return
if (this.return) {
ret = this.eval(body.nodes);
if (stack) this.stack.pop();
// Mixin
} else {
body = this.visit(body);
if (stack) this.stack.pop();
this.mixin(body.nodes, this.currentBlock);
ret = nodes.null;
}
if (filename) this.paths.pop();
return ret;
};
/**
* Mixin the given `nodes` to the given `block`.
*
* @param {Array} nodes
* @param {Block} block
* @api private
*/
Evaluator.prototype.mixin = function(nodes, block){
if (!nodes.length) return;
var len = block.nodes.length
, head = block.nodes.slice(0, block.index)
, tail = block.nodes.slice(block.index + 1, len);
this._mixin(nodes, head, block);
block.index = 0;
block.nodes = head.concat(tail);
};
/**
* Mixin the given `items` to the `dest` array.
*
* @param {Array} items
* @param {Array} dest
* @param {Block} block
* @api private
*/
Evaluator.prototype._mixin = function(items, dest, block){
var node
, len = items.length;
for (var i = 0; i < len; ++i) {
switch ((node = items[i]).nodeName) {
case 'return':
return;
case 'block':
this._mixin(node.nodes, dest, block);
break;
case 'media':
// fix link to the parent block
var parentNode = node.block.parent.node;
if (parentNode && 'call' != parentNode.nodeName) {
node.block.parent = block;
}
case 'property':
var val = node.expr;
// prevent `block` mixin recursion
if (node.literal && 'block' == val.first.name) {
val = utils.unwrap(val);
val.nodes[0] = new nodes.Literal('block');
}
default:
dest.push(node);
}
}
};
/**
* Mixin the given `node` to the current block.
*
* @param {Node} node
* @api private
*/
Evaluator.prototype.mixinNode = function(node){
node = this.visit(node.first);
switch (node.nodeName) {
case 'object':
this.mixinObject(node);
return nodes.null;
case 'block':
case 'atblock':
this.mixin(node.nodes, this.currentBlock);
return nodes.null;
}
};
/**
* Mixin the given `object` to the current block.
*
* @param {Object} object
* @api private
*/
Evaluator.prototype.mixinObject = function(object){
var Parser = require('../parser')
, root = this.root
, str = '$block ' + object.toBlock()
, parser = new Parser(str, utils.merge({ root: block }, this.options))
, block;
try {
block = parser.parse();
} catch (err) {
err.filename = this.filename;
err.lineno = parser.lexer.lineno;
err.column = parser.lexer.column;
err.input = str;
throw err;
}
block.parent = root;
block.scope = false;
var ret = this.visit(block)
, vals = ret.first.nodes;
for (var i = 0, len = vals.length; i < len; ++i) {
if (vals[i].block) {
this.mixin(vals[i].block.nodes, this.currentBlock);
break;
}
}
};
/**
* Evaluate the given `vals`.
*
* @param {Array} vals
* @return {Node}
* @api private
*/
Evaluator.prototype.eval = function(vals){
if (!vals) return nodes.null;
var len = vals.length
, node = nodes.null;
try {
for (var i = 0; i < len; ++i) {
node = vals[i];
switch (node.nodeName) {
case 'if':
if ('block' != node.block.nodeName) {
node = this.visit(node);
break;
}
case 'each':
case 'block':
node = this.visit(node);
if (node.nodes) node = this.eval(node.nodes);
break;
default:
node = this.visit(node);
}
}
} catch (err) {
if ('return' == err.nodeName) {
return err.expr;
} else {
throw err;
}
}
return node;
};
/**
* Literal function `call`.
*
* @param {Call} call
* @return {call}
* @api private
*/
Evaluator.prototype.literalCall = function(call){
call.args = this.visit(call.args);
return call;
};
/**
* Lookup property `name`.
*
* @param {String} name
* @return {Property}
* @api private
*/
Evaluator.prototype.lookupProperty = function(name){
var i = this.stack.length
, index = this.currentBlock.index
, top = i
, nodes
, block
, len
, other;
while (i--) {
block = this.stack[i].block;
if (!block.node) continue;
switch (block.node.nodeName) {
case 'group':
case 'function':
case 'if':
case 'each':
case 'atrule':
case 'media':
case 'atblock':
case 'call':
nodes = block.nodes;
// scan siblings from the property index up
if (i + 1 == top) {
while (index--) {
// ignore current property
if (this.property == nodes[index]) continue;
other = this.interpolate(nodes[index]);
if (name == other) return nodes[index].clone();
}
// sequential lookup for non-siblings (for now)
} else {
len = nodes.length;
while (len--) {
if ('property' != nodes[len].nodeName
|| this.property == nodes[len]) continue;
other = this.interpolate(nodes[len]);
if (name == other) return nodes[len].clone();
}
}
break;
}
}
return nodes.null;
};
/**
* Return the closest mixin-able `Block`.
*
* @return {Block}
* @api private
*/
Evaluator.prototype.__defineGetter__('closestBlock', function(){
var i = this.stack.length
, block;
while (i--) {
block = this.stack[i].block;
if (block.node) {
switch (block.node.nodeName) {
case 'group':
case 'keyframes':
case 'atrule':
case 'atblock':
case 'media':
case 'call':
return block;
}
}
}
});
/**
* Return the closest group block.
*
* @return {Block}
* @api private
*/
Evaluator.prototype.__defineGetter__('closestGroup', function(){
var i = this.stack.length
, block;
while (i--) {
block = this.stack[i].block;
if (block.node && 'group' == block.node.nodeName) {
return block;
}
}
});
/**
* Return the current selectors stack.
*
* @return {Array}
* @api private
*/
Evaluator.prototype.__defineGetter__('selectorStack', function(){
var block
, stack = [];
for (var i = 0, len = this.stack.length; i < len; ++i) {
block = this.stack[i].block;
if (block.node && 'group' == block.node.nodeName) {
block.node.nodes.forEach(function(selector) {
if (!selector.val) selector.val = this.interpolate(selector);
}, this);
stack.push(block.node.nodes);
}
}
return stack;
});
/**
* Lookup `name`, with support for JavaScript
* functions, and BIFs.
*
* @param {String} name
* @return {Node}
* @api private
*/
Evaluator.prototype.lookup = function(name){
var val;
if (this.ignoreColors && name in colors) return;
if (val = this.stack.lookup(name)) {
return utils.unwrap(val);
} else {
return this.lookupFunction(name);
}
};
/**
* Map segments in `node` returning a string.
*
* @param {Node} node
* @return {String}
* @api private
*/
Evaluator.prototype.interpolate = function(node){
var self = this
, isSelector = ('selector' == node.nodeName);
function toString(node) {
switch (node.nodeName) {
case 'function':
case 'ident':
return node.name;
case 'literal':
case 'string':
if (self.prefix && !node.prefixed && !node.val.nodeName) {
node.val = node.val.replace(/\./g, '.' + self.prefix);
node.prefixed = true;
}
return node.val;
case 'unit':
// Interpolation inside keyframes
return '%' == node.type ? node.val + '%' : node.val;
case 'member':
return toString(self.visit(node));
case 'expression':
// Prevent cyclic `selector()` calls.
if (self.calling && ~self.calling.indexOf('selector') && self._selector) return self._selector;
self.return++;
var ret = toString(self.visit(node).first);
self.return--;
if (isSelector) self._selector = ret;
return ret;
}
}
if (node.segments) {
return node.segments.map(toString).join('');
} else {
return toString(node);
}
};
/**
* Lookup JavaScript user-defined or built-in function.
*
* @param {String} name
* @return {Function}
* @api private
*/
Evaluator.prototype.lookupFunction = function(name){
var fn = this.functions[name] || bifs[name];
if (fn) return new nodes.Function(name, fn);
};
/**
* Check if the given `node` is an ident, and if it is defined.
*
* @param {Node} node
* @return {Boolean}
* @api private
*/
Evaluator.prototype.isDefined = function(node){
if ('ident' == node.nodeName) {
return nodes.Boolean(this.lookup(node.name));
} else {
throw new Error('invalid "is defined" check on non-variable ' + node);
}
};
/**
* Return `Expression` based on the given `prop`,
* replacing cyclic calls to the given function `name`
* with "__CALL__".
*
* @param {Property} prop
* @param {String} name
* @return {Expression}
* @api private
*/
Evaluator.prototype.propertyExpression = function(prop, name){
var expr = new nodes.Expression
, val = prop.expr.clone();
// name
expr.push(new nodes.String(prop.name));
// replace cyclic call with __CALL__
function replace(node) {
if ('call' == node.nodeName && name == node.name) {
return new nodes.Literal('__CALL__');
}
if (node.nodes) node.nodes = node.nodes.map(replace);
return node;
}
replace(val);
expr.push(val);
return expr;
};
/**
* Cast `expr` to the trailing ident.
*
* @param {Expression} expr
* @return {Unit}
* @api private
*/
Evaluator.prototype.cast = function(expr){
return new nodes.Unit(expr.first.val, expr.nodes[1].name);
};
/**
* Check if `expr` is castable.
*
* @param {Expression} expr
* @return {Boolean}
* @api private
*/
Evaluator.prototype.castable = function(expr){
return 2 == expr.nodes.length
&& 'unit' == expr.first.nodeName
&& ~units.indexOf(expr.nodes[1].name);
};
/**
* Warn with the given `msg`.
*
* @param {String} msg
* @api private
*/
Evaluator.prototype.warn = function(msg){
if (!this.warnings) return;
console.warn('\u001b[33mWarning:\u001b[0m ' + msg);
};
/**
* Return the current `Block`.
*
* @return {Block}
* @api private
*/
Evaluator.prototype.__defineGetter__('currentBlock', function(){
return this.stack.currentFrame.block;
});
/**
* Return an array of vendor names.
*
* @return {Array}
* @api private
*/
Evaluator.prototype.__defineGetter__('vendors', function(){
return this.lookup('vendors').nodes.map(function(node){
return node.string;
});
});
/**
* Return the property name without vendor prefix.
*
* @param {String} prop
* @return {String}
* @api public
*/
Evaluator.prototype.unvendorize = function(prop){
for (var i = 0, len = this.vendors.length; i < len; i++) {
if ('official' != this.vendors[i]) {
var vendor = '-' + this.vendors[i] + '-';
if (~prop.indexOf(vendor)) return prop.replace(vendor, '');
}
}
return prop;
};
/**
* Return the current frame `Scope`.
*
* @return {Scope}
* @api private
*/
Evaluator.prototype.__defineGetter__('currentScope', function(){
return this.stack.currentFrame.scope;
});
/**
* Return the current `Frame`.
*
* @return {Frame}
* @api private
*/
Evaluator.prototype.__defineGetter__('currentFrame', function(){
return this.stack.currentFrame;
});