/*! * Stylus - Evaluator * Copyright (c) Automattic * 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; });