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

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

Mise a jour de SCSSPhp en version 1.0.6

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