"Fossies" - the Fresh Open Source Software Archive

Member "4.6.1/composer/leafo/scssphp/src/Compiler.php" (8 Apr 2021, 186512 Bytes) of package /linux/www/studip-4.6.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "Compiler.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 /**
    3  * SCSSPHP
    4  *
    5  * @copyright 2012-2018 Leaf Corcoran
    6  *
    7  * @license http://opensource.org/licenses/MIT MIT
    8  *
    9  * @link http://leafo.github.io/scssphp
   10  */
   11 
   12 namespace Leafo\ScssPhp;
   13 
   14 use Leafo\ScssPhp\Base\Range;
   15 use Leafo\ScssPhp\Block;
   16 use Leafo\ScssPhp\Cache;
   17 use Leafo\ScssPhp\Colors;
   18 use Leafo\ScssPhp\Compiler\Environment;
   19 use Leafo\ScssPhp\Exception\CompilerException;
   20 use Leafo\ScssPhp\Formatter\OutputBlock;
   21 use Leafo\ScssPhp\Node;
   22 use Leafo\ScssPhp\SourceMap\SourceMapGenerator;
   23 use Leafo\ScssPhp\Type;
   24 use Leafo\ScssPhp\Parser;
   25 use Leafo\ScssPhp\Util;
   26 
   27 /**
   28  * The scss compiler and parser.
   29  *
   30  * Converting SCSS to CSS is a three stage process. The incoming file is parsed
   31  * by `Parser` into a syntax tree, then it is compiled into another tree
   32  * representing the CSS structure by `Compiler`. The CSS tree is fed into a
   33  * formatter, like `Formatter` which then outputs CSS as a string.
   34  *
   35  * During the first compile, all values are *reduced*, which means that their
   36  * types are brought to the lowest form before being dump as strings. This
   37  * handles math equations, variable dereferences, and the like.
   38  *
   39  * The `compile` function of `Compiler` is the entry point.
   40  *
   41  * In summary:
   42  *
   43  * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
   44  * then transforms the resulting tree to a CSS tree. This class also holds the
   45  * evaluation context, such as all available mixins and variables at any given
   46  * time.
   47  *
   48  * The `Parser` class is only concerned with parsing its input.
   49  *
   50  * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
   51  * handling things like indentation.
   52  */
   53 
   54 /**
   55  * SCSS compiler
   56  *
   57  * @author Leaf Corcoran <leafot@gmail.com>
   58  */
   59 class Compiler
   60 {
   61     const LINE_COMMENTS = 1;
   62     const DEBUG_INFO    = 2;
   63 
   64     const WITH_RULE     = 1;
   65     const WITH_MEDIA    = 2;
   66     const WITH_SUPPORTS = 4;
   67     const WITH_ALL      = 7;
   68 
   69     const SOURCE_MAP_NONE   = 0;
   70     const SOURCE_MAP_INLINE = 1;
   71     const SOURCE_MAP_FILE   = 2;
   72 
   73     /**
   74      * @var array
   75      */
   76     static protected $operatorNames = [
   77         '+'   => 'add',
   78         '-'   => 'sub',
   79         '*'   => 'mul',
   80         '/'   => 'div',
   81         '%'   => 'mod',
   82 
   83         '=='  => 'eq',
   84         '!='  => 'neq',
   85         '<'   => 'lt',
   86         '>'   => 'gt',
   87 
   88         '<='  => 'lte',
   89         '>='  => 'gte',
   90         '<=>' => 'cmp',
   91     ];
   92 
   93     /**
   94      * @var array
   95      */
   96     static protected $namespaces = [
   97         'special'  => '%',
   98         'mixin'    => '@',
   99         'function' => '^',
  100     ];
  101 
  102     static public $true         = [Type::T_KEYWORD, 'true'];
  103     static public $false        = [Type::T_KEYWORD, 'false'];
  104     static public $null         = [Type::T_NULL];
  105     static public $nullString   = [Type::T_STRING, '', []];
  106     static public $defaultValue = [Type::T_KEYWORD, ''];
  107     static public $selfSelector = [Type::T_SELF];
  108     static public $emptyList    = [Type::T_LIST, '', []];
  109     static public $emptyMap     = [Type::T_MAP, [], []];
  110     static public $emptyString  = [Type::T_STRING, '"', []];
  111     static public $with         = [Type::T_KEYWORD, 'with'];
  112     static public $without      = [Type::T_KEYWORD, 'without'];
  113 
  114     protected $importPaths = [''];
  115     protected $importCache = [];
  116     protected $importedFiles = [];
  117     protected $userFunctions = [];
  118     protected $registeredVars = [];
  119     protected $registeredFeatures = [
  120         'extend-selector-pseudoclass' => false,
  121         'at-error'                    => true,
  122         'units-level-3'               => false,
  123         'global-variable-shadowing'   => false,
  124     ];
  125 
  126     protected $encoding = null;
  127     protected $lineNumberStyle = null;
  128 
  129     protected $sourceMap = self::SOURCE_MAP_NONE;
  130     protected $sourceMapOptions = [];
  131 
  132     /**
  133      * @var string|\Leafo\ScssPhp\Formatter
  134      */
  135     protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
  136 
  137     protected $rootEnv;
  138     protected $rootBlock;
  139 
  140     /**
  141      * @var \Leafo\ScssPhp\Compiler\Environment
  142      */
  143     protected $env;
  144     protected $scope;
  145     protected $storeEnv;
  146     protected $charsetSeen;
  147     protected $sourceNames;
  148 
  149     protected $cache;
  150 
  151     protected $indentLevel;
  152     protected $extends;
  153     protected $extendsMap;
  154     protected $parsedFiles;
  155     protected $parser;
  156     protected $sourceIndex;
  157     protected $sourceLine;
  158     protected $sourceColumn;
  159     protected $stderr;
  160     protected $shouldEvaluate;
  161     protected $ignoreErrors;
  162 
  163     protected $callStack = [];
  164 
  165     /**
  166      * Constructor
  167      */
  168     public function __construct($cacheOptions = null)
  169     {
  170         $this->parsedFiles = [];
  171         $this->sourceNames = [];
  172 
  173         if ($cacheOptions) {
  174             $this->cache = new Cache($cacheOptions);
  175         }
  176     }
  177 
  178     public function getCompileOptions()
  179     {
  180         $options = [
  181             'importPaths'        => $this->importPaths,
  182             'registeredVars'     => $this->registeredVars,
  183             'registeredFeatures' => $this->registeredFeatures,
  184             'encoding'           => $this->encoding,
  185             'sourceMap'          => serialize($this->sourceMap),
  186             'sourceMapOptions'   => $this->sourceMapOptions,
  187             'formatter'          => $this->formatter,
  188         ];
  189 
  190         return $options;
  191     }
  192 
  193     /**
  194      * Compile scss
  195      *
  196      * @api
  197      *
  198      * @param string $code
  199      * @param string $path
  200      *
  201      * @return string
  202      */
  203     public function compile($code, $path = null)
  204     {
  205         if ($this->cache) {
  206             $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
  207             $compileOptions = $this->getCompileOptions();
  208             $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
  209 
  210             if (is_array($cache)
  211                 && isset($cache['dependencies'])
  212                 && isset($cache['out'])
  213             ) {
  214                 // check if any dependency file changed before accepting the cache
  215                 foreach ($cache['dependencies'] as $file => $mtime) {
  216                     if (! file_exists($file)
  217                         || filemtime($file) !== $mtime
  218                     ) {
  219                         unset($cache);
  220                         break;
  221                     }
  222                 }
  223 
  224                 if (isset($cache)) {
  225                     return $cache['out'];
  226                 }
  227             }
  228         }
  229 
  230 
  231         $this->indentLevel    = -1;
  232         $this->extends        = [];
  233         $this->extendsMap     = [];
  234         $this->sourceIndex    = null;
  235         $this->sourceLine     = null;
  236         $this->sourceColumn   = null;
  237         $this->env            = null;
  238         $this->scope          = null;
  239         $this->storeEnv       = null;
  240         $this->charsetSeen    = null;
  241         $this->shouldEvaluate = null;
  242         $this->stderr         = fopen('php://stderr', 'w');
  243 
  244         $this->parser = $this->parserFactory($path);
  245         $tree = $this->parser->parse($code);
  246         $this->parser = null;
  247 
  248         $this->formatter = new $this->formatter();
  249         $this->rootBlock = null;
  250         $this->rootEnv   = $this->pushEnv($tree);
  251 
  252         $this->injectVariables($this->registeredVars);
  253         $this->compileRoot($tree);
  254         $this->popEnv();
  255 
  256         $sourceMapGenerator = null;
  257 
  258         if ($this->sourceMap) {
  259             if (is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
  260                 $sourceMapGenerator = $this->sourceMap;
  261                 $this->sourceMap = self::SOURCE_MAP_FILE;
  262             } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
  263                 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
  264             }
  265         }
  266 
  267         $out = $this->formatter->format($this->scope, $sourceMapGenerator);
  268 
  269         if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
  270             $sourceMap    = $sourceMapGenerator->generateJson();
  271             $sourceMapUrl = null;
  272 
  273             switch ($this->sourceMap) {
  274                 case self::SOURCE_MAP_INLINE:
  275                     $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
  276                     break;
  277 
  278                 case self::SOURCE_MAP_FILE:
  279                     $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
  280                     break;
  281             }
  282 
  283             $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
  284         }
  285 
  286         if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
  287             $v = [
  288                 'dependencies' => $this->getParsedFiles(),
  289                 'out' => &$out,
  290             ];
  291 
  292             $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
  293         }
  294 
  295         return $out;
  296     }
  297 
  298     /**
  299      * Instantiate parser
  300      *
  301      * @param string $path
  302      *
  303      * @return \Leafo\ScssPhp\Parser
  304      */
  305     protected function parserFactory($path)
  306     {
  307         $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
  308 
  309         $this->sourceNames[] = $path;
  310         $this->addParsedFile($path);
  311 
  312         return $parser;
  313     }
  314 
  315     /**
  316      * Is self extend?
  317      *
  318      * @param array $target
  319      * @param array $origin
  320      *
  321      * @return boolean
  322      */
  323     protected function isSelfExtend($target, $origin)
  324     {
  325         foreach ($origin as $sel) {
  326             if (in_array($target, $sel)) {
  327                 return true;
  328             }
  329         }
  330 
  331         return false;
  332     }
  333 
  334     /**
  335      * Push extends
  336      *
  337      * @param array     $target
  338      * @param array     $origin
  339      * @param \stdClass $block
  340      */
  341     protected function pushExtends($target, $origin, $block)
  342     {
  343         if ($this->isSelfExtend($target, $origin)) {
  344             return;
  345         }
  346 
  347         $i = count($this->extends);
  348         $this->extends[] = [$target, $origin, $block];
  349 
  350         foreach ($target as $part) {
  351             if (isset($this->extendsMap[$part])) {
  352                 $this->extendsMap[$part][] = $i;
  353             } else {
  354                 $this->extendsMap[$part] = [$i];
  355             }
  356         }
  357     }
  358 
  359     /**
  360      * Make output block
  361      *
  362      * @param string $type
  363      * @param array  $selectors
  364      *
  365      * @return \Leafo\ScssPhp\Formatter\OutputBlock
  366      */
  367     protected function makeOutputBlock($type, $selectors = null)
  368     {
  369         $out = new OutputBlock;
  370         $out->type         = $type;
  371         $out->lines        = [];
  372         $out->children     = [];
  373         $out->parent       = $this->scope;
  374         $out->selectors    = $selectors;
  375         $out->depth        = $this->env->depth;
  376 
  377         if ($this->env->block instanceof Block) {
  378             $out->sourceName   = $this->env->block->sourceName;
  379             $out->sourceLine   = $this->env->block->sourceLine;
  380             $out->sourceColumn = $this->env->block->sourceColumn;
  381         } else {
  382             $out->sourceName   = null;
  383             $out->sourceLine   = null;
  384             $out->sourceColumn = null;
  385         }
  386 
  387         return $out;
  388     }
  389 
  390     /**
  391      * Compile root
  392      *
  393      * @param \Leafo\ScssPhp\Block $rootBlock
  394      */
  395     protected function compileRoot(Block $rootBlock)
  396     {
  397         $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
  398 
  399         $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
  400         $this->flattenSelectors($this->scope);
  401         $this->missingSelectors();
  402     }
  403 
  404     /**
  405      * Report missing selectors
  406      */
  407     protected function missingSelectors()
  408     {
  409         foreach ($this->extends as $extend) {
  410             if (isset($extend[3])) {
  411                 continue;
  412             }
  413 
  414             list($target, $origin, $block) = $extend;
  415 
  416             // ignore if !optional
  417             if ($block[2]) {
  418                 continue;
  419             }
  420 
  421             $target = implode(' ', $target);
  422             $origin = $this->collapseSelectors($origin);
  423 
  424             $this->sourceLine = $block[Parser::SOURCE_LINE];
  425             $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
  426         }
  427     }
  428 
  429     /**
  430      * Flatten selectors
  431      *
  432      * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
  433      * @param string                               $parentKey
  434      */
  435     protected function flattenSelectors(OutputBlock $block, $parentKey = null)
  436     {
  437         if ($block->selectors) {
  438             $selectors = [];
  439 
  440             foreach ($block->selectors as $s) {
  441                 $selectors[] = $s;
  442 
  443                 if (! is_array($s)) {
  444                     continue;
  445                 }
  446 
  447                 // check extends
  448                 if (! empty($this->extendsMap)) {
  449                     $this->matchExtends($s, $selectors);
  450 
  451                     // remove duplicates
  452                     array_walk($selectors, function (&$value) {
  453                         $value = serialize($value);
  454                     });
  455 
  456                     $selectors = array_unique($selectors);
  457 
  458                     array_walk($selectors, function (&$value) {
  459                         $value = unserialize($value);
  460                     });
  461                 }
  462             }
  463 
  464             $block->selectors = [];
  465             $placeholderSelector = false;
  466 
  467             foreach ($selectors as $selector) {
  468                 if ($this->hasSelectorPlaceholder($selector)) {
  469                     $placeholderSelector = true;
  470                     continue;
  471                 }
  472 
  473                 $block->selectors[] = $this->compileSelector($selector);
  474             }
  475 
  476             if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
  477                 unset($block->parent->children[$parentKey]);
  478 
  479                 return;
  480             }
  481         }
  482 
  483         foreach ($block->children as $key => $child) {
  484             $this->flattenSelectors($child, $key);
  485         }
  486     }
  487 
  488     /**
  489      * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
  490      *
  491      * @param array $parts
  492      *
  493      * @return array
  494      */
  495     protected function glueFunctionSelectors($parts)
  496     {
  497         $new = [];
  498 
  499         foreach ($parts as $part) {
  500             if (is_array($part)) {
  501                 $part = $this->glueFunctionSelectors($part);
  502                 $new[] = $part;
  503             } else {
  504                 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
  505                 // and need to be joined to this
  506                 if (count($new) && is_string($new[count($new) - 1])
  507                     && strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
  508                 ) {
  509                     $new[count($new) - 1] .= $part;
  510                 } else {
  511                     $new[] = $part;
  512                 }
  513             }
  514         }
  515 
  516         return $new;
  517     }
  518 
  519     /**
  520      * Match extends
  521      *
  522      * @param array   $selector
  523      * @param array   $out
  524      * @param integer $from
  525      * @param boolean $initial
  526      */
  527     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
  528     {
  529         static $partsPile = [];
  530 
  531         $selector = $this->glueFunctionSelectors($selector);
  532 
  533         foreach ($selector as $i => $part) {
  534             if ($i < $from) {
  535                 continue;
  536             }
  537 
  538             // check that we are not building an infinite loop of extensions
  539             // if the new part is just including a previous part don't try to extend anymore
  540             if (count($part) > 1) {
  541                 foreach ($partsPile as $previousPart) {
  542                     if (! count(array_diff($previousPart, $part))) {
  543                         continue 2;
  544                     }
  545                 }
  546             }
  547 
  548             if ($this->matchExtendsSingle($part, $origin)) {
  549                 $partsPile[] = $part;
  550                 $after       = array_slice($selector, $i + 1);
  551                 $before      = array_slice($selector, 0, $i);
  552 
  553                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
  554 
  555                 foreach ($origin as $new) {
  556                     $k = 0;
  557 
  558                     // remove shared parts
  559                     if (count($new) > 1) {
  560                         while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
  561                             $k++;
  562                         }
  563                     }
  564 
  565                     $replacement = [];
  566                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
  567 
  568                     for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
  569                         $slice = [];
  570 
  571                         foreach ($tempReplacement[$l] as $chunk) {
  572                             if (! in_array($chunk, $slice)) {
  573                                 $slice[] = $chunk;
  574                             }
  575                         }
  576 
  577                         array_unshift($replacement, $slice);
  578 
  579                         if (! $this->isImmediateRelationshipCombinator(end($slice))) {
  580                             break;
  581                         }
  582                     }
  583 
  584                     $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
  585 
  586                     // Merge shared direct relationships.
  587                     $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
  588 
  589                     $result = array_merge(
  590                         $before,
  591                         $mergedBefore,
  592                         $replacement,
  593                         $after
  594                     );
  595 
  596                     if ($result === $selector) {
  597                         continue;
  598                     }
  599 
  600                     $out[] = $result;
  601 
  602                     // recursively check for more matches
  603                     $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
  604                     $this->matchExtends($result, $out, $startRecurseFrom, false);
  605 
  606                     // selector sequence merging
  607                     if (! empty($before) && count($new) > 1) {
  608                         $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
  609                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
  610 
  611                         list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
  612 
  613                         $result2 = array_merge(
  614                             $preSharedParts,
  615                             $betweenSharedParts,
  616                             $postSharedParts,
  617                             $nonBreakable2,
  618                             $nonBreakableBefore,
  619                             $replacement,
  620                             $after
  621                         );
  622 
  623                         $out[] = $result2;
  624                     }
  625                 }
  626 
  627                 array_pop($partsPile);
  628             }
  629         }
  630     }
  631 
  632     /**
  633      * Match extends single
  634      *
  635      * @param array $rawSingle
  636      * @param array $outOrigin
  637      *
  638      * @return boolean
  639      */
  640     protected function matchExtendsSingle($rawSingle, &$outOrigin)
  641     {
  642         $counts = [];
  643         $single = [];
  644 
  645         // simple usual cases, no need to do the whole trick
  646         if (in_array($rawSingle, [['>'],['+'],['~']])) {
  647             return false;
  648         }
  649 
  650         foreach ($rawSingle as $part) {
  651             // matches Number
  652             if (! is_string($part)) {
  653                 return false;
  654             }
  655 
  656             if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
  657                 $single[count($single) - 1] .= $part;
  658             } else {
  659                 $single[] = $part;
  660             }
  661         }
  662 
  663         $extendingDecoratedTag = false;
  664 
  665         if (count($single) > 1) {
  666             $matches = null;
  667             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
  668         }
  669 
  670         foreach ($single as $part) {
  671             if (isset($this->extendsMap[$part])) {
  672                 foreach ($this->extendsMap[$part] as $idx) {
  673                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
  674                 }
  675             }
  676         }
  677 
  678         $outOrigin = [];
  679         $found = false;
  680 
  681         foreach ($counts as $idx => $count) {
  682             list($target, $origin, /* $block */) = $this->extends[$idx];
  683 
  684             $origin = $this->glueFunctionSelectors($origin);
  685 
  686             // check count
  687             if ($count !== count($target)) {
  688                 continue;
  689             }
  690 
  691             $this->extends[$idx][3] = true;
  692 
  693             $rem = array_diff($single, $target);
  694 
  695             foreach ($origin as $j => $new) {
  696                 // prevent infinite loop when target extends itself
  697                 if ($this->isSelfExtend($single, $origin)) {
  698                     return false;
  699                 }
  700 
  701                 $replacement = end($new);
  702 
  703                 // Extending a decorated tag with another tag is not possible.
  704                 if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
  705                     preg_match('/^[a-z0-9]+$/i', $replacement[0])
  706                 ) {
  707                     unset($origin[$j]);
  708                     continue;
  709                 }
  710 
  711                 $combined = $this->combineSelectorSingle($replacement, $rem);
  712 
  713                 if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
  714                     $origin[$j][count($origin[$j]) - 1] = $combined;
  715                 }
  716             }
  717 
  718             $outOrigin = array_merge($outOrigin, $origin);
  719 
  720             $found = true;
  721         }
  722 
  723         return $found;
  724     }
  725 
  726     /**
  727      * Extract a relationship from the fragment.
  728      *
  729      * When extracting the last portion of a selector we will be left with a
  730      * fragment which may end with a direction relationship combinator. This
  731      * method will extract the relationship fragment and return it along side
  732      * the rest.
  733      *
  734      * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
  735      *
  736      * @return array The selector without the relationship fragment if any, the relationship fragment.
  737      */
  738     protected function extractRelationshipFromFragment(array $fragment)
  739     {
  740         $parents = [];
  741         $children = [];
  742         $j = $i = count($fragment);
  743 
  744         for (;;) {
  745             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
  746             $parents = array_slice($fragment, 0, $j);
  747             $slice = end($parents);
  748 
  749             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
  750                 break;
  751             }
  752 
  753             $j -= 2;
  754         }
  755 
  756         return [$parents, $children];
  757     }
  758 
  759     /**
  760      * Combine selector single
  761      *
  762      * @param array $base
  763      * @param array $other
  764      *
  765      * @return array
  766      */
  767     protected function combineSelectorSingle($base, $other)
  768     {
  769         $tag = [];
  770         $out = [];
  771         $wasTag = true;
  772 
  773         foreach ([$base, $other] as $single) {
  774             foreach ($single as $part) {
  775                 if (preg_match('/^[\[.:#]/', $part)) {
  776                     $out[] = $part;
  777                     $wasTag = false;
  778                 } elseif (preg_match('/^[^_-]/', $part)) {
  779                     $tag[] = $part;
  780                     $wasTag = true;
  781                 } elseif ($wasTag) {
  782                     $tag[count($tag) - 1] .= $part;
  783                 } else {
  784                     $out[count($out) - 1] .= $part;
  785                 }
  786             }
  787         }
  788 
  789         if (count($tag)) {
  790             array_unshift($out, $tag[0]);
  791         }
  792 
  793         return $out;
  794     }
  795 
  796     /**
  797      * Compile media
  798      *
  799      * @param \Leafo\ScssPhp\Block $media
  800      */
  801     protected function compileMedia(Block $media)
  802     {
  803         $this->pushEnv($media);
  804 
  805         $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
  806 
  807         if (! empty($mediaQueries) && $mediaQueries) {
  808             $previousScope = $this->scope;
  809             $parentScope = $this->mediaParent($this->scope);
  810 
  811             foreach ($mediaQueries as $mediaQuery) {
  812                 $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
  813 
  814                 $parentScope->children[] = $this->scope;
  815                 $parentScope = $this->scope;
  816             }
  817 
  818             // top level properties in a media cause it to be wrapped
  819             $needsWrap = false;
  820 
  821             foreach ($media->children as $child) {
  822                 $type = $child[0];
  823 
  824                 if ($type !== Type::T_BLOCK &&
  825                     $type !== Type::T_MEDIA &&
  826                     $type !== Type::T_DIRECTIVE &&
  827                     $type !== Type::T_IMPORT
  828                 ) {
  829                     $needsWrap = true;
  830                     break;
  831                 }
  832             }
  833 
  834             if ($needsWrap) {
  835                 $wrapped = new Block;
  836                 $wrapped->sourceName = $media->sourceName;
  837                 $wrapped->sourceIndex = $media->sourceIndex;
  838                 $wrapped->sourceLine = $media->sourceLine;
  839                 $wrapped->sourceColumn = $media->sourceColumn;
  840                 $wrapped->selectors = [];
  841                 $wrapped->comments = [];
  842                 $wrapped->parent = $media;
  843                 $wrapped->children = $media->children;
  844 
  845                 $media->children = [[Type::T_BLOCK, $wrapped]];
  846                 if (isset($this->lineNumberStyle)) {
  847                     $annotation = $this->makeOutputBlock(Type::T_COMMENT);
  848                     $annotation->depth = 0;
  849 
  850                     $file = $this->sourceNames[$media->sourceIndex];
  851                     $line = $media->sourceLine;
  852 
  853                     switch ($this->lineNumberStyle) {
  854                         case static::LINE_COMMENTS:
  855                             $annotation->lines[] = '/* line ' . $line
  856                                                  . ($file ? ', ' . $file : '')
  857                                                  . ' */';
  858                             break;
  859 
  860                         case static::DEBUG_INFO:
  861                             $annotation->lines[] = '@media -sass-debug-info{'
  862                                                  . ($file ? 'filename{font-family:"' . $file . '"}' : '')
  863                                                  . 'line{font-family:' . $line . '}}';
  864                             break;
  865                     }
  866 
  867                     $this->scope->children[] = $annotation;
  868                 }
  869             }
  870 
  871             $this->compileChildrenNoReturn($media->children, $this->scope);
  872 
  873             $this->scope = $previousScope;
  874         }
  875 
  876         $this->popEnv();
  877     }
  878 
  879     /**
  880      * Media parent
  881      *
  882      * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
  883      *
  884      * @return \Leafo\ScssPhp\Formatter\OutputBlock
  885      */
  886     protected function mediaParent(OutputBlock $scope)
  887     {
  888         while (! empty($scope->parent)) {
  889             if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
  890                 break;
  891             }
  892 
  893             $scope = $scope->parent;
  894         }
  895 
  896         return $scope;
  897     }
  898 
  899     /**
  900      * Compile directive
  901      *
  902      * @param \Leafo\ScssPhp\Block $block
  903      */
  904     protected function compileDirective(Block $block)
  905     {
  906         $s = '@' . $block->name;
  907 
  908         if (! empty($block->value)) {
  909             $s .= ' ' . $this->compileValue($block->value);
  910         }
  911 
  912         if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
  913             $this->compileKeyframeBlock($block, [$s]);
  914         } else {
  915             $this->compileNestedBlock($block, [$s]);
  916         }
  917     }
  918 
  919     /**
  920      * Compile at-root
  921      *
  922      * @param \Leafo\ScssPhp\Block $block
  923      */
  924     protected function compileAtRoot(Block $block)
  925     {
  926         $env     = $this->pushEnv($block);
  927         $envs    = $this->compactEnv($env);
  928         $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
  929 
  930         // wrap inline selector
  931         if ($block->selector) {
  932             $wrapped = new Block;
  933             $wrapped->sourceName   = $block->sourceName;
  934             $wrapped->sourceIndex  = $block->sourceIndex;
  935             $wrapped->sourceLine   = $block->sourceLine;
  936             $wrapped->sourceColumn = $block->sourceColumn;
  937             $wrapped->selectors    = $block->selector;
  938             $wrapped->comments     = [];
  939             $wrapped->parent       = $block;
  940             $wrapped->children     = $block->children;
  941             $wrapped->selfParent   = $block->selfParent;
  942 
  943             $block->children = [[Type::T_BLOCK, $wrapped]];
  944             $block->selector = null;
  945         }
  946 
  947         $selfParent = $block->selfParent;
  948 
  949         if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
  950             isset($block->parent->selectors) && $block->parent->selectors
  951         ) {
  952             $selfParent = $block->parent;
  953         }
  954 
  955         $this->env = $this->filterWithout($envs, $without);
  956 
  957         $saveScope   = $this->scope;
  958         $this->scope = $this->filterScopeWithout($saveScope, $without);
  959 
  960         // propagate selfParent to the children where they still can be useful
  961         $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
  962 
  963         $this->scope = $this->completeScope($this->scope, $saveScope);
  964         $this->scope = $saveScope;
  965         $this->env   = $this->extractEnv($envs);
  966 
  967         $this->popEnv();
  968     }
  969 
  970     /**
  971      * Filter at-root scope depending of with/without option
  972      *
  973      * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
  974      * @param mixed                                $without
  975      *
  976      * @return mixed
  977      */
  978     protected function filterScopeWithout($scope, $without)
  979     {
  980         $filteredScopes = [];
  981 
  982         if ($scope->type === TYPE::T_ROOT) {
  983             return $scope;
  984         }
  985 
  986         // start from the root
  987         while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
  988             $scope = $scope->parent;
  989         }
  990 
  991         for (;;) {
  992             if (! $scope) {
  993                 break;
  994             }
  995 
  996             if (! $this->isWithout($without, $scope)) {
  997                 $s = clone $scope;
  998                 $s->children = [];
  999                 $s->lines = [];
 1000                 $s->parent = null;
 1001 
 1002                 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
 1003                     $s->selectors = [];
 1004                 }
 1005 
 1006                 $filteredScopes[] = $s;
 1007             }
 1008 
 1009             if ($scope->children) {
 1010                 $scope = end($scope->children);
 1011             } else {
 1012                 $scope = null;
 1013             }
 1014         }
 1015 
 1016         if (! count($filteredScopes)) {
 1017             return $this->rootBlock;
 1018         }
 1019 
 1020         $newScope = array_shift($filteredScopes);
 1021         $newScope->parent = $this->rootBlock;
 1022 
 1023         $this->rootBlock->children[] = $newScope;
 1024 
 1025         $p = &$newScope;
 1026 
 1027         while (count($filteredScopes)) {
 1028             $s = array_shift($filteredScopes);
 1029             $s->parent = $p;
 1030             $p->children[] = &$s;
 1031             $p = $s;
 1032         }
 1033 
 1034         return $newScope;
 1035     }
 1036 
 1037     /**
 1038      * found missing selector from a at-root compilation in the previous scope
 1039      * (if at-root is just enclosing a property, the selector is in the parent tree)
 1040      *
 1041      * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
 1042      * @param \Leafo\ScssPhp\Formatter\OutputBlock $previousScope
 1043      *
 1044      * @return mixed
 1045      */
 1046     protected function completeScope($scope, $previousScope)
 1047     {
 1048         if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
 1049             $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
 1050         }
 1051 
 1052         if ($scope->children) {
 1053             foreach ($scope->children as $k => $c) {
 1054                 $scope->children[$k] = $this->completeScope($c, $previousScope);
 1055             }
 1056         }
 1057 
 1058         return $scope;
 1059     }
 1060 
 1061     /**
 1062      * Find a selector by the depth node in the scope
 1063      *
 1064      * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
 1065      * @param integer                              $depth
 1066      *
 1067      * @return array
 1068      */
 1069     protected function findScopeSelectors($scope, $depth)
 1070     {
 1071         if ($scope->depth === $depth && $scope->selectors) {
 1072             return $scope->selectors;
 1073         }
 1074 
 1075         if ($scope->children) {
 1076             foreach (array_reverse($scope->children) as $c) {
 1077                 if ($s = $this->findScopeSelectors($c, $depth)) {
 1078                     return $s;
 1079                 }
 1080             }
 1081         }
 1082 
 1083         return [];
 1084     }
 1085 
 1086     /**
 1087      * Compile @at-root's with: inclusion / without: exclusion into filter flags
 1088      *
 1089      * @param array $with
 1090      *
 1091      * @return integer
 1092      */
 1093     protected function compileWith($with)
 1094     {
 1095         static $mapping = [
 1096             'rule'     => self::WITH_RULE,
 1097             'media'    => self::WITH_MEDIA,
 1098             'supports' => self::WITH_SUPPORTS,
 1099             'all'      => self::WITH_ALL,
 1100         ];
 1101 
 1102         // exclude selectors by default
 1103         $without = static::WITH_RULE;
 1104 
 1105         if ($this->libMapHasKey([$with, static::$with])) {
 1106             $without = static::WITH_ALL;
 1107 
 1108             $list = $this->coerceList($this->libMapGet([$with, static::$with]));
 1109 
 1110             foreach ($list[2] as $item) {
 1111                 $keyword = $this->compileStringContent($this->coerceString($item));
 1112 
 1113                 if (array_key_exists($keyword, $mapping)) {
 1114                     $without &= ~($mapping[$keyword]);
 1115                 }
 1116             }
 1117         }
 1118 
 1119         if ($this->libMapHasKey([$with, static::$without])) {
 1120             $without = 0;
 1121 
 1122             $list = $this->coerceList($this->libMapGet([$with, static::$without]));
 1123 
 1124             foreach ($list[2] as $item) {
 1125                 $keyword = $this->compileStringContent($this->coerceString($item));
 1126 
 1127                 if (array_key_exists($keyword, $mapping)) {
 1128                     $without |= $mapping[$keyword];
 1129                 }
 1130             }
 1131         }
 1132 
 1133         return $without;
 1134     }
 1135 
 1136     /**
 1137      * Filter env stack
 1138      *
 1139      * @param array   $envs
 1140      * @param integer $without
 1141      *
 1142      * @return \Leafo\ScssPhp\Compiler\Environment
 1143      */
 1144     protected function filterWithout($envs, $without)
 1145     {
 1146         $filtered = [];
 1147 
 1148         foreach ($envs as $e) {
 1149             if ($e->block && $this->isWithout($without, $e->block)) {
 1150                 $ec = clone $e;
 1151                 $ec->block = null;
 1152                 $ec->selectors = [];
 1153                 $filtered[] = $ec;
 1154             } else {
 1155                 $filtered[] = $e;
 1156             }
 1157         }
 1158 
 1159         return $this->extractEnv($filtered);
 1160     }
 1161 
 1162     /**
 1163      * Filter WITH rules
 1164      *
 1165      * @param integer                                                   $without
 1166      * @param \Leafo\ScssPhp\Block|\Leafo\ScssPhp\Formatter\OutputBlock $block
 1167      *
 1168      * @return boolean
 1169      */
 1170     protected function isWithout($without, $block)
 1171     {
 1172         if (isset($block->type)) {
 1173             if ($block->type === Type::T_MEDIA) {
 1174                 return ($without & static::WITH_MEDIA) ? true : false;
 1175             }
 1176 
 1177             if ($block->type === Type::T_DIRECTIVE) {
 1178                 if (isset($block->name) && $block->name === 'supports') {
 1179                     return ($without & static::WITH_SUPPORTS) ? true : false;
 1180                 }
 1181 
 1182                 if (isset($block->selectors) && strpos(serialize($block->selectors), '@supports') !== false) {
 1183                     return ($without & static::WITH_SUPPORTS) ? true : false;
 1184                 }
 1185             }
 1186         }
 1187 
 1188         if ((($without & static::WITH_RULE) && isset($block->selectors))) {
 1189             return true;
 1190         }
 1191 
 1192         return false;
 1193     }
 1194 
 1195     /**
 1196      * Compile keyframe block
 1197      *
 1198      * @param \Leafo\ScssPhp\Block $block
 1199      * @param array                $selectors
 1200      */
 1201     protected function compileKeyframeBlock(Block $block, $selectors)
 1202     {
 1203         $env = $this->pushEnv($block);
 1204 
 1205         $envs = $this->compactEnv($env);
 1206 
 1207         $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
 1208             return ! isset($e->block->selectors);
 1209         }));
 1210 
 1211         $this->scope = $this->makeOutputBlock($block->type, $selectors);
 1212         $this->scope->depth = 1;
 1213         $this->scope->parent->children[] = $this->scope;
 1214 
 1215         $this->compileChildrenNoReturn($block->children, $this->scope);
 1216 
 1217         $this->scope = $this->scope->parent;
 1218         $this->env   = $this->extractEnv($envs);
 1219 
 1220         $this->popEnv();
 1221     }
 1222 
 1223     /**
 1224      * Compile nested block
 1225      *
 1226      * @param \Leafo\ScssPhp\Block $block
 1227      * @param array                $selectors
 1228      */
 1229     protected function compileNestedBlock(Block $block, $selectors)
 1230     {
 1231         $this->pushEnv($block);
 1232 
 1233         $this->scope = $this->makeOutputBlock($block->type, $selectors);
 1234         $this->scope->parent->children[] = $this->scope;
 1235 
 1236         // wrap assign children in a block
 1237         // except for @font-face
 1238         if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
 1239             // need wrapping?
 1240             $needWrapping = false;
 1241 
 1242             foreach ($block->children as $child) {
 1243                 if ($child[0] === Type::T_ASSIGN) {
 1244                     $needWrapping = true;
 1245                     break;
 1246                 }
 1247             }
 1248 
 1249             if ($needWrapping) {
 1250                 $wrapped = new Block;
 1251                 $wrapped->sourceName = $block->sourceName;
 1252                 $wrapped->sourceIndex = $block->sourceIndex;
 1253                 $wrapped->sourceLine = $block->sourceLine;
 1254                 $wrapped->sourceColumn = $block->sourceColumn;
 1255                 $wrapped->selectors = [];
 1256                 $wrapped->comments = [];
 1257                 $wrapped->parent = $block;
 1258                 $wrapped->children = $block->children;
 1259                 $wrapped->selfParent = $block->selfParent;
 1260 
 1261                 $block->children = [[Type::T_BLOCK, $wrapped]];
 1262             }
 1263         }
 1264 
 1265         $this->compileChildrenNoReturn($block->children, $this->scope);
 1266 
 1267         $this->scope = $this->scope->parent;
 1268 
 1269         $this->popEnv();
 1270     }
 1271 
 1272     /**
 1273      * Recursively compiles a block.
 1274      *
 1275      * A block is analogous to a CSS block in most cases. A single SCSS document
 1276      * is encapsulated in a block when parsed, but it does not have parent tags
 1277      * so all of its children appear on the root level when compiled.
 1278      *
 1279      * Blocks are made up of selectors and children.
 1280      *
 1281      * The children of a block are just all the blocks that are defined within.
 1282      *
 1283      * Compiling the block involves pushing a fresh environment on the stack,
 1284      * and iterating through the props, compiling each one.
 1285      *
 1286      * @see Compiler::compileChild()
 1287      *
 1288      * @param \Leafo\ScssPhp\Block $block
 1289      */
 1290     protected function compileBlock(Block $block)
 1291     {
 1292         $env = $this->pushEnv($block);
 1293         $env->selectors = $this->evalSelectors($block->selectors);
 1294 
 1295         $out = $this->makeOutputBlock(null);
 1296 
 1297         if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
 1298             $annotation = $this->makeOutputBlock(Type::T_COMMENT);
 1299             $annotation->depth = 0;
 1300 
 1301             $file = $this->sourceNames[$block->sourceIndex];
 1302             $line = $block->sourceLine;
 1303 
 1304             switch ($this->lineNumberStyle) {
 1305                 case static::LINE_COMMENTS:
 1306                     $annotation->lines[] = '/* line ' . $line
 1307                                          . ($file ? ', ' . $file : '')
 1308                                          . ' */';
 1309                     break;
 1310 
 1311                 case static::DEBUG_INFO:
 1312                     $annotation->lines[] = '@media -sass-debug-info{'
 1313                                          . ($file ? 'filename{font-family:"' . $file . '"}' : '')
 1314                                          . 'line{font-family:' . $line . '}}';
 1315                     break;
 1316             }
 1317 
 1318             $this->scope->children[] = $annotation;
 1319         }
 1320 
 1321         $this->scope->children[] = $out;
 1322 
 1323         if (count($block->children)) {
 1324             $out->selectors = $this->multiplySelectors($env, $block->selfParent);
 1325 
 1326             // propagate selfParent to the children where they still can be useful
 1327             $selfParentSelectors = null;
 1328 
 1329             if (isset($block->selfParent->selectors)) {
 1330                 $selfParentSelectors = $block->selfParent->selectors;
 1331                 $block->selfParent->selectors = $out->selectors;
 1332             }
 1333 
 1334             $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
 1335 
 1336             // and revert for the following childs of the same block
 1337             if ($selfParentSelectors) {
 1338                 $block->selfParent->selectors = $selfParentSelectors;
 1339             }
 1340         }
 1341 
 1342         $this->formatter->stripSemicolon($out->lines);
 1343 
 1344         $this->popEnv();
 1345     }
 1346 
 1347     /**
 1348      * Compile root level comment
 1349      *
 1350      * @param array $block
 1351      */
 1352     protected function compileComment($block)
 1353     {
 1354         $out = $this->makeOutputBlock(Type::T_COMMENT);
 1355         $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]);
 1356 
 1357         $this->scope->children[] = $out;
 1358     }
 1359 
 1360     /**
 1361      * Evaluate selectors
 1362      *
 1363      * @param array $selectors
 1364      *
 1365      * @return array
 1366      */
 1367     protected function evalSelectors($selectors)
 1368     {
 1369         $this->shouldEvaluate = false;
 1370 
 1371         $selectors = array_map([$this, 'evalSelector'], $selectors);
 1372 
 1373         // after evaluating interpolates, we might need a second pass
 1374         if ($this->shouldEvaluate) {
 1375             $selectors = $this->revertSelfSelector($selectors);
 1376             $buffer = $this->collapseSelectors($selectors);
 1377             $parser = $this->parserFactory(__METHOD__);
 1378 
 1379             if ($parser->parseSelector($buffer, $newSelectors)) {
 1380                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
 1381             }
 1382         }
 1383 
 1384         return $selectors;
 1385     }
 1386 
 1387     /**
 1388      * Evaluate selector
 1389      *
 1390      * @param array $selector
 1391      *
 1392      * @return array
 1393      */
 1394     protected function evalSelector($selector)
 1395     {
 1396         return array_map([$this, 'evalSelectorPart'], $selector);
 1397     }
 1398 
 1399     /**
 1400      * Evaluate selector part; replaces all the interpolates, stripping quotes
 1401      *
 1402      * @param array $part
 1403      *
 1404      * @return array
 1405      */
 1406     protected function evalSelectorPart($part)
 1407     {
 1408         foreach ($part as &$p) {
 1409             if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
 1410                 $p = $this->compileValue($p);
 1411 
 1412                 // force re-evaluation
 1413                 if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
 1414                     $this->shouldEvaluate = true;
 1415                 }
 1416             } elseif (is_string($p) && strlen($p) >= 2 &&
 1417                 ($first = $p[0]) && ($first === '"' || $first === "'") &&
 1418                 substr($p, -1) === $first
 1419             ) {
 1420                 $p = substr($p, 1, -1);
 1421             }
 1422         }
 1423 
 1424         return $this->flattenSelectorSingle($part);
 1425     }
 1426 
 1427     /**
 1428      * Collapse selectors
 1429      *
 1430      * @param array   $selectors
 1431      * @param boolean $selectorFormat
 1432      *   if false return a collapsed string
 1433      *   if true return an array description of a structured selector
 1434      *
 1435      * @return string
 1436      */
 1437     protected function collapseSelectors($selectors, $selectorFormat = false)
 1438     {
 1439         $parts = [];
 1440 
 1441         foreach ($selectors as $selector) {
 1442             $output = [];
 1443             $glueNext = false;
 1444 
 1445             foreach ($selector as $node) {
 1446                 $compound = '';
 1447 
 1448                 array_walk_recursive(
 1449                     $node,
 1450                     function ($value, $key) use (&$compound) {
 1451                         $compound .= $value;
 1452                     }
 1453                 );
 1454 
 1455                 if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
 1456                     if (count($output)) {
 1457                         $output[count($output) - 1] .= ' ' . $compound;
 1458                     } else {
 1459                         $output[] = $compound;
 1460                     }
 1461                     $glueNext = true;
 1462                 } elseif ($glueNext) {
 1463                     $output[count($output) - 1] .= ' ' . $compound;
 1464                     $glueNext = false;
 1465                 } else {
 1466                     $output[] = $compound;
 1467                 }
 1468             }
 1469 
 1470             if ($selectorFormat) {
 1471                 foreach ($output as &$o) {
 1472                     $o = [Type::T_STRING, '', [$o]];
 1473                 }
 1474                 $output = [Type::T_LIST, ' ', $output];
 1475             } else {
 1476                 $output = implode(' ', $output);
 1477             }
 1478 
 1479             $parts[] = $output;
 1480         }
 1481 
 1482         if ($selectorFormat) {
 1483             $parts = [Type::T_LIST, ',', $parts];
 1484         } else {
 1485             $parts = implode(', ', $parts);
 1486         }
 1487 
 1488         return $parts;
 1489     }
 1490 
 1491     /**
 1492      * Parse down the selector and revert [self] to "&" before a reparsing
 1493      *
 1494      * @param array $selectors
 1495      *
 1496      * @return array
 1497      */
 1498     protected function revertSelfSelector($selectors)
 1499     {
 1500         foreach ($selectors as &$part) {
 1501             if (is_array($part)) {
 1502                 if ($part === [Type::T_SELF]) {
 1503                     $part = '&';
 1504                 } else {
 1505                     $part = $this->revertSelfSelector($part);
 1506                 }
 1507             }
 1508         }
 1509 
 1510         return $selectors;
 1511     }
 1512 
 1513     /**
 1514      * Flatten selector single; joins together .classes and #ids
 1515      *
 1516      * @param array $single
 1517      *
 1518      * @return array
 1519      */
 1520     protected function flattenSelectorSingle($single)
 1521     {
 1522         $joined = [];
 1523 
 1524         foreach ($single as $part) {
 1525             if (empty($joined) ||
 1526                 ! is_string($part) ||
 1527                 preg_match('/[\[.:#%]/', $part)
 1528             ) {
 1529                 $joined[] = $part;
 1530                 continue;
 1531             }
 1532 
 1533             if (is_array(end($joined))) {
 1534                 $joined[] = $part;
 1535             } else {
 1536                 $joined[count($joined) - 1] .= $part;
 1537             }
 1538         }
 1539 
 1540         return $joined;
 1541     }
 1542 
 1543     /**
 1544      * Compile selector to string; self(&) should have been replaced by now
 1545      *
 1546      * @param string|array $selector
 1547      *
 1548      * @return string
 1549      */
 1550     protected function compileSelector($selector)
 1551     {
 1552         if (! is_array($selector)) {
 1553             return $selector; // media and the like
 1554         }
 1555 
 1556         return implode(
 1557             ' ',
 1558             array_map(
 1559                 [$this, 'compileSelectorPart'],
 1560                 $selector
 1561             )
 1562         );
 1563     }
 1564 
 1565     /**
 1566      * Compile selector part
 1567      *
 1568      * @param array $piece
 1569      *
 1570      * @return string
 1571      */
 1572     protected function compileSelectorPart($piece)
 1573     {
 1574         foreach ($piece as &$p) {
 1575             if (! is_array($p)) {
 1576                 continue;
 1577             }
 1578 
 1579             switch ($p[0]) {
 1580                 case Type::T_SELF:
 1581                     $p = '&';
 1582                     break;
 1583 
 1584                 default:
 1585                     $p = $this->compileValue($p);
 1586                     break;
 1587             }
 1588         }
 1589 
 1590         return implode($piece);
 1591     }
 1592 
 1593     /**
 1594      * Has selector placeholder?
 1595      *
 1596      * @param array $selector
 1597      *
 1598      * @return boolean
 1599      */
 1600     protected function hasSelectorPlaceholder($selector)
 1601     {
 1602         if (! is_array($selector)) {
 1603             return false;
 1604         }
 1605 
 1606         foreach ($selector as $parts) {
 1607             foreach ($parts as $part) {
 1608                 if (strlen($part) && '%' === $part[0]) {
 1609                     return true;
 1610                 }
 1611             }
 1612         }
 1613 
 1614         return false;
 1615     }
 1616 
 1617     protected function pushCallStack($name = '')
 1618     {
 1619         $this->callStack[] = [
 1620           'n' => $name,
 1621           Parser::SOURCE_INDEX => $this->sourceIndex,
 1622           Parser::SOURCE_LINE => $this->sourceLine,
 1623           Parser::SOURCE_COLUMN => $this->sourceColumn
 1624         ];
 1625 
 1626         // infinite calling loop
 1627         if (count($this->callStack) > 25000) {
 1628             // not displayed but you can var_dump it to deep debug
 1629             $msg = $this->callStackMessage(true, 100);
 1630             $msg = "Infinite calling loop";
 1631             $this->throwError($msg);
 1632         }
 1633     }
 1634 
 1635     protected function popCallStack()
 1636     {
 1637         array_pop($this->callStack);
 1638     }
 1639 
 1640     /**
 1641      * Compile children and return result
 1642      *
 1643      * @param array                                $stms
 1644      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 1645      * @param string                               $traceName
 1646      *
 1647      * @return array|null
 1648      */
 1649     protected function compileChildren($stms, OutputBlock $out, $traceName = '')
 1650     {
 1651         $this->pushCallStack($traceName);
 1652 
 1653         foreach ($stms as $stm) {
 1654             $ret = $this->compileChild($stm, $out);
 1655 
 1656             if (isset($ret)) {
 1657                 return $ret;
 1658             }
 1659         }
 1660 
 1661         $this->popCallStack();
 1662 
 1663         return null;
 1664     }
 1665 
 1666     /**
 1667      * Compile children and throw exception if unexpected @return
 1668      *
 1669      * @param array                                $stms
 1670      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 1671      * @param \Leafo\ScssPhp\Block                 $selfParent
 1672      * @param string                               $traceName
 1673      *
 1674      * @throws \Exception
 1675      */
 1676     protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
 1677     {
 1678         $this->pushCallStack($traceName);
 1679 
 1680         foreach ($stms as $stm) {
 1681             if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
 1682                 $stm[1]->selfParent = $selfParent;
 1683                 $ret = $this->compileChild($stm, $out);
 1684                 $stm[1]->selfParent = null;
 1685             } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
 1686                 $stm['selfParent'] = $selfParent;
 1687                 $ret = $this->compileChild($stm, $out);
 1688                 unset($stm['selfParent']);
 1689             } else {
 1690                 $ret = $this->compileChild($stm, $out);
 1691             }
 1692 
 1693             if (isset($ret)) {
 1694                 $this->throwError('@return may only be used within a function');
 1695 
 1696                 return;
 1697             }
 1698         }
 1699 
 1700         $this->popCallStack();
 1701     }
 1702 
 1703 
 1704     /**
 1705      * evaluate media query : compile internal value keeping the structure inchanged
 1706      *
 1707      * @param array $queryList
 1708      *
 1709      * @return array
 1710      */
 1711     protected function evaluateMediaQuery($queryList)
 1712     {
 1713         foreach ($queryList as $kql => $query) {
 1714             foreach ($query as $kq => $q) {
 1715                 for ($i = 1; $i < count($q); $i++) {
 1716                     $value = $this->compileValue($q[$i]);
 1717 
 1718                     // the parser had no mean to know if media type or expression if it was an interpolation
 1719                     if ($q[0] == Type::T_MEDIA_TYPE &&
 1720                         (strpos($value, '(') !== false ||
 1721                         strpos($value, ')') !== false ||
 1722                         strpos($value, ':') !== false)
 1723                     ) {
 1724                         $queryList[$kql][$kq][0] = Type::T_MEDIA_EXPRESSION;
 1725 
 1726                         if (strpos($value, 'and') !== false) {
 1727                             $values = explode('and', $value);
 1728                             $value = trim(array_pop($values));
 1729 
 1730                             while ($v = trim(array_pop($values))) {
 1731                                 $type = Type::T_MEDIA_EXPRESSION;
 1732 
 1733                                 if (strpos($v, '(') === false &&
 1734                                     strpos($v, ')') === false &&
 1735                                     strpos($v, ':') === false
 1736                                 ) {
 1737                                     $type = Type::T_MEDIA_TYPE;
 1738                                 }
 1739 
 1740                                 if (substr($v, 0, 1) === '(' && substr($v, -1) === ')') {
 1741                                     $v = substr($v, 1, -1);
 1742                                 }
 1743 
 1744                                 $queryList[$kql][] = [$type,[Type::T_KEYWORD, $v]];
 1745                             }
 1746                         }
 1747 
 1748                         if (substr($value, 0, 1) === '(' && substr($value, -1) === ')') {
 1749                             $value = substr($value, 1, -1);
 1750                         }
 1751                     }
 1752 
 1753                     $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
 1754                 }
 1755             }
 1756         }
 1757 
 1758         return $queryList;
 1759     }
 1760 
 1761     /**
 1762      * Compile media query
 1763      *
 1764      * @param array $queryList
 1765      *
 1766      * @return array
 1767      */
 1768     protected function compileMediaQuery($queryList)
 1769     {
 1770         $start = '@media ';
 1771         $default = trim($start);
 1772         $out = [];
 1773         $current = "";
 1774 
 1775         foreach ($queryList as $query) {
 1776             $type = null;
 1777             $parts = [];
 1778 
 1779             $mediaTypeOnly = true;
 1780 
 1781             foreach ($query as $q) {
 1782                 if ($q[0] !== Type::T_MEDIA_TYPE) {
 1783                     $mediaTypeOnly = false;
 1784                     break;
 1785                 }
 1786             }
 1787 
 1788             foreach ($query as $q) {
 1789                 switch ($q[0]) {
 1790                     case Type::T_MEDIA_TYPE:
 1791                         $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
 1792                         // combining not and anything else than media type is too risky and should be avoided
 1793                         if (! $mediaTypeOnly) {
 1794                             if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
 1795                                 if ($type) {
 1796                                     array_unshift($parts, implode(' ', array_filter($type)));
 1797                                 }
 1798 
 1799                                 if (! empty($parts)) {
 1800                                     if (strlen($current)) {
 1801                                         $current .= $this->formatter->tagSeparator;
 1802                                     }
 1803 
 1804                                     $current .= implode(' and ', $parts);
 1805                                 }
 1806 
 1807                                 if ($current) {
 1808                                     $out[] = $start . $current;
 1809                                 }
 1810 
 1811                                 $current = "";
 1812                                 $type = null;
 1813                                 $parts = [];
 1814                             }
 1815                         }
 1816 
 1817                         if ($newType === ['all'] && $default) {
 1818                             $default = $start . 'all';
 1819                         }
 1820 
 1821                         // all can be safely ignored and mixed with whatever else
 1822                         if ($newType !== ['all']) {
 1823                             if ($type) {
 1824                                 $type = $this->mergeMediaTypes($type, $newType);
 1825 
 1826                                 if (empty($type)) {
 1827                                     // merge failed : ignore this query that is not valid, skip to the next one
 1828                                     $parts = [];
 1829                                     $default = ''; // if everything fail, no @media at all
 1830                                     continue 3;
 1831                                 }
 1832                             } else {
 1833                                 $type = $newType;
 1834                             }
 1835                         }
 1836                         break;
 1837 
 1838                     case Type::T_MEDIA_EXPRESSION:
 1839                         if (isset($q[2])) {
 1840                             $parts[] = '('
 1841                                 . $this->compileValue($q[1])
 1842                                 . $this->formatter->assignSeparator
 1843                                 . $this->compileValue($q[2])
 1844                                 . ')';
 1845                         } else {
 1846                             $parts[] = '('
 1847                                 . $this->compileValue($q[1])
 1848                                 . ')';
 1849                         }
 1850                         break;
 1851 
 1852                     case Type::T_MEDIA_VALUE:
 1853                         $parts[] = $this->compileValue($q[1]);
 1854                         break;
 1855                 }
 1856             }
 1857 
 1858             if ($type) {
 1859                 array_unshift($parts, implode(' ', array_filter($type)));
 1860             }
 1861 
 1862             if (! empty($parts)) {
 1863                 if (strlen($current)) {
 1864                     $current .= $this->formatter->tagSeparator;
 1865                 }
 1866 
 1867                 $current .= implode(' and ', $parts);
 1868             }
 1869         }
 1870 
 1871         if ($current) {
 1872             $out[] = $start . $current;
 1873         }
 1874 
 1875         // no @media type except all, and no conflict?
 1876         if (! $out && $default) {
 1877             $out[] = $default;
 1878         }
 1879 
 1880         return $out;
 1881     }
 1882 
 1883     /**
 1884      * Merge direct relationships between selectors
 1885      *
 1886      * @param array $selectors1
 1887      * @param array $selectors2
 1888      *
 1889      * @return array
 1890      */
 1891     protected function mergeDirectRelationships($selectors1, $selectors2)
 1892     {
 1893         if (empty($selectors1) || empty($selectors2)) {
 1894             return array_merge($selectors1, $selectors2);
 1895         }
 1896 
 1897         $part1 = end($selectors1);
 1898         $part2 = end($selectors2);
 1899 
 1900         if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
 1901             return array_merge($selectors1, $selectors2);
 1902         }
 1903 
 1904         $merged = [];
 1905 
 1906         do {
 1907             $part1 = array_pop($selectors1);
 1908             $part2 = array_pop($selectors2);
 1909 
 1910             if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
 1911                 if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
 1912                     array_unshift($merged, [$part1[0] . $part2[0]]);
 1913                     $merged = array_merge($selectors1, $selectors2, $merged);
 1914                 } else {
 1915                     $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
 1916                 }
 1917 
 1918                 break;
 1919             }
 1920 
 1921             array_unshift($merged, $part1);
 1922         } while (! empty($selectors1) && ! empty($selectors2));
 1923 
 1924         return $merged;
 1925     }
 1926 
 1927     /**
 1928      * Merge media types
 1929      *
 1930      * @param array $type1
 1931      * @param array $type2
 1932      *
 1933      * @return array|null
 1934      */
 1935     protected function mergeMediaTypes($type1, $type2)
 1936     {
 1937         if (empty($type1)) {
 1938             return $type2;
 1939         }
 1940 
 1941         if (empty($type2)) {
 1942             return $type1;
 1943         }
 1944 
 1945         $m1 = '';
 1946         $t1 = '';
 1947 
 1948         if (count($type1) > 1) {
 1949             $m1= strtolower($type1[0]);
 1950             $t1= strtolower($type1[1]);
 1951         } else {
 1952             $t1 = strtolower($type1[0]);
 1953         }
 1954 
 1955         $m2 = '';
 1956         $t2 = '';
 1957 
 1958         if (count($type2) > 1) {
 1959             $m2 = strtolower($type2[0]);
 1960             $t2 = strtolower($type2[1]);
 1961         } else {
 1962             $t2 = strtolower($type2[0]);
 1963         }
 1964 
 1965         if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
 1966             if ($t1 === $t2) {
 1967                 return null;
 1968             }
 1969 
 1970             return [
 1971                 $m1 === Type::T_NOT ? $m2 : $m1,
 1972                 $m1 === Type::T_NOT ? $t2 : $t1,
 1973             ];
 1974         }
 1975 
 1976         if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
 1977             // CSS has no way of representing "neither screen nor print"
 1978             if ($t1 !== $t2) {
 1979                 return null;
 1980             }
 1981 
 1982             return [Type::T_NOT, $t1];
 1983         }
 1984 
 1985         if ($t1 !== $t2) {
 1986             return null;
 1987         }
 1988 
 1989         // t1 == t2, neither m1 nor m2 are "not"
 1990         return [empty($m1)? $m2 : $m1, $t1];
 1991     }
 1992 
 1993     /**
 1994      * Compile import; returns true if the value was something that could be imported
 1995      *
 1996      * @param array                                $rawPath
 1997      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 1998      * @param boolean                              $once
 1999      *
 2000      * @return boolean
 2001      */
 2002     protected function compileImport($rawPath, OutputBlock $out, $once = false)
 2003     {
 2004         if ($rawPath[0] === Type::T_STRING) {
 2005             $path = $this->compileStringContent($rawPath);
 2006 
 2007             if ($path = $this->findImport($path)) {
 2008                 if (! $once || ! in_array($path, $this->importedFiles)) {
 2009                     $this->importFile($path, $out);
 2010                     $this->importedFiles[] = $path;
 2011                 }
 2012 
 2013                 return true;
 2014             }
 2015 
 2016             return false;
 2017         }
 2018 
 2019         if ($rawPath[0] === Type::T_LIST) {
 2020             // handle a list of strings
 2021             if (count($rawPath[2]) === 0) {
 2022                 return false;
 2023             }
 2024 
 2025             foreach ($rawPath[2] as $path) {
 2026                 if ($path[0] !== Type::T_STRING) {
 2027                     return false;
 2028                 }
 2029             }
 2030 
 2031             foreach ($rawPath[2] as $path) {
 2032                 $this->compileImport($path, $out);
 2033             }
 2034 
 2035             return true;
 2036         }
 2037 
 2038         return false;
 2039     }
 2040 
 2041     /**
 2042      * Compile child; returns a value to halt execution
 2043      *
 2044      * @param array                                $child
 2045      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 2046      *
 2047      * @return array
 2048      */
 2049     protected function compileChild($child, OutputBlock $out)
 2050     {
 2051         if (isset($child[Parser::SOURCE_LINE])) {
 2052             $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
 2053             $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
 2054             $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
 2055         } elseif (is_array($child) && isset($child[1]->sourceLine)) {
 2056             $this->sourceIndex = $child[1]->sourceIndex;
 2057             $this->sourceLine = $child[1]->sourceLine;
 2058             $this->sourceColumn = $child[1]->sourceColumn;
 2059         } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
 2060             $this->sourceLine = $out->sourceLine;
 2061             $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
 2062 
 2063             if ($this->sourceIndex === false) {
 2064                 $this->sourceIndex = null;
 2065             }
 2066         }
 2067 
 2068         switch ($child[0]) {
 2069             case Type::T_SCSSPHP_IMPORT_ONCE:
 2070                 $rawPath = $this->reduce($child[1]);
 2071 
 2072                 if (! $this->compileImport($rawPath, $out, true)) {
 2073                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
 2074                 }
 2075                 break;
 2076 
 2077             case Type::T_IMPORT:
 2078                 $rawPath = $this->reduce($child[1]);
 2079 
 2080                 if (! $this->compileImport($rawPath, $out)) {
 2081                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
 2082                 }
 2083                 break;
 2084 
 2085             case Type::T_DIRECTIVE:
 2086                 $this->compileDirective($child[1]);
 2087                 break;
 2088 
 2089             case Type::T_AT_ROOT:
 2090                 $this->compileAtRoot($child[1]);
 2091                 break;
 2092 
 2093             case Type::T_MEDIA:
 2094                 $this->compileMedia($child[1]);
 2095                 break;
 2096 
 2097             case Type::T_BLOCK:
 2098                 $this->compileBlock($child[1]);
 2099                 break;
 2100 
 2101             case Type::T_CHARSET:
 2102                 if (! $this->charsetSeen) {
 2103                     $this->charsetSeen = true;
 2104 
 2105                     $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
 2106                 }
 2107                 break;
 2108 
 2109             case Type::T_ASSIGN:
 2110                 list(, $name, $value) = $child;
 2111 
 2112                 if ($name[0] === Type::T_VARIABLE) {
 2113                     $flags = isset($child[3]) ? $child[3] : [];
 2114                     $isDefault = in_array('!default', $flags);
 2115                     $isGlobal = in_array('!global', $flags);
 2116 
 2117                     if ($isGlobal) {
 2118                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
 2119                         break;
 2120                     }
 2121 
 2122                     $shouldSet = $isDefault &&
 2123                         (($result = $this->get($name[1], false)) === null
 2124                         || $result === static::$null);
 2125 
 2126                     if (! $isDefault || $shouldSet) {
 2127                         $this->set($name[1], $this->reduce($value), true, null, $value);
 2128                     }
 2129                     break;
 2130                 }
 2131 
 2132                 $compiledName = $this->compileValue($name);
 2133 
 2134                 // handle shorthand syntax: size / line-height
 2135                 if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
 2136                     if ($value[0] === Type::T_VARIABLE) {
 2137                         // if the font value comes from variable, the content is already reduced
 2138                         // (i.e., formulas were already calculated), so we need the original unreduced value
 2139                         $value = $this->get($value[1], true, null, true);
 2140                     }
 2141 
 2142                     $fontValue=&$value;
 2143 
 2144                     if ($value[0] === Type::T_LIST && $value[1]==',') {
 2145                         // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
 2146                         // we need to handle the first list element
 2147                         $fontValue=&$value[2][0];
 2148                     }
 2149 
 2150                     if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
 2151                         $fontValue = $this->expToString($fontValue);
 2152                     } elseif ($fontValue[0] === Type::T_LIST) {
 2153                         foreach ($fontValue[2] as &$item) {
 2154                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
 2155                                 $item = $this->expToString($item);
 2156                             }
 2157                         }
 2158                     }
 2159                 }
 2160 
 2161                 // if the value reduces to null from something else then
 2162                 // the property should be discarded
 2163                 if ($value[0] !== Type::T_NULL) {
 2164                     $value = $this->reduce($value);
 2165 
 2166                     if ($value[0] === Type::T_NULL || $value === static::$nullString) {
 2167                         break;
 2168                     }
 2169                 }
 2170 
 2171                 $compiledValue = $this->compileValue($value);
 2172 
 2173                 $out->lines[] = $this->formatter->property(
 2174                     $compiledName,
 2175                     $compiledValue
 2176                 );
 2177                 break;
 2178 
 2179             case Type::T_COMMENT:
 2180                 if ($out->type === Type::T_ROOT) {
 2181                     $this->compileComment($child);
 2182                     break;
 2183                 }
 2184 
 2185                 $out->lines[] = $child[1];
 2186                 break;
 2187 
 2188             case Type::T_MIXIN:
 2189             case Type::T_FUNCTION:
 2190                 list(, $block) = $child;
 2191 
 2192                 $this->set(static::$namespaces[$block->type] . $block->name, $block);
 2193                 break;
 2194 
 2195             case Type::T_EXTEND:
 2196                 foreach ($child[1] as $sel) {
 2197                     $results = $this->evalSelectors([$sel]);
 2198 
 2199                     foreach ($results as $result) {
 2200                         // only use the first one
 2201                         $result = current($result);
 2202 
 2203                         $this->pushExtends($result, $out->selectors, $child);
 2204                     }
 2205                 }
 2206                 break;
 2207 
 2208             case Type::T_IF:
 2209                 list(, $if) = $child;
 2210 
 2211                 if ($this->isTruthy($this->reduce($if->cond, true))) {
 2212                     return $this->compileChildren($if->children, $out);
 2213                 }
 2214 
 2215                 foreach ($if->cases as $case) {
 2216                     if ($case->type === Type::T_ELSE ||
 2217                         $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
 2218                     ) {
 2219                         return $this->compileChildren($case->children, $out);
 2220                     }
 2221                 }
 2222                 break;
 2223 
 2224             case Type::T_EACH:
 2225                 list(, $each) = $child;
 2226 
 2227                 $list = $this->coerceList($this->reduce($each->list));
 2228 
 2229                 $this->pushEnv();
 2230 
 2231                 foreach ($list[2] as $item) {
 2232                     if (count($each->vars) === 1) {
 2233                         $this->set($each->vars[0], $item, true);
 2234                     } else {
 2235                         list(,, $values) = $this->coerceList($item);
 2236 
 2237                         foreach ($each->vars as $i => $var) {
 2238                             $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
 2239                         }
 2240                     }
 2241 
 2242                     $ret = $this->compileChildren($each->children, $out);
 2243 
 2244                     if ($ret) {
 2245                         if ($ret[0] !== Type::T_CONTROL) {
 2246                             $this->popEnv();
 2247 
 2248                             return $ret;
 2249                         }
 2250 
 2251                         if ($ret[1]) {
 2252                             break;
 2253                         }
 2254                     }
 2255                 }
 2256 
 2257                 $this->popEnv();
 2258                 break;
 2259 
 2260             case Type::T_WHILE:
 2261                 list(, $while) = $child;
 2262 
 2263                 while ($this->isTruthy($this->reduce($while->cond, true))) {
 2264                     $ret = $this->compileChildren($while->children, $out);
 2265 
 2266                     if ($ret) {
 2267                         if ($ret[0] !== Type::T_CONTROL) {
 2268                             return $ret;
 2269                         }
 2270 
 2271                         if ($ret[1]) {
 2272                             break;
 2273                         }
 2274                     }
 2275                 }
 2276                 break;
 2277 
 2278             case Type::T_FOR:
 2279                 list(, $for) = $child;
 2280 
 2281                 $start = $this->reduce($for->start, true);
 2282                 $end   = $this->reduce($for->end, true);
 2283 
 2284                 if (! ($start[2] == $end[2] || $end->unitless())) {
 2285                     $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
 2286 
 2287                     break;
 2288                 }
 2289 
 2290                 $unit  = $start[2];
 2291                 $start = $start[1];
 2292                 $end   = $end[1];
 2293 
 2294                 $d = $start < $end ? 1 : -1;
 2295 
 2296                 for (;;) {
 2297                     if ((! $for->until && $start - $d == $end) ||
 2298                         ($for->until && $start == $end)
 2299                     ) {
 2300                         break;
 2301                     }
 2302 
 2303                     $this->set($for->var, new Node\Number($start, $unit));
 2304                     $start += $d;
 2305 
 2306                     $ret = $this->compileChildren($for->children, $out);
 2307 
 2308                     if ($ret) {
 2309                         if ($ret[0] !== Type::T_CONTROL) {
 2310                             return $ret;
 2311                         }
 2312 
 2313                         if ($ret[1]) {
 2314                             break;
 2315                         }
 2316                     }
 2317                 }
 2318                 break;
 2319 
 2320             case Type::T_BREAK:
 2321                 return [Type::T_CONTROL, true];
 2322 
 2323             case Type::T_CONTINUE:
 2324                 return [Type::T_CONTROL, false];
 2325 
 2326             case Type::T_RETURN:
 2327                 return $this->reduce($child[1], true);
 2328 
 2329             case Type::T_NESTED_PROPERTY:
 2330                 list(, $prop) = $child;
 2331 
 2332                 $prefixed = [];
 2333                 $prefix = $this->compileValue($prop->prefix) . '-';
 2334 
 2335                 foreach ($prop->children as $child) {
 2336                     switch ($child[0]) {
 2337                         case Type::T_ASSIGN:
 2338                             array_unshift($child[1][2], $prefix);
 2339                             break;
 2340 
 2341                         case Type::T_NESTED_PROPERTY:
 2342                             array_unshift($child[1]->prefix[2], $prefix);
 2343                             break;
 2344                     }
 2345 
 2346                     $prefixed[] = $child;
 2347                 }
 2348 
 2349                 $this->compileChildrenNoReturn($prefixed, $out);
 2350                 break;
 2351 
 2352             case Type::T_INCLUDE:
 2353                 // including a mixin
 2354                 list(, $name, $argValues, $content) = $child;
 2355 
 2356                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
 2357 
 2358                 if (! $mixin) {
 2359                     $this->throwError("Undefined mixin $name");
 2360                     break;
 2361                 }
 2362 
 2363                 $callingScope = $this->getStoreEnv();
 2364 
 2365                 // push scope, apply args
 2366                 $this->pushEnv();
 2367                 $this->env->depth--;
 2368 
 2369                 $storeEnv = $this->storeEnv;
 2370                 $this->storeEnv = $this->env;
 2371 
 2372                 // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
 2373                 // and assign this fake parent to childs
 2374                 $selfParent = null;
 2375 
 2376                 if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
 2377                     $selfParent = $child['selfParent'];
 2378                 } else {
 2379                     $parentSelectors = $this->multiplySelectors($this->env);
 2380 
 2381                     if ($parentSelectors) {
 2382                         $parent = new Block();
 2383                         $parent->selectors = $parentSelectors;
 2384 
 2385                         foreach ($mixin->children as $k => $child) {
 2386                             if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
 2387                                 $mixin->children[$k][1]->parent = $parent;
 2388                             }
 2389                         }
 2390                     }
 2391                 }
 2392 
 2393                 // clone the stored content to not have its scope spoiled by a further call to the same mixin
 2394                 // i.e., recursive @include of the same mixin
 2395                 if (isset($content)) {
 2396                     $copyContent = clone $content;
 2397                     $copyContent->scope = $callingScope;
 2398 
 2399                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
 2400                 }
 2401 
 2402                 if (isset($mixin->args)) {
 2403                     $this->applyArguments($mixin->args, $argValues);
 2404                 }
 2405 
 2406                 $this->env->marker = 'mixin';
 2407 
 2408                 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
 2409 
 2410                 $this->storeEnv = $storeEnv;
 2411 
 2412                 $this->popEnv();
 2413                 break;
 2414 
 2415             case Type::T_MIXIN_CONTENT:
 2416                 $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
 2417                 $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
 2418 
 2419                 if (! $content) {
 2420                     $content = new \stdClass();
 2421                     $content->scope = new \stdClass();
 2422                     $content->children = $this->storeEnv->parent->block->children;
 2423                     break;
 2424                 }
 2425 
 2426                 $storeEnv = $this->storeEnv;
 2427                 $this->storeEnv = $content->scope;
 2428                 $this->compileChildrenNoReturn($content->children, $out);
 2429 
 2430                 $this->storeEnv = $storeEnv;
 2431                 break;
 2432 
 2433             case Type::T_DEBUG:
 2434                 list(, $value) = $child;
 2435 
 2436                 $fname = $this->sourceNames[$this->sourceIndex];
 2437                 $line = $this->sourceLine;
 2438                 $value = $this->compileValue($this->reduce($value, true));
 2439                 fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
 2440                 break;
 2441 
 2442             case Type::T_WARN:
 2443                 list(, $value) = $child;
 2444 
 2445                 $fname = $this->sourceNames[$this->sourceIndex];
 2446                 $line = $this->sourceLine;
 2447                 $value = $this->compileValue($this->reduce($value, true));
 2448                 fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
 2449                 break;
 2450 
 2451             case Type::T_ERROR:
 2452                 list(, $value) = $child;
 2453 
 2454                 $fname = $this->sourceNames[$this->sourceIndex];
 2455                 $line = $this->sourceLine;
 2456                 $value = $this->compileValue($this->reduce($value, true));
 2457                 $this->throwError("File $fname on line $line ERROR: $value\n");
 2458                 break;
 2459 
 2460             case Type::T_CONTROL:
 2461                 $this->throwError('@break/@continue not permitted in this scope');
 2462                 break;
 2463 
 2464             default:
 2465                 $this->throwError("unknown child type: $child[0]");
 2466         }
 2467     }
 2468 
 2469     /**
 2470      * Reduce expression to string
 2471      *
 2472      * @param array $exp
 2473      *
 2474      * @return array
 2475      */
 2476     protected function expToString($exp)
 2477     {
 2478         list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
 2479 
 2480         $content = [$this->reduce($left)];
 2481 
 2482         if ($whiteLeft) {
 2483             $content[] = ' ';
 2484         }
 2485 
 2486         $content[] = $op;
 2487 
 2488         if ($whiteRight) {
 2489             $content[] = ' ';
 2490         }
 2491 
 2492         $content[] = $this->reduce($right);
 2493 
 2494         return [Type::T_STRING, '', $content];
 2495     }
 2496 
 2497     /**
 2498      * Is truthy?
 2499      *
 2500      * @param array $value
 2501      *
 2502      * @return boolean
 2503      */
 2504     protected function isTruthy($value)
 2505     {
 2506         return $value !== static::$false && $value !== static::$null;
 2507     }
 2508 
 2509     /**
 2510      * Is the value a direct relationship combinator?
 2511      *
 2512      * @param string $value
 2513      *
 2514      * @return boolean
 2515      */
 2516     protected function isImmediateRelationshipCombinator($value)
 2517     {
 2518         return $value === '>' || $value === '+' || $value === '~';
 2519     }
 2520 
 2521     /**
 2522      * Should $value cause its operand to eval
 2523      *
 2524      * @param array $value
 2525      *
 2526      * @return boolean
 2527      */
 2528     protected function shouldEval($value)
 2529     {
 2530         switch ($value[0]) {
 2531             case Type::T_EXPRESSION:
 2532                 if ($value[1] === '/') {
 2533                     return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
 2534                 }
 2535 
 2536                 // fall-thru
 2537             case Type::T_VARIABLE:
 2538             case Type::T_FUNCTION_CALL:
 2539                 return true;
 2540         }
 2541 
 2542         return false;
 2543     }
 2544 
 2545     /**
 2546      * Reduce value
 2547      *
 2548      * @param array   $value
 2549      * @param boolean $inExp
 2550      *
 2551      * @return array|\Leafo\ScssPhp\Node\Number
 2552      */
 2553     protected function reduce($value, $inExp = false)
 2554     {
 2555 
 2556         switch ($value[0]) {
 2557             case Type::T_EXPRESSION:
 2558                 list(, $op, $left, $right, $inParens) = $value;
 2559 
 2560                 $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
 2561                 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
 2562 
 2563                 $left = $this->reduce($left, true);
 2564 
 2565                 if ($op !== 'and' && $op !== 'or') {
 2566                     $right = $this->reduce($right, true);
 2567                 }
 2568 
 2569                 // special case: looks like css shorthand
 2570                 if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
 2571                     && (($right[0] !== Type::T_NUMBER && $right[2] != '')
 2572                     || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
 2573                 ) {
 2574                     return $this->expToString($value);
 2575                 }
 2576 
 2577                 $left = $this->coerceForExpression($left);
 2578                 $right = $this->coerceForExpression($right);
 2579 
 2580                 $ltype = $left[0];
 2581                 $rtype = $right[0];
 2582 
 2583                 $ucOpName = ucfirst($opName);
 2584                 $ucLType  = ucfirst($ltype);
 2585                 $ucRType  = ucfirst($rtype);
 2586 
 2587                 // this tries:
 2588                 // 1. op[op name][left type][right type]
 2589                 // 2. op[left type][right type] (passing the op as first arg
 2590                 // 3. op[op name]
 2591                 $fn = "op${ucOpName}${ucLType}${ucRType}";
 2592 
 2593                 if (is_callable([$this, $fn]) ||
 2594                     (($fn = "op${ucLType}${ucRType}") &&
 2595                         is_callable([$this, $fn]) &&
 2596                         $passOp = true) ||
 2597                     (($fn = "op${ucOpName}") &&
 2598                         is_callable([$this, $fn]) &&
 2599                         $genOp = true)
 2600                 ) {
 2601                     $coerceUnit = false;
 2602 
 2603                     if (! isset($genOp) &&
 2604                         $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
 2605                     ) {
 2606                         $coerceUnit = true;
 2607 
 2608                         switch ($opName) {
 2609                             case 'mul':
 2610                                 $targetUnit = $left[2];
 2611 
 2612                                 foreach ($right[2] as $unit => $exp) {
 2613                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
 2614                                 }
 2615                                 break;
 2616 
 2617                             case 'div':
 2618                                 $targetUnit = $left[2];
 2619 
 2620                                 foreach ($right[2] as $unit => $exp) {
 2621                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
 2622                                 }
 2623                                 break;
 2624 
 2625                             case 'mod':
 2626                                 $targetUnit = $left[2];
 2627                                 break;
 2628 
 2629                             default:
 2630                                 $targetUnit = $left->unitless() ? $right[2] : $left[2];
 2631                         }
 2632 
 2633                         if (! $left->unitless() && ! $right->unitless()) {
 2634                             $left = $left->normalize();
 2635                             $right = $right->normalize();
 2636                         }
 2637                     }
 2638 
 2639                     $shouldEval = $inParens || $inExp;
 2640 
 2641                     if (isset($passOp)) {
 2642                         $out = $this->$fn($op, $left, $right, $shouldEval);
 2643                     } else {
 2644                         $out = $this->$fn($left, $right, $shouldEval);
 2645                     }
 2646 
 2647                     if (isset($out)) {
 2648                         if ($coerceUnit && $out[0] === Type::T_NUMBER) {
 2649                             $out = $out->coerce($targetUnit);
 2650                         }
 2651 
 2652                         return $out;
 2653                     }
 2654                 }
 2655 
 2656                 return $this->expToString($value);
 2657 
 2658             case Type::T_UNARY:
 2659                 list(, $op, $exp, $inParens) = $value;
 2660 
 2661                 $inExp = $inExp || $this->shouldEval($exp);
 2662                 $exp = $this->reduce($exp);
 2663 
 2664                 if ($exp[0] === Type::T_NUMBER) {
 2665                     switch ($op) {
 2666                         case '+':
 2667                             return new Node\Number($exp[1], $exp[2]);
 2668 
 2669                         case '-':
 2670                             return new Node\Number(-$exp[1], $exp[2]);
 2671                     }
 2672                 }
 2673 
 2674                 if ($op === 'not') {
 2675                     if ($inExp || $inParens) {
 2676                         if ($exp === static::$false || $exp === static::$null) {
 2677                             return static::$true;
 2678                         }
 2679 
 2680                         return static::$false;
 2681                     }
 2682 
 2683                     $op = $op . ' ';
 2684                 }
 2685 
 2686                 return [Type::T_STRING, '', [$op, $exp]];
 2687 
 2688             case Type::T_VARIABLE:
 2689                 return $this->reduce($this->get($value[1]));
 2690 
 2691             case Type::T_LIST:
 2692                 foreach ($value[2] as &$item) {
 2693                     $item = $this->reduce($item);
 2694                 }
 2695 
 2696                 return $value;
 2697 
 2698             case Type::T_MAP:
 2699                 foreach ($value[1] as &$item) {
 2700                     $item = $this->reduce($item);
 2701                 }
 2702 
 2703                 foreach ($value[2] as &$item) {
 2704                     $item = $this->reduce($item);
 2705                 }
 2706 
 2707                 return $value;
 2708 
 2709             case Type::T_STRING:
 2710                 foreach ($value[2] as &$item) {
 2711                     if (is_array($item) || $item instanceof \ArrayAccess) {
 2712                         $item = $this->reduce($item);
 2713                     }
 2714                 }
 2715 
 2716                 return $value;
 2717 
 2718             case Type::T_INTERPOLATE:
 2719                 $value[1] = $this->reduce($value[1]);
 2720                 if ($inExp) {
 2721                     return $value[1];
 2722                 }
 2723 
 2724                 return $value;
 2725 
 2726             case Type::T_FUNCTION_CALL:
 2727                 return $this->fncall($value[1], $value[2]);
 2728 
 2729             case Type::T_SELF:
 2730                 $selfSelector = $this->multiplySelectors($this->env);
 2731                 $selfSelector = $this->collapseSelectors($selfSelector, true);
 2732                 return $selfSelector;
 2733 
 2734             default:
 2735                 return $value;
 2736         }
 2737     }
 2738 
 2739     /**
 2740      * Function caller
 2741      *
 2742      * @param string $name
 2743      * @param array  $argValues
 2744      *
 2745      * @return array|null
 2746      */
 2747     protected function fncall($name, $argValues)
 2748     {
 2749         // SCSS @function
 2750         if ($this->callScssFunction($name, $argValues, $returnValue)) {
 2751             return $returnValue;
 2752         }
 2753 
 2754         // native PHP functions
 2755         if ($this->callNativeFunction($name, $argValues, $returnValue)) {
 2756             return $returnValue;
 2757         }
 2758 
 2759         // for CSS functions, simply flatten the arguments into a list
 2760         $listArgs = [];
 2761 
 2762         foreach ((array) $argValues as $arg) {
 2763             if (empty($arg[0])) {
 2764                 $listArgs[] = $this->reduce($arg[1]);
 2765             }
 2766         }
 2767 
 2768         return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
 2769     }
 2770 
 2771     /**
 2772      * Normalize name
 2773      *
 2774      * @param string $name
 2775      *
 2776      * @return string
 2777      */
 2778     protected function normalizeName($name)
 2779     {
 2780         return str_replace('-', '_', $name);
 2781     }
 2782 
 2783     /**
 2784      * Normalize value
 2785      *
 2786      * @param array $value
 2787      *
 2788      * @return array
 2789      */
 2790     public function normalizeValue($value)
 2791     {
 2792         $value = $this->coerceForExpression($this->reduce($value));
 2793 
 2794         switch ($value[0]) {
 2795             case Type::T_LIST:
 2796                 $value = $this->extractInterpolation($value);
 2797 
 2798                 if ($value[0] !== Type::T_LIST) {
 2799                     return [Type::T_KEYWORD, $this->compileValue($value)];
 2800                 }
 2801 
 2802                 foreach ($value[2] as $key => $item) {
 2803                     $value[2][$key] = $this->normalizeValue($item);
 2804                 }
 2805 
 2806                 return $value;
 2807 
 2808             case Type::T_STRING:
 2809                 return [$value[0], '"', [$this->compileStringContent($value)]];
 2810 
 2811             case Type::T_NUMBER:
 2812                 return $value->normalize();
 2813 
 2814             case Type::T_INTERPOLATE:
 2815                 return [Type::T_KEYWORD, $this->compileValue($value)];
 2816 
 2817             default:
 2818                 return $value;
 2819         }
 2820     }
 2821 
 2822     /**
 2823      * Add numbers
 2824      *
 2825      * @param array $left
 2826      * @param array $right
 2827      *
 2828      * @return \Leafo\ScssPhp\Node\Number
 2829      */
 2830     protected function opAddNumberNumber($left, $right)
 2831     {
 2832         return new Node\Number($left[1] + $right[1], $left[2]);
 2833     }
 2834 
 2835     /**
 2836      * Multiply numbers
 2837      *
 2838      * @param array $left
 2839      * @param array $right
 2840      *
 2841      * @return \Leafo\ScssPhp\Node\Number
 2842      */
 2843     protected function opMulNumberNumber($left, $right)
 2844     {
 2845         return new Node\Number($left[1] * $right[1], $left[2]);
 2846     }
 2847 
 2848     /**
 2849      * Subtract numbers
 2850      *
 2851      * @param array $left
 2852      * @param array $right
 2853      *
 2854      * @return \Leafo\ScssPhp\Node\Number
 2855      */
 2856     protected function opSubNumberNumber($left, $right)
 2857     {
 2858         return new Node\Number($left[1] - $right[1], $left[2]);
 2859     }
 2860 
 2861     /**
 2862      * Divide numbers
 2863      *
 2864      * @param array $left
 2865      * @param array $right
 2866      *
 2867      * @return array|\Leafo\ScssPhp\Node\Number
 2868      */
 2869     protected function opDivNumberNumber($left, $right)
 2870     {
 2871         if ($right[1] == 0) {
 2872             return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
 2873         }
 2874 
 2875         return new Node\Number($left[1] / $right[1], $left[2]);
 2876     }
 2877 
 2878     /**
 2879      * Mod numbers
 2880      *
 2881      * @param array $left
 2882      * @param array $right
 2883      *
 2884      * @return \Leafo\ScssPhp\Node\Number
 2885      */
 2886     protected function opModNumberNumber($left, $right)
 2887     {
 2888         return new Node\Number($left[1] % $right[1], $left[2]);
 2889     }
 2890 
 2891     /**
 2892      * Add strings
 2893      *
 2894      * @param array $left
 2895      * @param array $right
 2896      *
 2897      * @return array|null
 2898      */
 2899     protected function opAdd($left, $right)
 2900     {
 2901         if ($strLeft = $this->coerceString($left)) {
 2902             if ($right[0] === Type::T_STRING) {
 2903                 $right[1] = '';
 2904             }
 2905 
 2906             $strLeft[2][] = $right;
 2907 
 2908             return $strLeft;
 2909         }
 2910 
 2911         if ($strRight = $this->coerceString($right)) {
 2912             if ($left[0] === Type::T_STRING) {
 2913                 $left[1] = '';
 2914             }
 2915 
 2916             array_unshift($strRight[2], $left);
 2917 
 2918             return $strRight;
 2919         }
 2920 
 2921         return null;
 2922     }
 2923 
 2924     /**
 2925      * Boolean and
 2926      *
 2927      * @param array   $left
 2928      * @param array   $right
 2929      * @param boolean $shouldEval
 2930      *
 2931      * @return array|null
 2932      */
 2933     protected function opAnd($left, $right, $shouldEval)
 2934     {
 2935         $truthy = ($left === static::$null || $right === static::$null) ||
 2936                   ($left === static::$false || $left === static::$true) &&
 2937                   ($right === static::$false || $right === static::$true);
 2938 
 2939         if (! $shouldEval) {
 2940             if (! $truthy) {
 2941                 return null;
 2942             }
 2943         }
 2944 
 2945         if ($left !== static::$false && $left !== static::$null) {
 2946             return $this->reduce($right, true);
 2947         }
 2948 
 2949         return $left;
 2950     }
 2951 
 2952     /**
 2953      * Boolean or
 2954      *
 2955      * @param array   $left
 2956      * @param array   $right
 2957      * @param boolean $shouldEval
 2958      *
 2959      * @return array|null
 2960      */
 2961     protected function opOr($left, $right, $shouldEval)
 2962     {
 2963         $truthy = ($left === static::$null || $right === static::$null) ||
 2964                   ($left === static::$false || $left === static::$true) &&
 2965                   ($right === static::$false || $right === static::$true);
 2966 
 2967         if (! $shouldEval) {
 2968             if (! $truthy) {
 2969                 return null;
 2970             }
 2971         }
 2972 
 2973         if ($left !== static::$false && $left !== static::$null) {
 2974             return $left;
 2975         }
 2976 
 2977         return $this->reduce($right, true);
 2978     }
 2979 
 2980     /**
 2981      * Compare colors
 2982      *
 2983      * @param string $op
 2984      * @param array  $left
 2985      * @param array  $right
 2986      *
 2987      * @return array
 2988      */
 2989     protected function opColorColor($op, $left, $right)
 2990     {
 2991         $out = [Type::T_COLOR];
 2992 
 2993         foreach ([1, 2, 3] as $i) {
 2994             $lval = isset($left[$i]) ? $left[$i] : 0;
 2995             $rval = isset($right[$i]) ? $right[$i] : 0;
 2996 
 2997             switch ($op) {
 2998                 case '+':
 2999                     $out[] = $lval + $rval;
 3000                     break;
 3001 
 3002                 case '-':
 3003                     $out[] = $lval - $rval;
 3004                     break;
 3005 
 3006                 case '*':
 3007                     $out[] = $lval * $rval;
 3008                     break;
 3009 
 3010                 case '%':
 3011                     $out[] = $lval % $rval;
 3012                     break;
 3013 
 3014                 case '/':
 3015                     if ($rval == 0) {
 3016                         $this->throwError("color: Can't divide by zero");
 3017                         break 2;
 3018                     }
 3019 
 3020                     $out[] = (int) ($lval / $rval);
 3021                     break;
 3022 
 3023                 case '==':
 3024                     return $this->opEq($left, $right);
 3025 
 3026                 case '!=':
 3027                     return $this->opNeq($left, $right);
 3028 
 3029                 default:
 3030                     $this->throwError("color: unknown op $op");
 3031                     break 2;
 3032             }
 3033         }
 3034 
 3035         if (isset($left[4])) {
 3036             $out[4] = $left[4];
 3037         } elseif (isset($right[4])) {
 3038             $out[4] = $right[4];
 3039         }
 3040 
 3041         return $this->fixColor($out);
 3042     }
 3043 
 3044     /**
 3045      * Compare color and number
 3046      *
 3047      * @param string $op
 3048      * @param array  $left
 3049      * @param array  $right
 3050      *
 3051      * @return array
 3052      */
 3053     protected function opColorNumber($op, $left, $right)
 3054     {
 3055         $value = $right[1];
 3056 
 3057         return $this->opColorColor(
 3058             $op,
 3059             $left,
 3060             [Type::T_COLOR, $value, $value, $value]
 3061         );
 3062     }
 3063 
 3064     /**
 3065      * Compare number and color
 3066      *
 3067      * @param string $op
 3068      * @param array  $left
 3069      * @param array  $right
 3070      *
 3071      * @return array
 3072      */
 3073     protected function opNumberColor($op, $left, $right)
 3074     {
 3075         $value = $left[1];
 3076 
 3077         return $this->opColorColor(
 3078             $op,
 3079             [Type::T_COLOR, $value, $value, $value],
 3080             $right
 3081         );
 3082     }
 3083 
 3084     /**
 3085      * Compare number1 == number2
 3086      *
 3087      * @param array $left
 3088      * @param array $right
 3089      *
 3090      * @return array
 3091      */
 3092     protected function opEq($left, $right)
 3093     {
 3094         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
 3095             $lStr[1] = '';
 3096             $rStr[1] = '';
 3097 
 3098             $left = $this->compileValue($lStr);
 3099             $right = $this->compileValue($rStr);
 3100         }
 3101 
 3102         return $this->toBool($left === $right);
 3103     }
 3104 
 3105     /**
 3106      * Compare number1 != number2
 3107      *
 3108      * @param array $left
 3109      * @param array $right
 3110      *
 3111      * @return array
 3112      */
 3113     protected function opNeq($left, $right)
 3114     {
 3115         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
 3116             $lStr[1] = '';
 3117             $rStr[1] = '';
 3118 
 3119             $left = $this->compileValue($lStr);
 3120             $right = $this->compileValue($rStr);
 3121         }
 3122 
 3123         return $this->toBool($left !== $right);
 3124     }
 3125 
 3126     /**
 3127      * Compare number1 >= number2
 3128      *
 3129      * @param array $left
 3130      * @param array $right
 3131      *
 3132      * @return array
 3133      */
 3134     protected function opGteNumberNumber($left, $right)
 3135     {
 3136         return $this->toBool($left[1] >= $right[1]);
 3137     }
 3138 
 3139     /**
 3140      * Compare number1 > number2
 3141      *
 3142      * @param array $left
 3143      * @param array $right
 3144      *
 3145      * @return array
 3146      */
 3147     protected function opGtNumberNumber($left, $right)
 3148     {
 3149         return $this->toBool($left[1] > $right[1]);
 3150     }
 3151 
 3152     /**
 3153      * Compare number1 <= number2
 3154      *
 3155      * @param array $left
 3156      * @param array $right
 3157      *
 3158      * @return array
 3159      */
 3160     protected function opLteNumberNumber($left, $right)
 3161     {
 3162         return $this->toBool($left[1] <= $right[1]);
 3163     }
 3164 
 3165     /**
 3166      * Compare number1 < number2
 3167      *
 3168      * @param array $left
 3169      * @param array $right
 3170      *
 3171      * @return array
 3172      */
 3173     protected function opLtNumberNumber($left, $right)
 3174     {
 3175         return $this->toBool($left[1] < $right[1]);
 3176     }
 3177 
 3178     /**
 3179      * Three-way comparison, aka spaceship operator
 3180      *
 3181      * @param array $left
 3182      * @param array $right
 3183      *
 3184      * @return \Leafo\ScssPhp\Node\Number
 3185      */
 3186     protected function opCmpNumberNumber($left, $right)
 3187     {
 3188         $n = $left[1] - $right[1];
 3189 
 3190         return new Node\Number($n ? $n / abs($n) : 0, '');
 3191     }
 3192 
 3193     /**
 3194      * Cast to boolean
 3195      *
 3196      * @api
 3197      *
 3198      * @param mixed $thing
 3199      *
 3200      * @return array
 3201      */
 3202     public function toBool($thing)
 3203     {
 3204         return $thing ? static::$true : static::$false;
 3205     }
 3206 
 3207     /**
 3208      * Compiles a primitive value into a CSS property value.
 3209      *
 3210      * Values in scssphp are typed by being wrapped in arrays, their format is
 3211      * typically:
 3212      *
 3213      *     array(type, contents [, additional_contents]*)
 3214      *
 3215      * The input is expected to be reduced. This function will not work on
 3216      * things like expressions and variables.
 3217      *
 3218      * @api
 3219      *
 3220      * @param array $value
 3221      *
 3222      * @return string
 3223      */
 3224     public function compileValue($value)
 3225     {
 3226         $value = $this->reduce($value);
 3227 
 3228         switch ($value[0]) {
 3229             case Type::T_KEYWORD:
 3230                 return $value[1];
 3231 
 3232             case Type::T_COLOR:
 3233                 // [1] - red component (either number for a %)
 3234                 // [2] - green component
 3235                 // [3] - blue component
 3236                 // [4] - optional alpha component
 3237                 list(, $r, $g, $b) = $value;
 3238 
 3239                 $r = round($r);
 3240                 $g = round($g);
 3241                 $b = round($b);
 3242 
 3243                 if (count($value) === 5 && $value[4] !== 1) { // rgba
 3244                     $a = new Node\Number($value[4], '');
 3245 
 3246                     return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
 3247                 }
 3248 
 3249                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
 3250 
 3251                 // Converting hex color to short notation (e.g. #003399 to #039)
 3252                 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
 3253                     $h = '#' . $h[1] . $h[3] . $h[5];
 3254                 }
 3255 
 3256                 return $h;
 3257 
 3258             case Type::T_NUMBER:
 3259                 return $value->output($this);
 3260 
 3261             case Type::T_STRING:
 3262                 return $value[1] . $this->compileStringContent($value) . $value[1];
 3263 
 3264             case Type::T_FUNCTION:
 3265                 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
 3266 
 3267                 return "$value[1]($args)";
 3268 
 3269             case Type::T_LIST:
 3270                 $value = $this->extractInterpolation($value);
 3271 
 3272                 if ($value[0] !== Type::T_LIST) {
 3273                     return $this->compileValue($value);
 3274                 }
 3275 
 3276                 list(, $delim, $items) = $value;
 3277 
 3278                 if ($delim !== ' ') {
 3279                     $delim .= ' ';
 3280                 }
 3281 
 3282                 $filtered = [];
 3283 
 3284                 foreach ($items as $item) {
 3285                     if ($item[0] === Type::T_NULL) {
 3286                         continue;
 3287                     }
 3288 
 3289                     $filtered[] = $this->compileValue($item);
 3290                 }
 3291 
 3292                 return implode("$delim", $filtered);
 3293 
 3294             case Type::T_MAP:
 3295                 $keys = $value[1];
 3296                 $values = $value[2];
 3297                 $filtered = [];
 3298 
 3299                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
 3300                     $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
 3301                 }
 3302 
 3303                 array_walk($filtered, function (&$value, $key) {
 3304                     $value = $key . ': ' . $value;
 3305                 });
 3306 
 3307                 return '(' . implode(', ', $filtered) . ')';
 3308 
 3309             case Type::T_INTERPOLATED:
 3310                 // node created by extractInterpolation
 3311                 list(, $interpolate, $left, $right) = $value;
 3312                 list(,, $whiteLeft, $whiteRight) = $interpolate;
 3313 
 3314                 $left = count($left[2]) > 0 ?
 3315                     $this->compileValue($left) . $whiteLeft : '';
 3316 
 3317                 $right = count($right[2]) > 0 ?
 3318                     $whiteRight . $this->compileValue($right) : '';
 3319 
 3320                 return $left . $this->compileValue($interpolate) . $right;
 3321 
 3322             case Type::T_INTERPOLATE:
 3323                 // strip quotes if it's a string
 3324                 $reduced = $this->reduce($value[1]);
 3325 
 3326                 switch ($reduced[0]) {
 3327                     case Type::T_LIST:
 3328                         $reduced = $this->extractInterpolation($reduced);
 3329 
 3330                         if ($reduced[0] !== Type::T_LIST) {
 3331                             break;
 3332                         }
 3333 
 3334                         list(, $delim, $items) = $reduced;
 3335 
 3336                         if ($delim !== ' ') {
 3337                             $delim .= ' ';
 3338                         }
 3339 
 3340                         $filtered = [];
 3341 
 3342                         foreach ($items as $item) {
 3343                             if ($item[0] === Type::T_NULL) {
 3344                                 continue;
 3345                             }
 3346 
 3347                             $temp = $this->compileValue([Type::T_KEYWORD, $item]);
 3348                             if ($temp[0] === Type::T_STRING) {
 3349                                 $filtered[] = $this->compileStringContent($temp);
 3350                             } elseif ($temp[0] === Type::T_KEYWORD) {
 3351                                 $filtered[] = $temp[1];
 3352                             } else {
 3353                                 $filtered[] = $this->compileValue($temp);
 3354                             }
 3355                         }
 3356 
 3357                         $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
 3358                         break;
 3359 
 3360                     case Type::T_STRING:
 3361                         $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
 3362                         break;
 3363 
 3364                     case Type::T_NULL:
 3365                         $reduced = [Type::T_KEYWORD, ''];
 3366                 }
 3367 
 3368                 return $this->compileValue($reduced);
 3369 
 3370             case Type::T_NULL:
 3371                 return 'null';
 3372 
 3373             default:
 3374                 $this->throwError("unknown value type: $value[0]");
 3375         }
 3376     }
 3377 
 3378     /**
 3379      * Flatten list
 3380      *
 3381      * @param array $list
 3382      *
 3383      * @return string
 3384      */
 3385     protected function flattenList($list)
 3386     {
 3387         return $this->compileValue($list);
 3388     }
 3389 
 3390     /**
 3391      * Compile string content
 3392      *
 3393      * @param array $string
 3394      *
 3395      * @return string
 3396      */
 3397     protected function compileStringContent($string)
 3398     {
 3399         $parts = [];
 3400 
 3401         foreach ($string[2] as $part) {
 3402             if (is_array($part) || $part instanceof \ArrayAccess) {
 3403                 $parts[] = $this->compileValue($part);
 3404             } else {
 3405                 $parts[] = $part;
 3406             }
 3407         }
 3408 
 3409         return implode($parts);
 3410     }
 3411 
 3412     /**
 3413      * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
 3414      *
 3415      * @param array $list
 3416      *
 3417      * @return array
 3418      */
 3419     protected function extractInterpolation($list)
 3420     {
 3421         $items = $list[2];
 3422 
 3423         foreach ($items as $i => $item) {
 3424             if ($item[0] === Type::T_INTERPOLATE) {
 3425                 $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)];
 3426                 $after  = [Type::T_LIST, $list[1], array_slice($items, $i + 1)];
 3427 
 3428                 return [Type::T_INTERPOLATED, $item, $before, $after];
 3429             }
 3430         }
 3431 
 3432         return $list;
 3433     }
 3434 
 3435     /**
 3436      * Find the final set of selectors
 3437      *
 3438      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3439      * @param \Leafo\ScssPhp\Block                $selfParent
 3440      *
 3441      * @return array
 3442      */
 3443     protected function multiplySelectors(Environment $env, $selfParent = null)
 3444     {
 3445         $envs            = $this->compactEnv($env);
 3446         $selectors       = [];
 3447         $parentSelectors = [[]];
 3448 
 3449         $selfParentSelectors = null;
 3450 
 3451         if (! is_null($selfParent) && $selfParent->selectors) {
 3452             $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
 3453         }
 3454 
 3455         while ($env = array_pop($envs)) {
 3456             if (empty($env->selectors)) {
 3457                 continue;
 3458             }
 3459 
 3460             $selectors = $env->selectors;
 3461 
 3462             do {
 3463                 $stillHasSelf = false;
 3464                 $prevSelectors = $selectors;
 3465                 $selectors = [];
 3466 
 3467                 foreach ($prevSelectors as $selector) {
 3468                     foreach ($parentSelectors as $parent) {
 3469                         if ($selfParentSelectors) {
 3470                             foreach ($selfParentSelectors as $selfParent) {
 3471                                 // if no '&' in the selector, each call will give same result, only add once
 3472                                 $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
 3473                                 $selectors[serialize($s)] = $s;
 3474                             }
 3475                         } else {
 3476                             $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
 3477                             $selectors[serialize($s)] = $s;
 3478                         }
 3479                     }
 3480                 }
 3481             } while ($stillHasSelf);
 3482 
 3483             $parentSelectors = $selectors;
 3484         }
 3485 
 3486         $selectors = array_values($selectors);
 3487 
 3488         return $selectors;
 3489     }
 3490 
 3491     /**
 3492      * Join selectors; looks for & to replace, or append parent before child
 3493      *
 3494      * @param array   $parent
 3495      * @param array   $child
 3496      * @param boolean &$stillHasSelf
 3497      * @param array   $selfParentSelectors
 3498 
 3499      * @return array
 3500      */
 3501     protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
 3502     {
 3503         $setSelf = false;
 3504         $out = [];
 3505 
 3506         foreach ($child as $part) {
 3507             $newPart = [];
 3508 
 3509             foreach ($part as $p) {
 3510                 // only replace & once and should be recalled to be able to make combinations
 3511                 if ($p === static::$selfSelector && $setSelf) {
 3512                     $stillHasSelf = true;
 3513                 }
 3514 
 3515                 if ($p === static::$selfSelector && ! $setSelf) {
 3516                     $setSelf = true;
 3517 
 3518                     if (is_null($selfParentSelectors)) {
 3519                         $selfParentSelectors = $parent;
 3520                     }
 3521 
 3522                     foreach ($selfParentSelectors as $i => $parentPart) {
 3523                         if ($i > 0) {
 3524                             $out[] = $newPart;
 3525                             $newPart = [];
 3526                         }
 3527 
 3528                         foreach ($parentPart as $pp) {
 3529                             if (is_array($pp)) {
 3530                                 $flatten = [];
 3531                                 array_walk_recursive($pp, function ($a) use (&$flatten) {
 3532                                     $flatten[] = $a;
 3533                                 });
 3534                                 $pp = implode($flatten);
 3535                             }
 3536 
 3537                             $newPart[] = $pp;
 3538                         }
 3539                     }
 3540                 } else {
 3541                     $newPart[] = $p;
 3542                 }
 3543             }
 3544 
 3545             $out[] = $newPart;
 3546         }
 3547 
 3548         return $setSelf ? $out : array_merge($parent, $child);
 3549     }
 3550 
 3551     /**
 3552      * Multiply media
 3553      *
 3554      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3555      * @param array                               $childQueries
 3556      *
 3557      * @return array
 3558      */
 3559     protected function multiplyMedia(Environment $env = null, $childQueries = null)
 3560     {
 3561         if (! isset($env) ||
 3562             ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
 3563         ) {
 3564             return $childQueries;
 3565         }
 3566 
 3567         // plain old block, skip
 3568         if (empty($env->block->type)) {
 3569             return $this->multiplyMedia($env->parent, $childQueries);
 3570         }
 3571 
 3572         $parentQueries = isset($env->block->queryList)
 3573             ? $env->block->queryList
 3574             : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
 3575 
 3576         $store = [$this->env, $this->storeEnv];
 3577         $this->env = $env;
 3578         $this->storeEnv = null;
 3579         $parentQueries = $this->evaluateMediaQuery($parentQueries);
 3580         list($this->env, $this->storeEnv) = $store;
 3581 
 3582         if ($childQueries === null) {
 3583             $childQueries = $parentQueries;
 3584         } else {
 3585             $originalQueries = $childQueries;
 3586             $childQueries = [];
 3587 
 3588             foreach ($parentQueries as $parentQuery) {
 3589                 foreach ($originalQueries as $childQuery) {
 3590                     $childQueries[] = array_merge(
 3591                         $parentQuery,
 3592                         [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
 3593                         $childQuery
 3594                     );
 3595                 }
 3596             }
 3597         }
 3598 
 3599         return $this->multiplyMedia($env->parent, $childQueries);
 3600     }
 3601 
 3602     /**
 3603      * Convert env linked list to stack
 3604      *
 3605      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3606      *
 3607      * @return array
 3608      */
 3609     protected function compactEnv(Environment $env)
 3610     {
 3611         for ($envs = []; $env; $env = $env->parent) {
 3612             $envs[] = $env;
 3613         }
 3614 
 3615         return $envs;
 3616     }
 3617 
 3618     /**
 3619      * Convert env stack to singly linked list
 3620      *
 3621      * @param array $envs
 3622      *
 3623      * @return \Leafo\ScssPhp\Compiler\Environment
 3624      */
 3625     protected function extractEnv($envs)
 3626     {
 3627         for ($env = null; $e = array_pop($envs);) {
 3628             $e->parent = $env;
 3629             $env = $e;
 3630         }
 3631 
 3632         return $env;
 3633     }
 3634 
 3635     /**
 3636      * Push environment
 3637      *
 3638      * @param \Leafo\ScssPhp\Block $block
 3639      *
 3640      * @return \Leafo\ScssPhp\Compiler\Environment
 3641      */
 3642     protected function pushEnv(Block $block = null)
 3643     {
 3644         $env = new Environment;
 3645         $env->parent = $this->env;
 3646         $env->store  = [];
 3647         $env->block  = $block;
 3648         $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
 3649 
 3650         $this->env = $env;
 3651 
 3652         return $env;
 3653     }
 3654 
 3655     /**
 3656      * Pop environment
 3657      */
 3658     protected function popEnv()
 3659     {
 3660         $this->env = $this->env->parent;
 3661     }
 3662 
 3663     /**
 3664      * Get store environment
 3665      *
 3666      * @return \Leafo\ScssPhp\Compiler\Environment
 3667      */
 3668     protected function getStoreEnv()
 3669     {
 3670         return isset($this->storeEnv) ? $this->storeEnv : $this->env;
 3671     }
 3672 
 3673     /**
 3674      * Set variable
 3675      *
 3676      * @param string                              $name
 3677      * @param mixed                               $value
 3678      * @param boolean                             $shadow
 3679      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3680      * @param mixed                               $valueUnreduced
 3681      */
 3682     protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
 3683     {
 3684         $name = $this->normalizeName($name);
 3685 
 3686         if (! isset($env)) {
 3687             $env = $this->getStoreEnv();
 3688         }
 3689 
 3690         if ($shadow) {
 3691             $this->setRaw($name, $value, $env, $valueUnreduced);
 3692         } else {
 3693             $this->setExisting($name, $value, $env, $valueUnreduced);
 3694         }
 3695     }
 3696 
 3697     /**
 3698      * Set existing variable
 3699      *
 3700      * @param string                              $name
 3701      * @param mixed                               $value
 3702      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3703      * @param mixed                               $valueUnreduced
 3704      */
 3705     protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
 3706     {
 3707         $storeEnv = $env;
 3708 
 3709         $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
 3710 
 3711         for (;;) {
 3712             if (array_key_exists($name, $env->store)) {
 3713                 break;
 3714             }
 3715 
 3716             if (! $hasNamespace && isset($env->marker)) {
 3717                 $env = $storeEnv;
 3718                 break;
 3719             }
 3720 
 3721             if (! isset($env->parent)) {
 3722                 $env = $storeEnv;
 3723                 break;
 3724             }
 3725 
 3726             $env = $env->parent;
 3727         }
 3728 
 3729         $env->store[$name] = $value;
 3730 
 3731         if ($valueUnreduced) {
 3732             $env->storeUnreduced[$name] = $valueUnreduced;
 3733         }
 3734     }
 3735 
 3736     /**
 3737      * Set raw variable
 3738      *
 3739      * @param string                              $name
 3740      * @param mixed                               $value
 3741      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3742      * @param mixed                               $valueUnreduced
 3743      */
 3744     protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
 3745     {
 3746         $env->store[$name] = $value;
 3747 
 3748         if ($valueUnreduced) {
 3749             $env->storeUnreduced[$name] = $valueUnreduced;
 3750         }
 3751     }
 3752 
 3753     /**
 3754      * Get variable
 3755      *
 3756      * @api
 3757      *
 3758      * @param string                              $name
 3759      * @param boolean                             $shouldThrow
 3760      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3761      * @param boolean                             $unreduced
 3762      *
 3763      * @return mixed|null
 3764      */
 3765     public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
 3766     {
 3767         $normalizedName = $this->normalizeName($name);
 3768         $specialContentKey = static::$namespaces['special'] . 'content';
 3769 
 3770         if (! isset($env)) {
 3771             $env = $this->getStoreEnv();
 3772         }
 3773 
 3774         $nextIsRoot = false;
 3775         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
 3776 
 3777         $maxDepth = 10000;
 3778 
 3779         for (;;) {
 3780             if ($maxDepth-- <= 0) {
 3781                 break;
 3782             }
 3783 
 3784             if (array_key_exists($normalizedName, $env->store)) {
 3785                 if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
 3786                     return $env->storeUnreduced[$normalizedName];
 3787                 }
 3788 
 3789                 return $env->store[$normalizedName];
 3790             }
 3791 
 3792             if (! $hasNamespace && isset($env->marker)) {
 3793                 if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
 3794                     $env = $env->store[$specialContentKey]->scope;
 3795                     continue;
 3796                 }
 3797 
 3798                 $env = $this->rootEnv;
 3799                 continue;
 3800             }
 3801 
 3802             if (! isset($env->parent)) {
 3803                 break;
 3804             }
 3805 
 3806             $env = $env->parent;
 3807         }
 3808 
 3809         if ($shouldThrow) {
 3810             $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
 3811         }
 3812 
 3813         // found nothing
 3814         return null;
 3815     }
 3816 
 3817     /**
 3818      * Has variable?
 3819      *
 3820      * @param string                              $name
 3821      * @param \Leafo\ScssPhp\Compiler\Environment $env
 3822      *
 3823      * @return boolean
 3824      */
 3825     protected function has($name, Environment $env = null)
 3826     {
 3827         return $this->get($name, false, $env) !== null;
 3828     }
 3829 
 3830     /**
 3831      * Inject variables
 3832      *
 3833      * @param array $args
 3834      */
 3835     protected function injectVariables(array $args)
 3836     {
 3837         if (empty($args)) {
 3838             return;
 3839         }
 3840 
 3841         $parser = $this->parserFactory(__METHOD__);
 3842 
 3843         foreach ($args as $name => $strValue) {
 3844             if ($name[0] === '$') {
 3845                 $name = substr($name, 1);
 3846             }
 3847 
 3848             if (! $parser->parseValue($strValue, $value)) {
 3849                 $value = $this->coerceValue($strValue);
 3850             }
 3851 
 3852             $this->set($name, $value);
 3853         }
 3854     }
 3855 
 3856     /**
 3857      * Set variables
 3858      *
 3859      * @api
 3860      *
 3861      * @param array $variables
 3862      */
 3863     public function setVariables(array $variables)
 3864     {
 3865         $this->registeredVars = array_merge($this->registeredVars, $variables);
 3866     }
 3867 
 3868     /**
 3869      * Unset variable
 3870      *
 3871      * @api
 3872      *
 3873      * @param string $name
 3874      */
 3875     public function unsetVariable($name)
 3876     {
 3877         unset($this->registeredVars[$name]);
 3878     }
 3879 
 3880     /**
 3881      * Returns list of variables
 3882      *
 3883      * @api
 3884      *
 3885      * @return array
 3886      */
 3887     public function getVariables()
 3888     {
 3889         return $this->registeredVars;
 3890     }
 3891 
 3892     /**
 3893      * Adds to list of parsed files
 3894      *
 3895      * @api
 3896      *
 3897      * @param string $path
 3898      */
 3899     public function addParsedFile($path)
 3900     {
 3901         if (isset($path) && file_exists($path)) {
 3902             $this->parsedFiles[realpath($path)] = filemtime($path);
 3903         }
 3904     }
 3905 
 3906     /**
 3907      * Returns list of parsed files
 3908      *
 3909      * @api
 3910      *
 3911      * @return array
 3912      */
 3913     public function getParsedFiles()
 3914     {
 3915         return $this->parsedFiles;
 3916     }
 3917 
 3918     /**
 3919      * Add import path
 3920      *
 3921      * @api
 3922      *
 3923      * @param string|callable $path
 3924      */
 3925     public function addImportPath($path)
 3926     {
 3927         if (! in_array($path, $this->importPaths)) {
 3928             $this->importPaths[] = $path;
 3929         }
 3930     }
 3931 
 3932     /**
 3933      * Set import paths
 3934      *
 3935      * @api
 3936      *
 3937      * @param string|array $path
 3938      */
 3939     public function setImportPaths($path)
 3940     {
 3941         $this->importPaths = (array) $path;
 3942     }
 3943 
 3944     /**
 3945      * Set number precision
 3946      *
 3947      * @api
 3948      *
 3949      * @param integer $numberPrecision
 3950      */
 3951     public function setNumberPrecision($numberPrecision)
 3952     {
 3953         Node\Number::$precision = $numberPrecision;
 3954     }
 3955 
 3956     /**
 3957      * Set formatter
 3958      *
 3959      * @api
 3960      *
 3961      * @param string $formatterName
 3962      */
 3963     public function setFormatter($formatterName)
 3964     {
 3965         $this->formatter = $formatterName;
 3966     }
 3967 
 3968     /**
 3969      * Set line number style
 3970      *
 3971      * @api
 3972      *
 3973      * @param string $lineNumberStyle
 3974      */
 3975     public function setLineNumberStyle($lineNumberStyle)
 3976     {
 3977         $this->lineNumberStyle = $lineNumberStyle;
 3978     }
 3979 
 3980     /**
 3981      * Enable/disable source maps
 3982      *
 3983      * @api
 3984      *
 3985      * @param integer $sourceMap
 3986      */
 3987     public function setSourceMap($sourceMap)
 3988     {
 3989         $this->sourceMap = $sourceMap;
 3990     }
 3991 
 3992     /**
 3993      * Set source map options
 3994      *
 3995      * @api
 3996      *
 3997      * @param array $sourceMapOptions
 3998      */
 3999     public function setSourceMapOptions($sourceMapOptions)
 4000     {
 4001         $this->sourceMapOptions = $sourceMapOptions;
 4002     }
 4003 
 4004     /**
 4005      * Register function
 4006      *
 4007      * @api
 4008      *
 4009      * @param string   $name
 4010      * @param callable $func
 4011      * @param array    $prototype
 4012      */
 4013     public function registerFunction($name, $func, $prototype = null)
 4014     {
 4015         $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
 4016     }
 4017 
 4018     /**
 4019      * Unregister function
 4020      *
 4021      * @api
 4022      *
 4023      * @param string $name
 4024      */
 4025     public function unregisterFunction($name)
 4026     {
 4027         unset($this->userFunctions[$this->normalizeName($name)]);
 4028     }
 4029 
 4030     /**
 4031      * Add feature
 4032      *
 4033      * @api
 4034      *
 4035      * @param string $name
 4036      */
 4037     public function addFeature($name)
 4038     {
 4039         $this->registeredFeatures[$name] = true;
 4040     }
 4041 
 4042     /**
 4043      * Import file
 4044      *
 4045      * @param string                               $path
 4046      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
 4047      */
 4048     protected function importFile($path, OutputBlock $out)
 4049     {
 4050         // see if tree is cached
 4051         $realPath = realpath($path);
 4052 
 4053         if (isset($this->importCache[$realPath])) {
 4054             $this->handleImportLoop($realPath);
 4055 
 4056             $tree = $this->importCache[$realPath];
 4057         } else {
 4058             $code   = file_get_contents($path);
 4059             $parser = $this->parserFactory($path);
 4060             $tree   = $parser->parse($code);
 4061 
 4062             $this->importCache[$realPath] = $tree;
 4063         }
 4064 
 4065         $pi = pathinfo($path);
 4066         array_unshift($this->importPaths, $pi['dirname']);
 4067         $this->compileChildrenNoReturn($tree->children, $out);
 4068         array_shift($this->importPaths);
 4069     }
 4070 
 4071     /**
 4072      * Return the file path for an import url if it exists
 4073      *
 4074      * @api
 4075      *
 4076      * @param string $url
 4077      *
 4078      * @return string|null
 4079      */
 4080     public function findImport($url)
 4081     {
 4082         $urls = [];
 4083 
 4084         // for "normal" scss imports (ignore vanilla css and external requests)
 4085         if (! preg_match('/\.css$|^https?:\/\//', $url)) {
 4086             // try both normal and the _partial filename
 4087             $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
 4088         }
 4089 
 4090         $hasExtension = preg_match('/[.]s?css$/', $url);
 4091 
 4092         foreach ($this->importPaths as $dir) {
 4093             if (is_string($dir)) {
 4094                 // check urls for normal import paths
 4095                 foreach ($urls as $full) {
 4096                     $separator = (
 4097                         ! empty($dir) &&
 4098                         substr($dir, -1) !== '/' &&
 4099                         substr($full, 0, 1) !== '/'
 4100                     ) ? '/' : '';
 4101                     $full = $dir . $separator . $full;
 4102 
 4103                     if ($this->fileExists($file = $full . '.scss') ||
 4104                         ($hasExtension && $this->fileExists($file = $full))
 4105                     ) {
 4106                         return $file;
 4107                     }
 4108                 }
 4109             } elseif (is_callable($dir)) {
 4110                 // check custom callback for import path
 4111                 $file = call_user_func($dir, $url);
 4112 
 4113                 if ($file !== null) {
 4114                     return $file;
 4115                 }
 4116             }
 4117         }
 4118 
 4119         return null;
 4120     }
 4121 
 4122     /**
 4123      * Set encoding
 4124      *
 4125      * @api
 4126      *
 4127      * @param string $encoding
 4128      */
 4129     public function setEncoding($encoding)
 4130     {
 4131         $this->encoding = $encoding;
 4132     }
 4133 
 4134     /**
 4135      * Ignore errors?
 4136      *
 4137      * @api
 4138      *
 4139      * @param boolean $ignoreErrors
 4140      *
 4141      * @return \Leafo\ScssPhp\Compiler
 4142      */
 4143     public function setIgnoreErrors($ignoreErrors)
 4144     {
 4145         $this->ignoreErrors = $ignoreErrors;
 4146 
 4147         return $this;
 4148     }
 4149 
 4150     /**
 4151      * Throw error (exception)
 4152      *
 4153      * @api
 4154      *
 4155      * @param string $msg Message with optional sprintf()-style vararg parameters
 4156      *
 4157      * @throws \Leafo\ScssPhp\Exception\CompilerException
 4158      */
 4159     public function throwError($msg)
 4160     {
 4161         if ($this->ignoreErrors) {
 4162             return;
 4163         }
 4164 
 4165         $line   = $this->sourceLine;
 4166         $column = $this->sourceColumn;
 4167 
 4168         $loc = isset($this->sourceNames[$this->sourceIndex])
 4169              ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
 4170              : "line: $line, column: $column";
 4171 
 4172         if (func_num_args() > 1) {
 4173             $msg = call_user_func_array('sprintf', func_get_args());
 4174         }
 4175 
 4176         $msg = "$msg: $loc";
 4177 
 4178         $callStackMsg = $this->callStackMessage();
 4179 
 4180         if ($callStackMsg) {
 4181             $msg .= "\nCall Stack:\n" . $callStackMsg;
 4182         }
 4183 
 4184         throw new CompilerException($msg);
 4185     }
 4186 
 4187     /**
 4188      * Beautify call stack for output
 4189      *
 4190      * @param boolean $all
 4191      * @param null    $limit
 4192      *
 4193      * @return string
 4194      */
 4195     protected function callStackMessage($all = false, $limit = null)
 4196     {
 4197         $callStackMsg = [];
 4198         $ncall = 0;
 4199 
 4200         if ($this->callStack) {
 4201             foreach (array_reverse($this->callStack) as $call) {
 4202                 if ($all || (isset($call['n']) && $call['n'])) {
 4203                     $msg = "#" . $ncall++ . " " . $call['n'] . " ";
 4204                     $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
 4205                           ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
 4206                           : '(unknown file)');
 4207                     $msg .= " on line " . $call[Parser::SOURCE_LINE];
 4208                     $callStackMsg[] = $msg;
 4209 
 4210                     if (! is_null($limit) && $ncall>$limit) {
 4211                         break;
 4212                     }
 4213                 }
 4214             }
 4215         }
 4216 
 4217         return implode("\n", $callStackMsg);
 4218     }
 4219 
 4220     /**
 4221      * Handle import loop
 4222      *
 4223      * @param string $name
 4224      *
 4225      * @throws \Exception
 4226      */
 4227     protected function handleImportLoop($name)
 4228     {
 4229         for ($env = $this->env; $env; $env = $env->parent) {
 4230             $file = $this->sourceNames[$env->block->sourceIndex];
 4231 
 4232             if (realpath($file) === $name) {
 4233                 $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
 4234                 break;
 4235             }
 4236         }
 4237     }
 4238 
 4239     /**
 4240      * Does file exist?
 4241      *
 4242      * @param string $name
 4243      *
 4244      * @return boolean
 4245      */
 4246     protected function fileExists($name)
 4247     {
 4248         return file_exists($name) && is_file($name);
 4249     }
 4250 
 4251     /**
 4252      * Call SCSS @function
 4253      *
 4254      * @param string $name
 4255      * @param array  $argValues
 4256      * @param array  $returnValue
 4257      *
 4258      * @return boolean Returns true if returnValue is set; otherwise, false
 4259      */
 4260     protected function callScssFunction($name, $argValues, &$returnValue)
 4261     {
 4262         $func = $this->get(static::$namespaces['function'] . $name, false);
 4263 
 4264         if (! $func) {
 4265             return false;
 4266         }
 4267 
 4268         $this->pushEnv();
 4269 
 4270         $storeEnv = $this->storeEnv;
 4271         $this->storeEnv = $this->env;
 4272 
 4273         // set the args
 4274         if (isset($func->args)) {
 4275             $this->applyArguments($func->args, $argValues);
 4276         }
 4277 
 4278         // throw away lines and children
 4279         $tmp = new OutputBlock;
 4280         $tmp->lines    = [];
 4281         $tmp->children = [];
 4282 
 4283         $this->env->marker = 'function';
 4284 
 4285         $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
 4286 
 4287         $this->storeEnv = $storeEnv;
 4288 
 4289         $this->popEnv();
 4290 
 4291         $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
 4292 
 4293         return true;
 4294     }
 4295 
 4296     /**
 4297      * Call built-in and registered (PHP) functions
 4298      *
 4299      * @param string $name
 4300      * @param array  $args
 4301      * @param array  $returnValue
 4302      *
 4303      * @return boolean Returns true if returnValue is set; otherwise, false
 4304      */
 4305     protected function callNativeFunction($name, $args, &$returnValue)
 4306     {
 4307         // try a lib function
 4308         $name = $this->normalizeName($name);
 4309 
 4310         if (isset($this->userFunctions[$name])) {
 4311             // see if we can find a user function
 4312             list($f, $prototype) = $this->userFunctions[$name];
 4313         } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
 4314             $libName   = $f[1];
 4315             $prototype = isset(static::$$libName) ? static::$$libName : null;
 4316         } else {
 4317             return false;
 4318         }
 4319 
 4320         @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
 4321 
 4322         if ($name !== 'if' && $name !== 'call') {
 4323             foreach ($sorted as &$val) {
 4324                 $val = $this->reduce($val, true);
 4325             }
 4326         }
 4327 
 4328         $returnValue = call_user_func($f, $sorted, $kwargs);
 4329 
 4330         if (! isset($returnValue)) {
 4331             return false;
 4332         }
 4333 
 4334         $returnValue = $this->coerceValue($returnValue);
 4335 
 4336         return true;
 4337     }
 4338 
 4339     /**
 4340      * Get built-in function
 4341      *
 4342      * @param string $name Normalized name
 4343      *
 4344      * @return array
 4345      */
 4346     protected function getBuiltinFunction($name)
 4347     {
 4348         $libName = 'lib' . preg_replace_callback(
 4349             '/_(.)/',
 4350             function ($m) {
 4351                 return ucfirst($m[1]);
 4352             },
 4353             ucfirst($name)
 4354         );
 4355 
 4356         return [$this, $libName];
 4357     }
 4358 
 4359     /**
 4360      * Sorts keyword arguments
 4361      *
 4362      * @param array $prototype
 4363      * @param array $args
 4364      *
 4365      * @return array
 4366      */
 4367     protected function sortArgs($prototype, $args)
 4368     {
 4369         $keyArgs = [];
 4370         $posArgs = [];
 4371 
 4372         // separate positional and keyword arguments
 4373         foreach ($args as $arg) {
 4374             list($key, $value) = $arg;
 4375 
 4376             $key = $key[1];
 4377 
 4378             if (empty($key)) {
 4379                 $posArgs[] = empty($arg[2]) ? $value : $arg;
 4380             } else {
 4381                 $keyArgs[$key] = $value;
 4382             }
 4383         }
 4384 
 4385         if (! isset($prototype)) {
 4386             return [$posArgs, $keyArgs];
 4387         }
 4388 
 4389         // copy positional args
 4390         $finalArgs = array_pad($posArgs, count($prototype), null);
 4391 
 4392         // overwrite positional args with keyword args
 4393         foreach ($prototype as $i => $names) {
 4394             foreach ((array) $names as $name) {
 4395                 if (isset($keyArgs[$name])) {
 4396                     $finalArgs[$i] = $keyArgs[$name];
 4397                 }
 4398             }
 4399         }
 4400 
 4401         return [$finalArgs, $keyArgs];
 4402     }
 4403 
 4404     /**
 4405      * Apply argument values per definition
 4406      *
 4407      * @param array $argDef
 4408      * @param array $argValues
 4409      *
 4410      * @throws \Exception
 4411      */
 4412     protected function applyArguments($argDef, $argValues)
 4413     {
 4414         $storeEnv = $this->getStoreEnv();
 4415 
 4416         $env = new Environment;
 4417         $env->store = $storeEnv->store;
 4418 
 4419         $hasVariable = false;
 4420         $args = [];
 4421 
 4422         foreach ($argDef as $i => $arg) {
 4423             list($name, $default, $isVariable) = $argDef[$i];
 4424 
 4425             $args[$name] = [$i, $name, $default, $isVariable];
 4426             $hasVariable |= $isVariable;
 4427         }
 4428 
 4429         $keywordArgs = [];
 4430         $deferredKeywordArgs = [];
 4431         $remaining = [];
 4432 
 4433         // assign the keyword args
 4434         foreach ((array) $argValues as $arg) {
 4435             if (! empty($arg[0])) {
 4436                 if (! isset($args[$arg[0][1]])) {
 4437                     if ($hasVariable) {
 4438                         $deferredKeywordArgs[$arg[0][1]] = $arg[1];
 4439                     } else {
 4440                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
 4441                         break;
 4442                     }
 4443                 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
 4444                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
 4445                     break;
 4446                 } else {
 4447                     $keywordArgs[$arg[0][1]] = $arg[1];
 4448                 }
 4449             } elseif (count($keywordArgs)) {
 4450                 $this->throwError('Positional arguments must come before keyword arguments.');
 4451                 break;
 4452             } elseif ($arg[2] === true) {
 4453                 $val = $this->reduce($arg[1], true);
 4454 
 4455                 if ($val[0] === Type::T_LIST) {
 4456                     foreach ($val[2] as $name => $item) {
 4457                         if (! is_numeric($name)) {
 4458                             $keywordArgs[$name] = $item;
 4459                         } else {
 4460                             $remaining[] = $item;
 4461                         }
 4462                     }
 4463                 } elseif ($val[0] === Type::T_MAP) {
 4464                     foreach ($val[1] as $i => $name) {
 4465                         $name = $this->compileStringContent($this->coerceString($name));
 4466                         $item = $val[2][$i];
 4467 
 4468                         if (! is_numeric($name)) {
 4469                             $keywordArgs[$name] = $item;
 4470                         } else {
 4471                             $remaining[] = $item;
 4472                         }
 4473                     }
 4474                 } else {
 4475                     $remaining[] = $val;
 4476                 }
 4477             } else {
 4478                 $remaining[] = $arg[1];
 4479             }
 4480         }
 4481 
 4482         foreach ($args as $arg) {
 4483             list($i, $name, $default, $isVariable) = $arg;
 4484 
 4485             if ($isVariable) {
 4486                 $val = [Type::T_LIST, ',', [], $isVariable];
 4487 
 4488                 for ($count = count($remaining); $i < $count; $i++) {
 4489                     $val[2][] = $remaining[$i];
 4490                 }
 4491 
 4492                 foreach ($deferredKeywordArgs as $itemName => $item) {
 4493                     $val[2][$itemName] = $item;
 4494                 }
 4495             } elseif (isset($remaining[$i])) {
 4496                 $val = $remaining[$i];
 4497             } elseif (isset($keywordArgs[$name])) {
 4498                 $val = $keywordArgs[$name];
 4499             } elseif (! empty($default)) {
 4500                 continue;
 4501             } else {
 4502                 $this->throwError("Missing argument $name");
 4503                 break;
 4504             }
 4505 
 4506             $this->set($name, $this->reduce($val, true), true, $env);
 4507         }
 4508 
 4509         $storeEnv->store = $env->store;
 4510 
 4511         foreach ($args as $arg) {
 4512             list($i, $name, $default, $isVariable) = $arg;
 4513 
 4514             if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
 4515                 continue;
 4516             }
 4517 
 4518             $this->set($name, $this->reduce($default, true), true);
 4519         }
 4520     }
 4521 
 4522     /**
 4523      * Coerce a php value into a scss one
 4524      *
 4525      * @param mixed $value
 4526      *
 4527      * @return array|\Leafo\ScssPhp\Node\Number
 4528      */
 4529     protected function coerceValue($value)
 4530     {
 4531         if (is_array($value) || $value instanceof \ArrayAccess) {
 4532             return $value;
 4533         }
 4534 
 4535         if (is_bool($value)) {
 4536             return $this->toBool($value);
 4537         }
 4538 
 4539         if ($value === null) {
 4540             return static::$null;
 4541         }
 4542 
 4543         if (is_numeric($value)) {
 4544             return new Node\Number($value, '');
 4545         }
 4546 
 4547         if ($value === '') {
 4548             return static::$emptyString;
 4549         }
 4550 
 4551         if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
 4552             $color = [Type::T_COLOR];
 4553 
 4554             if (isset($m[3])) {
 4555                 $num = hexdec($m[3]);
 4556 
 4557                 foreach ([3, 2, 1] as $i) {
 4558                     $t = $num & 0xf;
 4559                     $color[$i] = $t << 4 | $t;
 4560                     $num >>= 4;
 4561                 }
 4562             } else {
 4563                 $num = hexdec($m[2]);
 4564 
 4565                 foreach ([3, 2, 1] as $i) {
 4566                     $color[$i] = $num & 0xff;
 4567                     $num >>= 8;
 4568                 }
 4569             }
 4570 
 4571             return $color;
 4572         }
 4573 
 4574         return [Type::T_KEYWORD, $value];
 4575     }
 4576 
 4577     /**
 4578      * Coerce something to map
 4579      *
 4580      * @param array $item
 4581      *
 4582      * @return array
 4583      */
 4584     protected function coerceMap($item)
 4585     {
 4586         if ($item[0] === Type::T_MAP) {
 4587             return $item;
 4588         }
 4589 
 4590         if ($item === static::$emptyList) {
 4591             return static::$emptyMap;
 4592         }
 4593 
 4594         return [Type::T_MAP, [$item], [static::$null]];
 4595     }
 4596 
 4597     /**
 4598      * Coerce something to list
 4599      *
 4600      * @param array  $item
 4601      * @param string $delim
 4602      *
 4603      * @return array
 4604      */
 4605     protected function coerceList($item, $delim = ',')
 4606     {
 4607         if (isset($item) && $item[0] === Type::T_LIST) {
 4608             return $item;
 4609         }
 4610 
 4611         if (isset($item) && $item[0] === Type::T_MAP) {
 4612             $keys = $item[1];
 4613             $values = $item[2];
 4614             $list = [];
 4615 
 4616             for ($i = 0, $s = count($keys); $i < $s; $i++) {
 4617                 $key = $keys[$i];
 4618                 $value = $values[$i];
 4619 
 4620                 $list[] = [
 4621                     Type::T_LIST,
 4622                     '',
 4623                     [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
 4624                 ];
 4625             }
 4626 
 4627             return [Type::T_LIST, ',', $list];
 4628         }
 4629 
 4630         return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
 4631     }
 4632 
 4633     /**
 4634      * Coerce color for expression
 4635      *
 4636      * @param array $value
 4637      *
 4638      * @return array|null
 4639      */
 4640     protected function coerceForExpression($value)
 4641     {
 4642         if ($color = $this->coerceColor($value)) {
 4643             return $color;
 4644         }
 4645 
 4646         return $value;
 4647     }
 4648 
 4649     /**
 4650      * Coerce value to color
 4651      *
 4652      * @param array $value
 4653      *
 4654      * @return array|null
 4655      */
 4656     protected function coerceColor($value)
 4657     {
 4658         switch ($value[0]) {
 4659             case Type::T_COLOR:
 4660                 return $value;
 4661 
 4662             case Type::T_KEYWORD:
 4663                 $name = strtolower($value[1]);
 4664 
 4665                 if (isset(Colors::$cssColors[$name])) {
 4666                     $rgba = explode(',', Colors::$cssColors[$name]);
 4667 
 4668                     return isset($rgba[3])
 4669                         ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
 4670                         : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
 4671                 }
 4672 
 4673                 return null;
 4674         }
 4675 
 4676         return null;
 4677     }
 4678 
 4679     /**
 4680      * Coerce value to string
 4681      *
 4682      * @param array $value
 4683      *
 4684      * @return array|null
 4685      */
 4686     protected function coerceString($value)
 4687     {
 4688         if ($value[0] === Type::T_STRING) {
 4689             return $value;
 4690         }
 4691 
 4692         return [Type::T_STRING, '', [$this->compileValue($value)]];
 4693     }
 4694 
 4695     /**
 4696      * Coerce value to a percentage
 4697      *
 4698      * @param array $value
 4699      *
 4700      * @return integer|float
 4701      */
 4702     protected function coercePercent($value)
 4703     {
 4704         if ($value[0] === Type::T_NUMBER) {
 4705             if (! empty($value[2]['%'])) {
 4706                 return $value[1] / 100;
 4707             }
 4708 
 4709             return $value[1];
 4710         }
 4711 
 4712         return 0;
 4713     }
 4714 
 4715     /**
 4716      * Assert value is a map
 4717      *
 4718      * @api
 4719      *
 4720      * @param array $value
 4721      *
 4722      * @return array
 4723      *
 4724      * @throws \Exception
 4725      */
 4726     public function assertMap($value)
 4727     {
 4728         $value = $this->coerceMap($value);
 4729 
 4730         if ($value[0] !== Type::T_MAP) {
 4731             $this->throwError('expecting map, %s received', $value[0]);
 4732         }
 4733 
 4734         return $value;
 4735     }
 4736 
 4737     /**
 4738      * Assert value is a list
 4739      *
 4740      * @api
 4741      *
 4742      * @param array $value
 4743      *
 4744      * @return array
 4745      *
 4746      * @throws \Exception
 4747      */
 4748     public function assertList($value)
 4749     {
 4750         if ($value[0] !== Type::T_LIST) {
 4751             $this->throwError('expecting list, %s received', $value[0]);
 4752         }
 4753 
 4754         return $value;
 4755     }
 4756 
 4757     /**
 4758      * Assert value is a color
 4759      *
 4760      * @api
 4761      *
 4762      * @param array $value
 4763      *
 4764      * @return array
 4765      *
 4766      * @throws \Exception
 4767      */
 4768     public function assertColor($value)
 4769     {
 4770         if ($color = $this->coerceColor($value)) {
 4771             return $color;
 4772         }
 4773 
 4774         $this->throwError('expecting color, %s received', $value[0]);
 4775     }
 4776 
 4777     /**
 4778      * Assert value is a number
 4779      *
 4780      * @api
 4781      *
 4782      * @param array $value
 4783      *
 4784      * @return integer|float
 4785      *
 4786      * @throws \Exception
 4787      */
 4788     public function assertNumber($value)
 4789     {
 4790         if ($value[0] !== Type::T_NUMBER) {
 4791             $this->throwError('expecting number, %s received', $value[0]);
 4792         }
 4793 
 4794         return $value[1];
 4795     }
 4796 
 4797     /**
 4798      * Make sure a color's components don't go out of bounds
 4799      *
 4800      * @param array $c
 4801      *
 4802      * @return array
 4803      */
 4804     protected function fixColor($c)
 4805     {
 4806         foreach ([1, 2, 3] as $i) {
 4807             if ($c[$i] < 0) {
 4808                 $c[$i] = 0;
 4809             }
 4810 
 4811             if ($c[$i] > 255) {
 4812                 $c[$i] = 255;
 4813             }
 4814         }
 4815 
 4816         return $c;
 4817     }
 4818 
 4819     /**
 4820      * Convert RGB to HSL
 4821      *
 4822      * @api
 4823      *
 4824      * @param integer $red
 4825      * @param integer $green
 4826      * @param integer $blue
 4827      *
 4828      * @return array
 4829      */
 4830     public function toHSL($red, $green, $blue)
 4831     {
 4832         $min = min($red, $green, $blue);
 4833         $max = max($red, $green, $blue);
 4834 
 4835         $l = $min + $max;
 4836         $d = $max - $min;
 4837 
 4838         if ((int) $d === 0) {
 4839             $h = $s = 0;
 4840         } else {
 4841             if ($l < 255) {
 4842                 $s = $d / $l;
 4843             } else {
 4844                 $s = $d / (510 - $l);
 4845             }
 4846 
 4847             if ($red == $max) {
 4848                 $h = 60 * ($green - $blue) / $d;
 4849             } elseif ($green == $max) {
 4850                 $h = 60 * ($blue - $red) / $d + 120;
 4851             } elseif ($blue == $max) {
 4852                 $h = 60 * ($red - $green) / $d + 240;
 4853             }
 4854         }
 4855 
 4856         return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
 4857     }
 4858 
 4859     /**
 4860      * Hue to RGB helper
 4861      *
 4862      * @param float $m1
 4863      * @param float $m2
 4864      * @param float $h
 4865      *
 4866      * @return float
 4867      */
 4868     protected function hueToRGB($m1, $m2, $h)
 4869     {
 4870         if ($h < 0) {
 4871             $h += 1;
 4872         } elseif ($h > 1) {
 4873             $h -= 1;
 4874         }
 4875 
 4876         if ($h * 6 < 1) {
 4877             return $m1 + ($m2 - $m1) * $h * 6;
 4878         }
 4879 
 4880         if ($h * 2 < 1) {
 4881             return $m2;
 4882         }
 4883 
 4884         if ($h * 3 < 2) {
 4885             return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
 4886         }
 4887 
 4888         return $m1;
 4889     }
 4890 
 4891     /**
 4892      * Convert HSL to RGB
 4893      *
 4894      * @api
 4895      *
 4896      * @param integer $hue        H from 0 to 360
 4897      * @param integer $saturation S from 0 to 100
 4898      * @param integer $lightness  L from 0 to 100
 4899      *
 4900      * @return array
 4901      */
 4902     public function toRGB($hue, $saturation, $lightness)
 4903     {
 4904         if ($hue < 0) {
 4905             $hue += 360;
 4906         }
 4907 
 4908         $h = $hue / 360;
 4909         $s = min(100, max(0, $saturation)) / 100;
 4910         $l = min(100, max(0, $lightness)) / 100;
 4911 
 4912         $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
 4913         $m1 = $l * 2 - $m2;
 4914 
 4915         $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
 4916         $g = $this->hueToRGB($m1, $m2, $h) * 255;
 4917         $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
 4918 
 4919         $out = [Type::T_COLOR, $r, $g, $b];
 4920 
 4921         return $out;
 4922     }
 4923 
 4924     // Built in functions
 4925 
 4926     //protected static $libCall = ['name', 'args...'];
 4927     protected function libCall($args, $kwargs)
 4928     {
 4929         $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
 4930 
 4931         $posArgs = [];
 4932 
 4933         foreach ($args as $arg) {
 4934             if (empty($arg[0])) {
 4935                 if ($arg[2] === true) {
 4936                     $tmp = $this->reduce($arg[1]);
 4937 
 4938                     if ($tmp[0] === Type::T_LIST) {
 4939                         foreach ($tmp[2] as $item) {
 4940                             $posArgs[] = [null, $item, false];
 4941                         }
 4942                     } else {
 4943                         $posArgs[] = [null, $tmp, true];
 4944                     }
 4945 
 4946                     continue;
 4947                 }
 4948 
 4949                 $posArgs[] = [null, $this->reduce($arg), false];
 4950                 continue;
 4951             }
 4952 
 4953             $posArgs[] = [null, $arg, false];
 4954         }
 4955 
 4956         if (count($kwargs)) {
 4957             foreach ($kwargs as $key => $value) {
 4958                 $posArgs[] = [[Type::T_VARIABLE, $key], $value, false];
 4959             }
 4960         }
 4961 
 4962         return $this->reduce([Type::T_FUNCTION_CALL, $name, $posArgs]);
 4963     }
 4964 
 4965     protected static $libIf = ['condition', 'if-true', 'if-false'];
 4966     protected function libIf($args)
 4967     {
 4968         list($cond, $t, $f) = $args;
 4969 
 4970         if (! $this->isTruthy($this->reduce($cond, true))) {
 4971             return $this->reduce($f, true);
 4972         }
 4973 
 4974         return $this->reduce($t, true);
 4975     }
 4976 
 4977     protected static $libIndex = ['list', 'value'];
 4978     protected function libIndex($args)
 4979     {
 4980         list($list, $value) = $args;
 4981 
 4982         if ($value[0] === Type::T_MAP) {
 4983             return static::$null;
 4984         }
 4985 
 4986         if ($list[0] === Type::T_MAP ||
 4987             $list[0] === Type::T_STRING ||
 4988             $list[0] === Type::T_KEYWORD ||
 4989             $list[0] === Type::T_INTERPOLATE
 4990         ) {
 4991             $list = $this->coerceList($list, ' ');
 4992         }
 4993 
 4994         if ($list[0] !== Type::T_LIST) {
 4995             return static::$null;
 4996         }
 4997 
 4998         $values = [];
 4999 
 5000         foreach ($list[2] as $item) {
 5001             $values[] = $this->normalizeValue($item);
 5002         }
 5003 
 5004         $key = array_search($this->normalizeValue($value), $values);
 5005 
 5006         return false === $key ? static::$null : $key + 1;
 5007     }
 5008 
 5009     protected static $libRgb = ['red', 'green', 'blue'];
 5010     protected function libRgb($args)
 5011     {
 5012         list($r, $g, $b) = $args;
 5013 
 5014         return [Type::T_COLOR, $r[1], $g[1], $b[1]];
 5015     }
 5016 
 5017     protected static $libRgba = [
 5018         ['red', 'color'],
 5019         'green', 'blue', 'alpha'];
 5020     protected function libRgba($args)
 5021     {
 5022         if ($color = $this->coerceColor($args[0])) {
 5023             $num = isset($args[3]) ? $args[3] : $args[1];
 5024             $alpha = $this->assertNumber($num);
 5025             $color[4] = $alpha;
 5026 
 5027             return $color;
 5028         }
 5029 
 5030         list($r, $g, $b, $a) = $args;
 5031 
 5032         return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
 5033     }
 5034 
 5035     // helper function for adjust_color, change_color, and scale_color
 5036     protected function alterColor($args, $fn)
 5037     {
 5038         $color = $this->assertColor($args[0]);
 5039 
 5040         foreach ([1, 2, 3, 7] as $i) {
 5041             if (isset($args[$i])) {
 5042                 $val = $this->assertNumber($args[$i]);
 5043                 $ii = $i === 7 ? 4 : $i; // alpha
 5044                 $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
 5045             }
 5046         }
 5047 
 5048         if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
 5049             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 5050 
 5051             foreach ([4, 5, 6] as $i) {
 5052                 if (isset($args[$i])) {
 5053                     $val = $this->assertNumber($args[$i]);
 5054                     $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
 5055                 }
 5056             }
 5057 
 5058             $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
 5059 
 5060             if (isset($color[4])) {
 5061                 $rgb[4] = $color[4];
 5062             }
 5063 
 5064             $color = $rgb;
 5065         }
 5066 
 5067         return $color;
 5068     }
 5069 
 5070     protected static $libAdjustColor = [
 5071         'color', 'red', 'green', 'blue',
 5072         'hue', 'saturation', 'lightness', 'alpha'
 5073     ];
 5074     protected function libAdjustColor($args)
 5075     {
 5076         return $this->alterColor($args, function ($base, $alter, $i) {
 5077             return $base + $alter;
 5078         });
 5079     }
 5080 
 5081     protected static $libChangeColor = [
 5082         'color', 'red', 'green', 'blue',
 5083         'hue', 'saturation', 'lightness', 'alpha'
 5084     ];
 5085     protected function libChangeColor($args)
 5086     {
 5087         return $this->alterColor($args, function ($base, $alter, $i) {
 5088             return $alter;
 5089         });
 5090     }
 5091 
 5092     protected static $libScaleColor = [
 5093         'color', 'red', 'green', 'blue',
 5094         'hue', 'saturation', 'lightness', 'alpha'
 5095     ];
 5096     protected function libScaleColor($args)
 5097     {
 5098         return $this->alterColor($args, function ($base, $scale, $i) {
 5099             // 1, 2, 3 - rgb
 5100             // 4, 5, 6 - hsl
 5101             // 7 - a
 5102             switch ($i) {
 5103                 case 1:
 5104                 case 2:
 5105                 case 3:
 5106                     $max = 255;
 5107                     break;
 5108 
 5109                 case 4:
 5110                     $max = 360;
 5111                     break;
 5112 
 5113                 case 7:
 5114                     $max = 1;
 5115                     break;
 5116 
 5117                 default:
 5118                     $max = 100;
 5119             }
 5120 
 5121             $scale = $scale / 100;
 5122 
 5123             if ($scale < 0) {
 5124                 return $base * $scale + $base;
 5125             }
 5126 
 5127             return ($max - $base) * $scale + $base;
 5128         });
 5129     }
 5130 
 5131     protected static $libIeHexStr = ['color'];
 5132     protected function libIeHexStr($args)
 5133     {
 5134         $color = $this->coerceColor($args[0]);
 5135         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
 5136 
 5137         return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
 5138     }
 5139 
 5140     protected static $libRed = ['color'];
 5141     protected function libRed($args)
 5142     {
 5143         $color = $this->coerceColor($args[0]);
 5144 
 5145         return $color[1];
 5146     }
 5147 
 5148     protected static $libGreen = ['color'];
 5149     protected function libGreen($args)
 5150     {
 5151         $color = $this->coerceColor($args[0]);
 5152 
 5153         return $color[2];
 5154     }
 5155 
 5156     protected static $libBlue = ['color'];
 5157     protected function libBlue($args)
 5158     {
 5159         $color = $this->coerceColor($args[0]);
 5160 
 5161         return $color[3];
 5162     }
 5163 
 5164     protected static $libAlpha = ['color'];
 5165     protected function libAlpha($args)
 5166     {
 5167         if ($color = $this->coerceColor($args[0])) {
 5168             return isset($color[4]) ? $color[4] : 1;
 5169         }
 5170 
 5171         // this might be the IE function, so return value unchanged
 5172         return null;
 5173     }
 5174 
 5175     protected static $libOpacity = ['color'];
 5176     protected function libOpacity($args)
 5177     {
 5178         $value = $args[0];
 5179 
 5180         if ($value[0] === Type::T_NUMBER) {
 5181             return null;
 5182         }
 5183 
 5184         return $this->libAlpha($args);
 5185     }
 5186 
 5187     // mix two colors
 5188     protected static $libMix = ['color-1', 'color-2', 'weight'];
 5189     protected function libMix($args)
 5190     {
 5191         list($first, $second, $weight) = $args;
 5192 
 5193         $first = $this->assertColor($first);
 5194         $second = $this->assertColor($second);
 5195 
 5196         if (! isset($weight)) {
 5197             $weight = 0.5;
 5198         } else {
 5199             $weight = $this->coercePercent($weight);
 5200         }
 5201 
 5202         $firstAlpha = isset($first[4]) ? $first[4] : 1;
 5203         $secondAlpha = isset($second[4]) ? $second[4] : 1;
 5204 
 5205         $w = $weight * 2 - 1;
 5206         $a = $firstAlpha - $secondAlpha;
 5207 
 5208         $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
 5209         $w2 = 1.0 - $w1;
 5210 
 5211         $new = [Type::T_COLOR,
 5212             $w1 * $first[1] + $w2 * $second[1],
 5213             $w1 * $first[2] + $w2 * $second[2],
 5214             $w1 * $first[3] + $w2 * $second[3],
 5215         ];
 5216 
 5217         if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
 5218             $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
 5219         }
 5220 
 5221         return $this->fixColor($new);
 5222     }
 5223 
 5224     protected static $libHsl = ['hue', 'saturation', 'lightness'];
 5225     protected function libHsl($args)
 5226     {
 5227         list($h, $s, $l) = $args;
 5228 
 5229         return $this->toRGB($h[1], $s[1], $l[1]);
 5230     }
 5231 
 5232     protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
 5233     protected function libHsla($args)
 5234     {
 5235         list($h, $s, $l, $a) = $args;
 5236 
 5237         $color = $this->toRGB($h[1], $s[1], $l[1]);
 5238         $color[4] = $a[1];
 5239 
 5240         return $color;
 5241     }
 5242 
 5243     protected static $libHue = ['color'];
 5244     protected function libHue($args)
 5245     {
 5246         $color = $this->assertColor($args[0]);
 5247         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 5248 
 5249         return new Node\Number($hsl[1], 'deg');
 5250     }
 5251 
 5252     protected static $libSaturation = ['color'];
 5253     protected function libSaturation($args)
 5254     {
 5255         $color = $this->assertColor($args[0]);
 5256         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 5257 
 5258         return new Node\Number($hsl[2], '%');
 5259     }
 5260 
 5261     protected static $libLightness = ['color'];
 5262     protected function libLightness($args)
 5263     {
 5264         $color = $this->assertColor($args[0]);
 5265         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 5266 
 5267         return new Node\Number($hsl[3], '%');
 5268     }
 5269 
 5270     protected function adjustHsl($color, $idx, $amount)
 5271     {
 5272         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 5273         $hsl[$idx] += $amount;
 5274         $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
 5275 
 5276         if (isset($color[4])) {
 5277             $out[4] = $color[4];
 5278         }
 5279 
 5280         return $out;
 5281     }
 5282 
 5283     protected static $libAdjustHue = ['color', 'degrees'];
 5284     protected function libAdjustHue($args)
 5285     {
 5286         $color = $this->assertColor($args[0]);
 5287         $degrees = $this->assertNumber($args[1]);
 5288 
 5289         return $this->adjustHsl($color, 1, $degrees);
 5290     }
 5291 
 5292     protected static $libLighten = ['color', 'amount'];
 5293     protected function libLighten($args)
 5294     {
 5295         $color = $this->assertColor($args[0]);
 5296         $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
 5297 
 5298         return $this->adjustHsl($color, 3, $amount);
 5299     }
 5300 
 5301     protected static $libDarken = ['color', 'amount'];
 5302     protected function libDarken($args)
 5303     {
 5304         $color = $this->assertColor($args[0]);
 5305         $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
 5306 
 5307         return $this->adjustHsl($color, 3, -$amount);
 5308     }
 5309 
 5310     protected static $libSaturate = ['color', 'amount'];
 5311     protected function libSaturate($args)
 5312     {
 5313         $value = $args[0];
 5314 
 5315         if ($value[0] === Type::T_NUMBER) {
 5316             return null;
 5317         }
 5318 
 5319         $color = <