hexo/node_modules/stylus/lib/visitor/evaluator.js

1614 lines
36 KiB
JavaScript
Raw Normal View History

2023-10-03 11:14:36 +08:00
/*!
* 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 {
if (!fn.params) {
fn.params = utils.params(fn);
}
args = fn.params.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(/\.(?=[\w-])|^\.$/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;
});