source: spip-zone/_plugins_/scssphp/trunk/lib/scssphp/src/Compiler.php @ 119818

Last change on this file since 119818 was 119818, checked in by cedric@…, 3 months ago

Mise a jour de la lib ScssPHP (corrige un bug de compilation avec BootStrap? 4.4)

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