1<?php
2/**
3 * SCSSPHP
4 *
5 * @copyright 2012-2020 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    protected static $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    protected static $namespaces = [
97        'special'  => '%',
98        'mixin'    => '@',
99        'function' => '^',
100    ];
101
102    public static $true         = [Type::T_KEYWORD, 'true'];
103    public static $false        = [Type::T_KEYWORD, 'false'];
104    public static $NaN          = [Type::T_KEYWORD, 'NaN'];
105    public static $Infinity     = [Type::T_KEYWORD, 'Infinity'];
106    public static $null         = [Type::T_NULL];
107    public static $nullString   = [Type::T_STRING, '', []];
108    public static $defaultValue = [Type::T_KEYWORD, ''];
109    public static $selfSelector = [Type::T_SELF];
110    public static $emptyList    = [Type::T_LIST, '', []];
111    public static $emptyMap     = [Type::T_MAP, [], []];
112    public static $emptyString  = [Type::T_STRING, '"', []];
113    public static $with         = [Type::T_KEYWORD, 'with'];
114    public static $without      = [Type::T_KEYWORD, 'without'];
115
116    protected $importPaths = [''];
117    protected $importCache = [];
118    protected $importedFiles = [];
119    protected $userFunctions = [];
120    protected $registeredVars = [];
121    protected $registeredFeatures = [
122        'extend-selector-pseudoclass' => false,
123        'at-error'                    => true,
124        'units-level-3'               => false,
125        'global-variable-shadowing'   => false,
126    ];
127
128    protected $encoding = null;
129    protected $lineNumberStyle = null;
130
131    protected $sourceMap = self::SOURCE_MAP_NONE;
132    protected $sourceMapOptions = [];
133
134    /**
135     * @var string|\ScssPhp\ScssPhp\Formatter
136     */
137    protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
138
139    protected $rootEnv;
140    protected $rootBlock;
141
142    /**
143     * @var \ScssPhp\ScssPhp\Compiler\Environment
144     */
145    protected $env;
146    protected $scope;
147    protected $storeEnv;
148    protected $charsetSeen;
149    protected $sourceNames;
150
151    protected $cache;
152
153    protected $indentLevel;
154    protected $extends;
155    protected $extendsMap;
156    protected $parsedFiles;
157    protected $parser;
158    protected $sourceIndex;
159    protected $sourceLine;
160    protected $sourceColumn;
161    protected $stderr;
162    protected $shouldEvaluate;
163    protected $ignoreErrors;
164    protected $ignoreCallStackMessage = false;
165
166    protected $callStack = [];
167
168    /**
169     * Constructor
170     *
171     * @param array|null $cacheOptions
172     */
173    public function __construct($cacheOptions = null)
174    {
175        $this->parsedFiles = [];
176        $this->sourceNames = [];
177
178        if ($cacheOptions) {
179            $this->cache = new Cache($cacheOptions);
180        }
181
182        $this->stderr = fopen('php://stderr', 'w');
183    }
184
185    /**
186     * Get compiler options
187     *
188     * @return array
189     */
190    public function getCompileOptions()
191    {
192        $options = [
193            'importPaths'        => $this->importPaths,
194            'registeredVars'     => $this->registeredVars,
195            'registeredFeatures' => $this->registeredFeatures,
196            'encoding'           => $this->encoding,
197            'sourceMap'          => serialize($this->sourceMap),
198            'sourceMapOptions'   => $this->sourceMapOptions,
199            'formatter'          => $this->formatter,
200        ];
201
202        return $options;
203    }
204
205    /**
206     * Set an alternative error output stream, for testing purpose only
207     *
208     * @param resource $handle
209     */
210    public function setErrorOuput($handle)
211    {
212        $this->stderr = $handle;
213    }
214
215    /**
216     * Compile scss
217     *
218     * @api
219     *
220     * @param string $code
221     * @param string $path
222     *
223     * @return string
224     */
225    public function compile($code, $path = null)
226    {
227        if ($this->cache) {
228            $cacheKey       = ($path ? $path : "(stdin)") . ":" . md5($code);
229            $compileOptions = $this->getCompileOptions();
230            $cache          = $this->cache->getCache("compile", $cacheKey, $compileOptions);
231
232            if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
233                // check if any dependency file changed before accepting the cache
234                foreach ($cache['dependencies'] as $file => $mtime) {
235                    if (! is_file($file) || filemtime($file) !== $mtime) {
236                        unset($cache);
237                        break;
238                    }
239                }
240
241                if (isset($cache)) {
242                    return $cache['out'];
243                }
244            }
245        }
246
247
248        $this->indentLevel    = -1;
249        $this->extends        = [];
250        $this->extendsMap     = [];
251        $this->sourceIndex    = null;
252        $this->sourceLine     = null;
253        $this->sourceColumn   = null;
254        $this->env            = null;
255        $this->scope          = null;
256        $this->storeEnv       = null;
257        $this->charsetSeen    = null;
258        $this->shouldEvaluate = null;
259        $this->ignoreCallStackMessage = false;
260
261        $this->parser = $this->parserFactory($path);
262        $tree         = $this->parser->parse($code);
263        $this->parser = null;
264
265        $this->formatter = new $this->formatter();
266        $this->rootBlock = null;
267        $this->rootEnv   = $this->pushEnv($tree);
268
269        $this->injectVariables($this->registeredVars);
270        $this->compileRoot($tree);
271        $this->popEnv();
272
273        $sourceMapGenerator = null;
274
275        if ($this->sourceMap) {
276            if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
277                $sourceMapGenerator = $this->sourceMap;
278                $this->sourceMap = self::SOURCE_MAP_FILE;
279            } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
280                $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
281            }
282        }
283
284        $out = $this->formatter->format($this->scope, $sourceMapGenerator);
285
286        if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
287            $sourceMap    = $sourceMapGenerator->generateJson();
288            $sourceMapUrl = null;
289
290            switch ($this->sourceMap) {
291                case self::SOURCE_MAP_INLINE:
292                    $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
293                    break;
294
295                case self::SOURCE_MAP_FILE:
296                    $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
297                    break;
298            }
299
300            $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
301        }
302
303        if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
304            $v = [
305                'dependencies' => $this->getParsedFiles(),
306                'out' => &$out,
307            ];
308
309            $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
310        }
311
312        return $out;
313    }
314
315    /**
316     * Instantiate parser
317     *
318     * @param string $path
319     *
320     * @return \ScssPhp\ScssPhp\Parser
321     */
322    protected function parserFactory($path)
323    {
324        // https://sass-lang.com/documentation/at-rules/import
325        // CSS files imported by Sass don’t allow any special Sass features.
326        // In order to make sure authors don’t accidentally write Sass in their CSS,
327        // all Sass features that aren’t also valid CSS will produce errors.
328        // Otherwise, the CSS will be rendered as-is. It can even be extended!
329        $cssOnly = false;
330
331        if (substr($path, '-4') === '.css') {
332            $cssOnly = true;
333        }
334
335        $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
336
337        $this->sourceNames[] = $path;
338        $this->addParsedFile($path);
339
340        return $parser;
341    }
342
343    /**
344     * Is self extend?
345     *
346     * @param array $target
347     * @param array $origin
348     *
349     * @return boolean
350     */
351    protected function isSelfExtend($target, $origin)
352    {
353        foreach ($origin as $sel) {
354            if (\in_array($target, $sel)) {
355                return true;
356            }
357        }
358
359        return false;
360    }
361
362    /**
363     * Push extends
364     *
365     * @param array      $target
366     * @param array      $origin
367     * @param array|null $block
368     */
369    protected function pushExtends($target, $origin, $block)
370    {
371        $i = \count($this->extends);
372        $this->extends[] = [$target, $origin, $block];
373
374        foreach ($target as $part) {
375            if (isset($this->extendsMap[$part])) {
376                $this->extendsMap[$part][] = $i;
377            } else {
378                $this->extendsMap[$part] = [$i];
379            }
380        }
381    }
382
383    /**
384     * Make output block
385     *
386     * @param string $type
387     * @param array  $selectors
388     *
389     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
390     */
391    protected function makeOutputBlock($type, $selectors = null)
392    {
393        $out = new OutputBlock;
394        $out->type      = $type;
395        $out->lines     = [];
396        $out->children  = [];
397        $out->parent    = $this->scope;
398        $out->selectors = $selectors;
399        $out->depth     = $this->env->depth;
400
401        if ($this->env->block instanceof Block) {
402            $out->sourceName   = $this->env->block->sourceName;
403            $out->sourceLine   = $this->env->block->sourceLine;
404            $out->sourceColumn = $this->env->block->sourceColumn;
405        } else {
406            $out->sourceName   = null;
407            $out->sourceLine   = null;
408            $out->sourceColumn = null;
409        }
410
411        return $out;
412    }
413
414    /**
415     * Compile root
416     *
417     * @param \ScssPhp\ScssPhp\Block $rootBlock
418     */
419    protected function compileRoot(Block $rootBlock)
420    {
421        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
422
423        $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
424        $this->flattenSelectors($this->scope);
425        $this->missingSelectors();
426    }
427
428    /**
429     * Report missing selectors
430     */
431    protected function missingSelectors()
432    {
433        foreach ($this->extends as $extend) {
434            if (isset($extend[3])) {
435                continue;
436            }
437
438            list($target, $origin, $block) = $extend;
439
440            // ignore if !optional
441            if ($block[2]) {
442                continue;
443            }
444
445            $target = implode(' ', $target);
446            $origin = $this->collapseSelectors($origin);
447
448            $this->sourceLine = $block[Parser::SOURCE_LINE];
449            $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
450        }
451    }
452
453    /**
454     * Flatten selectors
455     *
456     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
457     * @param string                                 $parentKey
458     */
459    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
460    {
461        if ($block->selectors) {
462            $selectors = [];
463
464            foreach ($block->selectors as $s) {
465                $selectors[] = $s;
466
467                if (! \is_array($s)) {
468                    continue;
469                }
470
471                // check extends
472                if (! empty($this->extendsMap)) {
473                    $this->matchExtends($s, $selectors);
474
475                    // remove duplicates
476                    array_walk($selectors, function (&$value) {
477                        $value = serialize($value);
478                    });
479
480                    $selectors = array_unique($selectors);
481
482                    array_walk($selectors, function (&$value) {
483                        $value = unserialize($value);
484                    });
485                }
486            }
487
488            $block->selectors = [];
489            $placeholderSelector = false;
490
491            foreach ($selectors as $selector) {
492                if ($this->hasSelectorPlaceholder($selector)) {
493                    $placeholderSelector = true;
494                    continue;
495                }
496
497                $block->selectors[] = $this->compileSelector($selector);
498            }
499
500            if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
501                unset($block->parent->children[$parentKey]);
502
503                return;
504            }
505        }
506
507        foreach ($block->children as $key => $child) {
508            $this->flattenSelectors($child, $key);
509        }
510    }
511
512    /**
513     * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
514     *
515     * @param array $parts
516     *
517     * @return array
518     */
519    protected function glueFunctionSelectors($parts)
520    {
521        $new = [];
522
523        foreach ($parts as $part) {
524            if (\is_array($part)) {
525                $part = $this->glueFunctionSelectors($part);
526                $new[] = $part;
527            } else {
528                // a selector part finishing with a ) is the last part of a :not( or :nth-child(
529                // and need to be joined to this
530                if (\count($new) && \is_string($new[\count($new) - 1]) &&
531                    \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
532                ) {
533                    while (\count($new)>1 && substr($new[\count($new) - 1], -1) !== '(') {
534                        $part = array_pop($new) . $part;
535                    }
536                    $new[\count($new) - 1] .= $part;
537                } else {
538                    $new[] = $part;
539                }
540            }
541        }
542
543        return $new;
544    }
545
546    /**
547     * Match extends
548     *
549     * @param array   $selector
550     * @param array   $out
551     * @param integer $from
552     * @param boolean $initial
553     */
554    protected function matchExtends($selector, &$out, $from = 0, $initial = true)
555    {
556        static $partsPile = [];
557        $selector = $this->glueFunctionSelectors($selector);
558
559        if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
560            return;
561        }
562
563        $outRecurs = [];
564
565        foreach ($selector as $i => $part) {
566            if ($i < $from) {
567                continue;
568            }
569
570            // check that we are not building an infinite loop of extensions
571            // if the new part is just including a previous part don't try to extend anymore
572            if (\count($part) > 1) {
573                foreach ($partsPile as $previousPart) {
574                    if (! \count(array_diff($previousPart, $part))) {
575                        continue 2;
576                    }
577                }
578            }
579
580            $partsPile[] = $part;
581
582            if ($this->matchExtendsSingle($part, $origin, $initial)) {
583                $after       = \array_slice($selector, $i + 1);
584                $before      = \array_slice($selector, 0, $i);
585                list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
586
587                foreach ($origin as $new) {
588                    $k = 0;
589
590                    // remove shared parts
591                    if (\count($new) > 1) {
592                        while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
593                            $k++;
594                        }
595                    }
596
597                    if (\count($nonBreakableBefore) and $k == \count($new)) {
598                        $k--;
599                    }
600
601                    $replacement = [];
602                    $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
603
604                    for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
605                        $slice = [];
606
607                        foreach ($tempReplacement[$l] as $chunk) {
608                            if (! \in_array($chunk, $slice)) {
609                                $slice[] = $chunk;
610                            }
611                        }
612
613                        array_unshift($replacement, $slice);
614
615                        if (! $this->isImmediateRelationshipCombinator(end($slice))) {
616                            break;
617                        }
618                    }
619
620                    $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
621
622                    // Merge shared direct relationships.
623                    $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
624
625                    $result = array_merge(
626                        $before,
627                        $mergedBefore,
628                        $replacement,
629                        $after
630                    );
631
632                    if ($result === $selector) {
633                        continue;
634                    }
635
636                    $this->pushOrMergeExtentedSelector($out, $result);
637
638                    // recursively check for more matches
639                    $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
640
641                    if (\count($origin) > 1) {
642                        $this->matchExtends($result, $out, $startRecurseFrom, false);
643                    } else {
644                        $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
645                    }
646
647                    // selector sequence merging
648                    if (! empty($before) && \count($new) > 1) {
649                        $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
650                        $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
651
652                        list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
653
654                        $result2 = array_merge(
655                            $preSharedParts,
656                            $betweenSharedParts,
657                            $postSharedParts,
658                            $nonBreakabl2,
659                            $nonBreakableBefore,
660                            $replacement,
661                            $after
662                        );
663
664                        $this->pushOrMergeExtentedSelector($out, $result2);
665                    }
666                }
667            }
668            array_pop($partsPile);
669        }
670
671        while (\count($outRecurs)) {
672            $result = array_shift($outRecurs);
673            $this->pushOrMergeExtentedSelector($out, $result);
674        }
675    }
676
677    /**
678     * Test a part for being a pseudo selector
679     *
680     * @param string $part
681     * @param array  $matches
682     *
683     * @return boolean
684     */
685    protected function isPseudoSelector($part, &$matches)
686    {
687        if (strpos($part, ":") === 0
688            && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
689        ) {
690            return true;
691        }
692
693        return false;
694    }
695
696    /**
697     * Push extended selector except if
698     *  - this is a pseudo selector
699     *  - same as previous
700     *  - in a white list
701     * in this case we merge the pseudo selector content
702     *
703     * @param array $out
704     * @param array $extended
705     */
706    protected function pushOrMergeExtentedSelector(&$out, $extended)
707    {
708        if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
709            $single = reset($extended);
710            $part = reset($single);
711
712            if ($this->isPseudoSelector($part, $matchesExtended) &&
713                \in_array($matchesExtended[1], [ 'slotted' ])
714            ) {
715                $prev = end($out);
716                $prev = $this->glueFunctionSelectors($prev);
717
718                if (\count($prev) === 1 && \count(reset($prev)) === 1) {
719                    $single = reset($prev);
720                    $part = reset($single);
721
722                    if ($this->isPseudoSelector($part, $matchesPrev) &&
723                        $matchesPrev[1] === $matchesExtended[1]
724                    ) {
725                        $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
726                        $extended[1] = $matchesPrev[2] . ", " . $extended[1];
727                        $extended = implode($matchesExtended[1] . '(', $extended);
728                        $extended = [ [ $extended ]];
729                        array_pop($out);
730                    }
731                }
732            }
733        }
734        $out[] = $extended;
735    }
736
737    /**
738     * Match extends single
739     *
740     * @param array   $rawSingle
741     * @param array   $outOrigin
742     * @param boolean $initial
743     *
744     * @return boolean
745     */
746    protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
747    {
748        $counts = [];
749        $single = [];
750
751        // simple usual cases, no need to do the whole trick
752        if (\in_array($rawSingle, [['>'],['+'],['~']])) {
753            return false;
754        }
755
756        foreach ($rawSingle as $part) {
757            // matches Number
758            if (! \is_string($part)) {
759                return false;
760            }
761
762            if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
763                $single[\count($single) - 1] .= $part;
764            } else {
765                $single[] = $part;
766            }
767        }
768
769        $extendingDecoratedTag = false;
770
771        if (\count($single) > 1) {
772            $matches = null;
773            $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
774        }
775
776        $outOrigin = [];
777        $found = false;
778
779        foreach ($single as $k => $part) {
780            if (isset($this->extendsMap[$part])) {
781                foreach ($this->extendsMap[$part] as $idx) {
782                    $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
783                }
784            }
785
786            if ($initial &&
787                $this->isPseudoSelector($part, $matches) &&
788                ! \in_array($matches[1], [ 'not' ])
789            ) {
790                $buffer    = $matches[2];
791                $parser    = $this->parserFactory(__METHOD__);
792
793                if ($parser->parseSelector($buffer, $subSelectors)) {
794                    foreach ($subSelectors as $ksub => $subSelector) {
795                        $subExtended = [];
796                        $this->matchExtends($subSelector, $subExtended, 0, false);
797
798                        if ($subExtended) {
799                            $subSelectorsExtended = $subSelectors;
800                            $subSelectorsExtended[$ksub] = $subExtended;
801
802                            foreach ($subSelectorsExtended as $ksse => $sse) {
803                                $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
804                            }
805
806                            $subSelectorsExtended = implode(', ', $subSelectorsExtended);
807                            $singleExtended = $single;
808                            $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
809                            $outOrigin[] = [ $singleExtended ];
810                            $found = true;
811                        }
812                    }
813                }
814            }
815        }
816
817        foreach ($counts as $idx => $count) {
818            list($target, $origin, /* $block */) = $this->extends[$idx];
819
820            $origin = $this->glueFunctionSelectors($origin);
821
822            // check count
823            if ($count !== \count($target)) {
824                continue;
825            }
826
827            $this->extends[$idx][3] = true;
828
829            $rem = array_diff($single, $target);
830
831            foreach ($origin as $j => $new) {
832                // prevent infinite loop when target extends itself
833                if ($this->isSelfExtend($single, $origin) and !$initial) {
834                    return false;
835                }
836
837                $replacement = end($new);
838
839                // Extending a decorated tag with another tag is not possible.
840                if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
841                    preg_match('/^[a-z0-9]+$/i', $replacement[0])
842                ) {
843                    unset($origin[$j]);
844                    continue;
845                }
846
847                $combined = $this->combineSelectorSingle($replacement, $rem);
848
849                if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
850                    $origin[$j][\count($origin[$j]) - 1] = $combined;
851                }
852            }
853
854            $outOrigin = array_merge($outOrigin, $origin);
855
856            $found = true;
857        }
858
859        return $found;
860    }
861
862    /**
863     * Extract a relationship from the fragment.
864     *
865     * When extracting the last portion of a selector we will be left with a
866     * fragment which may end with a direction relationship combinator. This
867     * method will extract the relationship fragment and return it along side
868     * the rest.
869     *
870     * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
871     *
872     * @return array The selector without the relationship fragment if any, the relationship fragment.
873     */
874    protected function extractRelationshipFromFragment(array $fragment)
875    {
876        $parents = [];
877        $children = [];
878
879        $j = $i = \count($fragment);
880
881        for (;;) {
882            $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
883            $parents  = \array_slice($fragment, 0, $j);
884            $slice    = end($parents);
885
886            if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
887                break;
888            }
889
890            $j -= 2;
891        }
892
893        return [$parents, $children];
894    }
895
896    /**
897     * Combine selector single
898     *
899     * @param array $base
900     * @param array $other
901     *
902     * @return array
903     */
904    protected function combineSelectorSingle($base, $other)
905    {
906        $tag    = [];
907        $out    = [];
908        $wasTag = false;
909        $pseudo = [];
910
911        while (\count($other) && strpos(end($other), ':')===0) {
912            array_unshift($pseudo, array_pop($other));
913        }
914
915        foreach ([array_reverse($base), array_reverse($other)] as $single) {
916            $rang = count($single);
917            foreach ($single as $part) {
918                if (preg_match('/^[\[:]/', $part)) {
919                    $out[] = $part;
920                    $wasTag = false;
921                } elseif (preg_match('/^[\.#]/', $part)) {
922                    array_unshift($out, $part);
923                    $wasTag = false;
924                } elseif (preg_match('/^[^_-]/', $part) and $rang==1) {
925                    $tag[] = $part;
926                    $wasTag = true;
927                } elseif ($wasTag) {
928                    $tag[\count($tag) - 1] .= $part;
929                } else {
930                    array_unshift($out, $part);
931                }
932                $rang--;
933            }
934        }
935
936        if (\count($tag)) {
937            array_unshift($out, $tag[0]);
938        }
939
940        while (\count($pseudo)) {
941            $out[] = array_shift($pseudo);
942        }
943
944        return $out;
945    }
946
947    /**
948     * Compile media
949     *
950     * @param \ScssPhp\ScssPhp\Block $media
951     */
952    protected function compileMedia(Block $media)
953    {
954        $this->pushEnv($media);
955
956        $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
957
958        if (! empty($mediaQueries) && $mediaQueries) {
959            $previousScope = $this->scope;
960            $parentScope = $this->mediaParent($this->scope);
961
962            foreach ($mediaQueries as $mediaQuery) {
963                $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
964
965                $parentScope->children[] = $this->scope;
966                $parentScope = $this->scope;
967            }
968
969            // top level properties in a media cause it to be wrapped
970            $needsWrap = false;
971
972            foreach ($media->children as $child) {
973                $type = $child[0];
974
975                if ($type !== Type::T_BLOCK &&
976                    $type !== Type::T_MEDIA &&
977                    $type !== Type::T_DIRECTIVE &&
978                    $type !== Type::T_IMPORT
979                ) {
980                    $needsWrap = true;
981                    break;
982                }
983            }
984
985            if ($needsWrap) {
986                $wrapped = new Block;
987                $wrapped->sourceName   = $media->sourceName;
988                $wrapped->sourceIndex  = $media->sourceIndex;
989                $wrapped->sourceLine   = $media->sourceLine;
990                $wrapped->sourceColumn = $media->sourceColumn;
991                $wrapped->selectors    = [];
992                $wrapped->comments     = [];
993                $wrapped->parent       = $media;
994                $wrapped->children     = $media->children;
995
996                $media->children = [[Type::T_BLOCK, $wrapped]];
997
998                if (isset($this->lineNumberStyle)) {
999                    $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1000                    $annotation->depth = 0;
1001
1002                    $file = $this->sourceNames[$media->sourceIndex];
1003                    $line = $media->sourceLine;
1004
1005                    switch ($this->lineNumberStyle) {
1006                        case static::LINE_COMMENTS:
1007                            $annotation->lines[] = '/* line ' . $line
1008                                                 . ($file ? ', ' . $file : '')
1009                                                 . ' */';
1010                            break;
1011
1012                        case static::DEBUG_INFO:
1013                            $annotation->lines[] = '@media -sass-debug-info{'
1014                                                 . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1015                                                 . 'line{font-family:' . $line . '}}';
1016                            break;
1017                    }
1018
1019                    $this->scope->children[] = $annotation;
1020                }
1021            }
1022
1023            $this->compileChildrenNoReturn($media->children, $this->scope);
1024
1025            $this->scope = $previousScope;
1026        }
1027
1028        $this->popEnv();
1029    }
1030
1031    /**
1032     * Media parent
1033     *
1034     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1035     *
1036     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1037     */
1038    protected function mediaParent(OutputBlock $scope)
1039    {
1040        while (! empty($scope->parent)) {
1041            if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
1042                break;
1043            }
1044
1045            $scope = $scope->parent;
1046        }
1047
1048        return $scope;
1049    }
1050
1051    /**
1052     * Compile directive
1053     *
1054     * @param \ScssPhp\ScssPhp\Block|array $block
1055     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1056     */
1057    protected function compileDirective($directive, OutputBlock $out)
1058    {
1059        if (\is_array($directive)) {
1060            $s = '@' . $directive[0];
1061
1062            if (! empty($directive[1])) {
1063                $s .= ' ' . $this->compileValue($directive[1]);
1064            }
1065
1066            $this->appendRootDirective($s . ';', $out);
1067        } else {
1068            $s = '@' . $directive->name;
1069
1070            if (! empty($directive->value)) {
1071                $s .= ' ' . $this->compileValue($directive->value);
1072            }
1073
1074            if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1075                $this->compileKeyframeBlock($directive, [$s]);
1076            } else {
1077                $this->compileNestedBlock($directive, [$s]);
1078            }
1079        }
1080    }
1081
1082    /**
1083     * Compile at-root
1084     *
1085     * @param \ScssPhp\ScssPhp\Block $block
1086     */
1087    protected function compileAtRoot(Block $block)
1088    {
1089        $env     = $this->pushEnv($block);
1090        $envs    = $this->compactEnv($env);
1091        list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1092
1093        // wrap inline selector
1094        if ($block->selector) {
1095            $wrapped = new Block;
1096            $wrapped->sourceName   = $block->sourceName;
1097            $wrapped->sourceIndex  = $block->sourceIndex;
1098            $wrapped->sourceLine   = $block->sourceLine;
1099            $wrapped->sourceColumn = $block->sourceColumn;
1100            $wrapped->selectors    = $block->selector;
1101            $wrapped->comments     = [];
1102            $wrapped->parent       = $block;
1103            $wrapped->children     = $block->children;
1104            $wrapped->selfParent   = $block->selfParent;
1105
1106            $block->children = [[Type::T_BLOCK, $wrapped]];
1107            $block->selector = null;
1108        }
1109
1110        $selfParent = $block->selfParent;
1111
1112        if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
1113            isset($block->parent->selectors) && $block->parent->selectors
1114        ) {
1115            $selfParent = $block->parent;
1116        }
1117
1118        $this->env = $this->filterWithWithout($envs, $with, $without);
1119
1120        $saveScope   = $this->scope;
1121        $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1122
1123        // propagate selfParent to the children where they still can be useful
1124        $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1125
1126        $this->scope = $this->completeScope($this->scope, $saveScope);
1127        $this->scope = $saveScope;
1128        $this->env   = $this->extractEnv($envs);
1129
1130        $this->popEnv();
1131    }
1132
1133    /**
1134     * Filter at-root scope depending of with/without option
1135     *
1136     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1137     * @param array                                  $with
1138     * @param array                                  $without
1139     *
1140     * @return mixed
1141     */
1142    protected function filterScopeWithWithout($scope, $with, $without)
1143    {
1144        $filteredScopes = [];
1145        $childStash = [];
1146
1147        if ($scope->type === TYPE::T_ROOT) {
1148            return $scope;
1149        }
1150
1151        // start from the root
1152        while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1153            array_unshift($childStash, $scope);
1154            $scope = $scope->parent;
1155        }
1156
1157        for (;;) {
1158            if (! $scope) {
1159                break;
1160            }
1161
1162            if ($this->isWith($scope, $with, $without)) {
1163                $s = clone $scope;
1164                $s->children = [];
1165                $s->lines    = [];
1166                $s->parent   = null;
1167
1168                if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1169                    $s->selectors = [];
1170                }
1171
1172                $filteredScopes[] = $s;
1173            }
1174
1175            if (\count($childStash)) {
1176                $scope = array_shift($childStash);
1177            } elseif ($scope->children) {
1178                $scope = end($scope->children);
1179            } else {
1180                $scope = null;
1181            }
1182        }
1183
1184        if (! \count($filteredScopes)) {
1185            return $this->rootBlock;
1186        }
1187
1188        $newScope = array_shift($filteredScopes);
1189        $newScope->parent = $this->rootBlock;
1190
1191        $this->rootBlock->children[] = $newScope;
1192
1193        $p = &$newScope;
1194
1195        while (\count($filteredScopes)) {
1196            $s = array_shift($filteredScopes);
1197            $s->parent = $p;
1198            $p->children[] = $s;
1199            $newScope = &$p->children[0];
1200            $p = &$p->children[0];
1201        }
1202
1203        return $newScope;
1204    }
1205
1206    /**
1207     * found missing selector from a at-root compilation in the previous scope
1208     * (if at-root is just enclosing a property, the selector is in the parent tree)
1209     *
1210     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1211     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1212     *
1213     * @return mixed
1214     */
1215    protected function completeScope($scope, $previousScope)
1216    {
1217        if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
1218            $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1219        }
1220
1221        if ($scope->children) {
1222            foreach ($scope->children as $k => $c) {
1223                $scope->children[$k] = $this->completeScope($c, $previousScope);
1224            }
1225        }
1226
1227        return $scope;
1228    }
1229
1230    /**
1231     * Find a selector by the depth node in the scope
1232     *
1233     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1234     * @param integer                                $depth
1235     *
1236     * @return array
1237     */
1238    protected function findScopeSelectors($scope, $depth)
1239    {
1240        if ($scope->depth === $depth && $scope->selectors) {
1241            return $scope->selectors;
1242        }
1243
1244        if ($scope->children) {
1245            foreach (array_reverse($scope->children) as $c) {
1246                if ($s = $this->findScopeSelectors($c, $depth)) {
1247                    return $s;
1248                }
1249            }
1250        }
1251
1252        return [];
1253    }
1254
1255    /**
1256     * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1257     *
1258     * @param array $withCondition
1259     *
1260     * @return array
1261     */
1262    protected function compileWith($withCondition)
1263    {
1264        // just compile what we have in 2 lists
1265        $with = [];
1266        $without = ['rule' => true];
1267
1268        if ($withCondition) {
1269            if ($this->libMapHasKey([$withCondition, static::$with])) {
1270                $without = []; // cancel the default
1271                $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1272
1273                foreach ($list[2] as $item) {
1274                    $keyword = $this->compileStringContent($this->coerceString($item));
1275
1276                    $with[$keyword] = true;
1277                }
1278            }
1279
1280            if ($this->libMapHasKey([$withCondition, static::$without])) {
1281                $without = []; // cancel the default
1282                $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1283
1284                foreach ($list[2] as $item) {
1285                    $keyword = $this->compileStringContent($this->coerceString($item));
1286
1287                    $without[$keyword] = true;
1288                }
1289            }
1290        }
1291
1292        return [$with, $without];
1293    }
1294
1295    /**
1296     * Filter env stack
1297     *
1298     * @param array $envs
1299     * @param array $with
1300     * @param array $without
1301     *
1302     * @return \ScssPhp\ScssPhp\Compiler\Environment
1303     */
1304    protected function filterWithWithout($envs, $with, $without)
1305    {
1306        $filtered = [];
1307
1308        foreach ($envs as $e) {
1309            if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1310                $ec = clone $e;
1311                $ec->block     = null;
1312                $ec->selectors = [];
1313
1314                $filtered[] = $ec;
1315            } else {
1316                $filtered[] = $e;
1317            }
1318        }
1319
1320        return $this->extractEnv($filtered);
1321    }
1322
1323    /**
1324     * Filter WITH rules
1325     *
1326     * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1327     * @param array                                                         $with
1328     * @param array                                                         $without
1329     *
1330     * @return boolean
1331     */
1332    protected function isWith($block, $with, $without)
1333    {
1334        if (isset($block->type)) {
1335            if ($block->type === Type::T_MEDIA) {
1336                return $this->testWithWithout('media', $with, $without);
1337            }
1338
1339            if ($block->type === Type::T_DIRECTIVE) {
1340                if (isset($block->name)) {
1341                    return $this->testWithWithout($block->name, $with, $without);
1342                } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1343                    return $this->testWithWithout($m[1], $with, $without);
1344                } else {
1345                    return $this->testWithWithout('???', $with, $without);
1346                }
1347            }
1348        } elseif (isset($block->selectors)) {
1349            // a selector starting with number is a keyframe rule
1350            if (\count($block->selectors)) {
1351                $s = reset($block->selectors);
1352
1353                while (\is_array($s)) {
1354                    $s = reset($s);
1355                }
1356
1357                if (\is_object($s) && $s instanceof Node\Number) {
1358                    return $this->testWithWithout('keyframes', $with, $without);
1359                }
1360            }
1361
1362            return $this->testWithWithout('rule', $with, $without);
1363        }
1364
1365        return true;
1366    }
1367
1368    /**
1369     * Test a single type of block against with/without lists
1370     *
1371     * @param string $what
1372     * @param array  $with
1373     * @param array  $without
1374     *
1375     * @return boolean
1376     *   true if the block should be kept, false to reject
1377     */
1378    protected function testWithWithout($what, $with, $without)
1379    {
1380
1381        // if without, reject only if in the list (or 'all' is in the list)
1382        if (\count($without)) {
1383            return (isset($without[$what]) || isset($without['all'])) ? false : true;
1384        }
1385
1386        // otherwise reject all what is not in the with list
1387        return (isset($with[$what]) || isset($with['all'])) ? true : false;
1388    }
1389
1390
1391    /**
1392     * Compile keyframe block
1393     *
1394     * @param \ScssPhp\ScssPhp\Block $block
1395     * @param array                  $selectors
1396     */
1397    protected function compileKeyframeBlock(Block $block, $selectors)
1398    {
1399        $env = $this->pushEnv($block);
1400
1401        $envs = $this->compactEnv($env);
1402
1403        $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1404            return ! isset($e->block->selectors);
1405        }));
1406
1407        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1408        $this->scope->depth = 1;
1409        $this->scope->parent->children[] = $this->scope;
1410
1411        $this->compileChildrenNoReturn($block->children, $this->scope);
1412
1413        $this->scope = $this->scope->parent;
1414        $this->env   = $this->extractEnv($envs);
1415
1416        $this->popEnv();
1417    }
1418
1419    /**
1420     * Compile nested properties lines
1421     *
1422     * @param \ScssPhp\ScssPhp\Block                 $block
1423     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1424     */
1425    protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1426    {
1427        $prefix = $this->compileValue($block->prefix) . '-';
1428
1429        $nested = $this->makeOutputBlock($block->type);
1430        $nested->parent = $out;
1431
1432        if ($block->hasValue) {
1433            $nested->depth = $out->depth + 1;
1434        }
1435
1436        $out->children[] = $nested;
1437
1438        foreach ($block->children as $child) {
1439            switch ($child[0]) {
1440                case Type::T_ASSIGN:
1441                    array_unshift($child[1][2], $prefix);
1442                    break;
1443
1444                case Type::T_NESTED_PROPERTY:
1445                    array_unshift($child[1]->prefix[2], $prefix);
1446                    break;
1447            }
1448
1449            $this->compileChild($child, $nested);
1450        }
1451    }
1452
1453    /**
1454     * Compile nested block
1455     *
1456     * @param \ScssPhp\ScssPhp\Block $block
1457     * @param array                  $selectors
1458     */
1459    protected function compileNestedBlock(Block $block, $selectors)
1460    {
1461        $this->pushEnv($block);
1462
1463        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1464        $this->scope->parent->children[] = $this->scope;
1465
1466        // wrap assign children in a block
1467        // except for @font-face
1468        if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
1469            // need wrapping?
1470            $needWrapping = false;
1471
1472            foreach ($block->children as $child) {
1473                if ($child[0] === Type::T_ASSIGN) {
1474                    $needWrapping = true;
1475                    break;
1476                }
1477            }
1478
1479            if ($needWrapping) {
1480                $wrapped = new Block;
1481                $wrapped->sourceName   = $block->sourceName;
1482                $wrapped->sourceIndex  = $block->sourceIndex;
1483                $wrapped->sourceLine   = $block->sourceLine;
1484                $wrapped->sourceColumn = $block->sourceColumn;
1485                $wrapped->selectors    = [];
1486                $wrapped->comments     = [];
1487                $wrapped->parent       = $block;
1488                $wrapped->children     = $block->children;
1489                $wrapped->selfParent   = $block->selfParent;
1490
1491                $block->children = [[Type::T_BLOCK, $wrapped]];
1492            }
1493        }
1494
1495        $this->compileChildrenNoReturn($block->children, $this->scope);
1496
1497        $this->scope = $this->scope->parent;
1498
1499        $this->popEnv();
1500    }
1501
1502    /**
1503     * Recursively compiles a block.
1504     *
1505     * A block is analogous to a CSS block in most cases. A single SCSS document
1506     * is encapsulated in a block when parsed, but it does not have parent tags
1507     * so all of its children appear on the root level when compiled.
1508     *
1509     * Blocks are made up of selectors and children.
1510     *
1511     * The children of a block are just all the blocks that are defined within.
1512     *
1513     * Compiling the block involves pushing a fresh environment on the stack,
1514     * and iterating through the props, compiling each one.
1515     *
1516     * @see Compiler::compileChild()
1517     *
1518     * @param \ScssPhp\ScssPhp\Block $block
1519     */
1520    protected function compileBlock(Block $block)
1521    {
1522        $env = $this->pushEnv($block);
1523        $env->selectors = $this->evalSelectors($block->selectors);
1524
1525        $out = $this->makeOutputBlock(null);
1526
1527        if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) {
1528            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1529            $annotation->depth = 0;
1530
1531            $file = $this->sourceNames[$block->sourceIndex];
1532            $line = $block->sourceLine;
1533
1534            switch ($this->lineNumberStyle) {
1535                case static::LINE_COMMENTS:
1536                    $annotation->lines[] = '/* line ' . $line
1537                                         . ($file ? ', ' . $file : '')
1538                                         . ' */';
1539                    break;
1540
1541                case static::DEBUG_INFO:
1542                    $annotation->lines[] = '@media -sass-debug-info{'
1543                                         . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1544                                         . 'line{font-family:' . $line . '}}';
1545                    break;
1546            }
1547
1548            $this->scope->children[] = $annotation;
1549        }
1550
1551        $this->scope->children[] = $out;
1552
1553        if (\count($block->children)) {
1554            $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1555
1556            // propagate selfParent to the children where they still can be useful
1557            $selfParentSelectors = null;
1558
1559            if (isset($block->selfParent->selectors)) {
1560                $selfParentSelectors = $block->selfParent->selectors;
1561                $block->selfParent->selectors = $out->selectors;
1562            }
1563
1564            $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1565
1566            // and revert for the following children of the same block
1567            if ($selfParentSelectors) {
1568                $block->selfParent->selectors = $selfParentSelectors;
1569            }
1570        }
1571
1572        $this->popEnv();
1573    }
1574
1575
1576    /**
1577     * Compile the value of a comment that can have interpolation
1578     *
1579     * @param array   $value
1580     * @param boolean $pushEnv
1581     *
1582     * @return array|mixed|string
1583     */
1584    protected function compileCommentValue($value, $pushEnv = false)
1585    {
1586        $c = $value[1];
1587
1588        if (isset($value[2])) {
1589            if ($pushEnv) {
1590                $this->pushEnv();
1591            }
1592
1593            $ignoreCallStackMessage = $this->ignoreCallStackMessage;
1594            $this->ignoreCallStackMessage = true;
1595
1596            try {
1597                $c = $this->compileValue($value[2]);
1598            } catch (\Exception $e) {
1599                // ignore error in comment compilation which are only interpolation
1600            }
1601
1602            $this->ignoreCallStackMessage = $ignoreCallStackMessage;
1603
1604            if ($pushEnv) {
1605                $this->popEnv();
1606            }
1607        }
1608
1609        return $c;
1610    }
1611
1612    /**
1613     * Compile root level comment
1614     *
1615     * @param array $block
1616     */
1617    protected function compileComment($block)
1618    {
1619        $out = $this->makeOutputBlock(Type::T_COMMENT);
1620        $out->lines[] = $this->compileCommentValue($block, true);
1621
1622        $this->scope->children[] = $out;
1623    }
1624
1625    /**
1626     * Evaluate selectors
1627     *
1628     * @param array $selectors
1629     *
1630     * @return array
1631     */
1632    protected function evalSelectors($selectors)
1633    {
1634        $this->shouldEvaluate = false;
1635
1636        $selectors = array_map([$this, 'evalSelector'], $selectors);
1637
1638        // after evaluating interpolates, we might need a second pass
1639        if ($this->shouldEvaluate) {
1640            $selectors = $this->revertSelfSelector($selectors);
1641            $buffer    = $this->collapseSelectors($selectors);
1642            $parser    = $this->parserFactory(__METHOD__);
1643
1644            if ($parser->parseSelector($buffer, $newSelectors)) {
1645                $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1646            }
1647        }
1648
1649        return $selectors;
1650    }
1651
1652    /**
1653     * Evaluate selector
1654     *
1655     * @param array $selector
1656     *
1657     * @return array
1658     */
1659    protected function evalSelector($selector)
1660    {
1661        return array_map([$this, 'evalSelectorPart'], $selector);
1662    }
1663
1664    /**
1665     * Evaluate selector part; replaces all the interpolates, stripping quotes
1666     *
1667     * @param array $part
1668     *
1669     * @return array
1670     */
1671    protected function evalSelectorPart($part)
1672    {
1673        foreach ($part as &$p) {
1674            if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1675                $p = $this->compileValue($p);
1676
1677                // force re-evaluation
1678                if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1679                    $this->shouldEvaluate = true;
1680                }
1681            } elseif (\is_string($p) && \strlen($p) >= 2 &&
1682                ($first = $p[0]) && ($first === '"' || $first === "'") &&
1683                substr($p, -1) === $first
1684            ) {
1685                $p = substr($p, 1, -1);
1686            }
1687        }
1688
1689        return $this->flattenSelectorSingle($part);
1690    }
1691
1692    /**
1693     * Collapse selectors
1694     *
1695     * @param array   $selectors
1696     * @param boolean $selectorFormat
1697     *   if false return a collapsed string
1698     *   if true return an array description of a structured selector
1699     *
1700     * @return string
1701     */
1702    protected function collapseSelectors($selectors, $selectorFormat = false)
1703    {
1704        $parts = [];
1705
1706        foreach ($selectors as $selector) {
1707            $output = [];
1708            $glueNext = false;
1709
1710            foreach ($selector as $node) {
1711                $compound = '';
1712
1713                array_walk_recursive(
1714                    $node,
1715                    function ($value, $key) use (&$compound) {
1716                        $compound .= $value;
1717                    }
1718                );
1719
1720                if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1721                    if (\count($output)) {
1722                        $output[\count($output) - 1] .= ' ' . $compound;
1723                    } else {
1724                        $output[] = $compound;
1725                    }
1726
1727                    $glueNext = true;
1728                } elseif ($glueNext) {
1729                    $output[\count($output) - 1] .= ' ' . $compound;
1730                    $glueNext = false;
1731                } else {
1732                    $output[] = $compound;
1733                }
1734            }
1735
1736            if ($selectorFormat) {
1737                foreach ($output as &$o) {
1738                    $o = [Type::T_STRING, '', [$o]];
1739                }
1740
1741                $output = [Type::T_LIST, ' ', $output];
1742            } else {
1743                $output = implode(' ', $output);
1744            }
1745
1746            $parts[] = $output;
1747        }
1748
1749        if ($selectorFormat) {
1750            $parts = [Type::T_LIST, ',', $parts];
1751        } else {
1752            $parts = implode(', ', $parts);
1753        }
1754
1755        return $parts;
1756    }
1757
1758    /**
1759     * Parse down the selector and revert [self] to "&" before a reparsing
1760     *
1761     * @param array $selectors
1762     *
1763     * @return array
1764     */
1765    protected function revertSelfSelector($selectors)
1766    {
1767        foreach ($selectors as &$part) {
1768            if (\is_array($part)) {
1769                if ($part === [Type::T_SELF]) {
1770                    $part = '&';
1771                } else {
1772                    $part = $this->revertSelfSelector($part);
1773                }
1774            }
1775        }
1776
1777        return $selectors;
1778    }
1779
1780    /**
1781     * Flatten selector single; joins together .classes and #ids
1782     *
1783     * @param array $single
1784     *
1785     * @return array
1786     */
1787    protected function flattenSelectorSingle($single)
1788    {
1789        $joined = [];
1790
1791        foreach ($single as $part) {
1792            if (empty($joined) ||
1793                ! \is_string($part) ||
1794                preg_match('/[\[.:#%]/', $part)
1795            ) {
1796                $joined[] = $part;
1797                continue;
1798            }
1799
1800            if (\is_array(end($joined))) {
1801                $joined[] = $part;
1802            } else {
1803                $joined[\count($joined) - 1] .= $part;
1804            }
1805        }
1806
1807        return $joined;
1808    }
1809
1810    /**
1811     * Compile selector to string; self(&) should have been replaced by now
1812     *
1813     * @param string|array $selector
1814     *
1815     * @return string
1816     */
1817    protected function compileSelector($selector)
1818    {
1819        if (! \is_array($selector)) {
1820            return $selector; // media and the like
1821        }
1822
1823        return implode(
1824            ' ',
1825            array_map(
1826                [$this, 'compileSelectorPart'],
1827                $selector
1828            )
1829        );
1830    }
1831
1832    /**
1833     * Compile selector part
1834     *
1835     * @param array $piece
1836     *
1837     * @return string
1838     */
1839    protected function compileSelectorPart($piece)
1840    {
1841        foreach ($piece as &$p) {
1842            if (! \is_array($p)) {
1843                continue;
1844            }
1845
1846            switch ($p[0]) {
1847                case Type::T_SELF:
1848                    $p = '&';
1849                    break;
1850
1851                default:
1852                    $p = $this->compileValue($p);
1853                    break;
1854            }
1855        }
1856
1857        return implode($piece);
1858    }
1859
1860    /**
1861     * Has selector placeholder?
1862     *
1863     * @param array $selector
1864     *
1865     * @return boolean
1866     */
1867    protected function hasSelectorPlaceholder($selector)
1868    {
1869        if (! \is_array($selector)) {
1870            return false;
1871        }
1872
1873        foreach ($selector as $parts) {
1874            foreach ($parts as $part) {
1875                if (\strlen($part) && '%' === $part[0]) {
1876                    return true;
1877                }
1878            }
1879        }
1880
1881        return false;
1882    }
1883
1884    protected function pushCallStack($name = '')
1885    {
1886        $this->callStack[] = [
1887          'n' => $name,
1888          Parser::SOURCE_INDEX => $this->sourceIndex,
1889          Parser::SOURCE_LINE => $this->sourceLine,
1890          Parser::SOURCE_COLUMN => $this->sourceColumn
1891        ];
1892
1893        // infinite calling loop
1894        if (\count($this->callStack) > 25000) {
1895            // not displayed but you can var_dump it to deep debug
1896            $msg = $this->callStackMessage(true, 100);
1897            $msg = "Infinite calling loop";
1898
1899            $this->throwError($msg);
1900        }
1901    }
1902
1903    protected function popCallStack()
1904    {
1905        array_pop($this->callStack);
1906    }
1907
1908    /**
1909     * Compile children and return result
1910     *
1911     * @param array                                  $stms
1912     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1913     * @param string                                 $traceName
1914     *
1915     * @return array|null
1916     */
1917    protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1918    {
1919        $this->pushCallStack($traceName);
1920
1921        foreach ($stms as $stm) {
1922            $ret = $this->compileChild($stm, $out);
1923
1924            if (isset($ret)) {
1925                $this->popCallStack();
1926
1927                return $ret;
1928            }
1929        }
1930
1931        $this->popCallStack();
1932
1933        return null;
1934    }
1935
1936    /**
1937     * Compile children and throw exception if unexpected @return
1938     *
1939     * @param array                                  $stms
1940     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1941     * @param \ScssPhp\ScssPhp\Block                 $selfParent
1942     * @param string                                 $traceName
1943     *
1944     * @throws \Exception
1945     */
1946    protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1947    {
1948        $this->pushCallStack($traceName);
1949
1950        foreach ($stms as $stm) {
1951            if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
1952                $stm[1]->selfParent = $selfParent;
1953                $ret = $this->compileChild($stm, $out);
1954                $stm[1]->selfParent = null;
1955            } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
1956                $stm['selfParent'] = $selfParent;
1957                $ret = $this->compileChild($stm, $out);
1958                unset($stm['selfParent']);
1959            } else {
1960                $ret = $this->compileChild($stm, $out);
1961            }
1962
1963            if (isset($ret)) {
1964                $this->throwError('@return may only be used within a function');
1965                $this->popCallStack();
1966
1967                return;
1968            }
1969        }
1970
1971        $this->popCallStack();
1972    }
1973
1974
1975    /**
1976     * evaluate media query : compile internal value keeping the structure inchanged
1977     *
1978     * @param array $queryList
1979     *
1980     * @return array
1981     */
1982    protected function evaluateMediaQuery($queryList)
1983    {
1984        static $parser = null;
1985
1986        $outQueryList = [];
1987
1988        foreach ($queryList as $kql => $query) {
1989            $shouldReparse = false;
1990
1991            foreach ($query as $kq => $q) {
1992                for ($i = 1; $i < \count($q); $i++) {
1993                    $value = $this->compileValue($q[$i]);
1994
1995                    // the parser had no mean to know if media type or expression if it was an interpolation
1996                    // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
1997                    if ($q[0] == Type::T_MEDIA_TYPE &&
1998                        (strpos($value, '(') !== false ||
1999                        strpos($value, ')') !== false ||
2000                        strpos($value, ':') !== false ||
2001                        strpos($value, ',') !== false)
2002                    ) {
2003                        $shouldReparse = true;
2004                    }
2005
2006                    $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
2007                }
2008            }
2009
2010            if ($shouldReparse) {
2011                if (\is_null($parser)) {
2012                    $parser = $this->parserFactory(__METHOD__);
2013                }
2014
2015                $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2016                $queryString = reset($queryString);
2017
2018                if (strpos($queryString, '@media ') === 0) {
2019                    $queryString = substr($queryString, 7);
2020                    $queries = [];
2021
2022                    if ($parser->parseMediaQueryList($queryString, $queries)) {
2023                        $queries = $this->evaluateMediaQuery($queries[2]);
2024
2025                        while (\count($queries)) {
2026                            $outQueryList[] = array_shift($queries);
2027                        }
2028
2029                        continue;
2030                    }
2031                }
2032            }
2033
2034            $outQueryList[] = $queryList[$kql];
2035        }
2036
2037        return $outQueryList;
2038    }
2039
2040    /**
2041     * Compile media query
2042     *
2043     * @param array $queryList
2044     *
2045     * @return array
2046     */
2047    protected function compileMediaQuery($queryList)
2048    {
2049        $start   = '@media ';
2050        $default = trim($start);
2051        $out     = [];
2052        $current = "";
2053
2054        foreach ($queryList as $query) {
2055            $type = null;
2056            $parts = [];
2057
2058            $mediaTypeOnly = true;
2059
2060            foreach ($query as $q) {
2061                if ($q[0] !== Type::T_MEDIA_TYPE) {
2062                    $mediaTypeOnly = false;
2063                    break;
2064                }
2065            }
2066
2067            foreach ($query as $q) {
2068                switch ($q[0]) {
2069                    case Type::T_MEDIA_TYPE:
2070                        $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
2071
2072                        // combining not and anything else than media type is too risky and should be avoided
2073                        if (! $mediaTypeOnly) {
2074                            if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
2075                                if ($type) {
2076                                    array_unshift($parts, implode(' ', array_filter($type)));
2077                                }
2078
2079                                if (! empty($parts)) {
2080                                    if (\strlen($current)) {
2081                                        $current .= $this->formatter->tagSeparator;
2082                                    }
2083
2084                                    $current .= implode(' and ', $parts);
2085                                }
2086
2087                                if ($current) {
2088                                    $out[] = $start . $current;
2089                                }
2090
2091                                $current = "";
2092                                $type    = null;
2093                                $parts   = [];
2094                            }
2095                        }
2096
2097                        if ($newType === ['all'] && $default) {
2098                            $default = $start . 'all';
2099                        }
2100
2101                        // all can be safely ignored and mixed with whatever else
2102                        if ($newType !== ['all']) {
2103                            if ($type) {
2104                                $type = $this->mergeMediaTypes($type, $newType);
2105
2106                                if (empty($type)) {
2107                                    // merge failed : ignore this query that is not valid, skip to the next one
2108                                    $parts = [];
2109                                    $default = ''; // if everything fail, no @media at all
2110                                    continue 3;
2111                                }
2112                            } else {
2113                                $type = $newType;
2114                            }
2115                        }
2116                        break;
2117
2118                    case Type::T_MEDIA_EXPRESSION:
2119                        if (isset($q[2])) {
2120                            $parts[] = '('
2121                                . $this->compileValue($q[1])
2122                                . $this->formatter->assignSeparator
2123                                . $this->compileValue($q[2])
2124                                . ')';
2125                        } else {
2126                            $parts[] = '('
2127                                . $this->compileValue($q[1])
2128                                . ')';
2129                        }
2130                        break;
2131
2132                    case Type::T_MEDIA_VALUE:
2133                        $parts[] = $this->compileValue($q[1]);
2134                        break;
2135                }
2136            }
2137
2138            if ($type) {
2139                array_unshift($parts, implode(' ', array_filter($type)));
2140            }
2141
2142            if (! empty($parts)) {
2143                if (\strlen($current)) {
2144                    $current .= $this->formatter->tagSeparator;
2145                }
2146
2147                $current .= implode(' and ', $parts);
2148            }
2149        }
2150
2151        if ($current) {
2152            $out[] = $start . $current;
2153        }
2154
2155        // no @media type except all, and no conflict?
2156        if (! $out && $default) {
2157            $out[] = $default;
2158        }
2159
2160        return $out;
2161    }
2162
2163    /**
2164     * Merge direct relationships between selectors
2165     *
2166     * @param array $selectors1
2167     * @param array $selectors2
2168     *
2169     * @return array
2170     */
2171    protected function mergeDirectRelationships($selectors1, $selectors2)
2172    {
2173        if (empty($selectors1) || empty($selectors2)) {
2174            return array_merge($selectors1, $selectors2);
2175        }
2176
2177        $part1 = end($selectors1);
2178        $part2 = end($selectors2);
2179
2180        if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2181            return array_merge($selectors1, $selectors2);
2182        }
2183
2184        $merged = [];
2185
2186        do {
2187            $part1 = array_pop($selectors1);
2188            $part2 = array_pop($selectors2);
2189
2190            if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2191                if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2192                    array_unshift($merged, [$part1[0] . $part2[0]]);
2193                    $merged = array_merge($selectors1, $selectors2, $merged);
2194                } else {
2195                    $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2196                }
2197
2198                break;
2199            }
2200
2201            array_unshift($merged, $part1);
2202        } while (! empty($selectors1) && ! empty($selectors2));
2203
2204        return $merged;
2205    }
2206
2207    /**
2208     * Merge media types
2209     *
2210     * @param array $type1
2211     * @param array $type2
2212     *
2213     * @return array|null
2214     */
2215    protected function mergeMediaTypes($type1, $type2)
2216    {
2217        if (empty($type1)) {
2218            return $type2;
2219        }
2220
2221        if (empty($type2)) {
2222            return $type1;
2223        }
2224
2225        if (\count($type1) > 1) {
2226            $m1 = strtolower($type1[0]);
2227            $t1 = strtolower($type1[1]);
2228        } else {
2229            $m1 = '';
2230            $t1 = strtolower($type1[0]);
2231        }
2232
2233        if (\count($type2) > 1) {
2234            $m2 = strtolower($type2[0]);
2235            $t2 = strtolower($type2[1]);
2236        } else {
2237            $m2 = '';
2238            $t2 = strtolower($type2[0]);
2239        }
2240
2241        if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2242            if ($t1 === $t2) {
2243                return null;
2244            }
2245
2246            return [
2247                $m1 === Type::T_NOT ? $m2 : $m1,
2248                $m1 === Type::T_NOT ? $t2 : $t1,
2249            ];
2250        }
2251
2252        if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2253            // CSS has no way of representing "neither screen nor print"
2254            if ($t1 !== $t2) {
2255                return null;
2256            }
2257
2258            return [Type::T_NOT, $t1];
2259        }
2260
2261        if ($t1 !== $t2) {
2262            return null;
2263        }
2264
2265        // t1 == t2, neither m1 nor m2 are "not"
2266        return [empty($m1)? $m2 : $m1, $t1];
2267    }
2268
2269    /**
2270     * Compile import; returns true if the value was something that could be imported
2271     *
2272     * @param array                                  $rawPath
2273     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2274     * @param boolean                                $once
2275     *
2276     * @return boolean
2277     */
2278    protected function compileImport($rawPath, OutputBlock $out, $once = false)
2279    {
2280        if ($rawPath[0] === Type::T_STRING) {
2281            $path = $this->compileStringContent($rawPath);
2282
2283            if ($path = $this->findImport($path)) {
2284                if (! $once || ! \in_array($path, $this->importedFiles)) {
2285                    $this->importFile($path, $out);
2286                    $this->importedFiles[] = $path;
2287                }
2288
2289                return true;
2290            }
2291
2292            $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
2293
2294            return false;
2295        }
2296
2297        if ($rawPath[0] === Type::T_LIST) {
2298            // handle a list of strings
2299            if (\count($rawPath[2]) === 0) {
2300                return false;
2301            }
2302
2303            foreach ($rawPath[2] as $path) {
2304                if ($path[0] !== Type::T_STRING) {
2305                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2306
2307                    return false;
2308                }
2309            }
2310
2311            foreach ($rawPath[2] as $path) {
2312                $this->compileImport($path, $out, $once);
2313            }
2314
2315            return true;
2316        }
2317
2318        $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2319
2320        return false;
2321    }
2322
2323
2324    /**
2325     * Append a root directive like @import or @charset as near as the possible from the source code
2326     * (keeping before comments, @import and @charset coming before in the source code)
2327     *
2328     * @param string                                        $line
2329     * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2330     * @param array                                         $allowed
2331     */
2332    protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2333    {
2334        $root = $out;
2335
2336        while ($root->parent) {
2337            $root = $root->parent;
2338        }
2339
2340        $i = 0;
2341
2342        while ($i < \count($root->children)) {
2343            if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2344                break;
2345            }
2346
2347            $i++;
2348        }
2349
2350        // remove incompatible children from the bottom of the list
2351        $saveChildren = [];
2352
2353        while ($i < \count($root->children)) {
2354            $saveChildren[] = array_pop($root->children);
2355        }
2356
2357        // insert the directive as a comment
2358        $child = $this->makeOutputBlock(Type::T_COMMENT);
2359        $child->lines[]      = $line;
2360        $child->sourceName   = $this->sourceNames[$this->sourceIndex];
2361        $child->sourceLine   = $this->sourceLine;
2362        $child->sourceColumn = $this->sourceColumn;
2363
2364        $root->children[] = $child;
2365
2366        // repush children
2367        while (\count($saveChildren)) {
2368            $root->children[] = array_pop($saveChildren);
2369        }
2370    }
2371
2372    /**
2373     * Append lines to the current output block:
2374     * directly to the block or through a child if necessary
2375     *
2376     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2377     * @param string                                 $type
2378     * @param string|mixed                           $line
2379     */
2380    protected function appendOutputLine(OutputBlock $out, $type, $line)
2381    {
2382        $outWrite = &$out;
2383
2384        if ($type === Type::T_COMMENT) {
2385            $parent = $out->parent;
2386
2387            if (end($parent->children) !== $out) {
2388                $outWrite = &$parent->children[\count($parent->children) - 1];
2389            }
2390        }
2391
2392        // check if it's a flat output or not
2393        if (\count($out->children)) {
2394            $lastChild = &$out->children[\count($out->children) - 1];
2395
2396            if ($lastChild->depth === $out->depth &&
2397                \is_null($lastChild->selectors) &&
2398                ! \count($lastChild->children)
2399            ) {
2400                $outWrite = $lastChild;
2401            } else {
2402                $nextLines = $this->makeOutputBlock($type);
2403                $nextLines->parent = $out;
2404                $nextLines->depth  = $out->depth;
2405
2406                $out->children[] = $nextLines;
2407                $outWrite = &$nextLines;
2408            }
2409        }
2410
2411        $outWrite->lines[] = $line;
2412    }
2413
2414    /**
2415     * Compile child; returns a value to halt execution
2416     *
2417     * @param array                                  $child
2418     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2419     *
2420     * @return array
2421     */
2422    protected function compileChild($child, OutputBlock $out)
2423    {
2424        if (isset($child[Parser::SOURCE_LINE])) {
2425            $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2426            $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2427            $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2428        } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
2429            $this->sourceIndex  = $child[1]->sourceIndex;
2430            $this->sourceLine   = $child[1]->sourceLine;
2431            $this->sourceColumn = $child[1]->sourceColumn;
2432        } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2433            $this->sourceLine   = $out->sourceLine;
2434            $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
2435            $this->sourceColumn = $out->sourceColumn;
2436
2437            if ($this->sourceIndex === false) {
2438                $this->sourceIndex = null;
2439            }
2440        }
2441
2442        switch ($child[0]) {
2443            case Type::T_SCSSPHP_IMPORT_ONCE:
2444                $rawPath = $this->reduce($child[1]);
2445
2446                $this->compileImport($rawPath, $out, true);
2447                break;
2448
2449            case Type::T_IMPORT:
2450                $rawPath = $this->reduce($child[1]);
2451
2452                $this->compileImport($rawPath, $out);
2453                break;
2454
2455            case Type::T_DIRECTIVE:
2456                $this->compileDirective($child[1], $out);
2457                break;
2458
2459            case Type::T_AT_ROOT:
2460                $this->compileAtRoot($child[1]);
2461                break;
2462
2463            case Type::T_MEDIA:
2464                $this->compileMedia($child[1]);
2465                break;
2466
2467            case Type::T_BLOCK:
2468                $this->compileBlock($child[1]);
2469                break;
2470
2471            case Type::T_CHARSET:
2472                if (! $this->charsetSeen) {
2473                    $this->charsetSeen = true;
2474                    $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2475                }
2476                break;
2477
2478            case Type::T_CUSTOM_PROPERTY:
2479                list(, $name, $value) = $child;
2480                $compiledName = $this->compileValue($name);
2481
2482                // if the value reduces to null from something else then
2483                // the property should be discarded
2484                if ($value[0] !== Type::T_NULL) {
2485                    $value = $this->reduce($value);
2486
2487                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2488                        break;
2489                    }
2490                }
2491
2492                $compiledValue = $this->compileValue($value);
2493
2494                $line = $this->formatter->customProperty(
2495                    $compiledName,
2496                    $compiledValue
2497                );
2498
2499                $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2500                break;
2501
2502            case Type::T_ASSIGN:
2503                list(, $name, $value) = $child;
2504
2505                if ($name[0] === Type::T_VARIABLE) {
2506                    $flags     = isset($child[3]) ? $child[3] : [];
2507                    $isDefault = \in_array('!default', $flags);
2508                    $isGlobal  = \in_array('!global', $flags);
2509
2510                    if ($isGlobal) {
2511                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2512                        break;
2513                    }
2514
2515                    $shouldSet = $isDefault &&
2516                        (\is_null($result = $this->get($name[1], false)) ||
2517                        $result === static::$null);
2518
2519                    if (! $isDefault || $shouldSet) {
2520                        $this->set($name[1], $this->reduce($value), true, null, $value);
2521                    }
2522                    break;
2523                }
2524
2525                $compiledName = $this->compileValue($name);
2526
2527                // handle shorthand syntaxes : size / line-height...
2528                if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2529                    if ($value[0] === Type::T_VARIABLE) {
2530                        // if the font value comes from variable, the content is already reduced
2531                        // (i.e., formulas were already calculated), so we need the original unreduced value
2532                        $value = $this->get($value[1], true, null, true);
2533                    }
2534
2535                    $shorthandValue=&$value;
2536
2537                    $shorthandDividerNeedsUnit = false;
2538                    $maxListElements           = null;
2539                    $maxShorthandDividers      = 1;
2540
2541                    switch ($compiledName) {
2542                        case 'border-radius':
2543                            $maxListElements = 4;
2544                            $shorthandDividerNeedsUnit = true;
2545                            break;
2546                    }
2547
2548                    if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
2549                        // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2550                        // we need to handle the first list element
2551                        $shorthandValue=&$value[2][0];
2552                    }
2553
2554                    if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2555                        $revert = true;
2556
2557                        if ($shorthandDividerNeedsUnit) {
2558                            $divider = $shorthandValue[3];
2559
2560                            if (\is_array($divider)) {
2561                                $divider = $this->reduce($divider, true);
2562                            }
2563
2564                            if (\intval($divider->dimension) and ! \count($divider->units)) {
2565                                $revert = false;
2566                            }
2567                        }
2568
2569                        if ($revert) {
2570                            $shorthandValue = $this->expToString($shorthandValue);
2571                        }
2572                    } elseif ($shorthandValue[0] === Type::T_LIST) {
2573                        foreach ($shorthandValue[2] as &$item) {
2574                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2575                                if ($maxShorthandDividers > 0) {
2576                                    $revert = true;
2577                                    // if the list of values is too long, this has to be a shorthand,
2578                                    // otherwise it could be a real division
2579                                    if (\is_null($maxListElements) or \count($shorthandValue[2]) <= $maxListElements) {
2580                                        if ($shorthandDividerNeedsUnit) {
2581                                            $divider = $item[3];
2582
2583                                            if (\is_array($divider)) {
2584                                                $divider = $this->reduce($divider, true);
2585                                            }
2586
2587                                            if (\intval($divider->dimension) and ! \count($divider->units)) {
2588                                                $revert = false;
2589                                            }
2590                                        }
2591                                    }
2592
2593                                    if ($revert) {
2594                                        $item = $this->expToString($item);
2595                                        $maxShorthandDividers--;
2596                                    }
2597                                }
2598                            }
2599                        }
2600                    }
2601                }
2602
2603                // if the value reduces to null from something else then
2604                // the property should be discarded
2605                if ($value[0] !== Type::T_NULL) {
2606                    $value = $this->reduce($value);
2607
2608                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2609                        break;
2610                    }
2611                }
2612
2613                $compiledValue = $this->compileValue($value);
2614
2615                // ignore empty value
2616                if (\strlen($compiledValue)) {
2617                    $line = $this->formatter->property(
2618                        $compiledName,
2619                        $compiledValue
2620                    );
2621                    $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2622                }
2623                break;
2624
2625            case Type::T_COMMENT:
2626                if ($out->type === Type::T_ROOT) {
2627                    $this->compileComment($child);
2628                    break;
2629                }
2630
2631                $line = $this->compileCommentValue($child, true);
2632                $this->appendOutputLine($out, Type::T_COMMENT, $line);
2633                break;
2634
2635            case Type::T_MIXIN:
2636            case Type::T_FUNCTION:
2637                list(, $block) = $child;
2638                // the block need to be able to go up to it's parent env to resolve vars
2639                $block->parentEnv = $this->getStoreEnv();
2640                $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2641                break;
2642
2643            case Type::T_EXTEND:
2644                foreach ($child[1] as $sel) {
2645                    $results = $this->evalSelectors([$sel]);
2646
2647                    foreach ($results as $result) {
2648                        // only use the first one
2649                        $result = current($result);
2650                        $selectors = $out->selectors;
2651
2652                        if (! $selectors && isset($child['selfParent'])) {
2653                            $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
2654                        }
2655
2656                        $this->pushExtends($result, $selectors, $child);
2657                    }
2658                }
2659                break;
2660
2661            case Type::T_IF:
2662                list(, $if) = $child;
2663
2664                if ($this->isTruthy($this->reduce($if->cond, true))) {
2665                    return $this->compileChildren($if->children, $out);
2666                }
2667
2668                foreach ($if->cases as $case) {
2669                    if ($case->type === Type::T_ELSE ||
2670                        $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2671                    ) {
2672                        return $this->compileChildren($case->children, $out);
2673                    }
2674                }
2675                break;
2676
2677            case Type::T_EACH:
2678                list(, $each) = $child;
2679
2680                $list = $this->coerceList($this->reduce($each->list), ',', true);
2681
2682                $this->pushEnv();
2683
2684                foreach ($list[2] as $item) {
2685                    if (\count($each->vars) === 1) {
2686                        $this->set($each->vars[0], $item, true);
2687                    } else {
2688                        list(,, $values) = $this->coerceList($item);
2689
2690                        foreach ($each->vars as $i => $var) {
2691                            $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
2692                        }
2693                    }
2694
2695                    $ret = $this->compileChildren($each->children, $out);
2696
2697                    if ($ret) {
2698                        if ($ret[0] !== Type::T_CONTROL) {
2699                            $store = $this->env->store;
2700                            $this->popEnv();
2701                            $this->backPropagateEnv($store, $each->vars);
2702
2703                            return $ret;
2704                        }
2705
2706                        if ($ret[1]) {
2707                            break;
2708                        }
2709                    }
2710                }
2711                $store = $this->env->store;
2712                $this->popEnv();
2713                $this->backPropagateEnv($store, $each->vars);
2714
2715                break;
2716
2717            case Type::T_WHILE:
2718                list(, $while) = $child;
2719
2720                while ($this->isTruthy($this->reduce($while->cond, true))) {
2721                    $ret = $this->compileChildren($while->children, $out);
2722
2723                    if ($ret) {
2724                        if ($ret[0] !== Type::T_CONTROL) {
2725                            return $ret;
2726                        }
2727
2728                        if ($ret[1]) {
2729                            break;
2730                        }
2731                    }
2732                }
2733                break;
2734
2735            case Type::T_FOR:
2736                list(, $for) = $child;
2737
2738                $start = $this->reduce($for->start, true);
2739                $end   = $this->reduce($for->end, true);
2740
2741                if (! ($start[2] == $end[2] || $end->unitless())) {
2742                    $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
2743
2744                    break;
2745                }
2746
2747                $unit  = $start[2];
2748                $start = $start[1];
2749                $end   = $end[1];
2750
2751                $d = $start < $end ? 1 : -1;
2752
2753                $this->pushEnv();
2754
2755                for (;;) {
2756                    if ((! $for->until && $start - $d == $end) ||
2757                        ($for->until && $start == $end)
2758                    ) {
2759                        break;
2760                    }
2761
2762                    $this->set($for->var, new Node\Number($start, $unit));
2763                    $start += $d;
2764
2765                    $ret = $this->compileChildren($for->children, $out);
2766
2767                    if ($ret) {
2768                        if ($ret[0] !== Type::T_CONTROL) {
2769                            $store = $this->env->store;
2770                            $this->popEnv();
2771                            $this->backPropagateEnv($store, [$for->var]);
2772                            return $ret;
2773                        }
2774
2775                        if ($ret[1]) {
2776                            break;
2777                        }
2778                    }
2779                }
2780
2781                $store = $this->env->store;
2782                $this->popEnv();
2783                $this->backPropagateEnv($store, [$for->var]);
2784
2785                break;
2786
2787            case Type::T_BREAK:
2788                return [Type::T_CONTROL, true];
2789
2790            case Type::T_CONTINUE:
2791                return [Type::T_CONTROL, false];
2792
2793            case Type::T_RETURN:
2794                return $this->reduce($child[1], true);
2795
2796            case Type::T_NESTED_PROPERTY:
2797                $this->compileNestedPropertiesBlock($child[1], $out);
2798                break;
2799
2800            case Type::T_INCLUDE:
2801                // including a mixin
2802                list(, $name, $argValues, $content, $argUsing) = $child;
2803
2804                $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2805
2806                if (! $mixin) {
2807                    $this->throwError("Undefined mixin $name");
2808                    break;
2809                }
2810
2811                $callingScope = $this->getStoreEnv();
2812
2813                // push scope, apply args
2814                $this->pushEnv();
2815                $this->env->depth--;
2816
2817                // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2818                // and assign this fake parent to childs
2819                $selfParent = null;
2820
2821                if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2822                    $selfParent = $child['selfParent'];
2823                } else {
2824                    $parentSelectors = $this->multiplySelectors($this->env);
2825
2826                    if ($parentSelectors) {
2827                        $parent = new Block();
2828                        $parent->selectors = $parentSelectors;
2829
2830                        foreach ($mixin->children as $k => $child) {
2831                            if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
2832                                $mixin->children[$k][1]->parent = $parent;
2833                            }
2834                        }
2835                    }
2836                }
2837
2838                // clone the stored content to not have its scope spoiled by a further call to the same mixin
2839                // i.e., recursive @include of the same mixin
2840                if (isset($content)) {
2841                    $copyContent = clone $content;
2842                    $copyContent->scope = clone $callingScope;
2843
2844                    $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2845                } else {
2846                    $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2847                }
2848
2849                // save the "using" argument list for applying it to when "@content" is invoked
2850                if (isset($argUsing)) {
2851                    $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
2852                } else {
2853                    $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
2854                }
2855
2856                if (isset($mixin->args)) {
2857                    $this->applyArguments($mixin->args, $argValues);
2858                }
2859
2860                $this->env->marker = 'mixin';
2861
2862                if (! empty($mixin->parentEnv)) {
2863                    $this->env->declarationScopeParent = $mixin->parentEnv;
2864                } else {
2865                    $this->throwError("@mixin $name() without parentEnv");
2866                }
2867
2868                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
2869
2870                $this->popEnv();
2871                break;
2872
2873            case Type::T_MIXIN_CONTENT:
2874                $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2875                $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
2876                $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
2877                $argContent = $child[1];
2878
2879                if (! $content) {
2880                    break;
2881                }
2882
2883                $storeEnv = $this->storeEnv;
2884                $varsUsing = [];
2885
2886                if (isset($argUsing) && isset($argContent)) {
2887                    // Get the arguments provided for the content with the names provided in the "using" argument list
2888                    $this->storeEnv = null;
2889                    $varsUsing = $this->applyArguments($argUsing, $argContent, false);
2890                }
2891
2892                // restore the scope from the @content
2893                $this->storeEnv = $content->scope;
2894
2895                // append the vars from using if any
2896                foreach ($varsUsing as $name => $val) {
2897                    $this->set($name, $val, true, $this->storeEnv);
2898                }
2899
2900                $this->compileChildrenNoReturn($content->children, $out);
2901
2902                $this->storeEnv = $storeEnv;
2903                break;
2904
2905            case Type::T_DEBUG:
2906                list(, $value) = $child;
2907
2908                $fname = $this->sourceNames[$this->sourceIndex];
2909                $line  = $this->sourceLine;
2910                $value = $this->compileValue($this->reduce($value, true));
2911
2912                fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
2913                break;
2914
2915            case Type::T_WARN:
2916                list(, $value) = $child;
2917
2918                $fname = $this->sourceNames[$this->sourceIndex];
2919                $line  = $this->sourceLine;
2920                $value = $this->compileValue($this->reduce($value, true));
2921
2922                fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
2923                break;
2924
2925            case Type::T_ERROR:
2926                list(, $value) = $child;
2927
2928                $fname = $this->sourceNames[$this->sourceIndex];
2929                $line  = $this->sourceLine;
2930                $value = $this->compileValue($this->reduce($value, true));
2931
2932                $this->throwError("File $fname on line $line ERROR: $value\n");
2933                break;
2934
2935            case Type::T_CONTROL:
2936                $this->throwError('@break/@continue not permitted in this scope');
2937                break;
2938
2939            default:
2940                $this->throwError("unknown child type: $child[0]");
2941        }
2942    }
2943
2944    /**
2945     * Reduce expression to string
2946     *
2947     * @param array $exp
2948     *
2949     * @return array
2950     */
2951    protected function expToString($exp)
2952    {
2953        list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
2954
2955        $content = [$this->reduce($left)];
2956
2957        if ($whiteLeft) {
2958            $content[] = ' ';
2959        }
2960
2961        $content[] = $op;
2962
2963        if ($whiteRight) {
2964            $content[] = ' ';
2965        }
2966
2967        $content[] = $this->reduce($right);
2968
2969        return [Type::T_STRING, '', $content];
2970    }
2971
2972    /**
2973     * Is truthy?
2974     *
2975     * @param array $value
2976     *
2977     * @return boolean
2978     */
2979    protected function isTruthy($value)
2980    {
2981        return $value !== static::$false && $value !== static::$null;
2982    }
2983
2984    /**
2985     * Is the value a direct relationship combinator?
2986     *
2987     * @param string $value
2988     *
2989     * @return boolean
2990     */
2991    protected function isImmediateRelationshipCombinator($value)
2992    {
2993        return $value === '>' || $value === '+' || $value === '~';
2994    }
2995
2996    /**
2997     * Should $value cause its operand to eval
2998     *
2999     * @param array $value
3000     *
3001     * @return boolean
3002     */
3003    protected function shouldEval($value)
3004    {
3005        switch ($value[0]) {
3006            case Type::T_EXPRESSION:
3007                if ($value[1] === '/') {
3008                    return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
3009                }
3010
3011                // fall-thru
3012            case Type::T_VARIABLE:
3013            case Type::T_FUNCTION_CALL:
3014                return true;
3015        }
3016
3017        return false;
3018    }
3019
3020    /**
3021     * Reduce value
3022     *
3023     * @param array   $value
3024     * @param boolean $inExp
3025     *
3026     * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
3027     */
3028    protected function reduce($value, $inExp = false)
3029    {
3030        if (\is_null($value)) {
3031            return null;
3032        }
3033
3034        switch ($value[0]) {
3035            case Type::T_EXPRESSION:
3036                list(, $op, $left, $right, $inParens) = $value;
3037
3038                $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
3039                $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
3040
3041                $left = $this->reduce($left, true);
3042
3043                if ($op !== 'and' && $op !== 'or') {
3044                    $right = $this->reduce($right, true);
3045                }
3046
3047                // special case: looks like css shorthand
3048                if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
3049                    (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
3050                    ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3051                ) {
3052                    return $this->expToString($value);
3053                }
3054
3055                $left  = $this->coerceForExpression($left);
3056                $right = $this->coerceForExpression($right);
3057                $ltype = $left[0];
3058                $rtype = $right[0];
3059
3060                $ucOpName = ucfirst($opName);
3061                $ucLType  = ucfirst($ltype);
3062                $ucRType  = ucfirst($rtype);
3063
3064                // this tries:
3065                // 1. op[op name][left type][right type]
3066                // 2. op[left type][right type] (passing the op as first arg
3067                // 3. op[op name]
3068                $fn = "op${ucOpName}${ucLType}${ucRType}";
3069
3070                if (\is_callable([$this, $fn]) ||
3071                    (($fn = "op${ucLType}${ucRType}") &&
3072                        \is_callable([$this, $fn]) &&
3073                        $passOp = true) ||
3074                    (($fn = "op${ucOpName}") &&
3075                        \is_callable([$this, $fn]) &&
3076                        $genOp = true)
3077                ) {
3078                    $coerceUnit = false;
3079
3080                    if (! isset($genOp) &&
3081                        $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
3082                    ) {
3083                        $coerceUnit = true;
3084
3085                        switch ($opName) {
3086                            case 'mul':
3087                                $targetUnit = $left[2];
3088
3089                                foreach ($right[2] as $unit => $exp) {
3090                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
3091                                }
3092                                break;
3093
3094                            case 'div':
3095                                $targetUnit = $left[2];
3096
3097                                foreach ($right[2] as $unit => $exp) {
3098                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
3099                                }
3100                                break;
3101
3102                            case 'mod':
3103                                $targetUnit = $left[2];
3104                                break;
3105
3106                            default:
3107                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
3108                        }
3109
3110                        $baseUnitLeft = $left->isNormalizable();
3111                        $baseUnitRight = $right->isNormalizable();
3112
3113                        if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) {
3114                            $left = $left->normalize();
3115                            $right = $right->normalize();
3116                        }
3117                        else {
3118                            if ($coerceUnit) {
3119                                $left = new Node\Number($left[1], []);
3120                            }
3121                        }
3122                    }
3123
3124                    $shouldEval = $inParens || $inExp;
3125
3126                    if (isset($passOp)) {
3127                        $out = $this->$fn($op, $left, $right, $shouldEval);
3128                    } else {
3129                        $out = $this->$fn($left, $right, $shouldEval);
3130                    }
3131
3132                    if (isset($out)) {
3133                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
3134                            $out = $out->coerce($targetUnit);
3135                        }
3136
3137                        return $out;
3138                    }
3139                }
3140
3141                return $this->expToString($value);
3142
3143            case Type::T_UNARY:
3144                list(, $op, $exp, $inParens) = $value;
3145
3146                $inExp = $inExp || $this->shouldEval($exp);
3147                $exp = $this->reduce($exp);
3148
3149                if ($exp[0] === Type::T_NUMBER) {
3150                    switch ($op) {
3151                        case '+':
3152                            return new Node\Number($exp[1], $exp[2]);
3153
3154                        case '-':
3155                            return new Node\Number(-$exp[1], $exp[2]);
3156                    }
3157                }
3158
3159                if ($op === 'not') {
3160                    if ($inExp || $inParens) {
3161                        if ($exp === static::$false || $exp === static::$null) {
3162                            return static::$true;
3163                        }
3164
3165                        return static::$false;
3166                    }
3167
3168                    $op = $op . ' ';
3169                }
3170
3171                return [Type::T_STRING, '', [$op, $exp]];
3172
3173            case Type::T_VARIABLE:
3174                return $this->reduce($this->get($value[1]));
3175
3176            case Type::T_LIST:
3177                foreach ($value[2] as &$item) {
3178                    $item = $this->reduce($item);
3179                }
3180
3181                return $value;
3182
3183            case Type::T_MAP:
3184                foreach ($value[1] as &$item) {
3185                    $item = $this->reduce($item);
3186                }
3187
3188                foreach ($value[2] as &$item) {
3189                    $item = $this->reduce($item);
3190                }
3191
3192                return $value;
3193
3194            case Type::T_STRING:
3195                foreach ($value[2] as &$item) {
3196                    if (\is_array($item) || $item instanceof \ArrayAccess) {
3197                        $item = $this->reduce($item);
3198                    }
3199                }
3200
3201                return $value;
3202
3203            case Type::T_INTERPOLATE:
3204                $value[1] = $this->reduce($value[1]);
3205
3206                if ($inExp) {
3207                    return $value[1];
3208                }
3209
3210                return $value;
3211
3212            case Type::T_FUNCTION_CALL:
3213                return $this->fncall($value[1], $value[2]);
3214
3215            case Type::T_SELF:
3216                $selfSelector = $this->multiplySelectors($this->env,!empty($this->env->block->selfParent) ? $this->env->block->selfParent : null);
3217                $selfSelector = $this->collapseSelectors($selfSelector, true);
3218
3219                return $selfSelector;
3220
3221            default:
3222                return $value;
3223        }
3224    }
3225
3226    /**
3227     * Function caller
3228     *
3229     * @param string $name
3230     * @param array  $argValues
3231     *
3232     * @return array|null
3233     */
3234    protected function fncall($name, $argValues)
3235    {
3236        // SCSS @function
3237        if ($this->callScssFunction($name, $argValues, $returnValue)) {
3238            return $returnValue;
3239        }
3240
3241        // native PHP functions
3242        if ($this->callNativeFunction($name, $argValues, $returnValue)) {
3243            return $returnValue;
3244        }
3245
3246        // for CSS functions, simply flatten the arguments into a list
3247        $listArgs = [];
3248
3249        foreach ((array) $argValues as $arg) {
3250            if (empty($arg[0])) {
3251                $listArgs[] = $this->reduce($arg[1]);
3252            }
3253        }
3254
3255        return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
3256    }
3257
3258    /**
3259     * Normalize name
3260     *
3261     * @param string $name
3262     *
3263     * @return string
3264     */
3265    protected function normalizeName($name)
3266    {
3267        return str_replace('-', '_', $name);
3268    }
3269
3270    /**
3271     * Normalize value
3272     *
3273     * @param array $value
3274     *
3275     * @return array
3276     */
3277    public function normalizeValue($value)
3278    {
3279        $value = $this->coerceForExpression($this->reduce($value));
3280
3281        switch ($value[0]) {
3282            case Type::T_LIST:
3283                $value = $this->extractInterpolation($value);
3284
3285                if ($value[0] !== Type::T_LIST) {
3286                    return [Type::T_KEYWORD, $this->compileValue($value)];
3287                }
3288
3289                foreach ($value[2] as $key => $item) {
3290                    $value[2][$key] = $this->normalizeValue($item);
3291                }
3292
3293                if (! empty($value['enclosing'])) {
3294                    unset($value['enclosing']);
3295                }
3296
3297                return $value;
3298
3299            case Type::T_STRING:
3300                return [$value[0], '"', [$this->compileStringContent($value)]];
3301
3302            case Type::T_NUMBER:
3303                return $value->normalize();
3304
3305            case Type::T_INTERPOLATE:
3306                return [Type::T_KEYWORD, $this->compileValue($value)];
3307
3308            default:
3309                return $value;
3310        }
3311    }
3312
3313    /**
3314     * Add numbers
3315     *
3316     * @param array $left
3317     * @param array $right
3318     *
3319     * @return \ScssPhp\ScssPhp\Node\Number
3320     */
3321    protected function opAddNumberNumber($left, $right)
3322    {
3323        return new Node\Number($left[1] + $right[1], $left[2]);
3324    }
3325
3326    /**
3327     * Multiply numbers
3328     *
3329     * @param array $left
3330     * @param array $right
3331     *
3332     * @return \ScssPhp\ScssPhp\Node\Number
3333     */
3334    protected function opMulNumberNumber($left, $right)
3335    {
3336        return new Node\Number($left[1] * $right[1], $left[2]);
3337    }
3338
3339    /**
3340     * Subtract numbers
3341     *
3342     * @param array $left
3343     * @param array $right
3344     *
3345     * @return \ScssPhp\ScssPhp\Node\Number
3346     */
3347    protected function opSubNumberNumber($left, $right)
3348    {
3349        return new Node\Number($left[1] - $right[1], $left[2]);
3350    }
3351
3352    /**
3353     * Divide numbers
3354     *
3355     * @param array $left
3356     * @param array $right
3357     *
3358     * @return array|\ScssPhp\ScssPhp\Node\Number
3359     */
3360    protected function opDivNumberNumber($left, $right)
3361    {
3362        if ($right[1] == 0) {
3363            return ($left[1] == 0) ? static::$NaN : static::$Infinity;
3364        }
3365
3366        return new Node\Number($left[1] / $right[1], $left[2]);
3367    }
3368
3369    /**
3370     * Mod numbers
3371     *
3372     * @param array $left
3373     * @param array $right
3374     *
3375     * @return \ScssPhp\ScssPhp\Node\Number
3376     */
3377    protected function opModNumberNumber($left, $right)
3378    {
3379        if ($right[1] == 0) {
3380            return static::$NaN;
3381        }
3382
3383        return new Node\Number($left[1] % $right[1], $left[2]);
3384    }
3385
3386    /**
3387     * Add strings
3388     *
3389     * @param array $left
3390     * @param array $right
3391     *
3392     * @return array|null
3393     */
3394    protected function opAdd($left, $right)
3395    {
3396        if ($strLeft = $this->coerceString($left)) {
3397            if ($right[0] === Type::T_STRING) {
3398                $right[1] = '';
3399            }
3400
3401            $strLeft[2][] = $right;
3402
3403            return $strLeft;
3404        }
3405
3406        if ($strRight = $this->coerceString($right)) {
3407            if ($left[0] === Type::T_STRING) {
3408                $left[1] = '';
3409            }
3410
3411            array_unshift($strRight[2], $left);
3412
3413            return $strRight;
3414        }
3415
3416        return null;
3417    }
3418
3419    /**
3420     * Boolean and
3421     *
3422     * @param array   $left
3423     * @param array   $right
3424     * @param boolean $shouldEval
3425     *
3426     * @return array|null
3427     */
3428    protected function opAnd($left, $right, $shouldEval)
3429    {
3430        $truthy = ($left === static::$null || $right === static::$null) ||
3431                  ($left === static::$false || $left === static::$true) &&
3432                  ($right === static::$false || $right === static::$true);
3433
3434        if (! $shouldEval) {
3435            if (! $truthy) {
3436                return null;
3437            }
3438        }
3439
3440        if ($left !== static::$false && $left !== static::$null) {
3441            return $this->reduce($right, true);
3442        }
3443
3444        return $left;
3445    }
3446
3447    /**
3448     * Boolean or
3449     *
3450     * @param array   $left
3451     * @param array   $right
3452     * @param boolean $shouldEval
3453     *
3454     * @return array|null
3455     */
3456    protected function opOr($left, $right, $shouldEval)
3457    {
3458        $truthy = ($left === static::$null || $right === static::$null) ||
3459                  ($left === static::$false || $left === static::$true) &&
3460                  ($right === static::$false || $right === static::$true);
3461
3462        if (! $shouldEval) {
3463            if (! $truthy) {
3464                return null;
3465            }
3466        }
3467
3468        if ($left !== static::$false && $left !== static::$null) {
3469            return $left;
3470        }
3471
3472        return $this->reduce($right, true);
3473    }
3474
3475    /**
3476     * Compare colors
3477     *
3478     * @param string $op
3479     * @param array  $left
3480     * @param array  $right
3481     *
3482     * @return array
3483     */
3484    protected function opColorColor($op, $left, $right)
3485    {
3486        $out = [Type::T_COLOR];
3487
3488        foreach ([1, 2, 3] as $i) {
3489            $lval = isset($left[$i]) ? $left[$i] : 0;
3490            $rval = isset($right[$i]) ? $right[$i] : 0;
3491
3492            switch ($op) {
3493                case '+':
3494                    $out[] = $lval + $rval;
3495                    break;
3496
3497                case '-':
3498                    $out[] = $lval - $rval;
3499                    break;
3500
3501                case '*':
3502                    $out[] = $lval * $rval;
3503                    break;
3504
3505                case '%':
3506                    $out[] = $lval % $rval;
3507                    break;
3508
3509                case '/':
3510                    if ($rval == 0) {
3511                        $this->throwError("color: Can't divide by zero");
3512                        break 2;
3513                    }
3514
3515                    $out[] = (int) ($lval / $rval);
3516                    break;
3517
3518                case '==':
3519                    return $this->opEq($left, $right);
3520
3521                case '!=':
3522                    return $this->opNeq($left, $right);
3523
3524                default:
3525                    $this->throwError("color: unknown op $op");
3526                    break 2;
3527            }
3528        }
3529
3530        if (isset($left[4])) {
3531            $out[4] = $left[4];
3532        } elseif (isset($right[4])) {
3533            $out[4] = $right[4];
3534        }
3535
3536        return $this->fixColor($out);
3537    }
3538
3539    /**
3540     * Compare color and number
3541     *
3542     * @param string $op
3543     * @param array  $left
3544     * @param array  $right
3545     *
3546     * @return array
3547     */
3548    protected function opColorNumber($op, $left, $right)
3549    {
3550        $value = $right[1];
3551
3552        return $this->opColorColor(
3553            $op,
3554            $left,
3555            [Type::T_COLOR, $value, $value, $value]
3556        );
3557    }
3558
3559    /**
3560     * Compare number and color
3561     *
3562     * @param string $op
3563     * @param array  $left
3564     * @param array  $right
3565     *
3566     * @return array
3567     */
3568    protected function opNumberColor($op, $left, $right)
3569    {
3570        $value = $left[1];
3571
3572        return $this->opColorColor(
3573            $op,
3574            [Type::T_COLOR, $value, $value, $value],
3575            $right
3576        );
3577    }
3578
3579    /**
3580     * Compare number1 == number2
3581     *
3582     * @param array $left
3583     * @param array $right
3584     *
3585     * @return array
3586     */
3587    protected function opEq($left, $right)
3588    {
3589        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3590            $lStr[1] = '';
3591            $rStr[1] = '';
3592
3593            $left = $this->compileValue($lStr);
3594            $right = $this->compileValue($rStr);
3595        }
3596
3597        return $this->toBool($left === $right);
3598    }
3599
3600    /**
3601     * Compare number1 != number2
3602     *
3603     * @param array $left
3604     * @param array $right
3605     *
3606     * @return array
3607     */
3608    protected function opNeq($left, $right)
3609    {
3610        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3611            $lStr[1] = '';
3612            $rStr[1] = '';
3613
3614            $left = $this->compileValue($lStr);
3615            $right = $this->compileValue($rStr);
3616        }
3617
3618        return $this->toBool($left !== $right);
3619    }
3620
3621    /**
3622     * Compare number1 >= number2
3623     *
3624     * @param array $left
3625     * @param array $right
3626     *
3627     * @return array
3628     */
3629    protected function opGteNumberNumber($left, $right)
3630    {
3631        return $this->toBool($left[1] >= $right[1]);
3632    }
3633
3634    /**
3635     * Compare number1 > number2
3636     *
3637     * @param array $left
3638     * @param array $right
3639     *
3640     * @return array
3641     */
3642    protected function opGtNumberNumber($left, $right)
3643    {
3644        return $this->toBool($left[1] > $right[1]);
3645    }
3646
3647    /**
3648     * Compare number1 <= number2
3649     *
3650     * @param array $left
3651     * @param array $right
3652     *
3653     * @return array
3654     */
3655    protected function opLteNumberNumber($left, $right)
3656    {
3657        return $this->toBool($left[1] <= $right[1]);
3658    }
3659
3660    /**
3661     * Compare number1 < number2
3662     *
3663     * @param array $left
3664     * @param array $right
3665     *
3666     * @return array
3667     */
3668    protected function opLtNumberNumber($left, $right)
3669    {
3670        return $this->toBool($left[1] < $right[1]);
3671    }
3672
3673    /**
3674     * Three-way comparison, aka spaceship operator
3675     *
3676     * @param array $left
3677     * @param array $right
3678     *
3679     * @return \ScssPhp\ScssPhp\Node\Number
3680     */
3681    protected function opCmpNumberNumber($left, $right)
3682    {
3683        $n = $left[1] - $right[1];
3684
3685        return new Node\Number($n ? $n / abs($n) : 0, '');
3686    }
3687
3688    /**
3689     * Cast to boolean
3690     *
3691     * @api
3692     *
3693     * @param mixed $thing
3694     *
3695     * @return array
3696     */
3697    public function toBool($thing)
3698    {
3699        return $thing ? static::$true : static::$false;
3700    }
3701
3702    /**
3703     * Compiles a primitive value into a CSS property value.
3704     *
3705     * Values in scssphp are typed by being wrapped in arrays, their format is
3706     * typically:
3707     *
3708     *     array(type, contents [, additional_contents]*)
3709     *
3710     * The input is expected to be reduced. This function will not work on
3711     * things like expressions and variables.
3712     *
3713     * @api
3714     *
3715     * @param array $value
3716     *
3717     * @return string|array
3718     */
3719    public function compileValue($value)
3720    {
3721        $value = $this->reduce($value);
3722
3723        switch ($value[0]) {
3724            case Type::T_KEYWORD:
3725                return $value[1];
3726
3727            case Type::T_COLOR:
3728                // [1] - red component (either number for a %)
3729                // [2] - green component
3730                // [3] - blue component
3731                // [4] - optional alpha component
3732                list(, $r, $g, $b) = $value;
3733
3734                $r = $this->compileRGBAValue($r);
3735                $g = $this->compileRGBAValue($g);
3736                $b = $this->compileRGBAValue($b);
3737
3738                if (\count($value) === 5) {
3739                    $alpha = $this->compileRGBAValue($value[4], true);
3740
3741                    if (! is_numeric($alpha) || $alpha < 1) {
3742                        $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
3743
3744                        if (! \is_null($colorName)) {
3745                            return $colorName;
3746                        }
3747
3748                        if (is_numeric($alpha)) {
3749                            $a = new Node\Number($alpha, '');
3750                        } else {
3751                            $a = $alpha;
3752                        }
3753
3754                        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
3755                    }
3756                }
3757
3758                if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
3759                    return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
3760                }
3761
3762                $colorName = Colors::RGBaToColorName($r, $g, $b);
3763
3764                if (! \is_null($colorName)) {
3765                    return $colorName;
3766                }
3767
3768                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
3769
3770                // Converting hex color to short notation (e.g. #003399 to #039)
3771                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
3772                    $h = '#' . $h[1] . $h[3] . $h[5];
3773                }
3774
3775                return $h;
3776
3777            case Type::T_NUMBER:
3778                return $value->output($this);
3779
3780            case Type::T_STRING:
3781                return $value[1] . $this->compileStringContent($value) . $value[1];
3782
3783            case Type::T_FUNCTION:
3784                $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
3785
3786                return "$value[1]($args)";
3787
3788            case Type::T_LIST:
3789                $value = $this->extractInterpolation($value);
3790
3791                if ($value[0] !== Type::T_LIST) {
3792                    return $this->compileValue($value);
3793                }
3794
3795                list(, $delim, $items) = $value;
3796                $pre = $post = "";
3797
3798                if (! empty($value['enclosing'])) {
3799                    switch ($value['enclosing']) {
3800                        case 'parent':
3801                            //$pre = "(";
3802                            //$post = ")";
3803                            break;
3804                        case 'forced_parent':
3805                            $pre = "(";
3806                            $post = ")";
3807                            break;
3808                        case 'bracket':
3809                        case 'forced_bracket':
3810                            $pre = "[";
3811                            $post = "]";
3812                            break;
3813                    }
3814                }
3815
3816                $prefix_value = '';
3817                if ($delim !== ' ') {
3818                    $prefix_value = ' ';
3819                }
3820
3821                $filtered = [];
3822
3823                foreach ($items as $item) {
3824                    if ($item[0] === Type::T_NULL) {
3825                        continue;
3826                    }
3827
3828                    $compiled = $this->compileValue($item);
3829                    if ($prefix_value && \strlen($compiled)) {
3830                        $compiled = $prefix_value . $compiled;
3831                    }
3832                    $filtered[] = $compiled;
3833                }
3834
3835                return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post;
3836
3837            case Type::T_MAP:
3838                $keys     = $value[1];
3839                $values   = $value[2];
3840                $filtered = [];
3841
3842                for ($i = 0, $s = \count($keys); $i < $s; $i++) {
3843                    $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
3844                }
3845
3846                array_walk($filtered, function (&$value, $key) {
3847                    $value = $key . ': ' . $value;
3848                });
3849
3850                return '(' . implode(', ', $filtered) . ')';
3851
3852            case Type::T_INTERPOLATED:
3853                // node created by extractInterpolation
3854                list(, $interpolate, $left, $right) = $value;
3855                list(,, $whiteLeft, $whiteRight) = $interpolate;
3856
3857                $delim = $left[1];
3858
3859                if ($delim && $delim !== ' ' && ! $whiteLeft) {
3860                    $delim .= ' ';
3861                }
3862
3863                $left = \count($left[2]) > 0 ?
3864                    $this->compileValue($left) . $delim . $whiteLeft: '';
3865
3866                $delim = $right[1];
3867
3868                if ($delim && $delim !== ' ') {
3869                    $delim .= ' ';
3870                }
3871
3872                $right = \count($right[2]) > 0 ?
3873                    $whiteRight . $delim . $this->compileValue($right) : '';
3874
3875                return $left . $this->compileValue($interpolate) . $right;
3876
3877            case Type::T_INTERPOLATE:
3878                // strip quotes if it's a string
3879                $reduced = $this->reduce($value[1]);
3880
3881                switch ($reduced[0]) {
3882                    case Type::T_LIST:
3883                        $reduced = $this->extractInterpolation($reduced);
3884
3885                        if ($reduced[0] !== Type::T_LIST) {
3886                            break;
3887                        }
3888
3889                        list(, $delim, $items) = $reduced;
3890
3891                        if ($delim !== ' ') {
3892                            $delim .= ' ';
3893                        }
3894
3895                        $filtered = [];
3896
3897                        foreach ($items as $item) {
3898                            if ($item[0] === Type::T_NULL) {
3899                                continue;
3900                            }
3901
3902                            $temp = $this->compileValue([Type::T_KEYWORD, $item]);
3903
3904                            if ($temp[0] === Type::T_STRING) {
3905                                $filtered[] = $this->compileStringContent($temp);
3906                            } elseif ($temp[0] === Type::T_KEYWORD) {
3907                                $filtered[] = $temp[1];
3908                            } else {
3909                                $filtered[] = $this->compileValue($temp);
3910                            }
3911                        }
3912
3913                        $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
3914                        break;
3915
3916                    case Type::T_STRING:
3917                        $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
3918                        break;
3919
3920                    case Type::T_NULL:
3921                        $reduced = [Type::T_KEYWORD, ''];
3922                }
3923
3924                return $this->compileValue($reduced);
3925
3926            case Type::T_NULL:
3927                return 'null';
3928
3929            case Type::T_COMMENT:
3930                return $this->compileCommentValue($value);
3931
3932            default:
3933                $this->throwError("unknown value type: ".json_encode($value));
3934        }
3935    }
3936
3937    /**
3938     * Flatten list
3939     *
3940     * @param array $list
3941     *
3942     * @return string
3943     */
3944    protected function flattenList($list)
3945    {
3946        return $this->compileValue($list);
3947    }
3948
3949    /**
3950     * Compile string content
3951     *
3952     * @param array $string
3953     *
3954     * @return string
3955     */
3956    protected function compileStringContent($string)
3957    {
3958        $parts = [];
3959
3960        foreach ($string[2] as $part) {
3961            if (\is_array($part) || $part instanceof \ArrayAccess) {
3962                $parts[] = $this->compileValue($part);
3963            } else {
3964                $parts[] = $part;
3965            }
3966        }
3967
3968        return implode($parts);
3969    }
3970
3971    /**
3972     * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
3973     *
3974     * @param array $list
3975     *
3976     * @return array
3977     */
3978    protected function extractInterpolation($list)
3979    {
3980        $items = $list[2];
3981
3982        foreach ($items as $i => $item) {
3983            if ($item[0] === Type::T_INTERPOLATE) {
3984                $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
3985                $after  = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
3986
3987                return [Type::T_INTERPOLATED, $item, $before, $after];
3988            }
3989        }
3990
3991        return $list;
3992    }
3993
3994    /**
3995     * Find the final set of selectors
3996     *
3997     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3998     * @param \ScssPhp\ScssPhp\Block                $selfParent
3999     *
4000     * @return array
4001     */
4002    protected function multiplySelectors(Environment $env, $selfParent = null)
4003    {
4004        $envs            = $this->compactEnv($env);
4005        $selectors       = [];
4006        $parentSelectors = [[]];
4007
4008        $selfParentSelectors = null;
4009
4010        if (! \is_null($selfParent) && $selfParent->selectors) {
4011            $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
4012        }
4013
4014        while ($env = array_pop($envs)) {
4015            if (empty($env->selectors)) {
4016                continue;
4017            }
4018
4019            $selectors = $env->selectors;
4020
4021            do {
4022                $stillHasSelf  = false;
4023                $prevSelectors = $selectors;
4024                $selectors     = [];
4025
4026                foreach ($prevSelectors as $selector) {
4027                    foreach ($parentSelectors as $parent) {
4028                        if ($selfParentSelectors) {
4029                            foreach ($selfParentSelectors as $selfParent) {
4030                                // if no '&' in the selector, each call will give same result, only add once
4031                                $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
4032                                $selectors[serialize($s)] = $s;
4033                            }
4034                        } else {
4035                            $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
4036                            $selectors[serialize($s)] = $s;
4037                        }
4038                    }
4039                }
4040            } while ($stillHasSelf);
4041
4042            $parentSelectors = $selectors;
4043        }
4044
4045        $selectors = array_values($selectors);
4046
4047        // case we are just starting a at-root : nothing to multiply but parentSelectors
4048        if (!$selectors and $selfParentSelectors) {
4049            $selectors = $selfParentSelectors;
4050        }
4051
4052        return $selectors;
4053    }
4054
4055    /**
4056     * Join selectors; looks for & to replace, or append parent before child
4057     *
4058     * @param array   $parent
4059     * @param array   $child
4060     * @param boolean $stillHasSelf
4061     * @param array   $selfParentSelectors
4062
4063     * @return array
4064     */
4065    protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4066    {
4067        $setSelf = false;
4068        $out = [];
4069
4070        foreach ($child as $part) {
4071            $newPart = [];
4072
4073            foreach ($part as $p) {
4074                // only replace & once and should be recalled to be able to make combinations
4075                if ($p === static::$selfSelector && $setSelf) {
4076                    $stillHasSelf = true;
4077                }
4078
4079                if ($p === static::$selfSelector && ! $setSelf) {
4080                    $setSelf = true;
4081
4082                    if (\is_null($selfParentSelectors)) {
4083                        $selfParentSelectors = $parent;
4084                    }
4085
4086                    foreach ($selfParentSelectors as $i => $parentPart) {
4087                        if ($i > 0) {
4088                            $out[] = $newPart;
4089                            $newPart = [];
4090                        }
4091
4092                        foreach ($parentPart as $pp) {
4093                            if (\is_array($pp)) {
4094                                $flatten = [];
4095
4096                                array_walk_recursive($pp, function ($a) use (&$flatten) {
4097                                    $flatten[] = $a;
4098                                });
4099
4100                                $pp = implode($flatten);
4101                            }
4102
4103                            $newPart[] = $pp;
4104                        }
4105                    }
4106                } else {
4107                    $newPart[] = $p;
4108                }
4109            }
4110
4111            $out[] = $newPart;
4112        }
4113
4114        return $setSelf ? $out : array_merge($parent, $child);
4115    }
4116
4117    /**
4118     * Multiply media
4119     *
4120     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4121     * @param array                                 $childQueries
4122     *
4123     * @return array
4124     */
4125    protected function multiplyMedia(Environment $env = null, $childQueries = null)
4126    {
4127        if (! isset($env) ||
4128            ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
4129        ) {
4130            return $childQueries;
4131        }
4132
4133        // plain old block, skip
4134        if (empty($env->block->type)) {
4135            return $this->multiplyMedia($env->parent, $childQueries);
4136        }
4137
4138        $parentQueries = isset($env->block->queryList)
4139            ? $env->block->queryList
4140            : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
4141
4142        $store = [$this->env, $this->storeEnv];
4143
4144        $this->env      = $env;
4145        $this->storeEnv = null;
4146        $parentQueries  = $this->evaluateMediaQuery($parentQueries);
4147
4148        list($this->env, $this->storeEnv) = $store;
4149
4150        if (\is_null($childQueries)) {
4151            $childQueries = $parentQueries;
4152        } else {
4153            $originalQueries = $childQueries;
4154            $childQueries = [];
4155
4156            foreach ($parentQueries as $parentQuery) {
4157                foreach ($originalQueries as $childQuery) {
4158                    $childQueries[] = array_merge(
4159                        $parentQuery,
4160                        [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
4161                        $childQuery
4162                    );
4163                }
4164            }
4165        }
4166
4167        return $this->multiplyMedia($env->parent, $childQueries);
4168    }
4169
4170    /**
4171     * Convert env linked list to stack
4172     *
4173     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4174     *
4175     * @return array
4176     */
4177    protected function compactEnv(Environment $env)
4178    {
4179        for ($envs = []; $env; $env = $env->parent) {
4180            $envs[] = $env;
4181        }
4182
4183        return $envs;
4184    }
4185
4186    /**
4187     * Convert env stack to singly linked list
4188     *
4189     * @param array $envs
4190     *
4191     * @return \ScssPhp\ScssPhp\Compiler\Environment
4192     */
4193    protected function extractEnv($envs)
4194    {
4195        for ($env = null; $e = array_pop($envs);) {
4196            $e->parent = $env;
4197            $env = $e;
4198        }
4199
4200        return $env;
4201    }
4202
4203    /**
4204     * Push environment
4205     *
4206     * @param \ScssPhp\ScssPhp\Block $block
4207     *
4208     * @return \ScssPhp\ScssPhp\Compiler\Environment
4209     */
4210    protected function pushEnv(Block $block = null)
4211    {
4212        $env = new Environment;
4213        $env->parent = $this->env;
4214        $env->parentStore = $this->storeEnv;
4215        $env->store  = [];
4216        $env->block  = $block;
4217        $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
4218
4219        $this->env = $env;
4220        $this->storeEnv = null;
4221
4222        return $env;
4223    }
4224
4225    /**
4226     * Pop environment
4227     */
4228    protected function popEnv()
4229    {
4230        $this->storeEnv = $this->env->parentStore;
4231        $this->env = $this->env->parent;
4232    }
4233
4234    /**
4235     * Propagate vars from a just poped Env (used in @each and @for)
4236     *
4237     * @param array      $store
4238     * @param null|array $excludedVars
4239     */
4240    protected function backPropagateEnv($store, $excludedVars = null)
4241    {
4242        foreach ($store as $key => $value) {
4243            if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
4244                $this->set($key, $value, true);
4245            }
4246        }
4247    }
4248
4249    /**
4250     * Get store environment
4251     *
4252     * @return \ScssPhp\ScssPhp\Compiler\Environment
4253     */
4254    protected function getStoreEnv()
4255    {
4256        return isset($this->storeEnv) ? $this->storeEnv : $this->env;
4257    }
4258
4259    /**
4260     * Set variable
4261     *
4262     * @param string                                $name
4263     * @param mixed                                 $value
4264     * @param boolean                               $shadow
4265     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4266     * @param mixed                                 $valueUnreduced
4267     */
4268    protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
4269    {
4270        $name = $this->normalizeName($name);
4271
4272        if (! isset($env)) {
4273            $env = $this->getStoreEnv();
4274        }
4275
4276        if ($shadow) {
4277            $this->setRaw($name, $value, $env, $valueUnreduced);
4278        } else {
4279            $this->setExisting($name, $value, $env, $valueUnreduced);
4280        }
4281    }
4282
4283    /**
4284     * Set existing variable
4285     *
4286     * @param string                                $name
4287     * @param mixed                                 $value
4288     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4289     * @param mixed                                 $valueUnreduced
4290     */
4291    protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4292    {
4293        $storeEnv = $env;
4294        $specialContentKey = static::$namespaces['special'] . 'content';
4295
4296        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
4297
4298        $maxDepth = 10000;
4299
4300        for (;;) {
4301            if ($maxDepth-- <= 0) {
4302                break;
4303            }
4304
4305            if (\array_key_exists($name, $env->store)) {
4306                break;
4307            }
4308
4309            if (! $hasNamespace && isset($env->marker)) {
4310                if (! empty($env->store[$specialContentKey])) {
4311                    $env = $env->store[$specialContentKey]->scope;
4312                    continue;
4313                }
4314
4315                if (! empty($env->declarationScopeParent)) {
4316                    $env = $env->declarationScopeParent;
4317                    continue;
4318                } else {
4319                    $env = $storeEnv;
4320                    break;
4321                }
4322            }
4323
4324            if (isset($env->parentStore)) {
4325                $env = $env->parentStore;
4326            } elseif (isset($env->parent)) {
4327                $env = $env->parent;
4328            } else {
4329                $env = $storeEnv;
4330                break;
4331            }
4332        }
4333
4334        $env->store[$name] = $value;
4335
4336        if ($valueUnreduced) {
4337            $env->storeUnreduced[$name] = $valueUnreduced;
4338        }
4339    }
4340
4341    /**
4342     * Set raw variable
4343     *
4344     * @param string                                $name
4345     * @param mixed                                 $value
4346     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4347     * @param mixed                                 $valueUnreduced
4348     */
4349    protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
4350    {
4351        $env->store[$name] = $value;
4352
4353        if ($valueUnreduced) {
4354            $env->storeUnreduced[$name] = $valueUnreduced;
4355        }
4356    }
4357
4358    /**
4359     * Get variable
4360     *
4361     * @api
4362     *
4363     * @param string                                $name
4364     * @param boolean                               $shouldThrow
4365     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4366     * @param boolean                               $unreduced
4367     *
4368     * @return mixed|null
4369     */
4370    public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
4371    {
4372        $normalizedName = $this->normalizeName($name);
4373        $specialContentKey = static::$namespaces['special'] . 'content';
4374
4375        if (! isset($env)) {
4376            $env = $this->getStoreEnv();
4377        }
4378
4379        $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
4380
4381        $maxDepth = 10000;
4382
4383        for (;;) {
4384            if ($maxDepth-- <= 0) {
4385                break;
4386            }
4387
4388            if (\array_key_exists($normalizedName, $env->store)) {
4389                if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
4390                    return $env->storeUnreduced[$normalizedName];
4391                }
4392
4393                return $env->store[$normalizedName];
4394            }
4395
4396            if (! $hasNamespace && isset($env->marker)) {
4397                if (! empty($env->store[$specialContentKey])) {
4398                    $env = $env->store[$specialContentKey]->scope;
4399                    continue;
4400                }
4401
4402                if (! empty($env->declarationScopeParent)) {
4403                    $env = $env->declarationScopeParent;
4404                } else {
4405                    $env = $this->rootEnv;
4406                }
4407                continue;
4408            }
4409
4410            if (isset($env->parentStore)) {
4411                $env = $env->parentStore;
4412            } elseif (isset($env->parent)) {
4413                $env = $env->parent;
4414            } else {
4415                break;
4416            }
4417        }
4418
4419        if ($shouldThrow) {
4420            $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
4421        }
4422
4423        // found nothing
4424        return null;
4425    }
4426
4427    /**
4428     * Has variable?
4429     *
4430     * @param string                                $name
4431     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4432     *
4433     * @return boolean
4434     */
4435    protected function has($name, Environment $env = null)
4436    {
4437        return ! \is_null($this->get($name, false, $env));
4438    }
4439
4440    /**
4441     * Inject variables
4442     *
4443     * @param array $args
4444     */
4445    protected function injectVariables(array $args)
4446    {
4447        if (empty($args)) {
4448            return;
4449        }
4450
4451        $parser = $this->parserFactory(__METHOD__);
4452
4453        foreach ($args as $name => $strValue) {
4454            if ($name[0] === '$') {
4455                $name = substr($name, 1);
4456            }
4457
4458            if (! $parser->parseValue($strValue, $value)) {
4459                $value = $this->coerceValue($strValue);
4460            }
4461
4462            $this->set($name, $value);
4463        }
4464    }
4465
4466    /**
4467     * Set variables
4468     *
4469     * @api
4470     *
4471     * @param array $variables
4472     */
4473    public function setVariables(array $variables)
4474    {
4475        $this->registeredVars = array_merge($this->registeredVars, $variables);
4476    }
4477
4478    /**
4479     * Unset variable
4480     *
4481     * @api
4482     *
4483     * @param string $name
4484     */
4485    public function unsetVariable($name)
4486    {
4487        unset($this->registeredVars[$name]);
4488    }
4489
4490    /**
4491     * Returns list of variables
4492     *
4493     * @api
4494     *
4495     * @return array
4496     */
4497    public function getVariables()
4498    {
4499        return $this->registeredVars;
4500    }
4501
4502    /**
4503     * Adds to list of parsed files
4504     *
4505     * @api
4506     *
4507     * @param string $path
4508     */
4509    public function addParsedFile($path)
4510    {
4511        if (isset($path) && is_file($path)) {
4512            $this->parsedFiles[realpath($path)] = filemtime($path);
4513        }
4514    }
4515
4516    /**
4517     * Returns list of parsed files
4518     *
4519     * @api
4520     *
4521     * @return array
4522     */
4523    public function getParsedFiles()
4524    {
4525        return $this->parsedFiles;
4526    }
4527
4528    /**
4529     * Add import path
4530     *
4531     * @api
4532     *
4533     * @param string|callable $path
4534     */
4535    public function addImportPath($path)
4536    {
4537        if (! \in_array($path, $this->importPaths)) {
4538            $this->importPaths[] = $path;
4539        }
4540    }
4541
4542    /**
4543     * Set import paths
4544     *
4545     * @api
4546     *
4547     * @param string|array $path
4548     */
4549    public function setImportPaths($path)
4550    {
4551        $this->importPaths = (array) $path;
4552    }
4553
4554    /**
4555     * Set number precision
4556     *
4557     * @api
4558     *
4559     * @param integer $numberPrecision
4560     */
4561    public function setNumberPrecision($numberPrecision)
4562    {
4563        Node\Number::$precision = $numberPrecision;
4564    }
4565
4566    /**
4567     * Set formatter
4568     *
4569     * @api
4570     *
4571     * @param string $formatterName
4572     */
4573    public function setFormatter($formatterName)
4574    {
4575        $this->formatter = $formatterName;
4576    }
4577
4578    /**
4579     * Set line number style
4580     *
4581     * @api
4582     *
4583     * @param string $lineNumberStyle
4584     */
4585    public function setLineNumberStyle($lineNumberStyle)
4586    {
4587        $this->lineNumberStyle = $lineNumberStyle;
4588    }
4589
4590    /**
4591     * Enable/disable source maps
4592     *
4593     * @api
4594     *
4595     * @param integer $sourceMap
4596     */
4597    public function setSourceMap($sourceMap)
4598    {
4599        $this->sourceMap = $sourceMap;
4600    }
4601
4602    /**
4603     * Set source map options
4604     *
4605     * @api
4606     *
4607     * @param array $sourceMapOptions
4608     */
4609    public function setSourceMapOptions($sourceMapOptions)
4610    {
4611        $this->sourceMapOptions = $sourceMapOptions;
4612    }
4613
4614    /**
4615     * Register function
4616     *
4617     * @api
4618     *
4619     * @param string   $name
4620     * @param callable $func
4621     * @param array    $prototype
4622     */
4623    public function registerFunction($name, $func, $prototype = null)
4624    {
4625        $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
4626    }
4627
4628    /**
4629     * Unregister function
4630     *
4631     * @api
4632     *
4633     * @param string $name
4634     */
4635    public function unregisterFunction($name)
4636    {
4637        unset($this->userFunctions[$this->normalizeName($name)]);
4638    }
4639
4640    /**
4641     * Add feature
4642     *
4643     * @api
4644     *
4645     * @param string $name
4646     */
4647    public function addFeature($name)
4648    {
4649        $this->registeredFeatures[$name] = true;
4650    }
4651
4652    /**
4653     * Import file
4654     *
4655     * @param string                                 $path
4656     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
4657     */
4658    protected function importFile($path, OutputBlock $out)
4659    {
4660        $this->pushCallStack('import '.$path);
4661        // see if tree is cached
4662        $realPath = realpath($path);
4663
4664        if (isset($this->importCache[$realPath])) {
4665            $this->handleImportLoop($realPath);
4666
4667            $tree = $this->importCache[$realPath];
4668        } else {
4669            $code   = file_get_contents($path);
4670            $parser = $this->parserFactory($path);
4671            $tree   = $parser->parse($code);
4672
4673            $this->importCache[$realPath] = $tree;
4674        }
4675
4676        $pi = pathinfo($path);
4677
4678        array_unshift($this->importPaths, $pi['dirname']);
4679        $this->compileChildrenNoReturn($tree->children, $out);
4680        array_shift($this->importPaths);
4681        $this->popCallStack();
4682    }
4683
4684    /**
4685     * Return the file path for an import url if it exists
4686     *
4687     * @api
4688     *
4689     * @param string $url
4690     *
4691     * @return string|null
4692     */
4693    public function findImport($url)
4694    {
4695        $urls = [];
4696
4697        $hasExtension = preg_match('/[.]s?css$/', $url);
4698
4699        // for "normal" scss imports (ignore vanilla css and external requests)
4700        if (! preg_match('~\.css$|^https?://|^//~', $url)) {
4701            $isPartial = (strpos(basename($url), '_') === 0);
4702
4703            // try both normal and the _partial filename
4704            $urls = [$url . ($hasExtension ? '' : '.scss')];
4705
4706            if (! $isPartial) {
4707                $urls[] = preg_replace('~[^/]+$~', '_\0', $url) . ($hasExtension ? '' : '.scss');
4708            }
4709
4710            if (! $hasExtension) {
4711                $urls[] = "$url/index.scss";
4712                $urls[] = "$url/_index.scss";
4713                // allow to find a plain css file, *if* no scss or partial scss is found
4714                $urls[] .= $url . ".css";
4715            }
4716        }
4717
4718        foreach ($this->importPaths as $dir) {
4719            if (\is_string($dir)) {
4720                // check urls for normal import paths
4721                foreach ($urls as $full) {
4722                    $separator = (
4723                        ! empty($dir) &&
4724                        substr($dir, -1) !== '/' &&
4725                        substr($full, 0, 1) !== '/'
4726                    ) ? '/' : '';
4727                    $full = $dir . $separator . $full;
4728
4729                    if (is_file($file = $full)) {
4730                        return $file;
4731                    }
4732                }
4733            } elseif (\is_callable($dir)) {
4734                // check custom callback for import path
4735                $file = \call_user_func($dir, $url);
4736
4737                if (! \is_null($file)) {
4738                    return $file;
4739                }
4740            }
4741        }
4742
4743        if ($urls) {
4744            if (! $hasExtension or preg_match('/[.]scss$/', $url)) {
4745                $this->throwError("`$url` file not found for @import");
4746            }
4747        }
4748
4749        return null;
4750    }
4751
4752    /**
4753     * Set encoding
4754     *
4755     * @api
4756     *
4757     * @param string $encoding
4758     */
4759    public function setEncoding($encoding)
4760    {
4761        $this->encoding = $encoding;
4762    }
4763
4764    /**
4765     * Ignore errors?
4766     *
4767     * @api
4768     *
4769     * @param boolean $ignoreErrors
4770     *
4771     * @return \ScssPhp\ScssPhp\Compiler
4772     */
4773    public function setIgnoreErrors($ignoreErrors)
4774    {
4775        $this->ignoreErrors = $ignoreErrors;
4776
4777        return $this;
4778    }
4779
4780    /**
4781     * Throw error (exception)
4782     *
4783     * @api
4784     *
4785     * @param string $msg Message with optional sprintf()-style vararg parameters
4786     *
4787     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
4788     */
4789    public function throwError($msg)
4790    {
4791        if ($this->ignoreErrors) {
4792            return;
4793        }
4794
4795        if (\func_num_args() > 1) {
4796            $msg = \call_user_func_array('sprintf', \func_get_args());
4797        }
4798
4799        if (! $this->ignoreCallStackMessage) {
4800            $line   = $this->sourceLine;
4801            $column = $this->sourceColumn;
4802
4803            $loc = isset($this->sourceNames[$this->sourceIndex])
4804                ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
4805                : "line: $line, column: $column";
4806
4807            $msg = "$msg: $loc";
4808
4809            $callStackMsg = $this->callStackMessage();
4810
4811            if ($callStackMsg) {
4812                $msg .= "\nCall Stack:\n" . $callStackMsg;
4813            }
4814        }
4815
4816        throw new CompilerException($msg);
4817    }
4818
4819    /**
4820     * Beautify call stack for output
4821     *
4822     * @param boolean $all
4823     * @param null    $limit
4824     *
4825     * @return string
4826     */
4827    protected function callStackMessage($all = false, $limit = null)
4828    {
4829        $callStackMsg = [];
4830        $ncall = 0;
4831
4832        if ($this->callStack) {
4833            foreach (array_reverse($this->callStack) as $call) {
4834                if ($all || (isset($call['n']) && $call['n'])) {
4835                    $msg = "#" . $ncall++ . " " . $call['n'] . " ";
4836                    $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
4837                          ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
4838                          : '(unknown file)');
4839                    $msg .= " on line " . $call[Parser::SOURCE_LINE];
4840
4841                    $callStackMsg[] = $msg;
4842
4843                    if (! \is_null($limit) && $ncall > $limit) {
4844                        break;
4845                    }
4846                }
4847            }
4848        }
4849
4850        return implode("\n", $callStackMsg);
4851    }
4852
4853    /**
4854     * Handle import loop
4855     *
4856     * @param string $name
4857     *
4858     * @throws \Exception
4859     */
4860    protected function handleImportLoop($name)
4861    {
4862        for ($env = $this->env; $env; $env = $env->parent) {
4863            if (! $env->block) {
4864                continue;
4865            }
4866
4867            $file = $this->sourceNames[$env->block->sourceIndex];
4868
4869            if (realpath($file) === $name) {
4870                $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
4871                break;
4872            }
4873        }
4874    }
4875
4876    /**
4877     * Call SCSS @function
4878     *
4879     * @param string $name
4880     * @param array  $argValues
4881     * @param array  $returnValue
4882     *
4883     * @return boolean Returns true if returnValue is set; otherwise, false
4884     */
4885    protected function callScssFunction($name, $argValues, &$returnValue)
4886    {
4887        $func = $this->get(static::$namespaces['function'] . $name, false);
4888
4889        if (! $func) {
4890            return false;
4891        }
4892
4893        $this->pushEnv();
4894
4895        // set the args
4896        if (isset($func->args)) {
4897            $this->applyArguments($func->args, $argValues);
4898        }
4899
4900        // throw away lines and children
4901        $tmp = new OutputBlock;
4902        $tmp->lines    = [];
4903        $tmp->children = [];
4904
4905        $this->env->marker = 'function';
4906
4907        if (! empty($func->parentEnv)) {
4908            $this->env->declarationScopeParent = $func->parentEnv;
4909        } else {
4910            $this->throwError("@function $name() without parentEnv");
4911        }
4912
4913        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
4914
4915        $this->popEnv();
4916
4917        $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
4918
4919        return true;
4920    }
4921
4922    /**
4923     * Call built-in and registered (PHP) functions
4924     *
4925     * @param string $name
4926     * @param array  $args
4927     * @param array  $returnValue
4928     *
4929     * @return boolean Returns true if returnValue is set; otherwise, false
4930     */
4931    protected function callNativeFunction($name, $args, &$returnValue)
4932    {
4933        // try a lib function
4934        $name = $this->normalizeName($name);
4935        $libName = null;
4936
4937        if (isset($this->userFunctions[$name])) {
4938            // see if we can find a user function
4939            list($f, $prototype) = $this->userFunctions[$name];
4940        } elseif (($f = $this->getBuiltinFunction($name)) && \is_callable($f)) {
4941            $libName   = $f[1];
4942            $prototype = isset(static::$$libName) ? static::$$libName : null;
4943        } else {
4944            return false;
4945        }
4946
4947        @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
4948
4949        if ($name !== 'if' && $name !== 'call') {
4950            $inExp = true;
4951
4952            if ($name === 'join') {
4953                $inExp = false;
4954            }
4955
4956            foreach ($sorted as &$val) {
4957                $val = $this->reduce($val, $inExp);
4958            }
4959        }
4960
4961        $returnValue = \call_user_func($f, $sorted, $kwargs);
4962
4963        if (! isset($returnValue)) {
4964            return false;
4965        }
4966
4967        $returnValue = $this->coerceValue($returnValue);
4968
4969        return true;
4970    }
4971
4972    /**
4973     * Get built-in function
4974     *
4975     * @param string $name Normalized name
4976     *
4977     * @return array
4978     */
4979    protected function getBuiltinFunction($name)
4980    {
4981        $libName = 'lib' . preg_replace_callback(
4982            '/_(.)/',
4983            function ($m) {
4984                return ucfirst($m[1]);
4985            },
4986            ucfirst($name)
4987        );
4988
4989        return [$this, $libName];
4990    }
4991
4992    /**
4993     * Sorts keyword arguments
4994     *
4995     * @param string $functionName
4996     * @param array  $prototypes
4997     * @param array  $args
4998     *
4999     * @return array
5000     */
5001    protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
5002    {
5003        static $parser = null;
5004
5005        if (! isset($prototypes)) {
5006            $keyArgs = [];
5007            $posArgs = [];
5008
5009            // separate positional and keyword arguments
5010            foreach ($args as $arg) {
5011                list($key, $value) = $arg;
5012
5013                $key = $key[1];
5014
5015                if (empty($key)) {
5016                    $posArgs[] = empty($arg[2]) ? $value : $arg;
5017                } else {
5018                    $keyArgs[$key] = $value;
5019                }
5020            }
5021
5022            return [$posArgs, $keyArgs];
5023        }
5024
5025        // specific cases ?
5026        if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5027            // notation 100 127 255 / 0 is in fact a simple list of 4 values
5028            foreach ($args as $k => $arg) {
5029                if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
5030                    $last = end($arg[1][2]);
5031
5032                    if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
5033                        array_pop($arg[1][2]);
5034                        $arg[1][2][] = $last[2];
5035                        $arg[1][2][] = $last[3];
5036                        $args[$k] = $arg;
5037                    }
5038                }
5039            }
5040        }
5041
5042        $finalArgs = [];
5043
5044        if (! \is_array(reset($prototypes))) {
5045            $prototypes = [$prototypes];
5046        }
5047
5048        $keyArgs = [];
5049
5050        // trying each prototypes
5051        $prototypeHasMatch = false;
5052        $exceptionMessage = '';
5053
5054        foreach ($prototypes as $prototype) {
5055            $argDef = [];
5056
5057            foreach ($prototype as $i => $p) {
5058                $default = null;
5059                $p       = explode(':', $p, 2);
5060                $name    = array_shift($p);
5061
5062                if (\count($p)) {
5063                    $p = trim(reset($p));
5064
5065                    if ($p === 'null') {
5066                        // differentiate this null from the static::$null
5067                        $default = [Type::T_KEYWORD, 'null'];
5068                    } else {
5069                        if (\is_null($parser)) {
5070                            $parser = $this->parserFactory(__METHOD__);
5071                        }
5072
5073                        $parser->parseValue($p, $default);
5074                    }
5075                }
5076
5077                $isVariable = false;
5078
5079                if (substr($name, -3) === '...') {
5080                    $isVariable = true;
5081                    $name = substr($name, 0, -3);
5082                }
5083
5084                $argDef[] = [$name, $default, $isVariable];
5085            }
5086
5087            $ignoreCallStackMessage = $this->ignoreCallStackMessage;
5088            $this->ignoreCallStackMessage = true;
5089
5090            try {
5091                $vars = $this->applyArguments($argDef, $args, false, false);
5092
5093                // ensure all args are populated
5094                foreach ($prototype as $i => $p) {
5095                    $name = explode(':', $p)[0];
5096
5097                    if (! isset($finalArgs[$i])) {
5098                        $finalArgs[$i] = null;
5099                    }
5100                }
5101
5102                // apply positional args
5103                foreach (array_values($vars) as $i => $val) {
5104                    $finalArgs[$i] = $val;
5105                }
5106
5107                $keyArgs = array_merge($keyArgs, $vars);
5108                $prototypeHasMatch = true;
5109
5110                // overwrite positional args with keyword args
5111                foreach ($prototype as $i => $p) {
5112                    $name = explode(':', $p)[0];
5113
5114                    if (isset($keyArgs[$name])) {
5115                        $finalArgs[$i] = $keyArgs[$name];
5116                    }
5117
5118                    // special null value as default: translate to real null here
5119                    if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
5120                        $finalArgs[$i] = null;
5121                    }
5122                }
5123                // should we break if this prototype seems fulfilled?
5124            } catch (CompilerException $e) {
5125                $exceptionMessage = $e->getMessage();
5126            }
5127            $this->ignoreCallStackMessage = $ignoreCallStackMessage;
5128        }
5129
5130        if ($exceptionMessage && ! $prototypeHasMatch) {
5131            $this->throwError($exceptionMessage);
5132        }
5133
5134        return [$finalArgs, $keyArgs];
5135    }
5136
5137    /**
5138     * Apply argument values per definition
5139     *
5140     * @param array   $argDef
5141     * @param array   $argValues
5142     * @param boolean $storeInEnv
5143     * @param boolean $reduce
5144     *   only used if $storeInEnv = false
5145     *
5146     * @return array
5147     *
5148     * @throws \Exception
5149     */
5150    protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5151    {
5152        $output = [];
5153
5154        if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) {
5155            array_pop($argValues);
5156        }
5157
5158        if ($storeInEnv) {
5159            $storeEnv = $this->getStoreEnv();
5160
5161            $env = new Environment;
5162            $env->store = $storeEnv->store;
5163        }
5164
5165        $hasVariable = false;
5166        $args = [];
5167
5168        foreach ($argDef as $i => $arg) {
5169            list($name, $default, $isVariable) = $argDef[$i];
5170
5171            $args[$name] = [$i, $name, $default, $isVariable];
5172            $hasVariable |= $isVariable;
5173        }
5174
5175        $splatSeparator      = null;
5176        $keywordArgs         = [];
5177        $deferredKeywordArgs = [];
5178        $remaining           = [];
5179        $hasKeywordArgument  = false;
5180
5181        // assign the keyword args
5182        foreach ((array) $argValues as $arg) {
5183            if (! empty($arg[0])) {
5184                $hasKeywordArgument = true;
5185
5186                $name = $arg[0][1];
5187                if (! isset($args[$name])) {
5188                    foreach (array_keys($args) as $an) {
5189                        if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5190                            $name = $an;
5191                            break;
5192                        }
5193                    }
5194                }
5195
5196                if (! isset($args[$name]) || $args[$name][3]) {
5197                    if ($hasVariable) {
5198                        $deferredKeywordArgs[$name] = $arg[1];
5199                    } else {
5200                        $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
5201                        break;
5202                    }
5203                } elseif ($args[$name][0] < \count($remaining)) {
5204                    $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
5205                    break;
5206                } else {
5207                    $keywordArgs[$name] = $arg[1];
5208                }
5209            } elseif ($arg[2] === true) {
5210                $val = $this->reduce($arg[1], true);
5211
5212                if ($val[0] === Type::T_LIST) {
5213                    foreach ($val[2] as $name => $item) {
5214                        if (! is_numeric($name)) {
5215                            if (! isset($args[$name])) {
5216                                foreach (array_keys($args) as $an) {
5217                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5218                                        $name = $an;
5219                                        break;
5220                                    }
5221                                }
5222                            }
5223
5224                            if ($hasVariable) {
5225                                $deferredKeywordArgs[$name] = $item;
5226                            } else {
5227                                $keywordArgs[$name] = $item;
5228                            }
5229                        } else {
5230                            if (\is_null($splatSeparator)) {
5231                                $splatSeparator = $val[1];
5232                            }
5233
5234                            $remaining[] = $item;
5235                        }
5236                    }
5237                } elseif ($val[0] === Type::T_MAP) {
5238                    foreach ($val[1] as $i => $name) {
5239                        $name = $this->compileStringContent($this->coerceString($name));
5240                        $item = $val[2][$i];
5241
5242                        if (! is_numeric($name)) {
5243                            if (! isset($args[$name])) {
5244                                foreach (array_keys($args) as $an) {
5245                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
5246                                        $name = $an;
5247                                        break;
5248                                    }
5249                                }
5250                            }
5251
5252                            if ($hasVariable) {
5253                                $deferredKeywordArgs[$name] = $item;
5254                            } else {
5255                                $keywordArgs[$name] = $item;
5256                            }
5257                        } else {
5258                            if (\is_null($splatSeparator)) {
5259                                $splatSeparator = $val[1];
5260                            }
5261
5262                            $remaining[] = $item;
5263                        }
5264                    }
5265                } else {
5266                    $remaining[] = $val;
5267                }
5268            } elseif ($hasKeywordArgument) {
5269                $this->throwError('Positional arguments must come before keyword arguments.');
5270                break;
5271            } else {
5272                $remaining[] = $arg[1];
5273            }
5274        }
5275
5276        foreach ($args as $arg) {
5277            list($i, $name, $default, $isVariable) = $arg;
5278
5279            if ($isVariable) {
5280                $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
5281
5282                for ($count = \count($remaining); $i < $count; $i++) {
5283                    $val[2][] = $remaining[$i];
5284                }
5285
5286                foreach ($deferredKeywordArgs as $itemName => $item) {
5287                    $val[2][$itemName] = $item;
5288                }
5289            } elseif (isset($remaining[$i])) {
5290                $val = $remaining[$i];
5291            } elseif (isset($keywordArgs[$name])) {
5292                $val = $keywordArgs[$name];
5293            } elseif (! empty($default)) {
5294                continue;
5295            } else {
5296                $this->throwError("Missing argument $name");
5297                break;
5298            }
5299
5300            if ($storeInEnv) {
5301                $this->set($name, $this->reduce($val, true), true, $env);
5302            } else {
5303                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
5304            }
5305        }
5306
5307        if ($storeInEnv) {
5308            $storeEnv->store = $env->store;
5309        }
5310
5311        foreach ($args as $arg) {
5312            list($i, $name, $default, $isVariable) = $arg;
5313
5314            if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
5315                continue;
5316            }
5317
5318            if ($storeInEnv) {
5319                $this->set($name, $this->reduce($default, true), true);
5320            } else {
5321                $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
5322            }
5323        }
5324
5325        return $output;
5326    }
5327
5328    /**
5329     * Coerce a php value into a scss one
5330     *
5331     * @param mixed $value
5332     *
5333     * @return array|\ScssPhp\ScssPhp\Node\Number
5334     */
5335    protected function coerceValue($value)
5336    {
5337        if (\is_array($value) || $value instanceof \ArrayAccess) {
5338            return $value;
5339        }
5340
5341        if (\is_bool($value)) {
5342            return $this->toBool($value);
5343        }
5344
5345        if (\is_null($value)) {
5346            return static::$null;
5347        }
5348
5349        if (is_numeric($value)) {
5350            return new Node\Number($value, '');
5351        }
5352
5353        if ($value === '') {
5354            return static::$emptyString;
5355        }
5356
5357        $value = [Type::T_KEYWORD, $value];
5358        $color = $this->coerceColor($value);
5359
5360        if ($color) {
5361            return $color;
5362        }
5363
5364        return $value;
5365    }
5366
5367    /**
5368     * Coerce something to map
5369     *
5370     * @param array $item
5371     *
5372     * @return array
5373     */
5374    protected function coerceMap($item)
5375    {
5376        if ($item[0] === Type::T_MAP) {
5377            return $item;
5378        }
5379
5380        if ($item[0] === static::$emptyList[0] &&
5381            $item[1] === static::$emptyList[1] &&
5382            $item[2] === static::$emptyList[2]
5383        ) {
5384            return static::$emptyMap;
5385        }
5386
5387        return [Type::T_MAP, [$item], [static::$null]];
5388    }
5389
5390    /**
5391     * Coerce something to list
5392     *
5393     * @param array   $item
5394     * @param string  $delim
5395     * @param boolean $removeTrailingNull
5396     *
5397     * @return array
5398     */
5399    protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
5400    {
5401        if (isset($item) && $item[0] === Type::T_LIST) {
5402            // remove trailing null from the list
5403            if ($removeTrailingNull && end($item[2]) === static::$null) {
5404                array_pop($item[2]);
5405            }
5406
5407            return $item;
5408        }
5409
5410        if (isset($item) && $item[0] === Type::T_MAP) {
5411            $keys = $item[1];
5412            $values = $item[2];
5413            $list = [];
5414
5415            for ($i = 0, $s = \count($keys); $i < $s; $i++) {
5416                $key = $keys[$i];
5417                $value = $values[$i];
5418
5419                switch ($key[0]) {
5420                    case Type::T_LIST:
5421                    case Type::T_MAP:
5422                    case Type::T_STRING:
5423                    case Type::T_NULL:
5424                        break;
5425
5426                    default:
5427                        $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
5428                        break;
5429                }
5430
5431                $list[] = [
5432                    Type::T_LIST,
5433                    '',
5434                    [$key, $value]
5435                ];
5436            }
5437
5438            return [Type::T_LIST, ',', $list];
5439        }
5440
5441        return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
5442    }
5443
5444    /**
5445     * Coerce color for expression
5446     *
5447     * @param array $value
5448     *
5449     * @return array|null
5450     */
5451    protected function coerceForExpression($value)
5452    {
5453        if ($color = $this->coerceColor($value)) {
5454            return $color;
5455        }
5456
5457        return $value;
5458    }
5459
5460    /**
5461     * Coerce value to color
5462     *
5463     * @param array $value
5464     *
5465     * @return array|null
5466     */
5467    protected function coerceColor($value, $inRGBFunction = false)
5468    {
5469        switch ($value[0]) {
5470            case Type::T_COLOR:
5471                for ($i = 1; $i <= 3; $i++) {
5472                    if (! is_numeric($value[$i])) {
5473                        $cv = $this->compileRGBAValue($value[$i]);
5474
5475                        if (! is_numeric($cv)) {
5476                            return null;
5477                        }
5478
5479                        $value[$i] = $cv;
5480                    }
5481
5482                    if (isset($value[4])) {
5483                        if (! is_numeric($value[4])) {
5484                            $cv = $this->compileRGBAValue($value[4], true);
5485
5486                            if (! is_numeric($cv)) {
5487                                return null;
5488                            }
5489
5490                            $value[4] = $cv;
5491                        }
5492                    }
5493                }
5494
5495                return $value;
5496
5497            case Type::T_LIST:
5498                if ($inRGBFunction) {
5499                    if (\count($value[2]) == 3 || \count($value[2]) == 4) {
5500                        $color = $value[2];
5501                        array_unshift($color, Type::T_COLOR);
5502
5503                        return $this->coerceColor($color);
5504                    }
5505                }
5506
5507                return null;
5508
5509            case Type::T_KEYWORD:
5510                if (! \is_string($value[1])) {
5511                    return null;
5512                }
5513
5514                $name = strtolower($value[1]);
5515
5516                // hexa color?
5517                if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
5518                    $nofValues = \strlen($m[1]);
5519
5520                    if (\in_array($nofValues, [3, 4, 6, 8])) {
5521                        $nbChannels = 3;
5522                        $color      = [];
5523                        $num        = hexdec($m[1]);
5524
5525                        switch ($nofValues) {
5526                            case 4:
5527                                $nbChannels = 4;
5528                                // then continuing with the case 3:
5529                            case 3:
5530                                for ($i = 0; $i < $nbChannels; $i++) {
5531                                    $t = $num & 0xf;
5532                                    array_unshift($color, $t << 4 | $t);
5533                                    $num >>= 4;
5534                                }
5535
5536                                break;
5537
5538                            case 8:
5539                                $nbChannels = 4;
5540                                // then continuing with the case 6:
5541                            case 6:
5542                                for ($i = 0; $i < $nbChannels; $i++) {
5543                                    array_unshift($color, $num & 0xff);
5544                                    $num >>= 8;
5545                                }
5546
5547                                break;
5548                        }
5549
5550                        if ($nbChannels === 4) {
5551                            if ($color[3] === 255) {
5552                                $color[3] = 1; // fully opaque
5553                            } else {
5554                                $color[3] = round($color[3] / 255, 3);
5555                            }
5556                        }
5557
5558                        array_unshift($color, Type::T_COLOR);
5559
5560                        return $color;
5561                    }
5562                }
5563
5564                if ($rgba = Colors::colorNameToRGBa($name)) {
5565                    return isset($rgba[3])
5566                        ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
5567                        : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
5568                }
5569
5570                return null;
5571        }
5572
5573        return null;
5574    }
5575
5576    /**
5577     * @param integer|\ScssPhp\ScssPhp\Node\Number $value
5578     * @param boolean                              $isAlpha
5579     *
5580     * @return integer|mixed
5581     */
5582    protected function compileRGBAValue($value, $isAlpha = false)
5583    {
5584        if ($isAlpha) {
5585            return $this->compileColorPartValue($value, 0, 1, false);
5586        }
5587
5588        return $this->compileColorPartValue($value, 0, 255, true);
5589    }
5590
5591    /**
5592     * @param mixed         $value
5593     * @param integer|float $min
5594     * @param integer|float $max
5595     * @param boolean       $isInt
5596     * @param boolean       $clamp
5597     * @param boolean       $modulo
5598     *
5599     * @return integer|mixed
5600     */
5601    protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
5602    {
5603        if (! is_numeric($value)) {
5604            if (\is_array($value)) {
5605                $reduced = $this->reduce($value);
5606
5607                if (\is_object($reduced) && $value->type === Type::T_NUMBER) {
5608                    $value = $reduced;
5609                }
5610            }
5611
5612            if (\is_object($value) && $value->type === Type::T_NUMBER) {
5613                $num = $value->dimension;
5614
5615                if (\count($value->units)) {
5616                    $unit = array_keys($value->units);
5617                    $unit = reset($unit);
5618
5619                    switch ($unit) {
5620                        case '%':
5621                            $num *= $max / 100;
5622                            break;
5623                        default:
5624                            break;
5625                    }
5626                }
5627
5628                $value = $num;
5629            } elseif (\is_array($value)) {
5630                $value = $this->compileValue($value);
5631            }
5632        }
5633
5634        if (is_numeric($value)) {
5635            if ($isInt) {
5636                $value = round($value);
5637            }
5638
5639            if ($clamp) {
5640                $value = min($max, max($min, $value));
5641            }
5642
5643            if ($modulo) {
5644                $value = $value % $max;
5645
5646                // still negative?
5647                while ($value < $min) {
5648                    $value += $max;
5649                }
5650            }
5651
5652            return $value;
5653        }
5654
5655        return $value;
5656    }
5657
5658    /**
5659     * Coerce value to string
5660     *
5661     * @param array $value
5662     *
5663     * @return array|null
5664     */
5665    protected function coerceString($value)
5666    {
5667        if ($value[0] === Type::T_STRING) {
5668            return $value;
5669        }
5670
5671        return [Type::T_STRING, '', [$this->compileValue($value)]];
5672    }
5673
5674    /**
5675     * Coerce value to a percentage
5676     *
5677     * @param array $value
5678     *
5679     * @return integer|float
5680     */
5681    protected function coercePercent($value)
5682    {
5683        if ($value[0] === Type::T_NUMBER) {
5684            if (! empty($value[2]['%'])) {
5685                return $value[1] / 100;
5686            }
5687
5688            return $value[1];
5689        }
5690
5691        return 0;
5692    }
5693
5694    /**
5695     * Assert value is a map
5696     *
5697     * @api
5698     *
5699     * @param array $value
5700     *
5701     * @return array
5702     *
5703     * @throws \Exception
5704     */
5705    public function assertMap($value)
5706    {
5707        $value = $this->coerceMap($value);
5708
5709        if ($value[0] !== Type::T_MAP) {
5710            $this->throwError('expecting map, %s received', $value[0]);
5711        }
5712
5713        return $value;
5714    }
5715
5716    /**
5717     * Assert value is a list
5718     *
5719     * @api
5720     *
5721     * @param array $value
5722     *
5723     * @return array
5724     *
5725     * @throws \Exception
5726     */
5727    public function assertList($value)
5728    {
5729        if ($value[0] !== Type::T_LIST) {
5730            $this->throwError('expecting list, %s received', $value[0]);
5731        }
5732
5733        return $value;
5734    }
5735
5736    /**
5737     * Assert value is a color
5738     *
5739     * @api
5740     *
5741     * @param array $value
5742     *
5743     * @return array
5744     *
5745     * @throws \Exception
5746     */
5747    public function assertColor($value)
5748    {
5749        if ($color = $this->coerceColor($value)) {
5750            return $color;
5751        }
5752
5753        $this->throwError('expecting color, %s received', $value[0]);
5754    }
5755
5756    /**
5757     * Assert value is a number
5758     *
5759     * @api
5760     *
5761     * @param array $value
5762     *
5763     * @return integer|float
5764     *
5765     * @throws \Exception
5766     */
5767    public function assertNumber($value)
5768    {
5769        if ($value[0] !== Type::T_NUMBER) {
5770            $this->throwError('expecting number, %s received', $value[0]);
5771        }
5772
5773        return $value[1];
5774    }
5775
5776    /**
5777     * Make sure a color's components don't go out of bounds
5778     *
5779     * @param array $c
5780     *
5781     * @return array
5782     */
5783    protected function fixColor($c)
5784    {
5785        foreach ([1, 2, 3] as $i) {
5786            if ($c[$i] < 0) {
5787                $c[$i] = 0;
5788            }
5789
5790            if ($c[$i] > 255) {
5791                $c[$i] = 255;
5792            }
5793        }
5794
5795        return $c;
5796    }
5797
5798    /**
5799     * Convert RGB to HSL
5800     *
5801     * @api
5802     *
5803     * @param integer $red
5804     * @param integer $green
5805     * @param integer $blue
5806     *
5807     * @return array
5808     */
5809    public function toHSL($red, $green, $blue)
5810    {
5811        $min = min($red, $green, $blue);
5812        $max = max($red, $green, $blue);
5813
5814        $l = $min + $max;
5815        $d = $max - $min;
5816
5817        if ((int) $d === 0) {
5818            $h = $s = 0;
5819        } else {
5820            if ($l < 255) {
5821                $s = $d / $l;
5822            } else {
5823                $s = $d / (510 - $l);
5824            }
5825
5826            if ($red == $max) {
5827                $h = 60 * ($green - $blue) / $d;
5828            } elseif ($green == $max) {
5829                $h = 60 * ($blue - $red) / $d + 120;
5830            } elseif ($blue == $max) {
5831                $h = 60 * ($red - $green) / $d + 240;
5832            }
5833        }
5834
5835        return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
5836    }
5837
5838    /**
5839     * Hue to RGB helper
5840     *
5841     * @param float $m1
5842     * @param float $m2
5843     * @param float $h
5844     *
5845     * @return float
5846     */
5847    protected function hueToRGB($m1, $m2, $h)
5848    {
5849        if ($h < 0) {
5850            $h += 1;
5851        } elseif ($h > 1) {
5852            $h -= 1;
5853        }
5854
5855        if ($h * 6 < 1) {
5856            return $m1 + ($m2 - $m1) * $h * 6;
5857        }
5858
5859        if ($h * 2 < 1) {
5860            return $m2;
5861        }
5862
5863        if ($h * 3 < 2) {
5864            return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
5865        }
5866
5867        return $m1;
5868    }
5869
5870    /**
5871     * Convert HSL to RGB
5872     *
5873     * @api
5874     *
5875     * @param integer $hue        H from 0 to 360
5876     * @param integer $saturation S from 0 to 100
5877     * @param integer $lightness  L from 0 to 100
5878     *
5879     * @return array
5880     */
5881    public function toRGB($hue, $saturation, $lightness)
5882    {
5883        if ($hue < 0) {
5884            $hue += 360;
5885        }
5886
5887        $h = $hue / 360;
5888        $s = min(100, max(0, $saturation)) / 100;
5889        $l = min(100, max(0, $lightness)) / 100;
5890
5891        $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
5892        $m1 = $l * 2 - $m2;
5893
5894        $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
5895        $g = $this->hueToRGB($m1, $m2, $h) * 255;
5896        $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
5897
5898        $out = [Type::T_COLOR, $r, $g, $b];
5899
5900        return $out;
5901    }
5902
5903    // Built in functions
5904
5905    protected static $libCall = ['name', 'args...'];
5906    protected function libCall($args, $kwargs)
5907    {
5908        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
5909        $callArgs = [];
5910
5911        // $kwargs['args'] is [Type::T_LIST, ',', [..]]
5912        foreach ($kwargs['args'][2] as $varname => $arg) {
5913            if (is_numeric($varname)) {
5914                $varname = null;
5915            } else {
5916                $varname = [ 'var', $varname];
5917            }
5918
5919            $callArgs[] = [$varname, $arg, false];
5920        }
5921
5922        return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
5923    }
5924
5925    protected static $libIf = ['condition', 'if-true', 'if-false:'];
5926    protected function libIf($args)
5927    {
5928        list($cond, $t, $f) = $args;
5929
5930        if (! $this->isTruthy($this->reduce($cond, true))) {
5931            return $this->reduce($f, true);
5932        }
5933
5934        return $this->reduce($t, true);
5935    }
5936
5937    protected static $libIndex = ['list', 'value'];
5938    protected function libIndex($args)
5939    {
5940        list($list, $value) = $args;
5941
5942        if ($list[0] === Type::T_MAP ||
5943            $list[0] === Type::T_STRING ||
5944            $list[0] === Type::T_KEYWORD ||
5945            $list[0] === Type::T_INTERPOLATE
5946        ) {
5947            $list = $this->coerceList($list, ' ');
5948        }
5949
5950        if ($list[0] !== Type::T_LIST) {
5951            return static::$null;
5952        }
5953
5954        $values = [];
5955
5956        foreach ($list[2] as $item) {
5957            $values[] = $this->normalizeValue($item);
5958        }
5959
5960        $key = array_search($this->normalizeValue($value), $values);
5961
5962        return false === $key ? static::$null : $key + 1;
5963    }
5964
5965    protected static $libRgb = [
5966        ['color'],
5967        ['color', 'alpha'],
5968        ['channels'],
5969        ['red', 'green', 'blue'],
5970        ['red', 'green', 'blue', 'alpha'] ];
5971    protected function libRgb($args, $kwargs, $funcName = 'rgb')
5972    {
5973        switch (\count($args)) {
5974            case 1:
5975                if (! $color = $this->coerceColor($args[0], true)) {
5976                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
5977                }
5978                break;
5979
5980            case 3:
5981                $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
5982
5983                if (! $color = $this->coerceColor($color)) {
5984                    $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
5985                }
5986
5987                return $color;
5988
5989            case 2:
5990                if ($color = $this->coerceColor($args[0], true)) {
5991                    $alpha = $this->compileRGBAValue($args[1], true);
5992
5993                    if (is_numeric($alpha)) {
5994                        $color[4] = $alpha;
5995                    } else {
5996                        $color = [Type::T_STRING, '',
5997                            [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
5998                    }
5999                } else {
6000                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6001                }
6002                break;
6003
6004            case 4:
6005            default:
6006                $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
6007
6008                if (! $color = $this->coerceColor($color)) {
6009                    $color = [Type::T_STRING, '',
6010                        [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6011                }
6012                break;
6013        }
6014
6015        return $color;
6016    }
6017
6018    protected static $libRgba = [
6019        ['color'],
6020        ['color', 'alpha'],
6021        ['channels'],
6022        ['red', 'green', 'blue'],
6023        ['red', 'green', 'blue', 'alpha'] ];
6024    protected function libRgba($args, $kwargs)
6025    {
6026        return $this->libRgb($args, $kwargs, 'rgba');
6027    }
6028
6029    // helper function for adjust_color, change_color, and scale_color
6030    protected function alterColor($args, $fn)
6031    {
6032        $color = $this->assertColor($args[0]);
6033
6034        foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
6035            if (isset($args[$iarg])) {
6036                $val = $this->assertNumber($args[$iarg]);
6037
6038                if (! isset($color[$irgba])) {
6039                    $color[$irgba] = (($irgba < 4) ? 0 : 1);
6040                }
6041
6042                $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg);
6043            }
6044        }
6045
6046        if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
6047            $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6048
6049            foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
6050                if (! empty($args[$iarg])) {
6051                    $val = $this->assertNumber($args[$iarg]);
6052                    $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
6053                }
6054            }
6055
6056            $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
6057
6058            if (isset($color[4])) {
6059                $rgb[4] = $color[4];
6060            }
6061
6062            $color = $rgb;
6063        }
6064
6065        return $color;
6066    }
6067
6068    protected static $libAdjustColor = [
6069        'color', 'red:null', 'green:null', 'blue:null',
6070        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6071    ];
6072    protected function libAdjustColor($args)
6073    {
6074        return $this->alterColor($args, function ($base, $alter, $i) {
6075            return $base + $alter;
6076        });
6077    }
6078
6079    protected static $libChangeColor = [
6080        'color', 'red:null', 'green:null', 'blue:null',
6081        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6082    ];
6083    protected function libChangeColor($args)
6084    {
6085        return $this->alterColor($args, function ($base, $alter, $i) {
6086            return $alter;
6087        });
6088    }
6089
6090    protected static $libScaleColor = [
6091        'color', 'red:null', 'green:null', 'blue:null',
6092        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6093    ];
6094    protected function libScaleColor($args)
6095    {
6096        return $this->alterColor($args, function ($base, $scale, $i) {
6097            // 1, 2, 3 - rgb
6098            // 4, 5, 6 - hsl
6099            // 7 - a
6100            switch ($i) {
6101                case 1:
6102                case 2:
6103                case 3:
6104                    $max = 255;
6105                    break;
6106
6107                case 4:
6108                    $max = 360;
6109                    break;
6110
6111                case 7:
6112                    $max = 1;
6113                    break;
6114
6115                default:
6116                    $max = 100;
6117            }
6118
6119            $scale = $scale / 100;
6120
6121            if ($scale < 0) {
6122                return $base * $scale + $base;
6123            }
6124
6125            return ($max - $base) * $scale + $base;
6126        });
6127    }
6128
6129    protected static $libIeHexStr = ['color'];
6130    protected function libIeHexStr($args)
6131    {
6132        $color = $this->coerceColor($args[0]);
6133        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
6134
6135        return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
6136    }
6137
6138    protected static $libRed = ['color'];
6139    protected function libRed($args)
6140    {
6141        $color = $this->coerceColor($args[0]);
6142
6143        return $color[1];
6144    }
6145
6146    protected static $libGreen = ['color'];
6147    protected function libGreen($args)
6148    {
6149        $color = $this->coerceColor($args[0]);
6150
6151        return $color[2];
6152    }
6153
6154    protected static $libBlue = ['color'];
6155    protected function libBlue($args)
6156    {
6157        $color = $this->coerceColor($args[0]);
6158
6159        return $color[3];
6160    }
6161
6162    protected static $libAlpha = ['color'];
6163    protected function libAlpha($args)
6164    {
6165        if ($color = $this->coerceColor($args[0])) {
6166            return isset($color[4]) ? $color[4] : 1;
6167        }
6168
6169        // this might be the IE function, so return value unchanged
6170        return null;
6171    }
6172
6173    protected static $libOpacity = ['color'];
6174    protected function libOpacity($args)
6175    {
6176        $value = $args[0];
6177
6178        if ($value[0] === Type::T_NUMBER) {
6179            return null;
6180        }
6181
6182        return $this->libAlpha($args);
6183    }
6184
6185    // mix two colors
6186    protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
6187    protected function libMix($args)
6188    {
6189        list($first, $second, $weight) = $args;
6190
6191        $first = $this->assertColor($first);
6192        $second = $this->assertColor($second);
6193
6194        if (! isset($weight)) {
6195            $weight = 0.5;
6196        } else {
6197            $weight = $this->coercePercent($weight);
6198        }
6199
6200        $firstAlpha = isset($first[4]) ? $first[4] : 1;
6201        $secondAlpha = isset($second[4]) ? $second[4] : 1;
6202
6203        $w = $weight * 2 - 1;
6204        $a = $firstAlpha - $secondAlpha;
6205
6206        $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
6207        $w2 = 1.0 - $w1;
6208
6209        $new = [Type::T_COLOR,
6210            $w1 * $first[1] + $w2 * $second[1],
6211            $w1 * $first[2] + $w2 * $second[2],
6212            $w1 * $first[3] + $w2 * $second[3],
6213        ];
6214
6215        if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
6216            $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
6217        }
6218
6219        return $this->fixColor($new);
6220    }
6221
6222    protected static $libHsl =[
6223        ['channels'],
6224        ['hue', 'saturation', 'lightness'],
6225        ['hue', 'saturation', 'lightness', 'alpha'] ];
6226    protected function libHsl($args, $kwargs, $funcName = 'hsl')
6227    {
6228        if (\count($args) == 1) {
6229            if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
6230                return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6231            }
6232
6233            $args = $args[0][2];
6234        }
6235
6236        $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
6237        $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
6238        $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
6239
6240        $alpha = null;
6241
6242        if (\count($args) === 4) {
6243            $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
6244
6245            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
6246                return [Type::T_STRING, '',
6247                    [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6248            }
6249        } else {
6250            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
6251                return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6252            }
6253        }
6254
6255        $color = $this->toRGB($hue, $saturation, $lightness);
6256
6257        if (! \is_null($alpha)) {
6258            $color[4] = $alpha;
6259        }
6260
6261        return $color;
6262    }
6263
6264    protected static $libHsla = [
6265            ['channels'],
6266            ['hue', 'saturation', 'lightness', 'alpha:1'] ];
6267    protected function libHsla($args, $kwargs)
6268    {
6269        return $this->libHsl($args, $kwargs, 'hsla');
6270    }
6271
6272    protected static $libHue = ['color'];
6273    protected function libHue($args)
6274    {
6275        $color = $this->assertColor($args[0]);
6276        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6277
6278        return new Node\Number($hsl[1], 'deg');
6279    }
6280
6281    protected static $libSaturation = ['color'];
6282    protected function libSaturation($args)
6283    {
6284        $color = $this->assertColor($args[0]);
6285        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6286
6287        return new Node\Number($hsl[2], '%');
6288    }
6289
6290    protected static $libLightness = ['color'];
6291    protected function libLightness($args)
6292    {
6293        $color = $this->assertColor($args[0]);
6294        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6295
6296        return new Node\Number($hsl[3], '%');
6297    }
6298
6299    protected function adjustHsl($color, $idx, $amount)
6300    {
6301        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6302        $hsl[$idx] += $amount;
6303        $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
6304
6305        if (isset($color[4])) {
6306            $out[4] = $color[4];
6307        }
6308
6309        return $out;
6310    }
6311
6312    protected static $libAdjustHue = ['color', 'degrees'];
6313    protected function libAdjustHue($args)
6314    {
6315        $color = $this->assertColor($args[0]);
6316        $degrees = $this->assertNumber($args[1]);
6317
6318        return $this->adjustHsl($color, 1, $degrees);
6319    }
6320
6321    protected static $libLighten = ['color', 'amount'];
6322    protected function libLighten($args)
6323    {
6324        $color = $this->assertColor($args[0]);
6325        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6326
6327        return $this->adjustHsl($color, 3, $amount);
6328    }
6329
6330    protected static $libDarken = ['color', 'amount'];
6331    protected function libDarken($args)
6332    {
6333        $color = $this->assertColor($args[0]);
6334        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6335
6336        return $this->adjustHsl($color, 3, -$amount);
6337    }
6338
6339    protected static $libSaturate = [['color', 'amount'], ['number']];
6340    protected function libSaturate($args)
6341    {
6342        $value = $args[0];
6343
6344        if ($value[0] === Type::T_NUMBER) {
6345            return null;
6346        }
6347
6348        $color = $this->assertColor($value);
6349        $amount = 100 * $this->coercePercent($args[1]);
6350
6351        return $this->adjustHsl($color, 2, $amount);
6352    }
6353
6354    protected static $libDesaturate = ['color', 'amount'];
6355    protected function libDesaturate($args)
6356    {
6357        $color = $this->assertColor($args[0]);
6358        $amount = 100 * $this->coercePercent($args[1]);
6359
6360        return $this->adjustHsl($color, 2, -$amount);
6361    }
6362
6363    protected static $libGrayscale = ['color'];
6364    protected function libGrayscale($args)
6365    {
6366        $value = $args[0];
6367
6368        if ($value[0] === Type::T_NUMBER) {
6369            return null;
6370        }
6371
6372        return $this->adjustHsl($this->assertColor($value), 2, -100);
6373    }
6374
6375    protected static $libComplement = ['color'];
6376    protected function libComplement($args)
6377    {
6378        return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
6379    }
6380
6381    protected static $libInvert = ['color', 'weight:1'];
6382    protected function libInvert($args)
6383    {
6384        list($value, $weight) = $args;
6385
6386        if (! isset($weight)) {
6387            $weight = 1;
6388        } else {
6389            $weight = $this->coercePercent($weight);
6390        }
6391
6392        if ($value[0] === Type::T_NUMBER) {
6393            return null;
6394        }
6395
6396        $color = $this->assertColor($value);
6397        $inverted = $color;
6398        $inverted[1] = 255 - $inverted[1];
6399        $inverted[2] = 255 - $inverted[2];
6400        $inverted[3] = 255 - $inverted[3];
6401
6402        if ($weight < 1) {
6403            return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
6404        }
6405
6406        return $inverted;
6407    }
6408
6409    // increases opacity by amount
6410    protected static $libOpacify = ['color', 'amount'];
6411    protected function libOpacify($args)
6412    {
6413        $color = $this->assertColor($args[0]);
6414        $amount = $this->coercePercent($args[1]);
6415
6416        $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
6417        $color[4] = min(1, max(0, $color[4]));
6418
6419        return $color;
6420    }
6421
6422    protected static $libFadeIn = ['color', 'amount'];
6423    protected function libFadeIn($args)
6424    {
6425        return $this->libOpacify($args);
6426    }
6427
6428    // decreases opacity by amount
6429    protected static $libTransparentize = ['color', 'amount'];
6430    protected function libTransparentize($args)
6431    {
6432        $color = $this->assertColor($args[0]);
6433        $amount = $this->coercePercent($args[1]);
6434
6435        $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
6436        $color[4] = min(1, max(0, $color[4]));
6437
6438        return $color;
6439    }
6440
6441    protected static $libFadeOut = ['color', 'amount'];
6442    protected function libFadeOut($args)
6443    {
6444        return $this->libTransparentize($args);
6445    }
6446
6447    protected static $libUnquote = ['string'];
6448    protected function libUnquote($args)
6449    {
6450        $str = $args[0];
6451
6452        if ($str[0] === Type::T_STRING) {
6453            $str[1] = '';
6454        }
6455
6456        return $str;
6457    }
6458
6459    protected static $libQuote = ['string'];
6460    protected function libQuote($args)
6461    {
6462        $value = $args[0];
6463
6464        if ($value[0] === Type::T_STRING && ! empty($value[1])) {
6465            return $value;
6466        }
6467
6468        return [Type::T_STRING, '"', [$value]];
6469    }
6470
6471    protected static $libPercentage = ['number'];
6472    protected function libPercentage($args)
6473    {
6474        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
6475    }
6476
6477    protected static $libRound = ['number'];
6478    protected function libRound($args)
6479    {
6480        $num = $args[0];
6481
6482        return new Node\Number(round($num[1]), $num[2]);
6483    }
6484
6485    protected static $libFloor = ['number'];
6486    protected function libFloor($args)
6487    {
6488        $num = $args[0];
6489
6490        return new Node\Number(floor($num[1]), $num[2]);
6491    }
6492
6493    protected static $libCeil = ['number'];
6494    protected function libCeil($args)
6495    {
6496        $num = $args[0];
6497
6498        return new Node\Number(ceil($num[1]), $num[2]);
6499    }
6500
6501    protected static $libAbs = ['number'];
6502    protected function libAbs($args)
6503    {
6504        $num = $args[0];
6505
6506        return new Node\Number(abs($num[1]), $num[2]);
6507    }
6508
6509    protected function libMin($args)
6510    {
6511        $numbers = $this->getNormalizedNumbers($args);
6512        $minOriginal = null;
6513        $minNormalized = null;
6514
6515        foreach ($numbers as $key => $pair) {
6516            list($original, $normalized) = $pair;
6517
6518            if (\is_null($normalized) or \is_null($minNormalized)) {
6519                if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) {
6520                    $minOriginal = $original;
6521                    $minNormalized = $normalized;
6522                }
6523            } elseif ($normalized[1] <= $minNormalized[1]) {
6524                $minOriginal = $original;
6525                $minNormalized = $normalized;
6526            }
6527        }
6528
6529        return $minOriginal;
6530    }
6531
6532    protected function libMax($args)
6533    {
6534        $numbers = $this->getNormalizedNumbers($args);
6535        $maxOriginal = null;
6536        $maxNormalized = null;
6537
6538        foreach ($numbers as $key => $pair) {
6539            list($original, $normalized) = $pair;
6540
6541            if (\is_null($normalized) or \is_null($maxNormalized)) {
6542                if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) {
6543                    $maxOriginal = $original;
6544                    $maxNormalized = $normalized;
6545                }
6546            } elseif ($normalized[1] >= $maxNormalized[1]) {
6547                $maxOriginal = $original;
6548                $maxNormalized = $normalized;
6549            }
6550        }
6551
6552        return $maxOriginal;
6553    }
6554
6555    /**
6556     * Helper to normalize args containing numbers
6557     *
6558     * @param array $args
6559     *
6560     * @return array
6561     */
6562    protected function getNormalizedNumbers($args)
6563    {
6564        $unit         = null;
6565        $originalUnit = null;
6566        $numbers      = [];
6567
6568        foreach ($args as $key => $item) {
6569            if ($item[0] !== Type::T_NUMBER) {
6570                $this->throwError('%s is not a number', $item[0]);
6571                break;
6572            }
6573
6574            $number = $item->normalize();
6575
6576            if (empty($unit)) {
6577                $unit = $number[2];
6578                $originalUnit = $item->unitStr();
6579            } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) {
6580                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
6581                break;
6582            }
6583
6584            $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number];
6585        }
6586
6587        return $numbers;
6588    }
6589
6590    protected static $libLength = ['list'];
6591    protected function libLength($args)
6592    {
6593        $list = $this->coerceList($args[0], ',', true);
6594
6595        return \count($list[2]);
6596    }
6597
6598    //protected static $libListSeparator = ['list...'];
6599    protected function libListSeparator($args)
6600    {
6601        if (\count($args) > 1) {
6602            return 'comma';
6603        }
6604
6605        $list = $this->coerceList($args[0]);
6606
6607        if (\count($list[2]) <= 1) {
6608            return 'space';
6609        }
6610
6611        if ($list[1] === ',') {
6612            return 'comma';
6613        }
6614
6615        return 'space';
6616    }
6617
6618    protected static $libNth = ['list', 'n'];
6619    protected function libNth($args)
6620    {
6621        $list = $this->coerceList($args[0], ',', false);
6622        $n = $this->assertNumber($args[1]);
6623
6624        if ($n > 0) {
6625            $n--;
6626        } elseif ($n < 0) {
6627            $n += \count($list[2]);
6628        }
6629
6630        return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
6631    }
6632
6633    protected static $libSetNth = ['list', 'n', 'value'];
6634    protected function libSetNth($args)
6635    {
6636        $list = $this->coerceList($args[0]);
6637        $n = $this->assertNumber($args[1]);
6638
6639        if ($n > 0) {
6640            $n--;
6641        } elseif ($n < 0) {
6642            $n += \count($list[2]);
6643        }
6644
6645        if (! isset($list[2][$n])) {
6646            $this->throwError('Invalid argument for "n"');
6647
6648            return null;
6649        }
6650
6651        $list[2][$n] = $args[2];
6652
6653        return $list;
6654    }
6655
6656    protected static $libMapGet = ['map', 'key'];
6657    protected function libMapGet($args)
6658    {
6659        $map = $this->assertMap($args[0]);
6660        $key = $args[1];
6661
6662        if (! \is_null($key)) {
6663            $key = $this->compileStringContent($this->coerceString($key));
6664
6665            for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
6666                if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
6667                    return $map[2][$i];
6668                }
6669            }
6670        }
6671
6672        return static::$null;
6673    }
6674
6675    protected static $libMapKeys = ['map'];
6676    protected function libMapKeys($args)
6677    {
6678        $map = $this->assertMap($args[0]);
6679        $keys = $map[1];
6680
6681        return [Type::T_LIST, ',', $keys];
6682    }
6683
6684    protected static $libMapValues = ['map'];
6685    protected function libMapValues($args)
6686    {
6687        $map = $this->assertMap($args[0]);
6688        $values = $map[2];
6689
6690        return [Type::T_LIST, ',', $values];
6691    }
6692
6693    protected static $libMapRemove = ['map', 'key'];
6694    protected function libMapRemove($args)
6695    {
6696        $map = $this->assertMap($args[0]);
6697        $key = $this->compileStringContent($this->coerceString($args[1]));
6698
6699        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
6700            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
6701                array_splice($map[1], $i, 1);
6702                array_splice($map[2], $i, 1);
6703            }
6704        }
6705
6706        return $map;
6707    }
6708
6709    protected static $libMapHasKey = ['map', 'key'];
6710    protected function libMapHasKey($args)
6711    {
6712        $map = $this->assertMap($args[0]);
6713        $key = $this->compileStringContent($this->coerceString($args[1]));
6714
6715        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
6716            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
6717                return true;
6718            }
6719        }
6720
6721        return false;
6722    }
6723
6724    protected static $libMapMerge = ['map-1', 'map-2'];
6725    protected function libMapMerge($args)
6726    {
6727        $map1 = $this->assertMap($args[0]);
6728        $map2 = $this->assertMap($args[1]);
6729
6730        foreach ($map2[1] as $i2 => $key2) {
6731            $key = $this->compileStringContent($this->coerceString($key2));
6732
6733            foreach ($map1[1] as $i1 => $key1) {
6734                if ($key === $this->compileStringContent($this->coerceString($key1))) {
6735                    $map1[2][$i1] = $map2[2][$i2];
6736                    continue 2;
6737                }
6738            }
6739
6740            $map1[1][] = $map2[1][$i2];
6741            $map1[2][] = $map2[2][$i2];
6742        }
6743
6744        return $map1;
6745    }
6746
6747    protected static $libKeywords = ['args'];
6748    protected function libKeywords($args)
6749    {
6750        $this->assertList($args[0]);
6751
6752        $keys = [];
6753        $values = [];
6754
6755        foreach ($args[0][2] as $name => $arg) {
6756            $keys[] = [Type::T_KEYWORD, $name];
6757            $values[] = $arg;
6758        }
6759
6760        return [Type::T_MAP, $keys, $values];
6761    }
6762
6763    protected static $libIsBracketed = ['list'];
6764    protected function libIsBracketed($args)
6765    {
6766        $list = $args[0];
6767        $this->coerceList($list, ' ');
6768
6769        if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
6770            return true;
6771        }
6772
6773        return false;
6774    }
6775
6776    protected function listSeparatorForJoin($list1, $sep)
6777    {
6778        if (! isset($sep)) {
6779            return $list1[1];
6780        }
6781
6782        switch ($this->compileValue($sep)) {
6783            case 'comma':
6784                return ',';
6785
6786            case 'space':
6787                return ' ';
6788
6789            default:
6790                return $list1[1];
6791        }
6792    }
6793
6794    protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
6795    protected function libJoin($args)
6796    {
6797        list($list1, $list2, $sep, $bracketed) = $args;
6798
6799        $list1 = $this->coerceList($list1, ' ', true);
6800        $list2 = $this->coerceList($list2, ' ', true);
6801        $sep   = $this->listSeparatorForJoin($list1, $sep);
6802
6803        if ($bracketed === static::$true) {
6804            $bracketed = true;
6805        } elseif ($bracketed === static::$false) {
6806            $bracketed = false;
6807        } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
6808            $bracketed = 'auto';
6809        } elseif ($bracketed === static::$null) {
6810            $bracketed = false;
6811        } else {
6812            $bracketed = $this->compileValue($bracketed);
6813            $bracketed = ! ! $bracketed;
6814
6815            if ($bracketed === true) {
6816                $bracketed = true;
6817            }
6818        }
6819
6820        if ($bracketed === 'auto') {
6821            $bracketed = false;
6822
6823            if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
6824                $bracketed = true;
6825            }
6826        }
6827
6828        $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
6829
6830        if (isset($list1['enclosing'])) {
6831            $res['enlcosing'] = $list1['enclosing'];
6832        }
6833
6834        if ($bracketed) {
6835            $res['enclosing'] = 'bracket';
6836        }
6837
6838        return $res;
6839    }
6840
6841    protected static $libAppend = ['list', 'val', 'separator:null'];
6842    protected function libAppend($args)
6843    {
6844        list($list1, $value, $sep) = $args;
6845
6846        $list1 = $this->coerceList($list1, ' ', true);
6847        $sep   = $this->listSeparatorForJoin($list1, $sep);
6848        $res   = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
6849
6850        if (isset($list1['enclosing'])) {
6851            $res['enclosing'] = $list1['enclosing'];
6852        }
6853
6854        return $res;
6855    }
6856
6857    protected function libZip($args)
6858    {
6859        foreach ($args as $key => $arg) {
6860            $args[$key] = $this->coerceList($arg);
6861        }
6862
6863        $lists = [];
6864        $firstList = array_shift($args);
6865
6866        foreach ($firstList[2] as $key => $item) {
6867            $list = [Type::T_LIST, '', [$item]];
6868
6869            foreach ($args as $arg) {
6870                if (isset($arg[2][$key])) {
6871                    $list[2][] = $arg[2][$key];
6872                } else {
6873                    break 2;
6874                }
6875            }
6876
6877            $lists[] = $list;
6878        }
6879
6880        return [Type::T_LIST, ',', $lists];
6881    }
6882
6883    protected static $libTypeOf = ['value'];
6884    protected function libTypeOf($args)
6885    {
6886        $value = $args[0];
6887
6888        switch ($value[0]) {
6889            case Type::T_KEYWORD:
6890                if ($value === static::$true || $value === static::$false) {
6891                    return 'bool';
6892                }
6893
6894                if ($this->coerceColor($value)) {
6895                    return 'color';
6896                }
6897
6898                // fall-thru
6899            case Type::T_FUNCTION:
6900                return 'string';
6901
6902            case Type::T_LIST:
6903                if (isset($value[3]) && $value[3]) {
6904                    return 'arglist';
6905                }
6906
6907                // fall-thru
6908            default:
6909                return $value[0];
6910        }
6911    }
6912
6913    protected static $libUnit = ['number'];
6914    protected function libUnit($args)
6915    {
6916        $num = $args[0];
6917
6918        if ($num[0] === Type::T_NUMBER) {
6919            return [Type::T_STRING, '"', [$num->unitStr()]];
6920        }
6921
6922        return '';
6923    }
6924
6925    protected static $libUnitless = ['number'];
6926    protected function libUnitless($args)
6927    {
6928        $value = $args[0];
6929
6930        return $value[0] === Type::T_NUMBER && $value->unitless();
6931    }
6932
6933    protected static $libComparable = ['number-1', 'number-2'];
6934    protected function libComparable($args)
6935    {
6936        list($number1, $number2) = $args;
6937
6938        if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
6939            ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
6940        ) {
6941            $this->throwError('Invalid argument(s) for "comparable"');
6942
6943            return null;
6944        }
6945
6946        $number1 = $number1->normalize();
6947        $number2 = $number2->normalize();
6948
6949        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
6950    }
6951
6952    protected static $libStrIndex = ['string', 'substring'];
6953    protected function libStrIndex($args)
6954    {
6955        $string = $this->coerceString($args[0]);
6956        $stringContent = $this->compileStringContent($string);
6957
6958        $substring = $this->coerceString($args[1]);
6959        $substringContent = $this->compileStringContent($substring);
6960
6961        $result = strpos($stringContent, $substringContent);
6962
6963        return $result === false ? static::$null : new Node\Number($result + 1, '');
6964    }
6965
6966    protected static $libStrInsert = ['string', 'insert', 'index'];
6967    protected function libStrInsert($args)
6968    {
6969        $string = $this->coerceString($args[0]);
6970        $stringContent = $this->compileStringContent($string);
6971
6972        $insert = $this->coerceString($args[1]);
6973        $insertContent = $this->compileStringContent($insert);
6974
6975        list(, $index) = $args[2];
6976
6977        $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
6978
6979        return $string;
6980    }
6981
6982    protected static $libStrLength = ['string'];
6983    protected function libStrLength($args)
6984    {
6985        $string = $this->coerceString($args[0]);
6986        $stringContent = $this->compileStringContent($string);
6987
6988        return new Node\Number(\strlen($stringContent), '');
6989    }
6990
6991    protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
6992    protected function libStrSlice($args)
6993    {
6994        if (isset($args[2]) && ! $args[2][1]) {
6995            return static::$nullString;
6996        }
6997
6998        $string = $this->coerceString($args[0]);
6999        $stringContent = $this->compileStringContent($string);
7000
7001        $start = (int) $args[1][1];
7002
7003        if ($start > 0) {
7004            $start--;
7005        }
7006
7007        $end    = isset($args[2]) ? (int) $args[2][1] : -1;
7008        $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
7009
7010        $string[2] = $length
7011            ? [substr($stringContent, $start, $length)]
7012            : [substr($stringContent, $start)];
7013
7014        return $string;
7015    }
7016
7017    protected static $libToLowerCase = ['string'];
7018    protected function libToLowerCase($args)
7019    {
7020        $string = $this->coerceString($args[0]);
7021        $stringContent = $this->compileStringContent($string);
7022
7023        $string[2] = [\function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
7024
7025        return $string;
7026    }
7027
7028    protected static $libToUpperCase = ['string'];
7029    protected function libToUpperCase($args)
7030    {
7031        $string = $this->coerceString($args[0]);
7032        $stringContent = $this->compileStringContent($string);
7033
7034        $string[2] = [\function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
7035
7036        return $string;
7037    }
7038
7039    protected static $libFeatureExists = ['feature'];
7040    protected function libFeatureExists($args)
7041    {
7042        $string = $this->coerceString($args[0]);
7043        $name = $this->compileStringContent($string);
7044
7045        return $this->toBool(
7046            \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
7047        );
7048    }
7049
7050    protected static $libFunctionExists = ['name'];
7051    protected function libFunctionExists($args)
7052    {
7053        $string = $this->coerceString($args[0]);
7054        $name = $this->compileStringContent($string);
7055
7056        // user defined functions
7057        if ($this->has(static::$namespaces['function'] . $name)) {
7058            return true;
7059        }
7060
7061        $name = $this->normalizeName($name);
7062
7063        if (isset($this->userFunctions[$name])) {
7064            return true;
7065        }
7066
7067        // built-in functions
7068        $f = $this->getBuiltinFunction($name);
7069
7070        return $this->toBool(\is_callable($f));
7071    }
7072
7073    protected static $libGlobalVariableExists = ['name'];
7074    protected function libGlobalVariableExists($args)
7075    {
7076        $string = $this->coerceString($args[0]);
7077        $name = $this->compileStringContent($string);
7078
7079        return $this->has($name, $this->rootEnv);
7080    }
7081
7082    protected static $libMixinExists = ['name'];
7083    protected function libMixinExists($args)
7084    {
7085        $string = $this->coerceString($args[0]);
7086        $name = $this->compileStringContent($string);
7087
7088        return $this->has(static::$namespaces['mixin'] . $name);
7089    }
7090
7091    protected static $libVariableExists = ['name'];
7092    protected function libVariableExists($args)
7093    {
7094        $string = $this->coerceString($args[0]);
7095        $name = $this->compileStringContent($string);
7096
7097        return $this->has($name);
7098    }
7099
7100    /**
7101     * Workaround IE7's content counter bug.
7102     *
7103     * @param array $args
7104     *
7105     * @return array
7106     */
7107    protected function libCounter($args)
7108    {
7109        $list = array_map([$this, 'compileValue'], $args);
7110
7111        return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
7112    }
7113
7114    protected static $libRandom = ['limit:1'];
7115    protected function libRandom($args)
7116    {
7117        if (isset($args[0])) {
7118            $n = $this->assertNumber($args[0]);
7119
7120            if ($n < 1) {
7121                $this->throwError("\$limit must be greater than or equal to 1");
7122
7123                return null;
7124            }
7125
7126            if ($n - \intval($n) > 0) {
7127                $this->throwError("Expected \$limit to be an integer but got $n for `random`");
7128
7129                return null;
7130            }
7131
7132            return new Node\Number(mt_rand(1, \intval($n)), '');
7133        }
7134
7135        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
7136    }
7137
7138    protected function libUniqueId()
7139    {
7140        static $id;
7141
7142        if (! isset($id)) {
7143            $id = PHP_INT_SIZE === 4
7144                ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
7145                : mt_rand(0, pow(36, 8));
7146        }
7147
7148        $id += mt_rand(0, 10) + 1;
7149
7150        return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
7151    }
7152
7153    protected function inspectFormatValue($value, $force_enclosing_display = false)
7154    {
7155        if ($value === static::$null) {
7156            $value = [Type::T_KEYWORD, 'null'];
7157        }
7158
7159        $stringValue = [$value];
7160
7161        if ($value[0] === Type::T_LIST) {
7162            if (end($value[2]) === static::$null) {
7163                array_pop($value[2]);
7164                $value[2][] = [Type::T_STRING, '', ['']];
7165                $force_enclosing_display = true;
7166            }
7167
7168            if (! empty($value['enclosing']) &&
7169                ($force_enclosing_display ||
7170                    ($value['enclosing'] === 'bracket') ||
7171                    ! \count($value[2]))
7172            ) {
7173                $value['enclosing'] = 'forced_'.$value['enclosing'];
7174                $force_enclosing_display = true;
7175            }
7176
7177            foreach ($value[2] as $k => $listelement) {
7178                $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
7179            }
7180
7181            $stringValue = [$value];
7182        }
7183
7184        return [Type::T_STRING, '', $stringValue];
7185    }
7186
7187    protected static $libInspect = ['value'];
7188    protected function libInspect($args)
7189    {
7190        $value = $args[0];
7191
7192        return $this->inspectFormatValue($value);
7193    }
7194
7195    /**
7196     * Preprocess selector args
7197     *
7198     * @param array $arg
7199     *
7200     * @return array|boolean
7201     */
7202    protected function getSelectorArg($arg)
7203    {
7204        static $parser = null;
7205
7206        if (\is_null($parser)) {
7207            $parser = $this->parserFactory(__METHOD__);
7208        }
7209
7210        $arg = $this->libUnquote([$arg]);
7211        $arg = $this->compileValue($arg);
7212
7213        $parsedSelector = [];
7214
7215        if ($parser->parseSelector($arg, $parsedSelector)) {
7216            $selector = $this->evalSelectors($parsedSelector);
7217            $gluedSelector = $this->glueFunctionSelectors($selector);
7218
7219            return $gluedSelector;
7220        }
7221
7222        return false;
7223    }
7224
7225    /**
7226     * Postprocess selector to output in right format
7227     *
7228     * @param array $selectors
7229     *
7230     * @return string
7231     */
7232    protected function formatOutputSelector($selectors)
7233    {
7234        $selectors = $this->collapseSelectors($selectors, true);
7235
7236        return $selectors;
7237    }
7238
7239    protected static $libIsSuperselector = ['super', 'sub'];
7240    protected function libIsSuperselector($args)
7241    {
7242        list($super, $sub) = $args;
7243
7244        $super = $this->getSelectorArg($super);
7245        $sub = $this->getSelectorArg($sub);
7246
7247        return $this->isSuperSelector($super, $sub);
7248    }
7249
7250    /**
7251     * Test a $super selector again $sub
7252     *
7253     * @param array $super
7254     * @param array $sub
7255     *
7256     * @return boolean
7257     */
7258    protected function isSuperSelector($super, $sub)
7259    {
7260        // one and only one selector for each arg
7261        if (! $super || \count($super) !== 1) {
7262            $this->throwError("Invalid super selector for isSuperSelector()");
7263        }
7264
7265        if (! $sub || \count($sub) !== 1) {
7266            $this->throwError("Invalid sub selector for isSuperSelector()");
7267        }
7268
7269        $super = reset($super);
7270        $sub = reset($sub);
7271
7272        $i = 0;
7273        $nextMustMatch = false;
7274
7275        foreach ($super as $node) {
7276            $compound = '';
7277
7278            array_walk_recursive(
7279                $node,
7280                function ($value, $key) use (&$compound) {
7281                    $compound .= $value;
7282                }
7283            );
7284
7285            if ($this->isImmediateRelationshipCombinator($compound)) {
7286                if ($node !== $sub[$i]) {
7287                    return false;
7288                }
7289
7290                $nextMustMatch = true;
7291                $i++;
7292            } else {
7293                while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
7294                    if ($nextMustMatch) {
7295                        return false;
7296                    }
7297
7298                    $i++;
7299                }
7300
7301                if ($i >= \count($sub)) {
7302                    return false;
7303                }
7304
7305                $nextMustMatch = false;
7306                $i++;
7307            }
7308        }
7309
7310        return true;
7311    }
7312
7313    /**
7314     * Test a part of super selector again a part of sub selector
7315     *
7316     * @param array $superParts
7317     * @param array $subParts
7318     *
7319     * @return boolean
7320     */
7321    protected function isSuperPart($superParts, $subParts)
7322    {
7323        $i = 0;
7324
7325        foreach ($superParts as $superPart) {
7326            while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
7327                $i++;
7328            }
7329
7330            if ($i >= \count($subParts)) {
7331                return false;
7332            }
7333
7334            $i++;
7335        }
7336
7337        return true;
7338    }
7339
7340    protected static $libSelectorAppend = ['selector...'];
7341    protected function libSelectorAppend($args)
7342    {
7343        // get the selector... list
7344        $args = reset($args);
7345        $args = $args[2];
7346
7347        if (\count($args) < 1) {
7348            $this->throwError("selector-append() needs at least 1 argument");
7349        }
7350
7351        $selectors = array_map([$this, 'getSelectorArg'], $args);
7352
7353        return $this->formatOutputSelector($this->selectorAppend($selectors));
7354    }
7355
7356    /**
7357     * Append parts of the last selector in the list to the previous, recursively
7358     *
7359     * @param array $selectors
7360     *
7361     * @return array
7362     *
7363     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
7364     */
7365    protected function selectorAppend($selectors)
7366    {
7367        $lastSelectors = array_pop($selectors);
7368
7369        if (! $lastSelectors) {
7370            $this->throwError("Invalid selector list in selector-append()");
7371        }
7372
7373        while (\count($selectors)) {
7374            $previousSelectors = array_pop($selectors);
7375
7376            if (! $previousSelectors) {
7377                $this->throwError("Invalid selector list in selector-append()");
7378            }
7379
7380            // do the trick, happening $lastSelector to $previousSelector
7381            $appended = [];
7382
7383            foreach ($lastSelectors as $lastSelector) {
7384                $previous = $previousSelectors;
7385
7386                foreach ($lastSelector as $lastSelectorParts) {
7387                    foreach ($lastSelectorParts as $lastSelectorPart) {
7388                        foreach ($previous as $i => $previousSelector) {
7389                            foreach ($previousSelector as $j => $previousSelectorParts) {
7390                                $previous[$i][$j][] = $lastSelectorPart;
7391                            }
7392                        }
7393                    }
7394                }
7395
7396                foreach ($previous as $ps) {
7397                    $appended[] = $ps;
7398                }
7399            }
7400
7401            $lastSelectors = $appended;
7402        }
7403
7404        return $lastSelectors;
7405    }
7406
7407    protected static $libSelectorExtend = ['selectors', 'extendee', 'extender'];
7408    protected function libSelectorExtend($args)
7409    {
7410        list($selectors, $extendee, $extender) = $args;
7411
7412        $selectors = $this->getSelectorArg($selectors);
7413        $extendee  = $this->getSelectorArg($extendee);
7414        $extender  = $this->getSelectorArg($extender);
7415
7416        if (! $selectors || ! $extendee || ! $extender) {
7417            $this->throwError("selector-extend() invalid arguments");
7418        }
7419
7420        $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
7421
7422        return $this->formatOutputSelector($extended);
7423    }
7424
7425    protected static $libSelectorReplace = ['selectors', 'original', 'replacement'];
7426    protected function libSelectorReplace($args)
7427    {
7428        list($selectors, $original, $replacement) = $args;
7429
7430        $selectors   = $this->getSelectorArg($selectors);
7431        $original    = $this->getSelectorArg($original);
7432        $replacement = $this->getSelectorArg($replacement);
7433
7434        if (! $selectors || ! $original || ! $replacement) {
7435            $this->throwError("selector-replace() invalid arguments");
7436        }
7437
7438        $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
7439
7440        return $this->formatOutputSelector($replaced);
7441    }
7442
7443    /**
7444     * Extend/replace in selectors
7445     * used by selector-extend and selector-replace that use the same logic
7446     *
7447     * @param array   $selectors
7448     * @param array   $extendee
7449     * @param array   $extender
7450     * @param boolean $replace
7451     *
7452     * @return array
7453     */
7454    protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
7455    {
7456        $saveExtends = $this->extends;
7457        $saveExtendsMap = $this->extendsMap;
7458
7459        $this->extends = [];
7460        $this->extendsMap = [];
7461
7462        foreach ($extendee as $es) {
7463            // only use the first one
7464            $this->pushExtends(reset($es), $extender, null);
7465        }
7466
7467        $extended = [];
7468
7469        foreach ($selectors as $selector) {
7470            if (! $replace) {
7471                $extended[] = $selector;
7472            }
7473
7474            $n = \count($extended);
7475
7476            $this->matchExtends($selector, $extended);
7477
7478            // if didnt match, keep the original selector if we are in a replace operation
7479            if ($replace and \count($extended) === $n) {
7480                $extended[] = $selector;
7481            }
7482        }
7483
7484        $this->extends = $saveExtends;
7485        $this->extendsMap = $saveExtendsMap;
7486
7487        return $extended;
7488    }
7489
7490    protected static $libSelectorNest = ['selector...'];
7491    protected function libSelectorNest($args)
7492    {
7493        // get the selector... list
7494        $args = reset($args);
7495        $args = $args[2];
7496
7497        if (\count($args) < 1) {
7498            $this->throwError("selector-nest() needs at least 1 argument");
7499        }
7500
7501        $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
7502        $envs = [];
7503
7504        foreach ($selectorsMap as $selectors) {
7505            $env = new Environment();
7506            $env->selectors = $selectors;
7507
7508            $envs[] = $env;
7509        }
7510
7511        $envs            = array_reverse($envs);
7512        $env             = $this->extractEnv($envs);
7513        $outputSelectors = $this->multiplySelectors($env);
7514
7515        return $this->formatOutputSelector($outputSelectors);
7516    }
7517
7518    protected static $libSelectorParse = ['selectors'];
7519    protected function libSelectorParse($args)
7520    {
7521        $selectors = reset($args);
7522        $selectors = $this->getSelectorArg($selectors);
7523
7524        return $this->formatOutputSelector($selectors);
7525    }
7526
7527    protected static $libSelectorUnify = ['selectors1', 'selectors2'];
7528    protected function libSelectorUnify($args)
7529    {
7530        list($selectors1, $selectors2) = $args;
7531
7532        $selectors1 = $this->getSelectorArg($selectors1);
7533        $selectors2 = $this->getSelectorArg($selectors2);
7534
7535        if (! $selectors1 || ! $selectors2) {
7536            $this->throwError("selector-unify() invalid arguments");
7537        }
7538
7539        // only consider the first compound of each
7540        $compound1 = reset($selectors1);
7541        $compound2 = reset($selectors2);
7542
7543        // unify them and that's it
7544        $unified = $this->unifyCompoundSelectors($compound1, $compound2);
7545
7546        return $this->formatOutputSelector($unified);
7547    }
7548
7549    /**
7550     * The selector-unify magic as its best
7551     * (at least works as expected on test cases)
7552     *
7553     * @param array $compound1
7554     * @param array $compound2
7555     *
7556     * @return array|mixed
7557     */
7558    protected function unifyCompoundSelectors($compound1, $compound2)
7559    {
7560        if (! \count($compound1)) {
7561            return $compound2;
7562        }
7563
7564        if (! \count($compound2)) {
7565            return $compound1;
7566        }
7567
7568        // check that last part are compatible
7569        $lastPart1 = array_pop($compound1);
7570        $lastPart2 = array_pop($compound2);
7571        $last      = $this->mergeParts($lastPart1, $lastPart2);
7572
7573        if (! $last) {
7574            return [[]];
7575        }
7576
7577        $unifiedCompound = [$last];
7578        $unifiedSelectors = [$unifiedCompound];
7579
7580        // do the rest
7581        while (\count($compound1) || \count($compound2)) {
7582            $part1 = end($compound1);
7583            $part2 = end($compound2);
7584
7585            if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
7586                list($compound2, $part2, $after2) = $match2;
7587
7588                if ($after2) {
7589                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
7590                }
7591
7592                $c = $this->mergeParts($part1, $part2);
7593                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
7594
7595                $part1 = $part2 = null;
7596
7597                array_pop($compound1);
7598            }
7599
7600            if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
7601                list($compound1, $part1, $after1) = $match1;
7602
7603                if ($after1) {
7604                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
7605                }
7606
7607                $c = $this->mergeParts($part2, $part1);
7608                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
7609
7610                $part1 = $part2 = null;
7611
7612                array_pop($compound2);
7613            }
7614
7615            $new = [];
7616
7617            if ($part1 && $part2) {
7618                array_pop($compound1);
7619                array_pop($compound2);
7620
7621                $s   = $this->prependSelectors($unifiedSelectors, [$part2]);
7622                $new = array_merge($new, $this->prependSelectors($s, [$part1]));
7623                $s   = $this->prependSelectors($unifiedSelectors, [$part1]);
7624                $new = array_merge($new, $this->prependSelectors($s, [$part2]));
7625            } elseif ($part1) {
7626                array_pop($compound1);
7627
7628                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
7629            } elseif ($part2) {
7630                array_pop($compound2);
7631
7632                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
7633            }
7634
7635            if ($new) {
7636                $unifiedSelectors = $new;
7637            }
7638        }
7639
7640        return $unifiedSelectors;
7641    }
7642
7643    /**
7644     * Prepend each selector from $selectors with $parts
7645     *
7646     * @param array $selectors
7647     * @param array $parts
7648     *
7649     * @return array
7650     */
7651    protected function prependSelectors($selectors, $parts)
7652    {
7653        $new = [];
7654
7655        foreach ($selectors as $compoundSelector) {
7656            array_unshift($compoundSelector, $parts);
7657
7658            $new[] = $compoundSelector;
7659        }
7660
7661        return $new;
7662    }
7663
7664    /**
7665     * Try to find a matching part in a compound:
7666     * - with same html tag name
7667     * - with some class or id or something in common
7668     *
7669     * @param array $part
7670     * @param array $compound
7671     *
7672     * @return array|boolean
7673     */
7674    protected function matchPartInCompound($part, $compound)
7675    {
7676        $partTag = $this->findTagName($part);
7677        $before  = $compound;
7678        $after   = [];
7679
7680        // try to find a match by tag name first
7681        while (\count($before)) {
7682            $p = array_pop($before);
7683
7684            if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
7685                return [$before, $p, $after];
7686            }
7687
7688            $after[] = $p;
7689        }
7690
7691        // try again matching a non empty intersection and a compatible tagname
7692        $before = $compound;
7693        $after = [];
7694
7695        while (\count($before)) {
7696            $p = array_pop($before);
7697
7698            if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
7699                if (\count(array_intersect($part, $p))) {
7700                    return [$before, $p, $after];
7701                }
7702            }
7703
7704            $after[] = $p;
7705        }
7706
7707        return false;
7708    }
7709
7710    /**
7711     * Merge two part list taking care that
7712     * - the html tag is coming first - if any
7713     * - the :something are coming last
7714     *
7715     * @param array $parts1
7716     * @param array $parts2
7717     *
7718     * @return array
7719     */
7720    protected function mergeParts($parts1, $parts2)
7721    {
7722        $tag1 = $this->findTagName($parts1);
7723        $tag2 = $this->findTagName($parts2);
7724        $tag  = $this->checkCompatibleTags($tag1, $tag2);
7725
7726        // not compatible tags
7727        if ($tag === false) {
7728            return [];
7729        }
7730
7731        if ($tag) {
7732            if ($tag1) {
7733                $parts1 = array_diff($parts1, [$tag1]);
7734            }
7735
7736            if ($tag2) {
7737                $parts2 = array_diff($parts2, [$tag2]);
7738            }
7739        }
7740
7741        $mergedParts = array_merge($parts1, $parts2);
7742        $mergedOrderedParts = [];
7743
7744        foreach ($mergedParts as $part) {
7745            if (strpos($part, ':') === 0) {
7746                $mergedOrderedParts[] = $part;
7747            }
7748        }
7749
7750        $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
7751        $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
7752
7753        if ($tag) {
7754            array_unshift($mergedParts, $tag);
7755        }
7756
7757        return $mergedParts;
7758    }
7759
7760    /**
7761     * Check the compatibility between two tag names:
7762     * if both are defined they should be identical or one has to be '*'
7763     *
7764     * @param string $tag1
7765     * @param string $tag2
7766     *
7767     * @return array|boolean
7768     */
7769    protected function checkCompatibleTags($tag1, $tag2)
7770    {
7771        $tags = [$tag1, $tag2];
7772        $tags = array_unique($tags);
7773        $tags = array_filter($tags);
7774
7775        if (\count($tags) > 1) {
7776            $tags = array_diff($tags, ['*']);
7777        }
7778
7779        // not compatible nodes
7780        if (\count($tags) > 1) {
7781            return false;
7782        }
7783
7784        return $tags;
7785    }
7786
7787    /**
7788     * Find the html tag name in a selector parts list
7789     *
7790     * @param array $parts
7791     *
7792     * @return mixed|string
7793     */
7794    protected function findTagName($parts)
7795    {
7796        foreach ($parts as $part) {
7797            if (! preg_match('/^[\[.:#%_-]/', $part)) {
7798                return $part;
7799            }
7800        }
7801
7802        return '';
7803    }
7804
7805    protected static $libSimpleSelectors = ['selector'];
7806    protected function libSimpleSelectors($args)
7807    {
7808        $selector = reset($args);
7809        $selector = $this->getSelectorArg($selector);
7810
7811        // remove selectors list layer, keeping the first one
7812        $selector = reset($selector);
7813
7814        // remove parts list layer, keeping the first part
7815        $part = reset($selector);
7816
7817        $listParts = [];
7818
7819        foreach ($part as $p) {
7820            $listParts[] = [Type::T_STRING, '', [$p]];
7821        }
7822
7823        return [Type::T_LIST, ',', $listParts];
7824    }
7825
7826    protected static $libScssphpGlob = ['pattern'];
7827    protected function libScssphpGlob($args)
7828    {
7829        $string = $this->coerceString($args[0]);
7830        $pattern = $this->compileStringContent($string);
7831        $matches = glob($pattern);
7832        $listParts = [];
7833
7834        foreach ($matches as $match) {
7835            if (! is_file($match)) {
7836                continue;
7837            }
7838
7839            $listParts[] = [Type::T_STRING, '"', [$match]];
7840        }
7841
7842        return [Type::T_LIST, ',', $listParts];
7843    }
7844}
7845