MDL-67379 libraries: Upgrade scssphp to 1.0.6
[moodle.git] / lib / scssphp / Compiler.php
1 <?php
2 /**
3  * SCSSPHP
4  *
5  * @copyright 2012-2019 Leaf Corcoran
6  *
7  * @license http://opensource.org/licenses/MIT MIT
8  *
9  * @link http://scssphp.github.io/scssphp
10  */
12 namespace ScssPhp\ScssPhp;
14 use ScssPhp\ScssPhp\Base\Range;
15 use ScssPhp\ScssPhp\Block;
16 use ScssPhp\ScssPhp\Cache;
17 use ScssPhp\ScssPhp\Colors;
18 use ScssPhp\ScssPhp\Compiler\Environment;
19 use ScssPhp\ScssPhp\Exception\CompilerException;
20 use ScssPhp\ScssPhp\Formatter\OutputBlock;
21 use ScssPhp\ScssPhp\Node;
22 use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
23 use ScssPhp\ScssPhp\Type;
24 use ScssPhp\ScssPhp\Parser;
25 use ScssPhp\ScssPhp\Util;
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  */
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;
64     const WITH_RULE     = 1;
65     const WITH_MEDIA    = 2;
66     const WITH_SUPPORTS = 4;
67     const WITH_ALL      = 7;
69     const SOURCE_MAP_NONE   = 0;
70     const SOURCE_MAP_INLINE = 1;
71     const SOURCE_MAP_FILE   = 2;
73     /**
74      * @var array
75      */
76     static protected $operatorNames = [
77         '+'   => 'add',
78         '-'   => 'sub',
79         '*'   => 'mul',
80         '/'   => 'div',
81         '%'   => 'mod',
83         '=='  => 'eq',
84         '!='  => 'neq',
85         '<'   => 'lt',
86         '>'   => 'gt',
88         '<='  => 'lte',
89         '>='  => 'gte',
90         '<=>' => 'cmp',
91     ];
93     /**
94      * @var array
95      */
96     static protected $namespaces = [
97         'special'  => '%',
98         'mixin'    => '@',
99         'function' => '^',
100     ];
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'];
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     ];
126     protected $encoding = null;
127     protected $lineNumberStyle = null;
129     protected $sourceMap = self::SOURCE_MAP_NONE;
130     protected $sourceMapOptions = [];
132     /**
133      * @var string|\ScssPhp\ScssPhp\Formatter
134      */
135     protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
137     protected $rootEnv;
138     protected $rootBlock;
140     /**
141      * @var \ScssPhp\ScssPhp\Compiler\Environment
142      */
143     protected $env;
144     protected $scope;
145     protected $storeEnv;
146     protected $charsetSeen;
147     protected $sourceNames;
149     protected $cache;
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;
163     protected $callStack = [];
165     /**
166      * Constructor
167      *
168      * @param array|null $cacheOptions
169      */
170     public function __construct($cacheOptions = null)
171     {
172         $this->parsedFiles = [];
173         $this->sourceNames = [];
175         if ($cacheOptions) {
176             $this->cache = new Cache($cacheOptions);
177         }
179         $this->stderr = fopen('php://stderr', 'w');
180     }
182     /**
183      * Get compiler options
184      *
185      * @return array
186      */
187     public function getCompileOptions()
188     {
189         $options = [
190             'importPaths'        => $this->importPaths,
191             'registeredVars'     => $this->registeredVars,
192             'registeredFeatures' => $this->registeredFeatures,
193             'encoding'           => $this->encoding,
194             'sourceMap'          => serialize($this->sourceMap),
195             'sourceMapOptions'   => $this->sourceMapOptions,
196             'formatter'          => $this->formatter,
197         ];
199         return $options;
200     }
202     /**
203      * Set an alternative error output stream, for testing purpose only
204      *
205      * @param resource $handle
206      */
207     public function setErrorOuput($handle)
208     {
209         $this->stderr = $handle;
210     }
212     /**
213      * Compile scss
214      *
215      * @api
216      *
217      * @param string $code
218      * @param string $path
219      *
220      * @return string
221      */
222     public function compile($code, $path = null)
223     {
224         if ($this->cache) {
225             $cacheKey       = ($path ? $path : "(stdin)") . ":" . md5($code);
226             $compileOptions = $this->getCompileOptions();
227             $cache          = $this->cache->getCache("compile", $cacheKey, $compileOptions);
229             if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
230                 // check if any dependency file changed before accepting the cache
231                 foreach ($cache['dependencies'] as $file => $mtime) {
232                     if (! is_file($file) || filemtime($file) !== $mtime) {
233                         unset($cache);
234                         break;
235                     }
236                 }
238                 if (isset($cache)) {
239                     return $cache['out'];
240                 }
241             }
242         }
245         $this->indentLevel    = -1;
246         $this->extends        = [];
247         $this->extendsMap     = [];
248         $this->sourceIndex    = null;
249         $this->sourceLine     = null;
250         $this->sourceColumn   = null;
251         $this->env            = null;
252         $this->scope          = null;
253         $this->storeEnv       = null;
254         $this->charsetSeen    = null;
255         $this->shouldEvaluate = null;
257         $this->parser = $this->parserFactory($path);
258         $tree         = $this->parser->parse($code);
259         $this->parser = null;
261         $this->formatter = new $this->formatter();
262         $this->rootBlock = null;
263         $this->rootEnv   = $this->pushEnv($tree);
265         $this->injectVariables($this->registeredVars);
266         $this->compileRoot($tree);
267         $this->popEnv();
269         $sourceMapGenerator = null;
271         if ($this->sourceMap) {
272             if (is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
273                 $sourceMapGenerator = $this->sourceMap;
274                 $this->sourceMap = self::SOURCE_MAP_FILE;
275             } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
276                 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
277             }
278         }
280         $out = $this->formatter->format($this->scope, $sourceMapGenerator);
282         if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
283             $sourceMap    = $sourceMapGenerator->generateJson();
284             $sourceMapUrl = null;
286             switch ($this->sourceMap) {
287                 case self::SOURCE_MAP_INLINE:
288                     $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
289                     break;
291                 case self::SOURCE_MAP_FILE:
292                     $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
293                     break;
294             }
296             $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
297         }
299         if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
300             $v = [
301                 'dependencies' => $this->getParsedFiles(),
302                 'out' => &$out,
303             ];
305             $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
306         }
308         return $out;
309     }
311     /**
312      * Instantiate parser
313      *
314      * @param string $path
315      *
316      * @return \ScssPhp\ScssPhp\Parser
317      */
318     protected function parserFactory($path)
319     {
320         $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
322         $this->sourceNames[] = $path;
323         $this->addParsedFile($path);
325         return $parser;
326     }
328     /**
329      * Is self extend?
330      *
331      * @param array $target
332      * @param array $origin
333      *
334      * @return boolean
335      */
336     protected function isSelfExtend($target, $origin)
337     {
338         foreach ($origin as $sel) {
339             if (in_array($target, $sel)) {
340                 return true;
341             }
342         }
344         return false;
345     }
347     /**
348      * Push extends
349      *
350      * @param array      $target
351      * @param array      $origin
352      * @param array|null $block
353      */
354     protected function pushExtends($target, $origin, $block)
355     {
356         if ($this->isSelfExtend($target, $origin)) {
357             return;
358         }
360         $i = count($this->extends);
361         $this->extends[] = [$target, $origin, $block];
363         foreach ($target as $part) {
364             if (isset($this->extendsMap[$part])) {
365                 $this->extendsMap[$part][] = $i;
366             } else {
367                 $this->extendsMap[$part] = [$i];
368             }
369         }
370     }
372     /**
373      * Make output block
374      *
375      * @param string $type
376      * @param array  $selectors
377      *
378      * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
379      */
380     protected function makeOutputBlock($type, $selectors = null)
381     {
382         $out = new OutputBlock;
383         $out->type      = $type;
384         $out->lines     = [];
385         $out->children  = [];
386         $out->parent    = $this->scope;
387         $out->selectors = $selectors;
388         $out->depth     = $this->env->depth;
390         if ($this->env->block instanceof Block) {
391             $out->sourceName   = $this->env->block->sourceName;
392             $out->sourceLine   = $this->env->block->sourceLine;
393             $out->sourceColumn = $this->env->block->sourceColumn;
394         } else {
395             $out->sourceName   = null;
396             $out->sourceLine   = null;
397             $out->sourceColumn = null;
398         }
400         return $out;
401     }
403     /**
404      * Compile root
405      *
406      * @param \ScssPhp\ScssPhp\Block $rootBlock
407      */
408     protected function compileRoot(Block $rootBlock)
409     {
410         $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
412         $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
413         $this->flattenSelectors($this->scope);
414         $this->missingSelectors();
415     }
417     /**
418      * Report missing selectors
419      */
420     protected function missingSelectors()
421     {
422         foreach ($this->extends as $extend) {
423             if (isset($extend[3])) {
424                 continue;
425             }
427             list($target, $origin, $block) = $extend;
429             // ignore if !optional
430             if ($block[2]) {
431                 continue;
432             }
434             $target = implode(' ', $target);
435             $origin = $this->collapseSelectors($origin);
437             $this->sourceLine = $block[Parser::SOURCE_LINE];
438             $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
439         }
440     }
442     /**
443      * Flatten selectors
444      *
445      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
446      * @param string                                 $parentKey
447      */
448     protected function flattenSelectors(OutputBlock $block, $parentKey = null)
449     {
450         if ($block->selectors) {
451             $selectors = [];
453             foreach ($block->selectors as $s) {
454                 $selectors[] = $s;
456                 if (! is_array($s)) {
457                     continue;
458                 }
460                 // check extends
461                 if (! empty($this->extendsMap)) {
462                     $this->matchExtends($s, $selectors);
464                     // remove duplicates
465                     array_walk($selectors, function (&$value) {
466                         $value = serialize($value);
467                     });
469                     $selectors = array_unique($selectors);
471                     array_walk($selectors, function (&$value) {
472                         $value = unserialize($value);
473                     });
474                 }
475             }
477             $block->selectors = [];
478             $placeholderSelector = false;
480             foreach ($selectors as $selector) {
481                 if ($this->hasSelectorPlaceholder($selector)) {
482                     $placeholderSelector = true;
483                     continue;
484                 }
486                 $block->selectors[] = $this->compileSelector($selector);
487             }
489             if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
490                 unset($block->parent->children[$parentKey]);
492                 return;
493             }
494         }
496         foreach ($block->children as $key => $child) {
497             $this->flattenSelectors($child, $key);
498         }
499     }
501     /**
502      * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
503      *
504      * @param array $parts
505      *
506      * @return array
507      */
508     protected function glueFunctionSelectors($parts)
509     {
510         $new = [];
512         foreach ($parts as $part) {
513             if (is_array($part)) {
514                 $part = $this->glueFunctionSelectors($part);
515                 $new[] = $part;
516             } else {
517                 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
518                 // and need to be joined to this
519                 if (count($new) && is_string($new[count($new) - 1]) &&
520                     strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
521                 ) {
522                     while (count($new)>1 && substr($new[count($new) - 1], -1) !== '(') {
523                         $part = array_pop($new) . $part;
524                     }
525                     $new[count($new) - 1] .= $part;
526                 } else {
527                     $new[] = $part;
528                 }
529             }
530         }
532         return $new;
533     }
535     /**
536      * Match extends
537      *
538      * @param array   $selector
539      * @param array   $out
540      * @param integer $from
541      * @param boolean $initial
542      */
543     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
544     {
545         static $partsPile = [];
546         $selector = $this->glueFunctionSelectors($selector);
548         if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
549             return;
550         }
552         $outRecurs = [];
553         foreach ($selector as $i => $part) {
554             if ($i < $from) {
555                 continue;
556             }
558             // check that we are not building an infinite loop of extensions
559             // if the new part is just including a previous part don't try to extend anymore
560             if (count($part) > 1) {
561                 foreach ($partsPile as $previousPart) {
562                     if (! count(array_diff($previousPart, $part))) {
563                         continue 2;
564                     }
565                 }
566             }
568             $partsPile[] = $part;
569             if ($this->matchExtendsSingle($part, $origin, $initial)) {
570                 $after       = array_slice($selector, $i + 1);
571                 $before      = array_slice($selector, 0, $i);
572                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
574                 foreach ($origin as $new) {
575                     $k = 0;
577                     // remove shared parts
578                     if (count($new) > 1) {
579                         while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
580                             $k++;
581                         }
582                     }
583                     if (count($nonBreakableBefore) and $k == count($new)) {
584                         $k--;
585                     }
587                     $replacement = [];
588                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
590                     for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
591                         $slice = [];
593                         foreach ($tempReplacement[$l] as $chunk) {
594                             if (! in_array($chunk, $slice)) {
595                                 $slice[] = $chunk;
596                             }
597                         }
599                         array_unshift($replacement, $slice);
601                         if (! $this->isImmediateRelationshipCombinator(end($slice))) {
602                             break;
603                         }
604                     }
606                     $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
608                     // Merge shared direct relationships.
609                     $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
611                     $result = array_merge(
612                         $before,
613                         $mergedBefore,
614                         $replacement,
615                         $after
616                     );
618                     if ($result === $selector) {
619                         continue;
620                     }
622                     $this->pushOrMergeExtentedSelector($out, $result);
624                     // recursively check for more matches
625                     $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
626                     if (count($origin) > 1) {
627                         $this->matchExtends($result, $out, $startRecurseFrom, false);
628                     } else {
629                         $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
630                     }
632                     // selector sequence merging
633                     if (! empty($before) && count($new) > 1) {
634                         $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
635                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
637                         list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
639                         $result2 = array_merge(
640                             $preSharedParts,
641                             $betweenSharedParts,
642                             $postSharedParts,
643                             $nonBreakabl2,
644                             $nonBreakableBefore,
645                             $replacement,
646                             $after
647                         );
649                         $this->pushOrMergeExtentedSelector($out, $result2);
650                     }
651                 }
652             }
653             array_pop($partsPile);
654         }
655         while (count($outRecurs)) {
656             $result = array_shift($outRecurs);
657             $this->pushOrMergeExtentedSelector($out, $result);
658         }
659     }
661     /**
662      * Test a part for being a pseudo selector
663      * @param string $part
664      * @param array $matches
665      * @return bool
666      */
667     protected function isPseudoSelector($part, &$matches)
668     {
669         if (strpos($part, ":") === 0
670             && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)) {
671             return true;
672         }
673         return false;
674     }
676     /**
677      * Push extended selector except if
678      *  - this is a pseudo selector
679      *  - same as previous
680      *  - in a white list
681      * in this case we merge the pseudo selector content
682      * @param array $out
683      * @param array $extended
684      */
685     protected function pushOrMergeExtentedSelector(&$out, $extended)
686     {
687         if (count($out) && count($extended) === 1 && count(reset($extended)) === 1) {
688             $single = reset($extended);
689             $part = reset($single);
690             if ($this->isPseudoSelector($part, $matchesExtended)
691               && in_array($matchesExtended[1], [ 'slotted' ])) {
692                 $prev = end($out);
693                 $prev = $this->glueFunctionSelectors($prev);
694                 if (count($prev) === 1 && count(reset($prev)) === 1) {
695                     $single = reset($prev);
696                     $part = reset($single);
697                     if ($this->isPseudoSelector($part, $matchesPrev)
698                       && $matchesPrev[1] === $matchesExtended[1]) {
699                         $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
700                         $extended[1] = $matchesPrev[2] . ", " . $extended[1];
701                         $extended = implode($matchesExtended[1] . '(', $extended);
702                         $extended = [ [ $extended ]];
703                         array_pop($out);
704                     }
705                 }
706             }
707         }
708         $out[] = $extended;
709     }
711     /**
712      * Match extends single
713      *
714      * @param array $rawSingle
715      * @param array $outOrigin
716      * @param bool $initial
717      *
718      * @return boolean
719      */
720     protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
721     {
722         $counts = [];
723         $single = [];
725         // simple usual cases, no need to do the whole trick
726         if (in_array($rawSingle, [['>'],['+'],['~']])) {
727             return false;
728         }
730         foreach ($rawSingle as $part) {
731             // matches Number
732             if (! is_string($part)) {
733                 return false;
734             }
736             if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
737                 $single[count($single) - 1] .= $part;
738             } else {
739                 $single[] = $part;
740             }
741         }
743         $extendingDecoratedTag = false;
745         if (count($single) > 1) {
746             $matches = null;
747             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
748         }
750         $outOrigin = [];
751         $found = false;
753         foreach ($single as $k => $part) {
754             if (isset($this->extendsMap[$part])) {
755                 foreach ($this->extendsMap[$part] as $idx) {
756                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
757                 }
758             }
759             if ($initial
760                 && $this->isPseudoSelector($part, $matches)
761                 && ! in_array($matches[1], [ 'not' ])) {
762                 $buffer    = $matches[2];
763                 $parser    = $this->parserFactory(__METHOD__);
764                 if ($parser->parseSelector($buffer, $subSelectors)) {
765                     foreach ($subSelectors as $ksub => $subSelector) {
766                         $subExtended = [];
767                         $this->matchExtends($subSelector, $subExtended, 0, false);
768                         if ($subExtended) {
769                             $subSelectorsExtended = $subSelectors;
770                             $subSelectorsExtended[$ksub] = $subExtended;
771                             foreach ($subSelectorsExtended as $ksse => $sse) {
772                                 $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
773                             }
774                             $subSelectorsExtended = implode(', ', $subSelectorsExtended);
775                             $singleExtended = $single;
776                             $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
777                             $outOrigin[] = [ $singleExtended ];
778                             $found = true;
779                         }
780                     }
781                 }
782             }
783         }
785         foreach ($counts as $idx => $count) {
786             list($target, $origin, /* $block */) = $this->extends[$idx];
788             $origin = $this->glueFunctionSelectors($origin);
790             // check count
791             if ($count !== count($target)) {
792                 continue;
793             }
795             $this->extends[$idx][3] = true;
797             $rem = array_diff($single, $target);
799             foreach ($origin as $j => $new) {
800                 // prevent infinite loop when target extends itself
801                 if ($this->isSelfExtend($single, $origin)) {
802                     return false;
803                 }
805                 $replacement = end($new);
807                 // Extending a decorated tag with another tag is not possible.
808                 if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
809                     preg_match('/^[a-z0-9]+$/i', $replacement[0])
810                 ) {
811                     unset($origin[$j]);
812                     continue;
813                 }
815                 $combined = $this->combineSelectorSingle($replacement, $rem);
817                 if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
818                     $origin[$j][count($origin[$j]) - 1] = $combined;
819                 }
820             }
822             $outOrigin = array_merge($outOrigin, $origin);
824             $found = true;
825         }
827         return $found;
828     }
830     /**
831      * Extract a relationship from the fragment.
832      *
833      * When extracting the last portion of a selector we will be left with a
834      * fragment which may end with a direction relationship combinator. This
835      * method will extract the relationship fragment and return it along side
836      * the rest.
837      *
838      * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
839      *
840      * @return array The selector without the relationship fragment if any, the relationship fragment.
841      */
842     protected function extractRelationshipFromFragment(array $fragment)
843     {
844         $parents = [];
845         $children = [];
847         $j = $i = count($fragment);
849         for (;;) {
850             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
851             $parents  = array_slice($fragment, 0, $j);
852             $slice    = end($parents);
854             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
855                 break;
856             }
858             $j -= 2;
859         }
861         return [$parents, $children];
862     }
864     /**
865      * Combine selector single
866      *
867      * @param array $base
868      * @param array $other
869      *
870      * @return array
871      */
872     protected function combineSelectorSingle($base, $other)
873     {
874         $tag    = [];
875         $out    = [];
876         $wasTag = false;
878         foreach ([array_reverse($base), array_reverse($other)] as $single) {
879             foreach ($single as $part) {
880                 if (preg_match('/^[\[:]/', $part)) {
881                     $out[] = $part;
882                     $wasTag = false;
883                 } elseif (preg_match('/^[\.#]/', $part)) {
884                     array_unshift($out, $part);
885                     $wasTag = false;
886                 } elseif (preg_match('/^[^_-]/', $part)) {
887                     $tag[] = $part;
888                     $wasTag = true;
889                 } elseif ($wasTag) {
890                     $tag[count($tag) - 1] .= $part;
891                 } else {
892                     $out[] = $part;
893                 }
894             }
895         }
897         if (count($tag)) {
898             array_unshift($out, $tag[0]);
899         }
901         return $out;
902     }
904     /**
905      * Compile media
906      *
907      * @param \ScssPhp\ScssPhp\Block $media
908      */
909     protected function compileMedia(Block $media)
910     {
911         $this->pushEnv($media);
913         $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
915         if (! empty($mediaQueries) && $mediaQueries) {
916             $previousScope = $this->scope;
917             $parentScope = $this->mediaParent($this->scope);
919             foreach ($mediaQueries as $mediaQuery) {
920                 $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
922                 $parentScope->children[] = $this->scope;
923                 $parentScope = $this->scope;
924             }
926             // top level properties in a media cause it to be wrapped
927             $needsWrap = false;
929             foreach ($media->children as $child) {
930                 $type = $child[0];
932                 if ($type !== Type::T_BLOCK &&
933                     $type !== Type::T_MEDIA &&
934                     $type !== Type::T_DIRECTIVE &&
935                     $type !== Type::T_IMPORT
936                 ) {
937                     $needsWrap = true;
938                     break;
939                 }
940             }
942             if ($needsWrap) {
943                 $wrapped = new Block;
944                 $wrapped->sourceName   = $media->sourceName;
945                 $wrapped->sourceIndex  = $media->sourceIndex;
946                 $wrapped->sourceLine   = $media->sourceLine;
947                 $wrapped->sourceColumn = $media->sourceColumn;
948                 $wrapped->selectors    = [];
949                 $wrapped->comments     = [];
950                 $wrapped->parent       = $media;
951                 $wrapped->children     = $media->children;
953                 $media->children = [[Type::T_BLOCK, $wrapped]];
955                 if (isset($this->lineNumberStyle)) {
956                     $annotation = $this->makeOutputBlock(Type::T_COMMENT);
957                     $annotation->depth = 0;
959                     $file = $this->sourceNames[$media->sourceIndex];
960                     $line = $media->sourceLine;
962                     switch ($this->lineNumberStyle) {
963                         case static::LINE_COMMENTS:
964                             $annotation->lines[] = '/* line ' . $line
965                                                  . ($file ? ', ' . $file : '')
966                                                  . ' */';
967                             break;
969                         case static::DEBUG_INFO:
970                             $annotation->lines[] = '@media -sass-debug-info{'
971                                                  . ($file ? 'filename{font-family:"' . $file . '"}' : '')
972                                                  . 'line{font-family:' . $line . '}}';
973                             break;
974                     }
976                     $this->scope->children[] = $annotation;
977                 }
978             }
980             $this->compileChildrenNoReturn($media->children, $this->scope);
982             $this->scope = $previousScope;
983         }
985         $this->popEnv();
986     }
988     /**
989      * Media parent
990      *
991      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
992      *
993      * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
994      */
995     protected function mediaParent(OutputBlock $scope)
996     {
997         while (! empty($scope->parent)) {
998             if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
999                 break;
1000             }
1002             $scope = $scope->parent;
1003         }
1005         return $scope;
1006     }
1008     /**
1009      * Compile directive
1010      *
1011      * @param \ScssPhp\ScssPhp\Block|array $block
1012      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1013      */
1014     protected function compileDirective($directive, OutputBlock $out)
1015     {
1016         if (is_array($directive)) {
1017             $s = '@' . $directive[0];
1018             if (! empty($directive[1])) {
1019                 $s .= ' ' . $this->compileValue($directive[1]);
1020             }
1021             $this->appendRootDirective($s . ';', $out);
1022         } else {
1023             $s = '@' . $directive->name;
1025             if (! empty($directive->value)) {
1026                 $s .= ' ' . $this->compileValue($directive->value);
1027             }
1029             if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1030                 $this->compileKeyframeBlock($directive, [$s]);
1031             } else {
1032                 $this->compileNestedBlock($directive, [$s]);
1033             }
1034         }
1035     }
1037     /**
1038      * Compile at-root
1039      *
1040      * @param \ScssPhp\ScssPhp\Block $block
1041      */
1042     protected function compileAtRoot(Block $block)
1043     {
1044         $env     = $this->pushEnv($block);
1045         $envs    = $this->compactEnv($env);
1046         list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1048         // wrap inline selector
1049         if ($block->selector) {
1050             $wrapped = new Block;
1051             $wrapped->sourceName   = $block->sourceName;
1052             $wrapped->sourceIndex  = $block->sourceIndex;
1053             $wrapped->sourceLine   = $block->sourceLine;
1054             $wrapped->sourceColumn = $block->sourceColumn;
1055             $wrapped->selectors    = $block->selector;
1056             $wrapped->comments     = [];
1057             $wrapped->parent       = $block;
1058             $wrapped->children     = $block->children;
1059             $wrapped->selfParent   = $block->selfParent;
1061             $block->children = [[Type::T_BLOCK, $wrapped]];
1062             $block->selector = null;
1063         }
1065         $selfParent = $block->selfParent;
1067         if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
1068             isset($block->parent->selectors) && $block->parent->selectors
1069         ) {
1070             $selfParent = $block->parent;
1071         }
1073         $this->env = $this->filterWithWithout($envs, $with, $without);
1075         $saveScope   = $this->scope;
1076         $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1078         // propagate selfParent to the children where they still can be useful
1079         $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1081         $this->scope = $this->completeScope($this->scope, $saveScope);
1082         $this->scope = $saveScope;
1083         $this->env   = $this->extractEnv($envs);
1085         $this->popEnv();
1086     }
1088     /**
1089      * Filter at-root scope depending of with/without option
1090      *
1091      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1092      * @param array                                  $with
1093      * @param array                                  $without
1094      *
1095      * @return mixed
1096      */
1097     protected function filterScopeWithWithout($scope, $with, $without)
1098     {
1099         $filteredScopes = [];
1100         $childStash = [];
1102         if ($scope->type === TYPE::T_ROOT) {
1103             return $scope;
1104         }
1106         // start from the root
1107         while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1108             array_unshift($childStash, $scope);
1109             $scope = $scope->parent;
1110         }
1112         for (;;) {
1113             if (! $scope) {
1114                 break;
1115             }
1117             if ($this->isWith($scope, $with, $without)) {
1118                 $s = clone $scope;
1119                 $s->children = [];
1120                 $s->lines    = [];
1121                 $s->parent   = null;
1123                 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1124                     $s->selectors = [];
1125                 }
1127                 $filteredScopes[] = $s;
1128             }
1130             if (count($childStash)) {
1131                 $scope = array_shift($childStash);
1132             } elseif ($scope->children) {
1133                 $scope = end($scope->children);
1134             } else {
1135                 $scope = null;
1136             }
1137         }
1139         if (! count($filteredScopes)) {
1140             return $this->rootBlock;
1141         }
1143         $newScope = array_shift($filteredScopes);
1144         $newScope->parent = $this->rootBlock;
1146         $this->rootBlock->children[] = $newScope;
1148         $p = &$newScope;
1150         while (count($filteredScopes)) {
1151             $s = array_shift($filteredScopes);
1152             $s->parent = $p;
1153             $p->children[] = $s;
1154             $newScope = &$p->children[0];
1155             $p = &$p->children[0];
1156         }
1158         return $newScope;
1159     }
1161     /**
1162      * found missing selector from a at-root compilation in the previous scope
1163      * (if at-root is just enclosing a property, the selector is in the parent tree)
1164      *
1165      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1166      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1167      *
1168      * @return mixed
1169      */
1170     protected function completeScope($scope, $previousScope)
1171     {
1172         if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
1173             $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1174         }
1176         if ($scope->children) {
1177             foreach ($scope->children as $k => $c) {
1178                 $scope->children[$k] = $this->completeScope($c, $previousScope);
1179             }
1180         }
1182         return $scope;
1183     }
1185     /**
1186      * Find a selector by the depth node in the scope
1187      *
1188      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1189      * @param integer                                $depth
1190      *
1191      * @return array
1192      */
1193     protected function findScopeSelectors($scope, $depth)
1194     {
1195         if ($scope->depth === $depth && $scope->selectors) {
1196             return $scope->selectors;
1197         }
1199         if ($scope->children) {
1200             foreach (array_reverse($scope->children) as $c) {
1201                 if ($s = $this->findScopeSelectors($c, $depth)) {
1202                     return $s;
1203                 }
1204             }
1205         }
1207         return [];
1208     }
1210     /**
1211      * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1212      *
1213      * @param array $withCondition
1214      *
1215      * @return array
1216      */
1217     protected function compileWith($withCondition)
1218     {
1219         // just compile what we have in 2 lists
1220         $with = [];
1221         $without = ['rule' => true];
1223         if ($withCondition) {
1224             if ($this->libMapHasKey([$withCondition, static::$with])) {
1225                 $without = []; // cancel the default
1226                 $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1228                 foreach ($list[2] as $item) {
1229                     $keyword = $this->compileStringContent($this->coerceString($item));
1231                     $with[$keyword] = true;
1232                 }
1233             }
1235             if ($this->libMapHasKey([$withCondition, static::$without])) {
1236                 $without = []; // cancel the default
1237                 $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1239                 foreach ($list[2] as $item) {
1240                     $keyword = $this->compileStringContent($this->coerceString($item));
1242                     $without[$keyword] = true;
1243                 }
1244             }
1245         }
1247         return [$with, $without];
1248     }
1250     /**
1251      * Filter env stack
1252      *
1253      * @param array   $envs
1254      * @param array $with
1255      * @param array $without
1256      *
1257      * @return \ScssPhp\ScssPhp\Compiler\Environment
1258      */
1259     protected function filterWithWithout($envs, $with, $without)
1260     {
1261         $filtered = [];
1263         foreach ($envs as $e) {
1264             if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1265                 $ec = clone $e;
1266                 $ec->block     = null;
1267                 $ec->selectors = [];
1269                 $filtered[] = $ec;
1270             } else {
1271                 $filtered[] = $e;
1272             }
1273         }
1275         return $this->extractEnv($filtered);
1276     }
1278     /**
1279      * Filter WITH rules
1280      *
1281      * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1282      * @param array                                                         $with
1283      * @param array                                                         $without
1284      *
1285      * @return boolean
1286      */
1287     protected function isWith($block, $with, $without)
1288     {
1289         if (isset($block->type)) {
1290             if ($block->type === Type::T_MEDIA) {
1291                 return $this->testWithWithout('media', $with, $without);
1292             }
1294             if ($block->type === Type::T_DIRECTIVE) {
1295                 if (isset($block->name)) {
1296                     return $this->testWithWithout($block->name, $with, $without);
1297                 } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1298                     return $this->testWithWithout($m[1], $with, $without);
1299                 } else {
1300                     return $this->testWithWithout('???', $with, $without);
1301                 }
1302             }
1303         } elseif (isset($block->selectors)) {
1304             // a selector starting with number is a keyframe rule
1305             if (count($block->selectors)) {
1306                 $s = reset($block->selectors);
1308                 while (is_array($s)) {
1309                     $s = reset($s);
1310                 }
1312                 if (is_object($s) && $s instanceof Node\Number) {
1313                     return $this->testWithWithout('keyframes', $with, $without);
1314                 }
1315             }
1317             return $this->testWithWithout('rule', $with, $without);
1318         }
1320         return true;
1321     }
1323     /**
1324      * Test a single type of block against with/without lists
1325      *
1326      * @param string $what
1327      * @param array  $with
1328      * @param array  $without
1329      *
1330      * @return boolean
1331      *   true if the block should be kept, false to reject
1332      */
1333     protected function testWithWithout($what, $with, $without)
1334     {
1336         // if without, reject only if in the list (or 'all' is in the list)
1337         if (count($without)) {
1338             return (isset($without[$what]) || isset($without['all'])) ? false : true;
1339         }
1341         // otherwise reject all what is not in the with list
1342         return (isset($with[$what]) || isset($with['all'])) ? true : false;
1343     }
1346     /**
1347      * Compile keyframe block
1348      *
1349      * @param \ScssPhp\ScssPhp\Block $block
1350      * @param array                  $selectors
1351      */
1352     protected function compileKeyframeBlock(Block $block, $selectors)
1353     {
1354         $env = $this->pushEnv($block);
1356         $envs = $this->compactEnv($env);
1358         $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1359             return ! isset($e->block->selectors);
1360         }));
1362         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1363         $this->scope->depth = 1;
1364         $this->scope->parent->children[] = $this->scope;
1366         $this->compileChildrenNoReturn($block->children, $this->scope);
1368         $this->scope = $this->scope->parent;
1369         $this->env   = $this->extractEnv($envs);
1371         $this->popEnv();
1372     }
1374     /**
1375      * Compile nested properties lines
1376      *
1377      * @param \ScssPhp\ScssPhp\Block $block
1378      * @param OutputBlock            $out
1379      */
1380     protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1381     {
1382         $prefix = $this->compileValue($block->prefix) . '-';
1384         $nested = $this->makeOutputBlock($block->type);
1385         $nested->parent = $out;
1387         if ($block->hasValue) {
1388             $nested->depth = $out->depth + 1;
1389         }
1391         $out->children[] = $nested;
1393         foreach ($block->children as $child) {
1394             switch ($child[0]) {
1395                 case Type::T_ASSIGN:
1396                     array_unshift($child[1][2], $prefix);
1397                     break;
1399                 case Type::T_NESTED_PROPERTY:
1400                     array_unshift($child[1]->prefix[2], $prefix);
1401                     break;
1402             }
1404             $this->compileChild($child, $nested);
1405         }
1406     }
1408     /**
1409      * Compile nested block
1410      *
1411      * @param \ScssPhp\ScssPhp\Block $block
1412      * @param array                  $selectors
1413      */
1414     protected function compileNestedBlock(Block $block, $selectors)
1415     {
1416         $this->pushEnv($block);
1418         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1419         $this->scope->parent->children[] = $this->scope;
1421         // wrap assign children in a block
1422         // except for @font-face
1423         if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
1424             // need wrapping?
1425             $needWrapping = false;
1427             foreach ($block->children as $child) {
1428                 if ($child[0] === Type::T_ASSIGN) {
1429                     $needWrapping = true;
1430                     break;
1431                 }
1432             }
1434             if ($needWrapping) {
1435                 $wrapped = new Block;
1436                 $wrapped->sourceName   = $block->sourceName;
1437                 $wrapped->sourceIndex  = $block->sourceIndex;
1438                 $wrapped->sourceLine   = $block->sourceLine;
1439                 $wrapped->sourceColumn = $block->sourceColumn;
1440                 $wrapped->selectors    = [];
1441                 $wrapped->comments     = [];
1442                 $wrapped->parent       = $block;
1443                 $wrapped->children     = $block->children;
1444                 $wrapped->selfParent   = $block->selfParent;
1446                 $block->children = [[Type::T_BLOCK, $wrapped]];
1447             }
1448         }
1450         $this->compileChildrenNoReturn($block->children, $this->scope);
1452         $this->scope = $this->scope->parent;
1454         $this->popEnv();
1455     }
1457     /**
1458      * Recursively compiles a block.
1459      *
1460      * A block is analogous to a CSS block in most cases. A single SCSS document
1461      * is encapsulated in a block when parsed, but it does not have parent tags
1462      * so all of its children appear on the root level when compiled.
1463      *
1464      * Blocks are made up of selectors and children.
1465      *
1466      * The children of a block are just all the blocks that are defined within.
1467      *
1468      * Compiling the block involves pushing a fresh environment on the stack,
1469      * and iterating through the props, compiling each one.
1470      *
1471      * @see Compiler::compileChild()
1472      *
1473      * @param \ScssPhp\ScssPhp\Block $block
1474      */
1475     protected function compileBlock(Block $block)
1476     {
1477         $env = $this->pushEnv($block);
1478         $env->selectors = $this->evalSelectors($block->selectors);
1480         $out = $this->makeOutputBlock(null);
1482         if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
1483             $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1484             $annotation->depth = 0;
1486             $file = $this->sourceNames[$block->sourceIndex];
1487             $line = $block->sourceLine;
1489             switch ($this->lineNumberStyle) {
1490                 case static::LINE_COMMENTS:
1491                     $annotation->lines[] = '/* line ' . $line
1492                                          . ($file ? ', ' . $file : '')
1493                                          . ' */';
1494                     break;
1496                 case static::DEBUG_INFO:
1497                     $annotation->lines[] = '@media -sass-debug-info{'
1498                                          . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1499                                          . 'line{font-family:' . $line . '}}';
1500                     break;
1501             }
1503             $this->scope->children[] = $annotation;
1504         }
1506         $this->scope->children[] = $out;
1508         if (count($block->children)) {
1509             $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1511             // propagate selfParent to the children where they still can be useful
1512             $selfParentSelectors = null;
1514             if (isset($block->selfParent->selectors)) {
1515                 $selfParentSelectors = $block->selfParent->selectors;
1516                 $block->selfParent->selectors = $out->selectors;
1517             }
1519             $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1521             // and revert for the following children of the same block
1522             if ($selfParentSelectors) {
1523                 $block->selfParent->selectors = $selfParentSelectors;
1524             }
1525         }
1527         $this->popEnv();
1528     }
1531     /**
1532      * Compile the value of a comment that can have interpolation
1533      *
1534      * @param array   $value
1535      * @param boolean $pushEnv
1536      *
1537      * @return array|mixed|string
1538      */
1539     protected function compileCommentValue($value, $pushEnv = false)
1540     {
1541         $c = $value[1];
1543         if (isset($value[2])) {
1544             if ($pushEnv) {
1545                 $this->pushEnv();
1546                 $storeEnv = $this->storeEnv;
1547                 $this->storeEnv = $this->env;
1548             }
1550             try {
1551                 $c = $this->compileValue($value[2]);
1552             } catch (\Exception $e) {
1553                 // ignore error in comment compilation which are only interpolation
1554             }
1556             if ($pushEnv) {
1557                 $this->storeEnv = $storeEnv;
1558                 $this->popEnv();
1559             }
1560         }
1562         return $c;
1563     }
1565     /**
1566      * Compile root level comment
1567      *
1568      * @param array $block
1569      */
1570     protected function compileComment($block)
1571     {
1572         $out = $this->makeOutputBlock(Type::T_COMMENT);
1573         $out->lines[] = $this->compileCommentValue($block, true);
1575         $this->scope->children[] = $out;
1576     }
1578     /**
1579      * Evaluate selectors
1580      *
1581      * @param array $selectors
1582      *
1583      * @return array
1584      */
1585     protected function evalSelectors($selectors)
1586     {
1587         $this->shouldEvaluate = false;
1589         $selectors = array_map([$this, 'evalSelector'], $selectors);
1591         // after evaluating interpolates, we might need a second pass
1592         if ($this->shouldEvaluate) {
1593             $selectors = $this->revertSelfSelector($selectors);
1594             $buffer    = $this->collapseSelectors($selectors);
1595             $parser    = $this->parserFactory(__METHOD__);
1597             if ($parser->parseSelector($buffer, $newSelectors)) {
1598                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1599             }
1600         }
1602         return $selectors;
1603     }
1605     /**
1606      * Evaluate selector
1607      *
1608      * @param array $selector
1609      *
1610      * @return array
1611      */
1612     protected function evalSelector($selector)
1613     {
1614         return array_map([$this, 'evalSelectorPart'], $selector);
1615     }
1617     /**
1618      * Evaluate selector part; replaces all the interpolates, stripping quotes
1619      *
1620      * @param array $part
1621      *
1622      * @return array
1623      */
1624     protected function evalSelectorPart($part)
1625     {
1626         foreach ($part as &$p) {
1627             if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1628                 $p = $this->compileValue($p);
1630                 // force re-evaluation
1631                 if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1632                     $this->shouldEvaluate = true;
1633                 }
1634             } elseif (is_string($p) && strlen($p) >= 2 &&
1635                 ($first = $p[0]) && ($first === '"' || $first === "'") &&
1636                 substr($p, -1) === $first
1637             ) {
1638                 $p = substr($p, 1, -1);
1639             }
1640         }
1642         return $this->flattenSelectorSingle($part);
1643     }
1645     /**
1646      * Collapse selectors
1647      *
1648      * @param array   $selectors
1649      * @param boolean $selectorFormat
1650      *   if false return a collapsed string
1651      *   if true return an array description of a structured selector
1652      *
1653      * @return string
1654      */
1655     protected function collapseSelectors($selectors, $selectorFormat = false)
1656     {
1657         $parts = [];
1659         foreach ($selectors as $selector) {
1660             $output = [];
1661             $glueNext = false;
1663             foreach ($selector as $node) {
1664                 $compound = '';
1666                 array_walk_recursive(
1667                     $node,
1668                     function ($value, $key) use (&$compound) {
1669                         $compound .= $value;
1670                     }
1671                 );
1673                 if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1674                     if (count($output)) {
1675                         $output[count($output) - 1] .= ' ' . $compound;
1676                     } else {
1677                         $output[] = $compound;
1678                     }
1680                     $glueNext = true;
1681                 } elseif ($glueNext) {
1682                     $output[count($output) - 1] .= ' ' . $compound;
1683                     $glueNext = false;
1684                 } else {
1685                     $output[] = $compound;
1686                 }
1687             }
1689             if ($selectorFormat) {
1690                 foreach ($output as &$o) {
1691                     $o = [Type::T_STRING, '', [$o]];
1692                 }
1694                 $output = [Type::T_LIST, ' ', $output];
1695             } else {
1696                 $output = implode(' ', $output);
1697             }
1699             $parts[] = $output;
1700         }
1702         if ($selectorFormat) {
1703             $parts = [Type::T_LIST, ',', $parts];
1704         } else {
1705             $parts = implode(', ', $parts);
1706         }
1708         return $parts;
1709     }
1711     /**
1712      * Parse down the selector and revert [self] to "&" before a reparsing
1713      *
1714      * @param array $selectors
1715      *
1716      * @return array
1717      */
1718     protected function revertSelfSelector($selectors)
1719     {
1720         foreach ($selectors as &$part) {
1721             if (is_array($part)) {
1722                 if ($part === [Type::T_SELF]) {
1723                     $part = '&';
1724                 } else {
1725                     $part = $this->revertSelfSelector($part);
1726                 }
1727             }
1728         }
1730         return $selectors;
1731     }
1733     /**
1734      * Flatten selector single; joins together .classes and #ids
1735      *
1736      * @param array $single
1737      *
1738      * @return array
1739      */
1740     protected function flattenSelectorSingle($single)
1741     {
1742         $joined = [];
1744         foreach ($single as $part) {
1745             if (empty($joined) ||
1746                 ! is_string($part) ||
1747                 preg_match('/[\[.:#%]/', $part)
1748             ) {
1749                 $joined[] = $part;
1750                 continue;
1751             }
1753             if (is_array(end($joined))) {
1754                 $joined[] = $part;
1755             } else {
1756                 $joined[count($joined) - 1] .= $part;
1757             }
1758         }
1760         return $joined;
1761     }
1763     /**
1764      * Compile selector to string; self(&) should have been replaced by now
1765      *
1766      * @param string|array $selector
1767      *
1768      * @return string
1769      */
1770     protected function compileSelector($selector)
1771     {
1772         if (! is_array($selector)) {
1773             return $selector; // media and the like
1774         }
1776         return implode(
1777             ' ',
1778             array_map(
1779                 [$this, 'compileSelectorPart'],
1780                 $selector
1781             )
1782         );
1783     }
1785     /**
1786      * Compile selector part
1787      *
1788      * @param array $piece
1789      *
1790      * @return string
1791      */
1792     protected function compileSelectorPart($piece)
1793     {
1794         foreach ($piece as &$p) {
1795             if (! is_array($p)) {
1796                 continue;
1797             }
1799             switch ($p[0]) {
1800                 case Type::T_SELF:
1801                     $p = '&';
1802                     break;
1804                 default:
1805                     $p = $this->compileValue($p);
1806                     break;
1807             }
1808         }
1810         return implode($piece);
1811     }
1813     /**
1814      * Has selector placeholder?
1815      *
1816      * @param array $selector
1817      *
1818      * @return boolean
1819      */
1820     protected function hasSelectorPlaceholder($selector)
1821     {
1822         if (! is_array($selector)) {
1823             return false;
1824         }
1826         foreach ($selector as $parts) {
1827             foreach ($parts as $part) {
1828                 if (strlen($part) && '%' === $part[0]) {
1829                     return true;
1830                 }
1831             }
1832         }
1834         return false;
1835     }
1837     protected function pushCallStack($name = '')
1838     {
1839         $this->callStack[] = [
1840           'n' => $name,
1841           Parser::SOURCE_INDEX => $this->sourceIndex,
1842           Parser::SOURCE_LINE => $this->sourceLine,
1843           Parser::SOURCE_COLUMN => $this->sourceColumn
1844         ];
1846         // infinite calling loop
1847         if (count($this->callStack) > 25000) {
1848             // not displayed but you can var_dump it to deep debug
1849             $msg = $this->callStackMessage(true, 100);
1850             $msg = "Infinite calling loop";
1852             $this->throwError($msg);
1853         }
1854     }
1856     protected function popCallStack()
1857     {
1858         array_pop($this->callStack);
1859     }
1861     /**
1862      * Compile children and return result
1863      *
1864      * @param array                                  $stms
1865      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1866      * @param string                                 $traceName
1867      *
1868      * @return array|null
1869      */
1870     protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1871     {
1872         $this->pushCallStack($traceName);
1874         foreach ($stms as $stm) {
1875             $ret = $this->compileChild($stm, $out);
1877             if (isset($ret)) {
1878                 return $ret;
1879             }
1880         }
1882         $this->popCallStack();
1884         return null;
1885     }
1887     /**
1888      * Compile children and throw exception if unexpected @return
1889      *
1890      * @param array                                  $stms
1891      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1892      * @param \ScssPhp\ScssPhp\Block                 $selfParent
1893      * @param string                                 $traceName
1894      *
1895      * @throws \Exception
1896      */
1897     protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1898     {
1899         $this->pushCallStack($traceName);
1901         foreach ($stms as $stm) {
1902             if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
1903                 $stm[1]->selfParent = $selfParent;
1904                 $ret = $this->compileChild($stm, $out);
1905                 $stm[1]->selfParent = null;
1906             } elseif ($selfParent && in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
1907                 $stm['selfParent'] = $selfParent;
1908                 $ret = $this->compileChild($stm, $out);
1909                 unset($stm['selfParent']);
1910             } else {
1911                 $ret = $this->compileChild($stm, $out);
1912             }
1914             if (isset($ret)) {
1915                 $this->throwError('@return may only be used within a function');
1917                 return;
1918             }
1919         }
1921         $this->popCallStack();
1922     }
1925     /**
1926      * evaluate media query : compile internal value keeping the structure inchanged
1927      *
1928      * @param array $queryList
1929      *
1930      * @return array
1931      */
1932     protected function evaluateMediaQuery($queryList)
1933     {
1934         static $parser = null;
1936         $outQueryList = [];
1938         foreach ($queryList as $kql => $query) {
1939             $shouldReparse = false;
1941             foreach ($query as $kq => $q) {
1942                 for ($i = 1; $i < count($q); $i++) {
1943                     $value = $this->compileValue($q[$i]);
1945                     // the parser had no mean to know if media type or expression if it was an interpolation
1946                     // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
1947                     if ($q[0] == Type::T_MEDIA_TYPE &&
1948                         (strpos($value, '(') !== false ||
1949                         strpos($value, ')') !== false ||
1950                         strpos($value, ':') !== false ||
1951                         strpos($value, ',') !== false)
1952                     ) {
1953                         $shouldReparse = true;
1954                     }
1956                     $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
1957                 }
1958             }
1960             if ($shouldReparse) {
1961                 if (is_null($parser)) {
1962                     $parser = $this->parserFactory(__METHOD__);
1963                 }
1965                 $queryString = $this->compileMediaQuery([$queryList[$kql]]);
1966                 $queryString = reset($queryString);
1968                 if (strpos($queryString, '@media ') === 0) {
1969                     $queryString = substr($queryString, 7);
1970                     $queries = [];
1972                     if ($parser->parseMediaQueryList($queryString, $queries)) {
1973                         $queries = $this->evaluateMediaQuery($queries[2]);
1975                         while (count($queries)) {
1976                             $outQueryList[] = array_shift($queries);
1977                         }
1979                         continue;
1980                     }
1981                 }
1982             }
1984             $outQueryList[] = $queryList[$kql];
1985         }
1987         return $outQueryList;
1988     }
1990     /**
1991      * Compile media query
1992      *
1993      * @param array $queryList
1994      *
1995      * @return array
1996      */
1997     protected function compileMediaQuery($queryList)
1998     {
1999         $start   = '@media ';
2000         $default = trim($start);
2001         $out     = [];
2002         $current = "";
2004         foreach ($queryList as $query) {
2005             $type = null;
2006             $parts = [];
2008             $mediaTypeOnly = true;
2010             foreach ($query as $q) {
2011                 if ($q[0] !== Type::T_MEDIA_TYPE) {
2012                     $mediaTypeOnly = false;
2013                     break;
2014                 }
2015             }
2017             foreach ($query as $q) {
2018                 switch ($q[0]) {
2019                     case Type::T_MEDIA_TYPE:
2020                         $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
2022                         // combining not and anything else than media type is too risky and should be avoided
2023                         if (! $mediaTypeOnly) {
2024                             if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
2025                                 if ($type) {
2026                                     array_unshift($parts, implode(' ', array_filter($type)));
2027                                 }
2029                                 if (! empty($parts)) {
2030                                     if (strlen($current)) {
2031                                         $current .= $this->formatter->tagSeparator;
2032                                     }
2034                                     $current .= implode(' and ', $parts);
2035                                 }
2037                                 if ($current) {
2038                                     $out[] = $start . $current;
2039                                 }
2041                                 $current = "";
2042                                 $type    = null;
2043                                 $parts   = [];
2044                             }
2045                         }
2047                         if ($newType === ['all'] && $default) {
2048                             $default = $start . 'all';
2049                         }
2051                         // all can be safely ignored and mixed with whatever else
2052                         if ($newType !== ['all']) {
2053                             if ($type) {
2054                                 $type = $this->mergeMediaTypes($type, $newType);
2056                                 if (empty($type)) {
2057                                     // merge failed : ignore this query that is not valid, skip to the next one
2058                                     $parts = [];
2059                                     $default = ''; // if everything fail, no @media at all
2060                                     continue 3;
2061                                 }
2062                             } else {
2063                                 $type = $newType;
2064                             }
2065                         }
2066                         break;
2068                     case Type::T_MEDIA_EXPRESSION:
2069                         if (isset($q[2])) {
2070                             $parts[] = '('
2071                                 . $this->compileValue($q[1])
2072                                 . $this->formatter->assignSeparator
2073                                 . $this->compileValue($q[2])
2074                                 . ')';
2075                         } else {
2076                             $parts[] = '('
2077                                 . $this->compileValue($q[1])
2078                                 . ')';
2079                         }
2080                         break;
2082                     case Type::T_MEDIA_VALUE:
2083                         $parts[] = $this->compileValue($q[1]);
2084                         break;
2085                 }
2086             }
2088             if ($type) {
2089                 array_unshift($parts, implode(' ', array_filter($type)));
2090             }
2092             if (! empty($parts)) {
2093                 if (strlen($current)) {
2094                     $current .= $this->formatter->tagSeparator;
2095                 }
2097                 $current .= implode(' and ', $parts);
2098             }
2099         }
2101         if ($current) {
2102             $out[] = $start . $current;
2103         }
2105         // no @media type except all, and no conflict?
2106         if (! $out && $default) {
2107             $out[] = $default;
2108         }
2110         return $out;
2111     }
2113     /**
2114      * Merge direct relationships between selectors
2115      *
2116      * @param array $selectors1
2117      * @param array $selectors2
2118      *
2119      * @return array
2120      */
2121     protected function mergeDirectRelationships($selectors1, $selectors2)
2122     {
2123         if (empty($selectors1) || empty($selectors2)) {
2124             return array_merge($selectors1, $selectors2);
2125         }
2127         $part1 = end($selectors1);
2128         $part2 = end($selectors2);
2130         if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2131             return array_merge($selectors1, $selectors2);
2132         }
2134         $merged = [];
2136         do {
2137             $part1 = array_pop($selectors1);
2138             $part2 = array_pop($selectors2);
2140             if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2141                 if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2142                     array_unshift($merged, [$part1[0] . $part2[0]]);
2143                     $merged = array_merge($selectors1, $selectors2, $merged);
2144                 } else {
2145                     $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2146                 }
2148                 break;
2149             }
2151             array_unshift($merged, $part1);
2152         } while (! empty($selectors1) && ! empty($selectors2));
2154         return $merged;
2155     }
2157     /**
2158      * Merge media types
2159      *
2160      * @param array $type1
2161      * @param array $type2
2162      *
2163      * @return array|null
2164      */
2165     protected function mergeMediaTypes($type1, $type2)
2166     {
2167         if (empty($type1)) {
2168             return $type2;
2169         }
2171         if (empty($type2)) {
2172             return $type1;
2173         }
2175         if (count($type1) > 1) {
2176             $m1 = strtolower($type1[0]);
2177             $t1 = strtolower($type1[1]);
2178         } else {
2179             $m1 = '';
2180             $t1 = strtolower($type1[0]);
2181         }
2183         if (count($type2) > 1) {
2184             $m2 = strtolower($type2[0]);
2185             $t2 = strtolower($type2[1]);
2186         } else {
2187             $m2 = '';
2188             $t2 = strtolower($type2[0]);
2189         }
2191         if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2192             if ($t1 === $t2) {
2193                 return null;
2194             }
2196             return [
2197                 $m1 === Type::T_NOT ? $m2 : $m1,
2198                 $m1 === Type::T_NOT ? $t2 : $t1,
2199             ];
2200         }
2202         if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2203             // CSS has no way of representing "neither screen nor print"
2204             if ($t1 !== $t2) {
2205                 return null;
2206             }
2208             return [Type::T_NOT, $t1];
2209         }
2211         if ($t1 !== $t2) {
2212             return null;
2213         }
2215         // t1 == t2, neither m1 nor m2 are "not"
2216         return [empty($m1)? $m2 : $m1, $t1];
2217     }
2219     /**
2220      * Compile import; returns true if the value was something that could be imported
2221      *
2222      * @param array                                  $rawPath
2223      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2224      * @param boolean                                $once
2225      *
2226      * @return boolean
2227      */
2228     protected function compileImport($rawPath, OutputBlock $out, $once = false)
2229     {
2230         if ($rawPath[0] === Type::T_STRING) {
2231             $path = $this->compileStringContent($rawPath);
2233             if ($path = $this->findImport($path)) {
2234                 if (! $once || ! in_array($path, $this->importedFiles)) {
2235                     $this->importFile($path, $out);
2236                     $this->importedFiles[] = $path;
2237                 }
2239                 return true;
2240             }
2242             $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
2244             return false;
2245         }
2247         if ($rawPath[0] === Type::T_LIST) {
2248             // handle a list of strings
2249             if (count($rawPath[2]) === 0) {
2250                 return false;
2251             }
2253             foreach ($rawPath[2] as $path) {
2254                 if ($path[0] !== Type::T_STRING) {
2255                     $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2257                     return false;
2258                 }
2259             }
2261             foreach ($rawPath[2] as $path) {
2262                 $this->compileImport($path, $out, $once);
2263             }
2265             return true;
2266         }
2268         $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2270         return false;
2271     }
2274     /**
2275      * Append a root directive like @import or @charset as near as the possible from the source code
2276      * (keeping before comments, @import and @charset coming before in the source code)
2277      *
2278      * @param string                                        $line
2279      * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2280      * @param array                                         $allowed
2281      */
2282     protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2283     {
2284         $root = $out;
2286         while ($root->parent) {
2287             $root = $root->parent;
2288         }
2290         $i = 0;
2292         while ($i < count($root->children)) {
2293             if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
2294                 break;
2295             }
2297             $i++;
2298         }
2300         // remove incompatible children from the bottom of the list
2301         $saveChildren = [];
2303         while ($i < count($root->children)) {
2304             $saveChildren[] = array_pop($root->children);
2305         }
2307         // insert the directive as a comment
2308         $child = $this->makeOutputBlock(Type::T_COMMENT);
2309         $child->lines[]      = $line;
2310         $child->sourceName   = $this->sourceNames[$this->sourceIndex];
2311         $child->sourceLine   = $this->sourceLine;
2312         $child->sourceColumn = $this->sourceColumn;
2314         $root->children[] = $child;
2316         // repush children
2317         while (count($saveChildren)) {
2318             $root->children[] = array_pop($saveChildren);
2319         }
2320     }
2322     /**
2323      * Append lines to the current output block:
2324      * directly to the block or through a child if necessary
2325      *
2326      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2327      * @param string                                 $type
2328      * @param string|mixed                           $line
2329      */
2330     protected function appendOutputLine(OutputBlock $out, $type, $line)
2331     {
2332         $outWrite = &$out;
2334         if ($type === Type::T_COMMENT) {
2335             $parent = $out->parent;
2337             if (end($parent->children) !== $out) {
2338                 $outWrite = &$parent->children[count($parent->children) - 1];
2339             }
2340         }
2342         // check if it's a flat output or not
2343         if (count($out->children)) {
2344             $lastChild = &$out->children[count($out->children) - 1];
2346             if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
2347                 $outWrite = $lastChild;
2348             } else {
2349                 $nextLines = $this->makeOutputBlock($type);
2350                 $nextLines->parent = $out;
2351                 $nextLines->depth  = $out->depth;
2353                 $out->children[] = $nextLines;
2354                 $outWrite = &$nextLines;
2355             }
2356         }
2358         $outWrite->lines[] = $line;
2359     }
2361     /**
2362      * Compile child; returns a value to halt execution
2363      *
2364      * @param array                                  $child
2365      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2366      *
2367      * @return array
2368      */
2369     protected function compileChild($child, OutputBlock $out)
2370     {
2371         if (isset($child[Parser::SOURCE_LINE])) {
2372             $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2373             $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2374             $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2375         } elseif (is_array($child) && isset($child[1]->sourceLine)) {
2376             $this->sourceIndex  = $child[1]->sourceIndex;
2377             $this->sourceLine   = $child[1]->sourceLine;
2378             $this->sourceColumn = $child[1]->sourceColumn;
2379         } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2380             $this->sourceLine   = $out->sourceLine;
2381             $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
2382             $this->sourceColumn = $out->sourceColumn;
2384             if ($this->sourceIndex === false) {
2385                 $this->sourceIndex = null;
2386             }
2387         }
2389         switch ($child[0]) {
2390             case Type::T_SCSSPHP_IMPORT_ONCE:
2391                 $rawPath = $this->reduce($child[1]);
2393                 $this->compileImport($rawPath, $out, true);
2394                 break;
2396             case Type::T_IMPORT:
2397                 $rawPath = $this->reduce($child[1]);
2399                 $this->compileImport($rawPath, $out);
2400                 break;
2402             case Type::T_DIRECTIVE:
2403                 $this->compileDirective($child[1], $out);
2404                 break;
2406             case Type::T_AT_ROOT:
2407                 $this->compileAtRoot($child[1]);
2408                 break;
2410             case Type::T_MEDIA:
2411                 $this->compileMedia($child[1]);
2412                 break;
2414             case Type::T_BLOCK:
2415                 $this->compileBlock($child[1]);
2416                 break;
2418             case Type::T_CHARSET:
2419                 if (! $this->charsetSeen) {
2420                     $this->charsetSeen = true;
2421                     $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2422                 }
2423                 break;
2425             case Type::T_ASSIGN:
2426                 list(, $name, $value) = $child;
2428                 if ($name[0] === Type::T_VARIABLE) {
2429                     $flags     = isset($child[3]) ? $child[3] : [];
2430                     $isDefault = in_array('!default', $flags);
2431                     $isGlobal  = in_array('!global', $flags);
2433                     if ($isGlobal) {
2434                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2435                         break;
2436                     }
2438                     $shouldSet = $isDefault &&
2439                         (is_null($result = $this->get($name[1], false)) ||
2440                         $result === static::$null);
2442                     if (! $isDefault || $shouldSet) {
2443                         $this->set($name[1], $this->reduce($value), true, null, $value);
2444                     }
2445                     break;
2446                 }
2448                 $compiledName = $this->compileValue($name);
2450                 // handle shorthand syntaxes : size / line-height...
2451                 if (in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2452                     if ($value[0] === Type::T_VARIABLE) {
2453                         // if the font value comes from variable, the content is already reduced
2454                         // (i.e., formulas were already calculated), so we need the original unreduced value
2455                         $value = $this->get($value[1], true, null, true);
2456                     }
2458                     $shorthandValue=&$value;
2460                     $shorthandDividerNeedsUnit = false;
2461                     $maxListElements           = null;
2462                     $maxShorthandDividers      = 1;
2464                     switch ($compiledName) {
2465                         case 'border-radius':
2466                             $maxListElements = 4;
2467                             $shorthandDividerNeedsUnit = true;
2468                             break;
2469                     }
2471                     if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
2472                         // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2473                         // we need to handle the first list element
2474                         $shorthandValue=&$value[2][0];
2475                     }
2477                     if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2478                         $revert = true;
2480                         if ($shorthandDividerNeedsUnit) {
2481                             $divider = $shorthandValue[3];
2483                             if (is_array($divider)) {
2484                                 $divider = $this->reduce($divider, true);
2485                             }
2487                             if (intval($divider->dimension) and !count($divider->units)) {
2488                                 $revert = false;
2489                             }
2490                         }
2492                         if ($revert) {
2493                             $shorthandValue = $this->expToString($shorthandValue);
2494                         }
2495                     } elseif ($shorthandValue[0] === Type::T_LIST) {
2496                         foreach ($shorthandValue[2] as &$item) {
2497                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2498                                 if ($maxShorthandDividers > 0) {
2499                                     $revert = true;
2500                                     // if the list of values is too long, this has to be a shorthand,
2501                                     // otherwise it could be a real division
2502                                     if (is_null($maxListElements) or count($shorthandValue[2]) <= $maxListElements) {
2503                                         if ($shorthandDividerNeedsUnit) {
2504                                             $divider = $item[3];
2506                                             if (is_array($divider)) {
2507                                                 $divider = $this->reduce($divider, true);
2508                                             }
2510                                             if (intval($divider->dimension) and !count($divider->units)) {
2511                                                 $revert = false;
2512                                             }
2513                                         }
2514                                     }
2516                                     if ($revert) {
2517                                         $item = $this->expToString($item);
2518                                         $maxShorthandDividers--;
2519                                     }
2520                                 }
2521                             }
2522                         }
2523                     }
2524                 }
2526                 // if the value reduces to null from something else then
2527                 // the property should be discarded
2528                 if ($value[0] !== Type::T_NULL) {
2529                     $value = $this->reduce($value);
2531                     if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2532                         break;
2533                     }
2534                 }
2536                 $compiledValue = $this->compileValue($value);
2538                 $line = $this->formatter->property(
2539                     $compiledName,
2540                     $compiledValue
2541                 );
2542                 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2543                 break;
2545             case Type::T_COMMENT:
2546                 if ($out->type === Type::T_ROOT) {
2547                     $this->compileComment($child);
2548                     break;
2549                 }
2551                 $line = $this->compileCommentValue($child, true);
2552                 $this->appendOutputLine($out, Type::T_COMMENT, $line);
2553                 break;
2555             case Type::T_MIXIN:
2556             case Type::T_FUNCTION:
2557                 list(, $block) = $child;
2558                 // the block need to be able to go up to it's parent env to resolve vars
2559                 $block->parentEnv = $this->getStoreEnv();
2560                 $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2561                 break;
2563             case Type::T_EXTEND:
2564                 foreach ($child[1] as $sel) {
2565                     $results = $this->evalSelectors([$sel]);
2567                     foreach ($results as $result) {
2568                         // only use the first one
2569                         $result = current($result);
2570                         $selectors = $out->selectors;
2572                         if (! $selectors && isset($child['selfParent'])) {
2573                             $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
2574                         }
2576                         $this->pushExtends($result, $selectors, $child);
2577                     }
2578                 }
2579                 break;
2581             case Type::T_IF:
2582                 list(, $if) = $child;
2584                 if ($this->isTruthy($this->reduce($if->cond, true))) {
2585                     return $this->compileChildren($if->children, $out);
2586                 }
2588                 foreach ($if->cases as $case) {
2589                     if ($case->type === Type::T_ELSE ||
2590                         $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2591                     ) {
2592                         return $this->compileChildren($case->children, $out);
2593                     }
2594                 }
2595                 break;
2597             case Type::T_EACH:
2598                 list(, $each) = $child;
2600                 $list = $this->coerceList($this->reduce($each->list));
2602                 $this->pushEnv();
2604                 foreach ($list[2] as $item) {
2605                     if (count($each->vars) === 1) {
2606                         $this->set($each->vars[0], $item, true);
2607                     } else {
2608                         list(,, $values) = $this->coerceList($item);
2610                         foreach ($each->vars as $i => $var) {
2611                             $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
2612                         }
2613                     }
2615                     $ret = $this->compileChildren($each->children, $out);
2617                     if ($ret) {
2618                         if ($ret[0] !== Type::T_CONTROL) {
2619                             $this->popEnv();
2621                             return $ret;
2622                         }
2624                         if ($ret[1]) {
2625                             break;
2626                         }
2627                     }
2628                 }
2630                 $this->popEnv();
2631                 break;
2633             case Type::T_WHILE:
2634                 list(, $while) = $child;
2636                 while ($this->isTruthy($this->reduce($while->cond, true))) {
2637                     $ret = $this->compileChildren($while->children, $out);
2639                     if ($ret) {
2640                         if ($ret[0] !== Type::T_CONTROL) {
2641                             return $ret;
2642                         }
2644                         if ($ret[1]) {
2645                             break;
2646                         }
2647                     }
2648                 }
2649                 break;
2651             case Type::T_FOR:
2652                 list(, $for) = $child;
2654                 $start = $this->reduce($for->start, true);
2655                 $end   = $this->reduce($for->end, true);
2657                 if (! ($start[2] == $end[2] || $end->unitless())) {
2658                     $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
2660                     break;
2661                 }
2663                 $unit  = $start[2];
2664                 $start = $start[1];
2665                 $end   = $end[1];
2667                 $d = $start < $end ? 1 : -1;
2669                 for (;;) {
2670                     if ((! $for->until && $start - $d == $end) ||
2671                         ($for->until && $start == $end)
2672                     ) {
2673                         break;
2674                     }
2676                     $this->set($for->var, new Node\Number($start, $unit));
2677                     $start += $d;
2679                     $ret = $this->compileChildren($for->children, $out);
2681                     if ($ret) {
2682                         if ($ret[0] !== Type::T_CONTROL) {
2683                             return $ret;
2684                         }
2686                         if ($ret[1]) {
2687                             break;
2688                         }
2689                     }
2690                 }
2691                 break;
2693             case Type::T_BREAK:
2694                 return [Type::T_CONTROL, true];
2696             case Type::T_CONTINUE:
2697                 return [Type::T_CONTROL, false];
2699             case Type::T_RETURN:
2700                 return $this->reduce($child[1], true);
2702             case Type::T_NESTED_PROPERTY:
2703                 $this->compileNestedPropertiesBlock($child[1], $out);
2704                 break;
2706             case Type::T_INCLUDE:
2707                 // including a mixin
2708                 list(, $name, $argValues, $content, $argUsing) = $child;
2710                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2712                 if (! $mixin) {
2713                     $this->throwError("Undefined mixin $name");
2714                     break;
2715                 }
2717                 $callingScope = $this->getStoreEnv();
2719                 // push scope, apply args
2720                 $this->pushEnv();
2721                 $this->env->depth--;
2723                 $storeEnv = $this->storeEnv;
2724                 $this->storeEnv = $this->env;
2726                 // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2727                 // and assign this fake parent to childs
2728                 $selfParent = null;
2730                 if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2731                     $selfParent = $child['selfParent'];
2732                 } else {
2733                     $parentSelectors = $this->multiplySelectors($this->env);
2735                     if ($parentSelectors) {
2736                         $parent = new Block();
2737                         $parent->selectors = $parentSelectors;
2739                         foreach ($mixin->children as $k => $child) {
2740                             if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
2741                                 $mixin->children[$k][1]->parent = $parent;
2742                             }
2743                         }
2744                     }
2745                 }
2747                 // clone the stored content to not have its scope spoiled by a further call to the same mixin
2748                 // i.e., recursive @include of the same mixin
2749                 if (isset($content)) {
2750                     $copyContent = clone $content;
2751                     $copyContent->scope = clone $callingScope;
2753                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2754                 } else {
2755                     $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2756                 }
2758                 // save the "using" argument list for applying it to when "@content" is invoked
2759                 if (isset($argUsing)) {
2760                     $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
2761                 } else {
2762                     $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
2763                 }
2765                 if (isset($mixin->args)) {
2766                     $this->applyArguments($mixin->args, $argValues);
2767                 }
2769                 $this->env->marker = 'mixin';
2771                 if (! empty($mixin->parentEnv)) {
2772                     $this->env->declarationScopeParent = $mixin->parentEnv;
2773                 } else {
2774                     $this->throwError("@mixin $name() without parentEnv");
2775                 }
2777                 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
2779                 $this->storeEnv = $storeEnv;
2781                 $this->popEnv();
2782                 break;
2784             case Type::T_MIXIN_CONTENT:
2785                 $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2786                 $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
2787                 $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
2788                 $argContent = $child[1];
2790                 if (! $content) {
2791                     $content = new \stdClass();
2792                     $content->scope    = new \stdClass();
2793                     $content->children = $env->parent->block->children;
2794                     break;
2795                 }
2797                 $storeEnv = $this->storeEnv;
2798                 $varsUsing = [];
2800                 if (isset($argUsing) && isset($argContent)) {
2801                     // Get the arguments provided for the content with the names provided in the "using" argument list
2802                     $this->storeEnv = $this->env;
2803                     $varsUsing = $this->applyArguments($argUsing, $argContent, false);
2804                 }
2806                 // restore the scope from the @content
2807                 $this->storeEnv = $content->scope;
2809                 // append the vars from using if any
2810                 foreach ($varsUsing as $name => $val) {
2811                     $this->set($name, $val, true, $this->storeEnv);
2812                 }
2814                 $this->compileChildrenNoReturn($content->children, $out);
2816                 $this->storeEnv = $storeEnv;
2817                 break;
2819             case Type::T_DEBUG:
2820                 list(, $value) = $child;
2822                 $fname = $this->sourceNames[$this->sourceIndex];
2823                 $line  = $this->sourceLine;
2824                 $value = $this->compileValue($this->reduce($value, true));
2826                 fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
2827                 break;
2829             case Type::T_WARN:
2830                 list(, $value) = $child;
2832                 $fname = $this->sourceNames[$this->sourceIndex];
2833                 $line  = $this->sourceLine;
2834                 $value = $this->compileValue($this->reduce($value, true));
2836                 fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
2837                 break;
2839             case Type::T_ERROR:
2840                 list(, $value) = $child;
2842                 $fname = $this->sourceNames[$this->sourceIndex];
2843                 $line  = $this->sourceLine;
2844                 $value = $this->compileValue($this->reduce($value, true));
2846                 $this->throwError("File $fname on line $line ERROR: $value\n");
2847                 break;
2849             case Type::T_CONTROL:
2850                 $this->throwError('@break/@continue not permitted in this scope');
2851                 break;
2853             default:
2854                 $this->throwError("unknown child type: $child[0]");
2855         }
2856     }
2858     /**
2859      * Reduce expression to string
2860      *
2861      * @param array $exp
2862      *
2863      * @return array
2864      */
2865     protected function expToString($exp)
2866     {
2867         list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
2869         $content = [$this->reduce($left)];
2871         if ($whiteLeft) {
2872             $content[] = ' ';
2873         }
2875         $content[] = $op;
2877         if ($whiteRight) {
2878             $content[] = ' ';
2879         }
2881         $content[] = $this->reduce($right);
2883         return [Type::T_STRING, '', $content];
2884     }
2886     /**
2887      * Is truthy?
2888      *
2889      * @param array $value
2890      *
2891      * @return boolean
2892      */
2893     protected function isTruthy($value)
2894     {
2895         return $value !== static::$false && $value !== static::$null;
2896     }
2898     /**
2899      * Is the value a direct relationship combinator?
2900      *
2901      * @param string $value
2902      *
2903      * @return boolean
2904      */
2905     protected function isImmediateRelationshipCombinator($value)
2906     {
2907         return $value === '>' || $value === '+' || $value === '~';
2908     }
2910     /**
2911      * Should $value cause its operand to eval
2912      *
2913      * @param array $value
2914      *
2915      * @return boolean
2916      */
2917     protected function shouldEval($value)
2918     {
2919         switch ($value[0]) {
2920             case Type::T_EXPRESSION:
2921                 if ($value[1] === '/') {
2922                     return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
2923                 }
2925                 // fall-thru
2926             case Type::T_VARIABLE:
2927             case Type::T_FUNCTION_CALL:
2928                 return true;
2929         }
2931         return false;
2932     }
2934     /**
2935      * Reduce value
2936      *
2937      * @param array   $value
2938      * @param boolean $inExp
2939      *
2940      * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
2941      */
2942     protected function reduce($value, $inExp = false)
2943     {
2944         if (is_null($value)) {
2945             return null;
2946         }
2948         switch ($value[0]) {
2949             case Type::T_EXPRESSION:
2950                 list(, $op, $left, $right, $inParens) = $value;
2952                 $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
2953                 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
2955                 $left = $this->reduce($left, true);
2957                 if ($op !== 'and' && $op !== 'or') {
2958                     $right = $this->reduce($right, true);
2959                 }
2961                 // special case: looks like css shorthand
2962                 if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
2963                     (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
2964                     ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2965                 ) {
2966                     return $this->expToString($value);
2967                 }
2969                 $left  = $this->coerceForExpression($left);
2970                 $right = $this->coerceForExpression($right);
2971                 $ltype = $left[0];
2972                 $rtype = $right[0];
2974                 $ucOpName = ucfirst($opName);
2975                 $ucLType  = ucfirst($ltype);
2976                 $ucRType  = ucfirst($rtype);
2978                 // this tries:
2979                 // 1. op[op name][left type][right type]
2980                 // 2. op[left type][right type] (passing the op as first arg
2981                 // 3. op[op name]
2982                 $fn = "op${ucOpName}${ucLType}${ucRType}";
2984                 if (is_callable([$this, $fn]) ||
2985                     (($fn = "op${ucLType}${ucRType}") &&
2986                         is_callable([$this, $fn]) &&
2987                         $passOp = true) ||
2988                     (($fn = "op${ucOpName}") &&
2989                         is_callable([$this, $fn]) &&
2990                         $genOp = true)
2991                 ) {
2992                     $coerceUnit = false;
2994                     if (! isset($genOp) &&
2995                         $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
2996                     ) {
2997                         $coerceUnit = true;
2999                         switch ($opName) {
3000                             case 'mul':
3001                                 $targetUnit = $left[2];
3003                                 foreach ($right[2] as $unit => $exp) {
3004                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
3005                                 }
3006                                 break;
3008                             case 'div':
3009                                 $targetUnit = $left[2];
3011                                 foreach ($right[2] as $unit => $exp) {
3012                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
3013                                 }
3014                                 break;
3016                             case 'mod':
3017                                 $targetUnit = $left[2];
3018                                 break;
3020                             default:
3021                                 $targetUnit = $left->unitless() ? $right[2] : $left[2];
3022                         }
3024                         if (! $left->unitless() && ! $right->unitless()) {
3025                             $left = $left->normalize();
3026                             $right = $right->normalize();
3027                         }
3028                     }
3030                     $shouldEval = $inParens || $inExp;
3032                     if (isset($passOp)) {
3033                         $out = $this->$fn($op, $left, $right, $shouldEval);
3034                     } else {
3035                         $out = $this->$fn($left, $right, $shouldEval);
3036                     }
3038                     if (isset($out)) {
3039                         if ($coerceUnit && $out[0] === Type::T_NUMBER) {
3040                             $out = $out->coerce($targetUnit);
3041                         }
3043                         return $out;
3044                     }
3045                 }
3047                 return $this->expToString($value);
3049             case Type::T_UNARY:
3050                 list(, $op, $exp, $inParens) = $value;
3052                 $inExp = $inExp || $this->shouldEval($exp);
3053                 $exp = $this->reduce($exp);
3055                 if ($exp[0] === Type::T_NUMBER) {
3056                     switch ($op) {
3057                         case '+':
3058                             return new Node\Number($exp[1], $exp[2]);
3060                         case '-':
3061                             return new Node\Number(-$exp[1], $exp[2]);
3062                     }
3063                 }
3065                 if ($op === 'not') {
3066                     if ($inExp || $inParens) {
3067                         if ($exp === static::$false || $exp === static::$null) {
3068                             return static::$true;
3069                         }
3071                         return static::$false;
3072                     }
3074                     $op = $op . ' ';
3075                 }
3077                 return [Type::T_STRING, '', [$op, $exp]];
3079             case Type::T_VARIABLE:
3080                 return $this->reduce($this->get($value[1]));
3082             case Type::T_LIST:
3083                 foreach ($value[2] as &$item) {
3084                     $item = $this->reduce($item);
3085                 }
3087                 return $value;
3089             case Type::T_MAP:
3090                 foreach ($value[1] as &$item) {
3091                     $item = $this->reduce($item);
3092                 }
3094                 foreach ($value[2] as &$item) {
3095                     $item = $this->reduce($item);
3096                 }
3098                 return $value;
3100             case Type::T_STRING:
3101                 foreach ($value[2] as &$item) {
3102                     if (is_array($item) || $item instanceof \ArrayAccess) {
3103                         $item = $this->reduce($item);
3104                     }
3105                 }
3107                 return $value;
3109             case Type::T_INTERPOLATE:
3110                 $value[1] = $this->reduce($value[1]);
3112                 if ($inExp) {
3113                     return $value[1];
3114                 }
3116                 return $value;
3118             case Type::T_FUNCTION_CALL:
3119                 return $this->fncall($value[1], $value[2]);
3121             case Type::T_SELF:
3122                 $selfSelector = $this->multiplySelectors($this->env);
3123                 $selfSelector = $this->collapseSelectors($selfSelector, true);
3125                 return $selfSelector;
3127             default:
3128                 return $value;
3129         }
3130     }
3132     /**
3133      * Function caller
3134      *
3135      * @param string $name
3136      * @param array  $argValues
3137      *
3138      * @return array|null
3139      */
3140     protected function fncall($name, $argValues)
3141     {
3142         // SCSS @function
3143         if ($this->callScssFunction($name, $argValues, $returnValue)) {
3144             return $returnValue;
3145         }
3147         // native PHP functions
3148         if ($this->callNativeFunction($name, $argValues, $returnValue)) {
3149             return $returnValue;
3150         }
3152         // for CSS functions, simply flatten the arguments into a list
3153         $listArgs = [];
3155         foreach ((array) $argValues as $arg) {
3156             if (empty($arg[0])) {
3157                 $listArgs[] = $this->reduce($arg[1]);
3158             }
3159         }
3161         return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
3162     }
3164     /**
3165      * Normalize name
3166      *
3167      * @param string $name
3168      *
3169      * @return string
3170      */
3171     protected function normalizeName($name)
3172     {
3173         return str_replace('-', '_', $name);
3174     }
3176     /**
3177      * Normalize value
3178      *
3179      * @param array $value
3180      *
3181      * @return array
3182      */
3183     public function normalizeValue($value)
3184     {
3185         $value = $this->coerceForExpression($this->reduce($value));
3187         switch ($value[0]) {
3188             case Type::T_LIST:
3189                 $value = $this->extractInterpolation($value);
3191                 if ($value[0] !== Type::T_LIST) {
3192                     return [Type::T_KEYWORD, $this->compileValue($value)];
3193                 }
3195                 foreach ($value[2] as $key => $item) {
3196                     $value[2][$key] = $this->normalizeValue($item);
3197                 }
3199                 if (! empty($value['enclosing'])) {
3200                     unset($value['enclosing']);
3201                 }
3203                 return $value;
3205             case Type::T_STRING:
3206                 return [$value[0], '"', [$this->compileStringContent($value)]];
3208             case Type::T_NUMBER:
3209                 return $value->normalize();
3211             case Type::T_INTERPOLATE:
3212                 return [Type::T_KEYWORD, $this->compileValue($value)];
3214             default:
3215                 return $value;
3216         }
3217     }
3219     /**
3220      * Add numbers
3221      *
3222      * @param array $left
3223      * @param array $right
3224      *
3225      * @return \ScssPhp\ScssPhp\Node\Number
3226      */
3227     protected function opAddNumberNumber($left, $right)
3228     {
3229         return new Node\Number($left[1] + $right[1], $left[2]);
3230     }
3232     /**
3233      * Multiply numbers
3234      *
3235      * @param array $left
3236      * @param array $right
3237      *
3238      * @return \ScssPhp\ScssPhp\Node\Number
3239      */
3240     protected function opMulNumberNumber($left, $right)
3241     {
3242         return new Node\Number($left[1] * $right[1], $left[2]);
3243     }
3245     /**
3246      * Subtract numbers
3247      *
3248      * @param array $left
3249      * @param array $right
3250      *
3251      * @return \ScssPhp\ScssPhp\Node\Number
3252      */
3253     protected function opSubNumberNumber($left, $right)
3254     {
3255         return new Node\Number($left[1] - $right[1], $left[2]);
3256     }
3258     /**
3259      * Divide numbers
3260      *
3261      * @param array $left
3262      * @param array $right
3263      *
3264      * @return array|\ScssPhp\ScssPhp\Node\Number
3265      */
3266     protected function opDivNumberNumber($left, $right)
3267     {
3268         if ($right[1] == 0) {
3269             return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
3270         }
3272         return new Node\Number($left[1] / $right[1], $left[2]);
3273     }
3275     /**
3276      * Mod numbers
3277      *
3278      * @param array $left
3279      * @param array $right
3280      *
3281      * @return \ScssPhp\ScssPhp\Node\Number
3282      */
3283     protected function opModNumberNumber($left, $right)
3284     {
3285         return new Node\Number($left[1] % $right[1], $left[2]);
3286     }
3288     /**
3289      * Add strings
3290      *
3291      * @param array $left
3292      * @param array $right
3293      *
3294      * @return array|null
3295      */
3296     protected function opAdd($left, $right)
3297     {
3298         if ($strLeft = $this->coerceString($left)) {
3299             if ($right[0] === Type::T_STRING) {
3300                 $right[1] = '';
3301             }
3303             $strLeft[2][] = $right;
3305             return $strLeft;
3306         }
3308         if ($strRight = $this->coerceString($right)) {
3309             if ($left[0] === Type::T_STRING) {
3310                 $left[1] = '';
3311             }
3313             array_unshift($strRight[2], $left);
3315             return $strRight;
3316         }
3318         return null;
3319     }
3321     /**
3322      * Boolean and
3323      *
3324      * @param array   $left
3325      * @param array   $right
3326      * @param boolean $shouldEval
3327      *
3328      * @return array|null
3329      */
3330     protected function opAnd($left, $right, $shouldEval)
3331     {
3332         $truthy = ($left === static::$null || $right === static::$null) ||
3333                   ($left === static::$false || $left === static::$true) &&
3334                   ($right === static::$false || $right === static::$true);
3336         if (! $shouldEval) {
3337             if (! $truthy) {
3338                 return null;
3339             }
3340         }
3342         if ($left !== static::$false && $left !== static::$null) {
3343             return $this->reduce($right, true);
3344         }
3346         return $left;
3347     }
3349     /**
3350      * Boolean or
3351      *
3352      * @param array   $left
3353      * @param array   $right
3354      * @param boolean $shouldEval
3355      *
3356      * @return array|null
3357      */
3358     protected function opOr($left, $right, $shouldEval)
3359     {
3360         $truthy = ($left === static::$null || $right === static::$null) ||
3361                   ($left === static::$false || $left === static::$true) &&
3362                   ($right === static::$false || $right === static::$true);
3364         if (! $shouldEval) {
3365             if (! $truthy) {
3366                 return null;
3367             }
3368         }
3370         if ($left !== static::$false && $left !== static::$null) {
3371             return $left;
3372         }
3374         return $this->reduce($right, true);
3375     }
3377     /**
3378      * Compare colors
3379      *
3380      * @param string $op
3381      * @param array  $left
3382      * @param array  $right
3383      *
3384      * @return array
3385      */
3386     protected function opColorColor($op, $left, $right)
3387     {
3388         $out = [Type::T_COLOR];
3390         foreach ([1, 2, 3] as $i) {
3391             $lval = isset($left[$i]) ? $left[$i] : 0;
3392             $rval = isset($right[$i]) ? $right[$i] : 0;
3394             switch ($op) {
3395                 case '+':
3396                     $out[] = $lval + $rval;
3397                     break;
3399                 case '-':
3400                     $out[] = $lval - $rval;
3401                     break;
3403                 case '*':
3404                     $out[] = $lval * $rval;
3405                     break;
3407                 case '%':
3408                     $out[] = $lval % $rval;
3409                     break;
3411                 case '/':
3412                     if ($rval == 0) {
3413                         $this->throwError("color: Can't divide by zero");
3414                         break 2;
3415                     }
3417                     $out[] = (int) ($lval / $rval);
3418                     break;
3420                 case '==':