/ * !
* 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 ;
} ) ;