1<?php
2
3/**
4 * SCSSPHP
5 *
6 * @copyright 2012-2020 Leaf Corcoran
7 *
8 * @license http://opensource.org/licenses/MIT MIT
9 *
10 * @link http://scssphp.github.io/scssphp
11 */
12
13namespace ScssPhp\ScssPhp;
14
15use ScssPhp\ScssPhp\Base\Range;
16use ScssPhp\ScssPhp\Compiler\CachedResult;
17use ScssPhp\ScssPhp\Compiler\Environment;
18use ScssPhp\ScssPhp\Exception\CompilerException;
19use ScssPhp\ScssPhp\Exception\ParserException;
20use ScssPhp\ScssPhp\Exception\SassException;
21use ScssPhp\ScssPhp\Exception\SassScriptException;
22use ScssPhp\ScssPhp\Formatter\Compressed;
23use ScssPhp\ScssPhp\Formatter\Expanded;
24use ScssPhp\ScssPhp\Formatter\OutputBlock;
25use ScssPhp\ScssPhp\Logger\LoggerInterface;
26use ScssPhp\ScssPhp\Logger\StreamLogger;
27use ScssPhp\ScssPhp\Node\Number;
28use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
29use ScssPhp\ScssPhp\Util\Path;
30
31/**
32 * The scss compiler and parser.
33 *
34 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
35 * by `Parser` into a syntax tree, then it is compiled into another tree
36 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
37 * formatter, like `Formatter` which then outputs CSS as a string.
38 *
39 * During the first compile, all values are *reduced*, which means that their
40 * types are brought to the lowest form before being dump as strings. This
41 * handles math equations, variable dereferences, and the like.
42 *
43 * The `compile` function of `Compiler` is the entry point.
44 *
45 * In summary:
46 *
47 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
48 * then transforms the resulting tree to a CSS tree. This class also holds the
49 * evaluation context, such as all available mixins and variables at any given
50 * time.
51 *
52 * The `Parser` class is only concerned with parsing its input.
53 *
54 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
55 * handling things like indentation.
56 */
57
58/**
59 * SCSS compiler
60 *
61 * @author Leaf Corcoran <leafot@gmail.com>
62 *
63 * @final Extending the Compiler is deprecated
64 */
65class Compiler
66{
67    /**
68     * @deprecated
69     */
70    const LINE_COMMENTS = 1;
71    /**
72     * @deprecated
73     */
74    const DEBUG_INFO    = 2;
75
76    /**
77     * @deprecated
78     */
79    const WITH_RULE     = 1;
80    /**
81     * @deprecated
82     */
83    const WITH_MEDIA    = 2;
84    /**
85     * @deprecated
86     */
87    const WITH_SUPPORTS = 4;
88    /**
89     * @deprecated
90     */
91    const WITH_ALL      = 7;
92
93    const SOURCE_MAP_NONE   = 0;
94    const SOURCE_MAP_INLINE = 1;
95    const SOURCE_MAP_FILE   = 2;
96
97    /**
98     * @var array<string, string>
99     */
100    protected static $operatorNames = [
101        '+'   => 'add',
102        '-'   => 'sub',
103        '*'   => 'mul',
104        '/'   => 'div',
105        '%'   => 'mod',
106
107        '=='  => 'eq',
108        '!='  => 'neq',
109        '<'   => 'lt',
110        '>'   => 'gt',
111
112        '<='  => 'lte',
113        '>='  => 'gte',
114    ];
115
116    /**
117     * @var array<string, string>
118     */
119    protected static $namespaces = [
120        'special'  => '%',
121        'mixin'    => '@',
122        'function' => '^',
123    ];
124
125    public static $true         = [Type::T_KEYWORD, 'true'];
126    public static $false        = [Type::T_KEYWORD, 'false'];
127    /** @deprecated */
128    public static $NaN          = [Type::T_KEYWORD, 'NaN'];
129    /** @deprecated */
130    public static $Infinity     = [Type::T_KEYWORD, 'Infinity'];
131    public static $null         = [Type::T_NULL];
132    public static $nullString   = [Type::T_STRING, '', []];
133    public static $defaultValue = [Type::T_KEYWORD, ''];
134    public static $selfSelector = [Type::T_SELF];
135    public static $emptyList    = [Type::T_LIST, '', []];
136    public static $emptyMap     = [Type::T_MAP, [], []];
137    public static $emptyString  = [Type::T_STRING, '"', []];
138    public static $with         = [Type::T_KEYWORD, 'with'];
139    public static $without      = [Type::T_KEYWORD, 'without'];
140
141    /**
142     * @var array<int, string|callable>
143     */
144    protected $importPaths = [];
145    /**
146     * @var array<string, Block>
147     */
148    protected $importCache = [];
149
150    /**
151     * @var string[]
152     */
153    protected $importedFiles = [];
154
155    /**
156     * @var array
157     * @phpstan-var array<string, array{0: callable, 1: array|null}>
158     */
159    protected $userFunctions = [];
160    /**
161     * @var array<string, mixed>
162     */
163    protected $registeredVars = [];
164    /**
165     * @var array<string, bool>
166     */
167    protected $registeredFeatures = [
168        'extend-selector-pseudoclass' => false,
169        'at-error'                    => true,
170        'units-level-3'               => true,
171        'global-variable-shadowing'   => false,
172    ];
173
174    /**
175     * @var string|null
176     */
177    protected $encoding = null;
178    /**
179     * @var null
180     * @deprecated
181     */
182    protected $lineNumberStyle = null;
183
184    /**
185     * @var int|SourceMapGenerator
186     * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
187     */
188    protected $sourceMap = self::SOURCE_MAP_NONE;
189
190    /**
191     * @var array
192     * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
193     */
194    protected $sourceMapOptions = [];
195
196    /**
197     * @var bool
198     */
199    private $charset = true;
200
201    /**
202     * @var string|\ScssPhp\ScssPhp\Formatter
203     */
204    protected $formatter = Expanded::class;
205
206    /**
207     * @var Environment
208     */
209    protected $rootEnv;
210    /**
211     * @var OutputBlock|null
212     */
213    protected $rootBlock;
214
215    /**
216     * @var \ScssPhp\ScssPhp\Compiler\Environment
217     */
218    protected $env;
219    /**
220     * @var OutputBlock|null
221     */
222    protected $scope;
223    /**
224     * @var Environment|null
225     */
226    protected $storeEnv;
227    /**
228     * @var bool|null
229     *
230     * @deprecated
231     */
232    protected $charsetSeen;
233    /**
234     * @var array<int, string|null>
235     */
236    protected $sourceNames;
237
238    /**
239     * @var Cache|null
240     */
241    protected $cache;
242
243    /**
244     * @var bool
245     */
246    protected $cacheCheckImportResolutions = false;
247
248    /**
249     * @var int
250     */
251    protected $indentLevel;
252    /**
253     * @var array[]
254     */
255    protected $extends;
256    /**
257     * @var array<string, int[]>
258     */
259    protected $extendsMap;
260
261    /**
262     * @var array<string, int>
263     */
264    protected $parsedFiles = [];
265
266    /**
267     * @var Parser|null
268     */
269    protected $parser;
270    /**
271     * @var int|null
272     */
273    protected $sourceIndex;
274    /**
275     * @var int|null
276     */
277    protected $sourceLine;
278    /**
279     * @var int|null
280     */
281    protected $sourceColumn;
282    /**
283     * @var bool|null
284     */
285    protected $shouldEvaluate;
286    /**
287     * @var null
288     * @deprecated
289     */
290    protected $ignoreErrors;
291    /**
292     * @var bool
293     */
294    protected $ignoreCallStackMessage = false;
295
296    /**
297     * @var array[]
298     */
299    protected $callStack = [];
300
301    /**
302     * @var array
303     * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
304     */
305    private $resolvedImports = [];
306
307    /**
308     * The directory of the currently processed file
309     *
310     * @var string|null
311     */
312    private $currentDirectory;
313
314    /**
315     * The directory of the input file
316     *
317     * @var string
318     */
319    private $rootDirectory;
320
321    /**
322     * @var bool
323     */
324    private $legacyCwdImportPath = true;
325
326    /**
327     * @var LoggerInterface
328     */
329    private $logger;
330
331    /**
332     * @var array<string, bool>
333     */
334    private $warnedChildFunctions = [];
335
336    /**
337     * Constructor
338     *
339     * @param array|null $cacheOptions
340     * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions
341     */
342    public function __construct($cacheOptions = null)
343    {
344        $this->sourceNames = [];
345
346        if ($cacheOptions) {
347            $this->cache = new Cache($cacheOptions);
348            if (!empty($cacheOptions['checkImportResolutions'])) {
349                $this->cacheCheckImportResolutions = true;
350            }
351        }
352
353        $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
354    }
355
356    /**
357     * Get compiler options
358     *
359     * @return array<string, mixed>
360     *
361     * @internal
362     */
363    public function getCompileOptions()
364    {
365        $options = [
366            'importPaths'        => $this->importPaths,
367            'registeredVars'     => $this->registeredVars,
368            'registeredFeatures' => $this->registeredFeatures,
369            'encoding'           => $this->encoding,
370            'sourceMap'          => serialize($this->sourceMap),
371            'sourceMapOptions'   => $this->sourceMapOptions,
372            'formatter'          => $this->formatter,
373            'legacyImportPath'   => $this->legacyCwdImportPath,
374        ];
375
376        return $options;
377    }
378
379    /**
380     * Sets an alternative logger.
381     *
382     * Changing the logger in the middle of the compilation is not
383     * supported and will result in an undefined behavior.
384     *
385     * @param LoggerInterface $logger
386     *
387     * @return void
388     */
389    public function setLogger(LoggerInterface $logger)
390    {
391        $this->logger = $logger;
392    }
393
394    /**
395     * Set an alternative error output stream, for testing purpose only
396     *
397     * @param resource $handle
398     *
399     * @return void
400     *
401     * @deprecated Use {@see setLogger} instead
402     */
403    public function setErrorOuput($handle)
404    {
405        @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED);
406
407        $this->logger = new StreamLogger($handle);
408    }
409
410    /**
411     * Compile scss
412     *
413     * @param string      $code
414     * @param string|null $path
415     *
416     * @return string
417     *
418     * @throws SassException when the source fails to compile
419     *
420     * @deprecated Use {@see compileString} instead.
421     */
422    public function compile($code, $path = null)
423    {
424        @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED);
425
426        $result = $this->compileString($code, $path);
427
428        $sourceMap = $result->getSourceMap();
429
430        if ($sourceMap !== null) {
431            if ($this->sourceMap instanceof SourceMapGenerator) {
432                $this->sourceMap->saveMap($sourceMap);
433            } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) {
434                $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
435                $sourceMapGenerator->saveMap($sourceMap);
436            }
437        }
438
439        return $result->getCss();
440    }
441
442    /**
443     * Compile scss
444     *
445     * @param string      $source
446     * @param string|null $path
447     *
448     * @return CompilationResult
449     *
450     * @throws SassException when the source fails to compile
451     */
452    public function compileString($source, $path = null)
453    {
454        if ($this->cache) {
455            $cacheKey       = ($path ? $path : '(stdin)') . ':' . md5($source);
456            $compileOptions = $this->getCompileOptions();
457            $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions);
458
459            if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) {
460                return $cachedResult->getResult();
461            }
462        }
463
464        $this->indentLevel    = -1;
465        $this->extends        = [];
466        $this->extendsMap     = [];
467        $this->sourceIndex    = null;
468        $this->sourceLine     = null;
469        $this->sourceColumn   = null;
470        $this->env            = null;
471        $this->scope          = null;
472        $this->storeEnv       = null;
473        $this->shouldEvaluate = null;
474        $this->ignoreCallStackMessage = false;
475        $this->parsedFiles = [];
476        $this->importedFiles = [];
477        $this->resolvedImports = [];
478
479        if (!\is_null($path) && is_file($path)) {
480            $path = realpath($path) ?: $path;
481            $this->currentDirectory = dirname($path);
482            $this->rootDirectory = $this->currentDirectory;
483        } else {
484            $this->currentDirectory = null;
485            $this->rootDirectory = getcwd();
486        }
487
488        try {
489            $this->parser = $this->parserFactory($path);
490            $tree         = $this->parser->parse($source);
491            $this->parser = null;
492
493            $this->formatter = new $this->formatter();
494            $this->rootBlock = null;
495            $this->rootEnv   = $this->pushEnv($tree);
496
497            $warnCallback = function ($message, $deprecation) {
498                $this->logger->warn($message, $deprecation);
499            };
500            $previousWarnCallback = Warn::setCallback($warnCallback);
501
502            try {
503                $this->injectVariables($this->registeredVars);
504                $this->compileRoot($tree);
505                $this->popEnv();
506            } finally {
507                Warn::setCallback($previousWarnCallback);
508            }
509
510            $sourceMapGenerator = null;
511
512            if ($this->sourceMap) {
513                if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
514                    $sourceMapGenerator = $this->sourceMap;
515                    $this->sourceMap = self::SOURCE_MAP_FILE;
516                } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
517                    $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
518                }
519            }
520
521            $out = $this->formatter->format($this->scope, $sourceMapGenerator);
522
523            $prefix = '';
524
525            if ($this->charset && strlen($out) !== Util::mbStrlen($out)) {
526                $prefix = '@charset "UTF-8";' . "\n";
527                $out = $prefix . $out;
528            }
529
530            $sourceMap = null;
531
532            if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
533                $sourceMap = $sourceMapGenerator->generateJson($prefix);
534                $sourceMapUrl = null;
535
536                switch ($this->sourceMap) {
537                    case self::SOURCE_MAP_INLINE:
538                        $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
539                        break;
540
541                    case self::SOURCE_MAP_FILE:
542                        if (isset($this->sourceMapOptions['sourceMapURL'])) {
543                            $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
544                        }
545                        break;
546                }
547
548                if ($sourceMapUrl !== null) {
549                    $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
550                }
551            }
552        } catch (SassScriptException $e) {
553            throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
554        }
555
556        $includedFiles = [];
557
558        foreach ($this->resolvedImports as $resolvedImport) {
559            $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath'];
560        }
561
562        $result = new CompilationResult($out, $sourceMap, array_values($includedFiles));
563
564        if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
565            $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions);
566        }
567
568        // Reset state to free memory
569        // TODO in 2.0, reset parsedFiles as well when the getter is removed.
570        $this->resolvedImports = [];
571        $this->importedFiles = [];
572
573        return $result;
574    }
575
576    /**
577     * @param CachedResult $result
578     *
579     * @return bool
580     */
581    private function isFreshCachedResult(CachedResult $result)
582    {
583        // check if any dependency file changed since the result was compiled
584        foreach ($result->getParsedFiles() as $file => $mtime) {
585            if (! is_file($file) || filemtime($file) !== $mtime) {
586                return false;
587            }
588        }
589
590        if ($this->cacheCheckImportResolutions) {
591            $resolvedImports = [];
592
593            foreach ($result->getResolvedImports() as $import) {
594                $currentDir = $import['currentDir'];
595                $path = $import['path'];
596                // store the check across all the results in memory to avoid multiple findImport() on the same path
597                // with same context.
598                // this is happening in a same hit with multiple compilations (especially with big frameworks)
599                if (empty($resolvedImports[$currentDir][$path])) {
600                    $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir);
601                }
602
603                if ($resolvedImports[$currentDir][$path] !== $import['filePath']) {
604                    return false;
605                }
606            }
607        }
608
609        return true;
610    }
611
612    /**
613     * Instantiate parser
614     *
615     * @param string|null $path
616     *
617     * @return \ScssPhp\ScssPhp\Parser
618     */
619    protected function parserFactory($path)
620    {
621        // https://sass-lang.com/documentation/at-rules/import
622        // CSS files imported by Sass don’t allow any special Sass features.
623        // In order to make sure authors don’t accidentally write Sass in their CSS,
624        // all Sass features that aren’t also valid CSS will produce errors.
625        // Otherwise, the CSS will be rendered as-is. It can even be extended!
626        $cssOnly = false;
627
628        if ($path !== null && substr($path, -4) === '.css') {
629            $cssOnly = true;
630        }
631
632        $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger);
633
634        $this->sourceNames[] = $path;
635        $this->addParsedFile($path);
636
637        return $parser;
638    }
639
640    /**
641     * Is self extend?
642     *
643     * @param array $target
644     * @param array $origin
645     *
646     * @return boolean
647     */
648    protected function isSelfExtend($target, $origin)
649    {
650        foreach ($origin as $sel) {
651            if (\in_array($target, $sel)) {
652                return true;
653            }
654        }
655
656        return false;
657    }
658
659    /**
660     * Push extends
661     *
662     * @param array      $target
663     * @param array      $origin
664     * @param array|null $block
665     *
666     * @return void
667     */
668    protected function pushExtends($target, $origin, $block)
669    {
670        $i = \count($this->extends);
671        $this->extends[] = [$target, $origin, $block];
672
673        foreach ($target as $part) {
674            if (isset($this->extendsMap[$part])) {
675                $this->extendsMap[$part][] = $i;
676            } else {
677                $this->extendsMap[$part] = [$i];
678            }
679        }
680    }
681
682    /**
683     * Make output block
684     *
685     * @param string|null   $type
686     * @param string[]|null $selectors
687     *
688     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
689     */
690    protected function makeOutputBlock($type, $selectors = null)
691    {
692        $out = new OutputBlock();
693        $out->type      = $type;
694        $out->lines     = [];
695        $out->children  = [];
696        $out->parent    = $this->scope;
697        $out->selectors = $selectors;
698        $out->depth     = $this->env->depth;
699
700        if ($this->env->block instanceof Block) {
701            $out->sourceName   = $this->env->block->sourceName;
702            $out->sourceLine   = $this->env->block->sourceLine;
703            $out->sourceColumn = $this->env->block->sourceColumn;
704        } else {
705            $out->sourceName   = null;
706            $out->sourceLine   = null;
707            $out->sourceColumn = null;
708        }
709
710        return $out;
711    }
712
713    /**
714     * Compile root
715     *
716     * @param \ScssPhp\ScssPhp\Block $rootBlock
717     *
718     * @return void
719     */
720    protected function compileRoot(Block $rootBlock)
721    {
722        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
723
724        $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
725        $this->flattenSelectors($this->scope);
726        $this->missingSelectors();
727    }
728
729    /**
730     * Report missing selectors
731     *
732     * @return void
733     */
734    protected function missingSelectors()
735    {
736        foreach ($this->extends as $extend) {
737            if (isset($extend[3])) {
738                continue;
739            }
740
741            list($target, $origin, $block) = $extend;
742
743            // ignore if !optional
744            if ($block[2]) {
745                continue;
746            }
747
748            $target = implode(' ', $target);
749            $origin = $this->collapseSelectors($origin);
750
751            $this->sourceLine = $block[Parser::SOURCE_LINE];
752            throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
753        }
754    }
755
756    /**
757     * Flatten selectors
758     *
759     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
760     * @param string                                 $parentKey
761     *
762     * @return void
763     */
764    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
765    {
766        if ($block->selectors) {
767            $selectors = [];
768
769            foreach ($block->selectors as $s) {
770                $selectors[] = $s;
771
772                if (! \is_array($s)) {
773                    continue;
774                }
775
776                // check extends
777                if (! empty($this->extendsMap)) {
778                    $this->matchExtends($s, $selectors);
779
780                    // remove duplicates
781                    array_walk($selectors, function (&$value) {
782                        $value = serialize($value);
783                    });
784
785                    $selectors = array_unique($selectors);
786
787                    array_walk($selectors, function (&$value) {
788                        $value = unserialize($value);
789                    });
790                }
791            }
792
793            $block->selectors = [];
794            $placeholderSelector = false;
795
796            foreach ($selectors as $selector) {
797                if ($this->hasSelectorPlaceholder($selector)) {
798                    $placeholderSelector = true;
799                    continue;
800                }
801
802                $block->selectors[] = $this->compileSelector($selector);
803            }
804
805            if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
806                unset($block->parent->children[$parentKey]);
807
808                return;
809            }
810        }
811
812        foreach ($block->children as $key => $child) {
813            $this->flattenSelectors($child, $key);
814        }
815    }
816
817    /**
818     * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts
819     *
820     * @param array $parts
821     *
822     * @return array
823     */
824    protected function glueFunctionSelectors($parts)
825    {
826        $new = [];
827
828        foreach ($parts as $part) {
829            if (\is_array($part)) {
830                $part = $this->glueFunctionSelectors($part);
831                $new[] = $part;
832            } else {
833                // a selector part finishing with a ) is the last part of a :not( or :nth-child(
834                // and need to be joined to this
835                if (
836                    \count($new) && \is_string($new[\count($new) - 1]) &&
837                    \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
838                ) {
839                    while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
840                        $part = array_pop($new) . $part;
841                    }
842                    $new[\count($new) - 1] .= $part;
843                } else {
844                    $new[] = $part;
845                }
846            }
847        }
848
849        return $new;
850    }
851
852    /**
853     * Match extends
854     *
855     * @param array   $selector
856     * @param array   $out
857     * @param integer $from
858     * @param boolean $initial
859     *
860     * @return void
861     */
862    protected function matchExtends($selector, &$out, $from = 0, $initial = true)
863    {
864        static $partsPile = [];
865        $selector = $this->glueFunctionSelectors($selector);
866
867        if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
868            return;
869        }
870
871        $outRecurs = [];
872
873        foreach ($selector as $i => $part) {
874            if ($i < $from) {
875                continue;
876            }
877
878            // check that we are not building an infinite loop of extensions
879            // if the new part is just including a previous part don't try to extend anymore
880            if (\count($part) > 1) {
881                foreach ($partsPile as $previousPart) {
882                    if (! \count(array_diff($previousPart, $part))) {
883                        continue 2;
884                    }
885                }
886            }
887
888            $partsPile[] = $part;
889
890            if ($this->matchExtendsSingle($part, $origin, $initial)) {
891                $after       = \array_slice($selector, $i + 1);
892                $before      = \array_slice($selector, 0, $i);
893                list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
894
895                foreach ($origin as $new) {
896                    $k = 0;
897
898                    // remove shared parts
899                    if (\count($new) > 1) {
900                        while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
901                            $k++;
902                        }
903                    }
904
905                    if (\count($nonBreakableBefore) && $k === \count($new)) {
906                        $k--;
907                    }
908
909                    $replacement = [];
910                    $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
911
912                    for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
913                        $slice = [];
914
915                        foreach ($tempReplacement[$l] as $chunk) {
916                            if (! \in_array($chunk, $slice)) {
917                                $slice[] = $chunk;
918                            }
919                        }
920
921                        array_unshift($replacement, $slice);
922
923                        if (! $this->isImmediateRelationshipCombinator(end($slice))) {
924                            break;
925                        }
926                    }
927
928                    $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
929
930                    // Merge shared direct relationships.
931                    $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
932
933                    $result = array_merge(
934                        $before,
935                        $mergedBefore,
936                        $replacement,
937                        $after
938                    );
939
940                    if ($result === $selector) {
941                        continue;
942                    }
943
944                    $this->pushOrMergeExtentedSelector($out, $result);
945
946                    // recursively check for more matches
947                    $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
948
949                    if (\count($origin) > 1) {
950                        $this->matchExtends($result, $out, $startRecurseFrom, false);
951                    } else {
952                        $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
953                    }
954
955                    // selector sequence merging
956                    if (! empty($before) && \count($new) > 1) {
957                        $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
958                        $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
959
960                        list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
961
962                        $result2 = array_merge(
963                            $preSharedParts,
964                            $betweenSharedParts,
965                            $postSharedParts,
966                            $nonBreakabl2,
967                            $nonBreakableBefore,
968                            $replacement,
969                            $after
970                        );
971
972                        $this->pushOrMergeExtentedSelector($out, $result2);
973                    }
974                }
975            }
976            array_pop($partsPile);
977        }
978
979        while (\count($outRecurs)) {
980            $result = array_shift($outRecurs);
981            $this->pushOrMergeExtentedSelector($out, $result);
982        }
983    }
984
985    /**
986     * Test a part for being a pseudo selector
987     *
988     * @param string $part
989     * @param array  $matches
990     *
991     * @return boolean
992     */
993    protected function isPseudoSelector($part, &$matches)
994    {
995        if (
996            strpos($part, ':') === 0 &&
997            preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
998        ) {
999            return true;
1000        }
1001
1002        return false;
1003    }
1004
1005    /**
1006     * Push extended selector except if
1007     *  - this is a pseudo selector
1008     *  - same as previous
1009     *  - in a white list
1010     * in this case we merge the pseudo selector content
1011     *
1012     * @param array $out
1013     * @param array $extended
1014     *
1015     * @return void
1016     */
1017    protected function pushOrMergeExtentedSelector(&$out, $extended)
1018    {
1019        if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
1020            $single = reset($extended);
1021            $part = reset($single);
1022
1023            if (
1024                $this->isPseudoSelector($part, $matchesExtended) &&
1025                \in_array($matchesExtended[1], [ 'slotted' ])
1026            ) {
1027                $prev = end($out);
1028                $prev = $this->glueFunctionSelectors($prev);
1029
1030                if (\count($prev) === 1 && \count(reset($prev)) === 1) {
1031                    $single = reset($prev);
1032                    $part = reset($single);
1033
1034                    if (
1035                        $this->isPseudoSelector($part, $matchesPrev) &&
1036                        $matchesPrev[1] === $matchesExtended[1]
1037                    ) {
1038                        $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
1039                        $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
1040                        $extended = implode($matchesExtended[1] . '(', $extended);
1041                        $extended = [ [ $extended ]];
1042                        array_pop($out);
1043                    }
1044                }
1045            }
1046        }
1047        $out[] = $extended;
1048    }
1049
1050    /**
1051     * Match extends single
1052     *
1053     * @param array   $rawSingle
1054     * @param array   $outOrigin
1055     * @param boolean $initial
1056     *
1057     * @return boolean
1058     */
1059    protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
1060    {
1061        $counts = [];
1062        $single = [];
1063
1064        // simple usual cases, no need to do the whole trick
1065        if (\in_array($rawSingle, [['>'],['+'],['~']])) {
1066            return false;
1067        }
1068
1069        foreach ($rawSingle as $part) {
1070            // matches Number
1071            if (! \is_string($part)) {
1072                return false;
1073            }
1074
1075            if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
1076                $single[\count($single) - 1] .= $part;
1077            } else {
1078                $single[] = $part;
1079            }
1080        }
1081
1082        $extendingDecoratedTag = false;
1083
1084        if (\count($single) > 1) {
1085            $matches = null;
1086            $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
1087        }
1088
1089        $outOrigin = [];
1090        $found = false;
1091
1092        foreach ($single as $k => $part) {
1093            if (isset($this->extendsMap[$part])) {
1094                foreach ($this->extendsMap[$part] as $idx) {
1095                    $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
1096                }
1097            }
1098
1099            if (
1100                $initial &&
1101                $this->isPseudoSelector($part, $matches) &&
1102                ! \in_array($matches[1], [ 'not' ])
1103            ) {
1104                $buffer    = $matches[2];
1105                $parser    = $this->parserFactory(__METHOD__);
1106
1107                if ($parser->parseSelector($buffer, $subSelectors, false)) {
1108                    foreach ($subSelectors as $ksub => $subSelector) {
1109                        $subExtended = [];
1110                        $this->matchExtends($subSelector, $subExtended, 0, false);
1111
1112                        if ($subExtended) {
1113                            $subSelectorsExtended = $subSelectors;
1114                            $subSelectorsExtended[$ksub] = $subExtended;
1115
1116                            foreach ($subSelectorsExtended as $ksse => $sse) {
1117                                $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
1118                            }
1119
1120                            $subSelectorsExtended = implode(', ', $subSelectorsExtended);
1121                            $singleExtended = $single;
1122                            $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
1123                            $outOrigin[] = [ $singleExtended ];
1124                            $found = true;
1125                        }
1126                    }
1127                }
1128            }
1129        }
1130
1131        foreach ($counts as $idx => $count) {
1132            list($target, $origin, /* $block */) = $this->extends[$idx];
1133
1134            $origin = $this->glueFunctionSelectors($origin);
1135
1136            // check count
1137            if ($count !== \count($target)) {
1138                continue;
1139            }
1140
1141            $this->extends[$idx][3] = true;
1142
1143            $rem = array_diff($single, $target);
1144
1145            foreach ($origin as $j => $new) {
1146                // prevent infinite loop when target extends itself
1147                if ($this->isSelfExtend($single, $origin) && ! $initial) {
1148                    return false;
1149                }
1150
1151                $replacement = end($new);
1152
1153                // Extending a decorated tag with another tag is not possible.
1154                if (
1155                    $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
1156                    preg_match('/^[a-z0-9]+$/i', $replacement[0])
1157                ) {
1158                    unset($origin[$j]);
1159                    continue;
1160                }
1161
1162                $combined = $this->combineSelectorSingle($replacement, $rem);
1163
1164                if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
1165                    $origin[$j][\count($origin[$j]) - 1] = $combined;
1166                }
1167            }
1168
1169            $outOrigin = array_merge($outOrigin, $origin);
1170
1171            $found = true;
1172        }
1173
1174        return $found;
1175    }
1176
1177    /**
1178     * Extract a relationship from the fragment.
1179     *
1180     * When extracting the last portion of a selector we will be left with a
1181     * fragment which may end with a direction relationship combinator. This
1182     * method will extract the relationship fragment and return it along side
1183     * the rest.
1184     *
1185     * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
1186     *
1187     * @return array The selector without the relationship fragment if any, the relationship fragment.
1188     */
1189    protected function extractRelationshipFromFragment(array $fragment)
1190    {
1191        $parents = [];
1192        $children = [];
1193
1194        $j = $i = \count($fragment);
1195
1196        for (;;) {
1197            $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
1198            $parents  = \array_slice($fragment, 0, $j);
1199            $slice    = end($parents);
1200
1201            if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
1202                break;
1203            }
1204
1205            $j -= 2;
1206        }
1207
1208        return [$parents, $children];
1209    }
1210
1211    /**
1212     * Combine selector single
1213     *
1214     * @param array $base
1215     * @param array $other
1216     *
1217     * @return array
1218     */
1219    protected function combineSelectorSingle($base, $other)
1220    {
1221        $tag    = [];
1222        $out    = [];
1223        $wasTag = false;
1224        $pseudo = [];
1225
1226        while (\count($other) && strpos(end($other), ':') === 0) {
1227            array_unshift($pseudo, array_pop($other));
1228        }
1229
1230        foreach ([array_reverse($base), array_reverse($other)] as $single) {
1231            $rang = count($single);
1232
1233            foreach ($single as $part) {
1234                if (preg_match('/^[\[:]/', $part)) {
1235                    $out[] = $part;
1236                    $wasTag = false;
1237                } elseif (preg_match('/^[\.#]/', $part)) {
1238                    array_unshift($out, $part);
1239                    $wasTag = false;
1240                } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
1241                    $tag[] = $part;
1242                    $wasTag = true;
1243                } elseif ($wasTag) {
1244                    $tag[\count($tag) - 1] .= $part;
1245                } else {
1246                    array_unshift($out, $part);
1247                }
1248                $rang--;
1249            }
1250        }
1251
1252        if (\count($tag)) {
1253            array_unshift($out, $tag[0]);
1254        }
1255
1256        while (\count($pseudo)) {
1257            $out[] = array_shift($pseudo);
1258        }
1259
1260        return $out;
1261    }
1262
1263    /**
1264     * Compile media
1265     *
1266     * @param \ScssPhp\ScssPhp\Block $media
1267     *
1268     * @return void
1269     */
1270    protected function compileMedia(Block $media)
1271    {
1272        $this->pushEnv($media);
1273
1274        $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
1275
1276        if (! empty($mediaQueries)) {
1277            $previousScope = $this->scope;
1278            $parentScope = $this->mediaParent($this->scope);
1279
1280            foreach ($mediaQueries as $mediaQuery) {
1281                $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
1282
1283                $parentScope->children[] = $this->scope;
1284                $parentScope = $this->scope;
1285            }
1286
1287            // top level properties in a media cause it to be wrapped
1288            $needsWrap = false;
1289
1290            foreach ($media->children as $child) {
1291                $type = $child[0];
1292
1293                if (
1294                    $type !== Type::T_BLOCK &&
1295                    $type !== Type::T_MEDIA &&
1296                    $type !== Type::T_DIRECTIVE &&
1297                    $type !== Type::T_IMPORT
1298                ) {
1299                    $needsWrap = true;
1300                    break;
1301                }
1302            }
1303
1304            if ($needsWrap) {
1305                $wrapped = new Block();
1306                $wrapped->sourceName   = $media->sourceName;
1307                $wrapped->sourceIndex  = $media->sourceIndex;
1308                $wrapped->sourceLine   = $media->sourceLine;
1309                $wrapped->sourceColumn = $media->sourceColumn;
1310                $wrapped->selectors    = [];
1311                $wrapped->comments     = [];
1312                $wrapped->parent       = $media;
1313                $wrapped->children     = $media->children;
1314
1315                $media->children = [[Type::T_BLOCK, $wrapped]];
1316            }
1317
1318            $this->compileChildrenNoReturn($media->children, $this->scope);
1319
1320            $this->scope = $previousScope;
1321        }
1322
1323        $this->popEnv();
1324    }
1325
1326    /**
1327     * Media parent
1328     *
1329     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1330     *
1331     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1332     */
1333    protected function mediaParent(OutputBlock $scope)
1334    {
1335        while (! empty($scope->parent)) {
1336            if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
1337                break;
1338            }
1339
1340            $scope = $scope->parent;
1341        }
1342
1343        return $scope;
1344    }
1345
1346    /**
1347     * Compile directive
1348     *
1349     * @param \ScssPhp\ScssPhp\Block|array $directive
1350     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1351     *
1352     * @return void
1353     */
1354    protected function compileDirective($directive, OutputBlock $out)
1355    {
1356        if (\is_array($directive)) {
1357            $directiveName = $this->compileDirectiveName($directive[0]);
1358            $s = '@' . $directiveName;
1359
1360            if (! empty($directive[1])) {
1361                $s .= ' ' . $this->compileValue($directive[1]);
1362            }
1363            // sass-spec compliance on newline after directives, a bit tricky :/
1364            $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
1365            if (\is_array($directive[0]) && empty($directive[1])) {
1366                $appendNewLine = "\n";
1367            }
1368
1369            if (empty($directive[3])) {
1370                $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
1371            } else {
1372                $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
1373            }
1374        } else {
1375            $directive->name = $this->compileDirectiveName($directive->name);
1376            $s = '@' . $directive->name;
1377
1378            if (! empty($directive->value)) {
1379                $s .= ' ' . $this->compileValue($directive->value);
1380            }
1381
1382            if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1383                $this->compileKeyframeBlock($directive, [$s]);
1384            } else {
1385                $this->compileNestedBlock($directive, [$s]);
1386            }
1387        }
1388    }
1389
1390    /**
1391     * directive names can include some interpolation
1392     *
1393     * @param string|array $directiveName
1394     * @return string
1395     * @throws CompilerException
1396     */
1397    protected function compileDirectiveName($directiveName)
1398    {
1399        if (is_string($directiveName)) {
1400            return $directiveName;
1401        }
1402
1403        return $this->compileValue($directiveName);
1404    }
1405
1406    /**
1407     * Compile at-root
1408     *
1409     * @param \ScssPhp\ScssPhp\Block $block
1410     *
1411     * @return void
1412     */
1413    protected function compileAtRoot(Block $block)
1414    {
1415        $env     = $this->pushEnv($block);
1416        $envs    = $this->compactEnv($env);
1417        list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1418
1419        // wrap inline selector
1420        if ($block->selector) {
1421            $wrapped = new Block();
1422            $wrapped->sourceName   = $block->sourceName;
1423            $wrapped->sourceIndex  = $block->sourceIndex;
1424            $wrapped->sourceLine   = $block->sourceLine;
1425            $wrapped->sourceColumn = $block->sourceColumn;
1426            $wrapped->selectors    = $block->selector;
1427            $wrapped->comments     = [];
1428            $wrapped->parent       = $block;
1429            $wrapped->children     = $block->children;
1430            $wrapped->selfParent   = $block->selfParent;
1431
1432            $block->children = [[Type::T_BLOCK, $wrapped]];
1433            $block->selector = null;
1434        }
1435
1436        $selfParent = $block->selfParent;
1437        assert($selfParent !== null, 'at-root blocks must have a selfParent set.');
1438
1439        if (
1440            ! $selfParent->selectors &&
1441            isset($block->parent) && $block->parent &&
1442            isset($block->parent->selectors) && $block->parent->selectors
1443        ) {
1444            $selfParent = $block->parent;
1445        }
1446
1447        $this->env = $this->filterWithWithout($envs, $with, $without);
1448
1449        $saveScope   = $this->scope;
1450        $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1451
1452        // propagate selfParent to the children where they still can be useful
1453        $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1454
1455        $this->scope = $this->completeScope($this->scope, $saveScope);
1456        $this->scope = $saveScope;
1457        $this->env   = $this->extractEnv($envs);
1458
1459        $this->popEnv();
1460    }
1461
1462    /**
1463     * Filter at-root scope depending of with/without option
1464     *
1465     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1466     * @param array                                  $with
1467     * @param array                                  $without
1468     *
1469     * @return OutputBlock
1470     */
1471    protected function filterScopeWithWithout($scope, $with, $without)
1472    {
1473        $filteredScopes = [];
1474        $childStash = [];
1475
1476        if ($scope->type === Type::T_ROOT) {
1477            return $scope;
1478        }
1479
1480        // start from the root
1481        while ($scope->parent && $scope->parent->type !== Type::T_ROOT) {
1482            array_unshift($childStash, $scope);
1483            $scope = $scope->parent;
1484        }
1485
1486        for (;;) {
1487            if (! $scope) {
1488                break;
1489            }
1490
1491            if ($this->isWith($scope, $with, $without)) {
1492                $s = clone $scope;
1493                $s->children = [];
1494                $s->lines    = [];
1495                $s->parent   = null;
1496
1497                if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1498                    $s->selectors = [];
1499                }
1500
1501                $filteredScopes[] = $s;
1502            }
1503
1504            if (\count($childStash)) {
1505                $scope = array_shift($childStash);
1506            } elseif ($scope->children) {
1507                $scope = end($scope->children);
1508            } else {
1509                $scope = null;
1510            }
1511        }
1512
1513        if (! \count($filteredScopes)) {
1514            return $this->rootBlock;
1515        }
1516
1517        $newScope = array_shift($filteredScopes);
1518        $newScope->parent = $this->rootBlock;
1519
1520        $this->rootBlock->children[] = $newScope;
1521
1522        $p = &$newScope;
1523
1524        while (\count($filteredScopes)) {
1525            $s = array_shift($filteredScopes);
1526            $s->parent = $p;
1527            $p->children[] = $s;
1528            $newScope = &$p->children[0];
1529            $p = &$p->children[0];
1530        }
1531
1532        return $newScope;
1533    }
1534
1535    /**
1536     * found missing selector from a at-root compilation in the previous scope
1537     * (if at-root is just enclosing a property, the selector is in the parent tree)
1538     *
1539     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1540     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1541     *
1542     * @return OutputBlock
1543     */
1544    protected function completeScope($scope, $previousScope)
1545    {
1546        if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
1547            $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1548        }
1549
1550        if ($scope->children) {
1551            foreach ($scope->children as $k => $c) {
1552                $scope->children[$k] = $this->completeScope($c, $previousScope);
1553            }
1554        }
1555
1556        return $scope;
1557    }
1558
1559    /**
1560     * Find a selector by the depth node in the scope
1561     *
1562     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1563     * @param integer                                $depth
1564     *
1565     * @return array
1566     */
1567    protected function findScopeSelectors($scope, $depth)
1568    {
1569        if ($scope->depth === $depth && $scope->selectors) {
1570            return $scope->selectors;
1571        }
1572
1573        if ($scope->children) {
1574            foreach (array_reverse($scope->children) as $c) {
1575                if ($s = $this->findScopeSelectors($c, $depth)) {
1576                    return $s;
1577                }
1578            }
1579        }
1580
1581        return [];
1582    }
1583
1584    /**
1585     * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1586     *
1587     * @param array $withCondition
1588     *
1589     * @return array
1590     */
1591    protected function compileWith($withCondition)
1592    {
1593        // just compile what we have in 2 lists
1594        $with = [];
1595        $without = ['rule' => true];
1596
1597        if ($withCondition) {
1598            if ($withCondition[0] === Type::T_INTERPOLATE) {
1599                $w = $this->compileValue($withCondition);
1600
1601                $buffer = "($w)";
1602                $parser = $this->parserFactory(__METHOD__);
1603
1604                if ($parser->parseValue($buffer, $reParsedWith)) {
1605                    $withCondition = $reParsedWith;
1606                }
1607            }
1608
1609            if ($this->mapHasKey($withCondition, static::$with)) {
1610                $without = []; // cancel the default
1611                $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1612
1613                foreach ($list[2] as $item) {
1614                    $keyword = $this->compileStringContent($this->coerceString($item));
1615
1616                    $with[$keyword] = true;
1617                }
1618            }
1619
1620            if ($this->mapHasKey($withCondition, static::$without)) {
1621                $without = []; // cancel the default
1622                $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1623
1624                foreach ($list[2] as $item) {
1625                    $keyword = $this->compileStringContent($this->coerceString($item));
1626
1627                    $without[$keyword] = true;
1628                }
1629            }
1630        }
1631
1632        return [$with, $without];
1633    }
1634
1635    /**
1636     * Filter env stack
1637     *
1638     * @param Environment[] $envs
1639     * @param array $with
1640     * @param array $without
1641     *
1642     * @return Environment
1643     *
1644     * @phpstan-param  non-empty-array<Environment> $envs
1645     */
1646    protected function filterWithWithout($envs, $with, $without)
1647    {
1648        $filtered = [];
1649
1650        foreach ($envs as $e) {
1651            if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1652                $ec = clone $e;
1653                $ec->block     = null;
1654                $ec->selectors = [];
1655
1656                $filtered[] = $ec;
1657            } else {
1658                $filtered[] = $e;
1659            }
1660        }
1661
1662        return $this->extractEnv($filtered);
1663    }
1664
1665    /**
1666     * Filter WITH rules
1667     *
1668     * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1669     * @param array                                                         $with
1670     * @param array                                                         $without
1671     *
1672     * @return boolean
1673     */
1674    protected function isWith($block, $with, $without)
1675    {
1676        if (isset($block->type)) {
1677            if ($block->type === Type::T_MEDIA) {
1678                return $this->testWithWithout('media', $with, $without);
1679            }
1680
1681            if ($block->type === Type::T_DIRECTIVE) {
1682                if (isset($block->name)) {
1683                    return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
1684                } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1685                    return $this->testWithWithout($m[1], $with, $without);
1686                } else {
1687                    return $this->testWithWithout('???', $with, $without);
1688                }
1689            }
1690        } elseif (isset($block->selectors)) {
1691            // a selector starting with number is a keyframe rule
1692            if (\count($block->selectors)) {
1693                $s = reset($block->selectors);
1694
1695                while (\is_array($s)) {
1696                    $s = reset($s);
1697                }
1698
1699                if (\is_object($s) && $s instanceof Number) {
1700                    return $this->testWithWithout('keyframes', $with, $without);
1701                }
1702            }
1703
1704            return $this->testWithWithout('rule', $with, $without);
1705        }
1706
1707        return true;
1708    }
1709
1710    /**
1711     * Test a single type of block against with/without lists
1712     *
1713     * @param string $what
1714     * @param array  $with
1715     * @param array  $without
1716     *
1717     * @return boolean
1718     *   true if the block should be kept, false to reject
1719     */
1720    protected function testWithWithout($what, $with, $without)
1721    {
1722        // if without, reject only if in the list (or 'all' is in the list)
1723        if (\count($without)) {
1724            return (isset($without[$what]) || isset($without['all'])) ? false : true;
1725        }
1726
1727        // otherwise reject all what is not in the with list
1728        return (isset($with[$what]) || isset($with['all'])) ? true : false;
1729    }
1730
1731
1732    /**
1733     * Compile keyframe block
1734     *
1735     * @param \ScssPhp\ScssPhp\Block $block
1736     * @param string[]               $selectors
1737     *
1738     * @return void
1739     */
1740    protected function compileKeyframeBlock(Block $block, $selectors)
1741    {
1742        $env = $this->pushEnv($block);
1743
1744        $envs = $this->compactEnv($env);
1745
1746        $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1747            return ! isset($e->block->selectors);
1748        }));
1749
1750        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1751        $this->scope->depth = 1;
1752        $this->scope->parent->children[] = $this->scope;
1753
1754        $this->compileChildrenNoReturn($block->children, $this->scope);
1755
1756        $this->scope = $this->scope->parent;
1757        $this->env   = $this->extractEnv($envs);
1758
1759        $this->popEnv();
1760    }
1761
1762    /**
1763     * Compile nested properties lines
1764     *
1765     * @param \ScssPhp\ScssPhp\Block                 $block
1766     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1767     *
1768     * @return void
1769     */
1770    protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1771    {
1772        $prefix = $this->compileValue($block->prefix) . '-';
1773
1774        $nested = $this->makeOutputBlock($block->type);
1775        $nested->parent = $out;
1776
1777        if ($block->hasValue) {
1778            $nested->depth = $out->depth + 1;
1779        }
1780
1781        $out->children[] = $nested;
1782
1783        foreach ($block->children as $child) {
1784            switch ($child[0]) {
1785                case Type::T_ASSIGN:
1786                    array_unshift($child[1][2], $prefix);
1787                    break;
1788
1789                case Type::T_NESTED_PROPERTY:
1790                    array_unshift($child[1]->prefix[2], $prefix);
1791                    break;
1792            }
1793
1794            $this->compileChild($child, $nested);
1795        }
1796    }
1797
1798    /**
1799     * Compile nested block
1800     *
1801     * @param \ScssPhp\ScssPhp\Block $block
1802     * @param string[]               $selectors
1803     *
1804     * @return void
1805     */
1806    protected function compileNestedBlock(Block $block, $selectors)
1807    {
1808        $this->pushEnv($block);
1809
1810        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1811        $this->scope->parent->children[] = $this->scope;
1812
1813        // wrap assign children in a block
1814        // except for @font-face
1815        if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') {
1816            // need wrapping?
1817            $needWrapping = false;
1818
1819            foreach ($block->children as $child) {
1820                if ($child[0] === Type::T_ASSIGN) {
1821                    $needWrapping = true;
1822                    break;
1823                }
1824            }
1825
1826            if ($needWrapping) {
1827                $wrapped = new Block();
1828                $wrapped->sourceName   = $block->sourceName;
1829                $wrapped->sourceIndex  = $block->sourceIndex;
1830                $wrapped->sourceLine   = $block->sourceLine;
1831                $wrapped->sourceColumn = $block->sourceColumn;
1832                $wrapped->selectors    = [];
1833                $wrapped->comments     = [];
1834                $wrapped->parent       = $block;
1835                $wrapped->children     = $block->children;
1836                $wrapped->selfParent   = $block->selfParent;
1837
1838                $block->children = [[Type::T_BLOCK, $wrapped]];
1839            }
1840        }
1841
1842        $this->compileChildrenNoReturn($block->children, $this->scope);
1843
1844        $this->scope = $this->scope->parent;
1845
1846        $this->popEnv();
1847    }
1848
1849    /**
1850     * Recursively compiles a block.
1851     *
1852     * A block is analogous to a CSS block in most cases. A single SCSS document
1853     * is encapsulated in a block when parsed, but it does not have parent tags
1854     * so all of its children appear on the root level when compiled.
1855     *
1856     * Blocks are made up of selectors and children.
1857     *
1858     * The children of a block are just all the blocks that are defined within.
1859     *
1860     * Compiling the block involves pushing a fresh environment on the stack,
1861     * and iterating through the props, compiling each one.
1862     *
1863     * @see Compiler::compileChild()
1864     *
1865     * @param \ScssPhp\ScssPhp\Block $block
1866     *
1867     * @return void
1868     */
1869    protected function compileBlock(Block $block)
1870    {
1871        $env = $this->pushEnv($block);
1872        $env->selectors = $this->evalSelectors($block->selectors);
1873
1874        $out = $this->makeOutputBlock(null);
1875
1876        $this->scope->children[] = $out;
1877
1878        if (\count($block->children)) {
1879            $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1880
1881            // propagate selfParent to the children where they still can be useful
1882            $selfParentSelectors = null;
1883
1884            if (isset($block->selfParent->selectors)) {
1885                $selfParentSelectors = $block->selfParent->selectors;
1886                $block->selfParent->selectors = $out->selectors;
1887            }
1888
1889            $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1890
1891            // and revert for the following children of the same block
1892            if ($selfParentSelectors) {
1893                $block->selfParent->selectors = $selfParentSelectors;
1894            }
1895        }
1896
1897        $this->popEnv();
1898    }
1899
1900
1901    /**
1902     * Compile the value of a comment that can have interpolation
1903     *
1904     * @param array   $value
1905     * @param boolean $pushEnv
1906     *
1907     * @return string
1908     */
1909    protected function compileCommentValue($value, $pushEnv = false)
1910    {
1911        $c = $value[1];
1912
1913        if (isset($value[2])) {
1914            if ($pushEnv) {
1915                $this->pushEnv();
1916            }
1917
1918            try {
1919                $c = $this->compileValue($value[2]);
1920            } catch (SassScriptException $e) {
1921                $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true);
1922                // ignore error in comment compilation which are only interpolation
1923            } catch (SassException $e) {
1924                $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true);
1925                // ignore error in comment compilation which are only interpolation
1926            }
1927
1928            if ($pushEnv) {
1929                $this->popEnv();
1930            }
1931        }
1932
1933        return $c;
1934    }
1935
1936    /**
1937     * Compile root level comment
1938     *
1939     * @param array $block
1940     *
1941     * @return void
1942     */
1943    protected function compileComment($block)
1944    {
1945        $out = $this->makeOutputBlock(Type::T_COMMENT);
1946        $out->lines[] = $this->compileCommentValue($block, true);
1947
1948        $this->scope->children[] = $out;
1949    }
1950
1951    /**
1952     * Evaluate selectors
1953     *
1954     * @param array $selectors
1955     *
1956     * @return array
1957     */
1958    protected function evalSelectors($selectors)
1959    {
1960        $this->shouldEvaluate = false;
1961
1962        $selectors = array_map([$this, 'evalSelector'], $selectors);
1963
1964        // after evaluating interpolates, we might need a second pass
1965        if ($this->shouldEvaluate) {
1966            $selectors = $this->replaceSelfSelector($selectors, '&');
1967            $buffer    = $this->collapseSelectors($selectors);
1968            $parser    = $this->parserFactory(__METHOD__);
1969
1970            try {
1971                $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1972            } catch (ParserException $e) {
1973                throw $this->error($e->getMessage());
1974            }
1975
1976            if ($isValid) {
1977                $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1978            }
1979        }
1980
1981        return $selectors;
1982    }
1983
1984    /**
1985     * Evaluate selector
1986     *
1987     * @param array $selector
1988     *
1989     * @return array
1990     */
1991    protected function evalSelector($selector)
1992    {
1993        return array_map([$this, 'evalSelectorPart'], $selector);
1994    }
1995
1996    /**
1997     * Evaluate selector part; replaces all the interpolates, stripping quotes
1998     *
1999     * @param array $part
2000     *
2001     * @return array
2002     */
2003    protected function evalSelectorPart($part)
2004    {
2005        foreach ($part as &$p) {
2006            if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
2007                $p = $this->compileValue($p);
2008
2009                // force re-evaluation if self char or non standard char
2010                if (preg_match(',[^\w-],', $p)) {
2011                    $this->shouldEvaluate = true;
2012                }
2013            } elseif (
2014                \is_string($p) && \strlen($p) >= 2 &&
2015                ($first = $p[0]) && ($first === '"' || $first === "'") &&
2016                substr($p, -1) === $first
2017            ) {
2018                $p = substr($p, 1, -1);
2019            }
2020        }
2021
2022        return $this->flattenSelectorSingle($part);
2023    }
2024
2025    /**
2026     * Collapse selectors
2027     *
2028     * @param array $selectors
2029     *
2030     * @return string
2031     */
2032    protected function collapseSelectors($selectors)
2033    {
2034        $parts = [];
2035
2036        foreach ($selectors as $selector) {
2037            $output = [];
2038
2039            foreach ($selector as $node) {
2040                $compound = '';
2041
2042                array_walk_recursive(
2043                    $node,
2044                    function ($value, $key) use (&$compound) {
2045                        $compound .= $value;
2046                    }
2047                );
2048
2049                $output[] = $compound;
2050            }
2051
2052            $parts[] = implode(' ', $output);
2053        }
2054
2055        return implode(', ', $parts);
2056    }
2057
2058    /**
2059     * Collapse selectors
2060     *
2061     * @param array $selectors
2062     *
2063     * @return array
2064     */
2065    private function collapseSelectorsAsList($selectors)
2066    {
2067        $parts = [];
2068
2069        foreach ($selectors as $selector) {
2070            $output = [];
2071            $glueNext = false;
2072
2073            foreach ($selector as $node) {
2074                $compound = '';
2075
2076                array_walk_recursive(
2077                    $node,
2078                    function ($value, $key) use (&$compound) {
2079                        $compound .= $value;
2080                    }
2081                );
2082
2083                if ($this->isImmediateRelationshipCombinator($compound)) {
2084                    if (\count($output)) {
2085                        $output[\count($output) - 1] .= ' ' . $compound;
2086                    } else {
2087                        $output[] = $compound;
2088                    }
2089
2090                    $glueNext = true;
2091                } elseif ($glueNext) {
2092                    $output[\count($output) - 1] .= ' ' . $compound;
2093                    $glueNext = false;
2094                } else {
2095                    $output[] = $compound;
2096                }
2097            }
2098
2099            foreach ($output as &$o) {
2100                $o = [Type::T_STRING, '', [$o]];
2101            }
2102
2103            $parts[] = [Type::T_LIST, ' ', $output];
2104        }
2105
2106        return [Type::T_LIST, ',', $parts];
2107    }
2108
2109    /**
2110     * Parse down the selector and revert [self] to "&" before a reparsing
2111     *
2112     * @param array       $selectors
2113     * @param string|null $replace
2114     *
2115     * @return array
2116     */
2117    protected function replaceSelfSelector($selectors, $replace = null)
2118    {
2119        foreach ($selectors as &$part) {
2120            if (\is_array($part)) {
2121                if ($part === [Type::T_SELF]) {
2122                    if (\is_null($replace)) {
2123                        $replace = $this->reduce([Type::T_SELF]);
2124                        $replace = $this->compileValue($replace);
2125                    }
2126                    $part = $replace;
2127                } else {
2128                    $part = $this->replaceSelfSelector($part, $replace);
2129                }
2130            }
2131        }
2132
2133        return $selectors;
2134    }
2135
2136    /**
2137     * Flatten selector single; joins together .classes and #ids
2138     *
2139     * @param array $single
2140     *
2141     * @return array
2142     */
2143    protected function flattenSelectorSingle($single)
2144    {
2145        $joined = [];
2146
2147        foreach ($single as $part) {
2148            if (
2149                empty($joined) ||
2150                ! \is_string($part) ||
2151                preg_match('/[\[.:#%]/', $part)
2152            ) {
2153                $joined[] = $part;
2154                continue;
2155            }
2156
2157            if (\is_array(end($joined))) {
2158                $joined[] = $part;
2159            } else {
2160                $joined[\count($joined) - 1] .= $part;
2161            }
2162        }
2163
2164        return $joined;
2165    }
2166
2167    /**
2168     * Compile selector to string; self(&) should have been replaced by now
2169     *
2170     * @param string|array $selector
2171     *
2172     * @return string
2173     */
2174    protected function compileSelector($selector)
2175    {
2176        if (! \is_array($selector)) {
2177            return $selector; // media and the like
2178        }
2179
2180        return implode(
2181            ' ',
2182            array_map(
2183                [$this, 'compileSelectorPart'],
2184                $selector
2185            )
2186        );
2187    }
2188
2189    /**
2190     * Compile selector part
2191     *
2192     * @param array $piece
2193     *
2194     * @return string
2195     */
2196    protected function compileSelectorPart($piece)
2197    {
2198        foreach ($piece as &$p) {
2199            if (! \is_array($p)) {
2200                continue;
2201            }
2202
2203            switch ($p[0]) {
2204                case Type::T_SELF:
2205                    $p = '&';
2206                    break;
2207
2208                default:
2209                    $p = $this->compileValue($p);
2210                    break;
2211            }
2212        }
2213
2214        return implode($piece);
2215    }
2216
2217    /**
2218     * Has selector placeholder?
2219     *
2220     * @param array $selector
2221     *
2222     * @return boolean
2223     */
2224    protected function hasSelectorPlaceholder($selector)
2225    {
2226        if (! \is_array($selector)) {
2227            return false;
2228        }
2229
2230        foreach ($selector as $parts) {
2231            foreach ($parts as $part) {
2232                if (\strlen($part) && '%' === $part[0]) {
2233                    return true;
2234                }
2235            }
2236        }
2237
2238        return false;
2239    }
2240
2241    /**
2242     * @param string $name
2243     *
2244     * @return void
2245     */
2246    protected function pushCallStack($name = '')
2247    {
2248        $this->callStack[] = [
2249          'n' => $name,
2250          Parser::SOURCE_INDEX => $this->sourceIndex,
2251          Parser::SOURCE_LINE => $this->sourceLine,
2252          Parser::SOURCE_COLUMN => $this->sourceColumn
2253        ];
2254
2255        // infinite calling loop
2256        if (\count($this->callStack) > 25000) {
2257            // not displayed but you can var_dump it to deep debug
2258            $msg = $this->callStackMessage(true, 100);
2259            $msg = 'Infinite calling loop';
2260
2261            throw $this->error($msg);
2262        }
2263    }
2264
2265    /**
2266     * @return void
2267     */
2268    protected function popCallStack()
2269    {
2270        array_pop($this->callStack);
2271    }
2272
2273    /**
2274     * Compile children and return result
2275     *
2276     * @param array                                  $stms
2277     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2278     * @param string                                 $traceName
2279     *
2280     * @return array|Number|null
2281     */
2282    protected function compileChildren($stms, OutputBlock $out, $traceName = '')
2283    {
2284        $this->pushCallStack($traceName);
2285
2286        foreach ($stms as $stm) {
2287            $ret = $this->compileChild($stm, $out);
2288
2289            if (isset($ret)) {
2290                $this->popCallStack();
2291
2292                return $ret;
2293            }
2294        }
2295
2296        $this->popCallStack();
2297
2298        return null;
2299    }
2300
2301    /**
2302     * Compile children and throw exception if unexpected `@return`
2303     *
2304     * @param array                                  $stms
2305     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2306     * @param \ScssPhp\ScssPhp\Block                 $selfParent
2307     * @param string                                 $traceName
2308     *
2309     * @return void
2310     *
2311     * @throws \Exception
2312     */
2313    protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
2314    {
2315        $this->pushCallStack($traceName);
2316
2317        foreach ($stms as $stm) {
2318            if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
2319                $stm[1]->selfParent = $selfParent;
2320                $ret = $this->compileChild($stm, $out);
2321                $stm[1]->selfParent = null;
2322            } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) {
2323                $stm['selfParent'] = $selfParent;
2324                $ret = $this->compileChild($stm, $out);
2325                unset($stm['selfParent']);
2326            } else {
2327                $ret = $this->compileChild($stm, $out);
2328            }
2329
2330            if (isset($ret)) {
2331                throw $this->error('@return may only be used within a function');
2332            }
2333        }
2334
2335        $this->popCallStack();
2336    }
2337
2338
2339    /**
2340     * evaluate media query : compile internal value keeping the structure unchanged
2341     *
2342     * @param array $queryList
2343     *
2344     * @return array
2345     */
2346    protected function evaluateMediaQuery($queryList)
2347    {
2348        static $parser = null;
2349
2350        $outQueryList = [];
2351
2352        foreach ($queryList as $kql => $query) {
2353            $shouldReparse = false;
2354
2355            foreach ($query as $kq => $q) {
2356                for ($i = 1; $i < \count($q); $i++) {
2357                    $value = $this->compileValue($q[$i]);
2358
2359                    // the parser had no mean to know if media type or expression if it was an interpolation
2360                    // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
2361                    if (
2362                        $q[0] == Type::T_MEDIA_TYPE &&
2363                        (strpos($value, '(') !== false ||
2364                        strpos($value, ')') !== false ||
2365                        strpos($value, ':') !== false ||
2366                        strpos($value, ',') !== false)
2367                    ) {
2368                        $shouldReparse = true;
2369                    }
2370
2371                    $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
2372                }
2373            }
2374
2375            if ($shouldReparse) {
2376                if (\is_null($parser)) {
2377                    $parser = $this->parserFactory(__METHOD__);
2378                }
2379
2380                $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2381                $queryString = reset($queryString);
2382
2383                if (strpos($queryString, '@media ') === 0) {
2384                    $queryString = substr($queryString, 7);
2385                    $queries = [];
2386
2387                    if ($parser->parseMediaQueryList($queryString, $queries)) {
2388                        $queries = $this->evaluateMediaQuery($queries[2]);
2389
2390                        while (\count($queries)) {
2391                            $outQueryList[] = array_shift($queries);
2392                        }
2393
2394                        continue;
2395                    }
2396                }
2397            }
2398
2399            $outQueryList[] = $queryList[$kql];
2400        }
2401
2402        return $outQueryList;
2403    }
2404
2405    /**
2406     * Compile media query
2407     *
2408     * @param array $queryList
2409     *
2410     * @return string[]
2411     */
2412    protected function compileMediaQuery($queryList)
2413    {
2414        $start   = '@media ';
2415        $default = trim($start);
2416        $out     = [];
2417        $current = '';
2418
2419        foreach ($queryList as $query) {
2420            $type = null;
2421            $parts = [];
2422
2423            $mediaTypeOnly = true;
2424
2425            foreach ($query as $q) {
2426                if ($q[0] !== Type::T_MEDIA_TYPE) {
2427                    $mediaTypeOnly = false;
2428                    break;
2429                }
2430            }
2431
2432            foreach ($query as $q) {
2433                switch ($q[0]) {
2434                    case Type::T_MEDIA_TYPE:
2435                        $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
2436
2437                        // combining not and anything else than media type is too risky and should be avoided
2438                        if (! $mediaTypeOnly) {
2439                            if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
2440                                if ($type) {
2441                                    array_unshift($parts, implode(' ', array_filter($type)));
2442                                }
2443
2444                                if (! empty($parts)) {
2445                                    if (\strlen($current)) {
2446                                        $current .= $this->formatter->tagSeparator;
2447                                    }
2448
2449                                    $current .= implode(' and ', $parts);
2450                                }
2451
2452                                if ($current) {
2453                                    $out[] = $start . $current;
2454                                }
2455
2456                                $current = '';
2457                                $type    = null;
2458                                $parts   = [];
2459                            }
2460                        }
2461
2462                        if ($newType === ['all'] && $default) {
2463                            $default = $start . 'all';
2464                        }
2465
2466                        // all can be safely ignored and mixed with whatever else
2467                        if ($newType !== ['all']) {
2468                            if ($type) {
2469                                $type = $this->mergeMediaTypes($type, $newType);
2470
2471                                if (empty($type)) {
2472                                    // merge failed : ignore this query that is not valid, skip to the next one
2473                                    $parts = [];
2474                                    $default = ''; // if everything fail, no @media at all
2475                                    continue 3;
2476                                }
2477                            } else {
2478                                $type = $newType;
2479                            }
2480                        }
2481                        break;
2482
2483                    case Type::T_MEDIA_EXPRESSION:
2484                        if (isset($q[2])) {
2485                            $parts[] = '('
2486                                . $this->compileValue($q[1])
2487                                . $this->formatter->assignSeparator
2488                                . $this->compileValue($q[2])
2489                                . ')';
2490                        } else {
2491                            $parts[] = '('
2492                                . $this->compileValue($q[1])
2493                                . ')';
2494                        }
2495                        break;
2496
2497                    case Type::T_MEDIA_VALUE:
2498                        $parts[] = $this->compileValue($q[1]);
2499                        break;
2500                }
2501            }
2502
2503            if ($type) {
2504                array_unshift($parts, implode(' ', array_filter($type)));
2505            }
2506
2507            if (! empty($parts)) {
2508                if (\strlen($current)) {
2509                    $current .= $this->formatter->tagSeparator;
2510                }
2511
2512                $current .= implode(' and ', $parts);
2513            }
2514        }
2515
2516        if ($current) {
2517            $out[] = $start . $current;
2518        }
2519
2520        // no @media type except all, and no conflict?
2521        if (! $out && $default) {
2522            $out[] = $default;
2523        }
2524
2525        return $out;
2526    }
2527
2528    /**
2529     * Merge direct relationships between selectors
2530     *
2531     * @param array $selectors1
2532     * @param array $selectors2
2533     *
2534     * @return array
2535     */
2536    protected function mergeDirectRelationships($selectors1, $selectors2)
2537    {
2538        if (empty($selectors1) || empty($selectors2)) {
2539            return array_merge($selectors1, $selectors2);
2540        }
2541
2542        $part1 = end($selectors1);
2543        $part2 = end($selectors2);
2544
2545        if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2546            return array_merge($selectors1, $selectors2);
2547        }
2548
2549        $merged = [];
2550
2551        do {
2552            $part1 = array_pop($selectors1);
2553            $part2 = array_pop($selectors2);
2554
2555            if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2556                if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2557                    array_unshift($merged, [$part1[0] . $part2[0]]);
2558                    $merged = array_merge($selectors1, $selectors2, $merged);
2559                } else {
2560                    $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2561                }
2562
2563                break;
2564            }
2565
2566            array_unshift($merged, $part1);
2567        } while (! empty($selectors1) && ! empty($selectors2));
2568
2569        return $merged;
2570    }
2571
2572    /**
2573     * Merge media types
2574     *
2575     * @param array $type1
2576     * @param array $type2
2577     *
2578     * @return array|null
2579     */
2580    protected function mergeMediaTypes($type1, $type2)
2581    {
2582        if (empty($type1)) {
2583            return $type2;
2584        }
2585
2586        if (empty($type2)) {
2587            return $type1;
2588        }
2589
2590        if (\count($type1) > 1) {
2591            $m1 = strtolower($type1[0]);
2592            $t1 = strtolower($type1[1]);
2593        } else {
2594            $m1 = '';
2595            $t1 = strtolower($type1[0]);
2596        }
2597
2598        if (\count($type2) > 1) {
2599            $m2 = strtolower($type2[0]);
2600            $t2 = strtolower($type2[1]);
2601        } else {
2602            $m2 = '';
2603            $t2 = strtolower($type2[0]);
2604        }
2605
2606        if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2607            if ($t1 === $t2) {
2608                return null;
2609            }
2610
2611            return [
2612                $m1 === Type::T_NOT ? $m2 : $m1,
2613                $m1 === Type::T_NOT ? $t2 : $t1,
2614            ];
2615        }
2616
2617        if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2618            // CSS has no way of representing "neither screen nor print"
2619            if ($t1 !== $t2) {
2620                return null;
2621            }
2622
2623            return [Type::T_NOT, $t1];
2624        }
2625
2626        if ($t1 !== $t2) {
2627            return null;
2628        }
2629
2630        // t1 == t2, neither m1 nor m2 are "not"
2631        return [empty($m1) ? $m2 : $m1, $t1];
2632    }
2633
2634    /**
2635     * Compile import; returns true if the value was something that could be imported
2636     *
2637     * @param array                                  $rawPath
2638     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2639     * @param boolean                                $once
2640     *
2641     * @return boolean
2642     */
2643    protected function compileImport($rawPath, OutputBlock $out, $once = false)
2644    {
2645        if ($rawPath[0] === Type::T_STRING) {
2646            $path = $this->compileStringContent($rawPath);
2647
2648            if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) {
2649                $this->registerImport($this->currentDirectory, $path, $filePath);
2650
2651                if (! $once || ! \in_array($filePath, $this->importedFiles)) {
2652                    $this->importFile($filePath, $out);
2653                    $this->importedFiles[] = $filePath;
2654                }
2655
2656                return true;
2657            }
2658
2659            $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2660
2661            return false;
2662        }
2663
2664        if ($rawPath[0] === Type::T_LIST) {
2665            // handle a list of strings
2666            if (\count($rawPath[2]) === 0) {
2667                return false;
2668            }
2669
2670            foreach ($rawPath[2] as $path) {
2671                if ($path[0] !== Type::T_STRING) {
2672                    $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2673
2674                    return false;
2675                }
2676            }
2677
2678            foreach ($rawPath[2] as $path) {
2679                $this->compileImport($path, $out, $once);
2680            }
2681
2682            return true;
2683        }
2684
2685        $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2686
2687        return false;
2688    }
2689
2690    /**
2691     * @param array $rawPath
2692     * @return string
2693     * @throws CompilerException
2694     */
2695    protected function compileImportPath($rawPath)
2696    {
2697        $path = $this->compileValue($rawPath);
2698
2699        // case url() without quotes : suppress \r \n remaining in the path
2700        // if this is a real string there can not be CR or LF char
2701        if (strpos($path, 'url(') === 0) {
2702            $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2703        } else {
2704            // if this is a file name in a string, spaces should be escaped
2705            $path = $this->reduce($rawPath);
2706            $path = $this->escapeImportPathString($path);
2707            $path = $this->compileValue($path);
2708        }
2709
2710        return $path;
2711    }
2712
2713    /**
2714     * @param array $path
2715     * @return array
2716     * @throws CompilerException
2717     */
2718    protected function escapeImportPathString($path)
2719    {
2720        switch ($path[0]) {
2721            case Type::T_LIST:
2722                foreach ($path[2] as $k => $v) {
2723                    $path[2][$k] = $this->escapeImportPathString($v);
2724                }
2725                break;
2726            case Type::T_STRING:
2727                if ($path[1]) {
2728                    $path = $this->compileValue($path);
2729                    $path = str_replace(' ', '\\ ', $path);
2730                    $path = [Type::T_KEYWORD, $path];
2731                }
2732                break;
2733        }
2734
2735        return $path;
2736    }
2737
2738    /**
2739     * Append a root directive like @import or @charset as near as the possible from the source code
2740     * (keeping before comments, @import and @charset coming before in the source code)
2741     *
2742     * @param string                                 $line
2743     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2744     * @param array                                  $allowed
2745     *
2746     * @return void
2747     */
2748    protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2749    {
2750        $root = $out;
2751
2752        while ($root->parent) {
2753            $root = $root->parent;
2754        }
2755
2756        $i = 0;
2757
2758        while ($i < \count($root->children)) {
2759            if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2760                break;
2761            }
2762
2763            $i++;
2764        }
2765
2766        // remove incompatible children from the bottom of the list
2767        $saveChildren = [];
2768
2769        while ($i < \count($root->children)) {
2770            $saveChildren[] = array_pop($root->children);
2771        }
2772
2773        // insert the directive as a comment
2774        $child = $this->makeOutputBlock(Type::T_COMMENT);
2775        $child->lines[]      = $line;
2776        $child->sourceName   = $this->sourceNames[$this->sourceIndex];
2777        $child->sourceLine   = $this->sourceLine;
2778        $child->sourceColumn = $this->sourceColumn;
2779
2780        $root->children[] = $child;
2781
2782        // repush children
2783        while (\count($saveChildren)) {
2784            $root->children[] = array_pop($saveChildren);
2785        }
2786    }
2787
2788    /**
2789     * Append lines to the current output block:
2790     * directly to the block or through a child if necessary
2791     *
2792     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2793     * @param string                                 $type
2794     * @param string                                 $line
2795     *
2796     * @return void
2797     */
2798    protected function appendOutputLine(OutputBlock $out, $type, $line)
2799    {
2800        $outWrite = &$out;
2801
2802        // check if it's a flat output or not
2803        if (\count($out->children)) {
2804            $lastChild = &$out->children[\count($out->children) - 1];
2805
2806            if (
2807                $lastChild->depth === $out->depth &&
2808                \is_null($lastChild->selectors) &&
2809                ! \count($lastChild->children)
2810            ) {
2811                $outWrite = $lastChild;
2812            } else {
2813                $nextLines = $this->makeOutputBlock($type);
2814                $nextLines->parent = $out;
2815                $nextLines->depth  = $out->depth;
2816
2817                $out->children[] = $nextLines;
2818                $outWrite = &$nextLines;
2819            }
2820        }
2821
2822        $outWrite->lines[] = $line;
2823    }
2824
2825    /**
2826     * Compile child; returns a value to halt execution
2827     *
2828     * @param array                                  $child
2829     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2830     *
2831     * @return array|Number|null
2832     */
2833    protected function compileChild($child, OutputBlock $out)
2834    {
2835        if (isset($child[Parser::SOURCE_LINE])) {
2836            $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2837            $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2838            $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2839        } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
2840            $this->sourceIndex  = $child[1]->sourceIndex;
2841            $this->sourceLine   = $child[1]->sourceLine;
2842            $this->sourceColumn = $child[1]->sourceColumn;
2843        } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2844            $this->sourceLine   = $out->sourceLine;
2845            $sourceIndex  = array_search($out->sourceName, $this->sourceNames);
2846            $this->sourceColumn = $out->sourceColumn;
2847
2848            if ($sourceIndex === false) {
2849                $sourceIndex = null;
2850            }
2851            $this->sourceIndex = $sourceIndex;
2852        }
2853
2854        switch ($child[0]) {
2855            case Type::T_SCSSPHP_IMPORT_ONCE:
2856                $rawPath = $this->reduce($child[1]);
2857
2858                $this->compileImport($rawPath, $out, true);
2859                break;
2860
2861            case Type::T_IMPORT:
2862                $rawPath = $this->reduce($child[1]);
2863
2864                $this->compileImport($rawPath, $out);
2865                break;
2866
2867            case Type::T_DIRECTIVE:
2868                $this->compileDirective($child[1], $out);
2869                break;
2870
2871            case Type::T_AT_ROOT:
2872                $this->compileAtRoot($child[1]);
2873                break;
2874
2875            case Type::T_MEDIA:
2876                $this->compileMedia($child[1]);
2877                break;
2878
2879            case Type::T_BLOCK:
2880                $this->compileBlock($child[1]);
2881                break;
2882
2883            case Type::T_CHARSET:
2884                break;
2885
2886            case Type::T_CUSTOM_PROPERTY:
2887                list(, $name, $value) = $child;
2888                $compiledName = $this->compileValue($name);
2889
2890                // if the value reduces to null from something else then
2891                // the property should be discarded
2892                if ($value[0] !== Type::T_NULL) {
2893                    $value = $this->reduce($value);
2894
2895                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2896                        break;
2897                    }
2898                }
2899
2900                $compiledValue = $this->compileValue($value);
2901
2902                $line = $this->formatter->customProperty(
2903                    $compiledName,
2904                    $compiledValue
2905                );
2906
2907                $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2908                break;
2909
2910            case Type::T_ASSIGN:
2911                list(, $name, $value) = $child;
2912
2913                if ($name[0] === Type::T_VARIABLE) {
2914                    $flags     = isset($child[3]) ? $child[3] : [];
2915                    $isDefault = \in_array('!default', $flags);
2916                    $isGlobal  = \in_array('!global', $flags);
2917
2918                    if ($isGlobal) {
2919                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2920                        break;
2921                    }
2922
2923                    $shouldSet = $isDefault &&
2924                        (\is_null($result = $this->get($name[1], false)) ||
2925                        $result === static::$null);
2926
2927                    if (! $isDefault || $shouldSet) {
2928                        $this->set($name[1], $this->reduce($value), true, null, $value);
2929                    }
2930                    break;
2931                }
2932
2933                $compiledName = $this->compileValue($name);
2934
2935                // handle shorthand syntaxes : size / line-height...
2936                if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2937                    if ($value[0] === Type::T_VARIABLE) {
2938                        // if the font value comes from variable, the content is already reduced
2939                        // (i.e., formulas were already calculated), so we need the original unreduced value
2940                        $value = $this->get($value[1], true, null, true);
2941                    }
2942
2943                    $shorthandValue=&$value;
2944
2945                    $shorthandDividerNeedsUnit = false;
2946                    $maxListElements           = null;
2947                    $maxShorthandDividers      = 1;
2948
2949                    switch ($compiledName) {
2950                        case 'border-radius':
2951                            $maxListElements = 4;
2952                            $shorthandDividerNeedsUnit = true;
2953                            break;
2954                    }
2955
2956                    if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
2957                        // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2958                        // we need to handle the first list element
2959                        $shorthandValue=&$value[2][0];
2960                    }
2961
2962                    if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2963                        $revert = true;
2964
2965                        if ($shorthandDividerNeedsUnit) {
2966                            $divider = $shorthandValue[3];
2967
2968                            if (\is_array($divider)) {
2969                                $divider = $this->reduce($divider, true);
2970                            }
2971
2972                            if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
2973                                $revert = false;
2974                            }
2975                        }
2976
2977                        if ($revert) {
2978                            $shorthandValue = $this->expToString($shorthandValue);
2979                        }
2980                    } elseif ($shorthandValue[0] === Type::T_LIST) {
2981                        foreach ($shorthandValue[2] as &$item) {
2982                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2983                                if ($maxShorthandDividers > 0) {
2984                                    $revert = true;
2985
2986                                    // if the list of values is too long, this has to be a shorthand,
2987                                    // otherwise it could be a real division
2988                                    if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
2989                                        if ($shorthandDividerNeedsUnit) {
2990                                            $divider = $item[3];
2991
2992                                            if (\is_array($divider)) {
2993                                                $divider = $this->reduce($divider, true);
2994                                            }
2995
2996                                            if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
2997                                                $revert = false;
2998                                            }
2999                                        }
3000                                    }
3001
3002                                    if ($revert) {
3003                                        $item = $this->expToString($item);
3004                                        $maxShorthandDividers--;
3005                                    }
3006                                }
3007                            }
3008                        }
3009                    }
3010                }
3011
3012                // if the value reduces to null from something else then
3013                // the property should be discarded
3014                if ($value[0] !== Type::T_NULL) {
3015                    $value = $this->reduce($value);
3016
3017                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
3018                        break;
3019                    }
3020                }
3021
3022                $compiledValue = $this->compileValue($value);
3023
3024                // ignore empty value
3025                if (\strlen($compiledValue)) {
3026                    $line = $this->formatter->property(
3027                        $compiledName,
3028                        $compiledValue
3029                    );
3030                    $this->appendOutputLine($out, Type::T_ASSIGN, $line);
3031                }
3032                break;
3033
3034            case Type::T_COMMENT:
3035                if ($out->type === Type::T_ROOT) {
3036                    $this->compileComment($child);
3037                    break;
3038                }
3039
3040                $line = $this->compileCommentValue($child, true);
3041                $this->appendOutputLine($out, Type::T_COMMENT, $line);
3042                break;
3043
3044            case Type::T_MIXIN:
3045            case Type::T_FUNCTION:
3046                list(, $block) = $child;
3047                // the block need to be able to go up to it's parent env to resolve vars
3048                $block->parentEnv = $this->getStoreEnv();
3049                $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
3050                break;
3051
3052            case Type::T_EXTEND:
3053                foreach ($child[1] as $sel) {
3054                    $replacedSel = $this->replaceSelfSelector($sel);
3055
3056                    if ($replacedSel !== $sel) {
3057                        throw $this->error('Parent selectors aren\'t allowed here.');
3058                    }
3059
3060                    $results = $this->evalSelectors([$sel]);
3061
3062                    foreach ($results as $result) {
3063                        if (\count($result) !== 1) {
3064                            throw $this->error('complex selectors may not be extended.');
3065                        }
3066
3067                        // only use the first one
3068                        $result = $result[0];
3069                        $selectors = $out->selectors;
3070
3071                        if (! $selectors && isset($child['selfParent'])) {
3072                            $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
3073                        }
3074
3075                        if (\count($result) > 1) {
3076                            $replacement = implode(', ', $result);
3077                            $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3078                            $line = $this->sourceLine;
3079
3080                            $message = <<<EOL
3081on line $line of $fname:
3082Compound selectors may no longer be extended.
3083Consider `@extend $replacement` instead.
3084See http://bit.ly/ExtendCompound for details.
3085EOL;
3086
3087                            $this->logger->warn($message);
3088                        }
3089
3090                        $this->pushExtends($result, $selectors, $child);
3091                    }
3092                }
3093                break;
3094
3095            case Type::T_IF:
3096                list(, $if) = $child;
3097
3098                if ($this->isTruthy($this->reduce($if->cond, true))) {
3099                    return $this->compileChildren($if->children, $out);
3100                }
3101
3102                foreach ($if->cases as $case) {
3103                    if (
3104                        $case->type === Type::T_ELSE ||
3105                        $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
3106                    ) {
3107                        return $this->compileChildren($case->children, $out);
3108                    }
3109                }
3110                break;
3111
3112            case Type::T_EACH:
3113                list(, $each) = $child;
3114
3115                $list = $this->coerceList($this->reduce($each->list), ',', true);
3116
3117                $this->pushEnv();
3118
3119                foreach ($list[2] as $item) {
3120                    if (\count($each->vars) === 1) {
3121                        $this->set($each->vars[0], $item, true);
3122                    } else {
3123                        list(,, $values) = $this->coerceList($item);
3124
3125                        foreach ($each->vars as $i => $var) {
3126                            $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
3127                        }
3128                    }
3129
3130                    $ret = $this->compileChildren($each->children, $out);
3131
3132                    if ($ret) {
3133                        $store = $this->env->store;
3134                        $this->popEnv();
3135                        $this->backPropagateEnv($store, $each->vars);
3136
3137                        return $ret;
3138                    }
3139                }
3140                $store = $this->env->store;
3141                $this->popEnv();
3142                $this->backPropagateEnv($store, $each->vars);
3143
3144                break;
3145
3146            case Type::T_WHILE:
3147                list(, $while) = $child;
3148
3149                while ($this->isTruthy($this->reduce($while->cond, true))) {
3150                    $ret = $this->compileChildren($while->children, $out);
3151
3152                    if ($ret) {
3153                        return $ret;
3154                    }
3155                }
3156                break;
3157
3158            case Type::T_FOR:
3159                list(, $for) = $child;
3160
3161                $startNumber = $this->assertNumber($this->reduce($for->start, true));
3162                $endNumber = $this->assertNumber($this->reduce($for->end, true));
3163
3164                $start = $this->assertInteger($startNumber);
3165
3166                $numeratorUnits = $startNumber->getNumeratorUnits();
3167                $denominatorUnits = $startNumber->getDenominatorUnits();
3168
3169                $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits));
3170
3171                $d = $start < $end ? 1 : -1;
3172
3173                $this->pushEnv();
3174
3175                for (;;) {
3176                    if (
3177                        (! $for->until && $start - $d == $end) ||
3178                        ($for->until && $start == $end)
3179                    ) {
3180                        break;
3181                    }
3182
3183                    $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
3184                    $start += $d;
3185
3186                    $ret = $this->compileChildren($for->children, $out);
3187
3188                    if ($ret) {
3189                        $store = $this->env->store;
3190                        $this->popEnv();
3191                        $this->backPropagateEnv($store, [$for->var]);
3192
3193                        return $ret;
3194                    }
3195                }
3196
3197                $store = $this->env->store;
3198                $this->popEnv();
3199                $this->backPropagateEnv($store, [$for->var]);
3200
3201                break;
3202
3203            case Type::T_RETURN:
3204                return $this->reduce($child[1], true);
3205
3206            case Type::T_NESTED_PROPERTY:
3207                $this->compileNestedPropertiesBlock($child[1], $out);
3208                break;
3209
3210            case Type::T_INCLUDE:
3211                // including a mixin
3212                list(, $name, $argValues, $content, $argUsing) = $child;
3213
3214                $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
3215
3216                if (! $mixin) {
3217                    throw $this->error("Undefined mixin $name");
3218                }
3219
3220                $callingScope = $this->getStoreEnv();
3221
3222                // push scope, apply args
3223                $this->pushEnv();
3224                $this->env->depth--;
3225
3226                // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
3227                // and assign this fake parent to childs
3228                $selfParent = null;
3229
3230                if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
3231                    $selfParent = $child['selfParent'];
3232                } else {
3233                    $parentSelectors = $this->multiplySelectors($this->env);
3234
3235                    if ($parentSelectors) {
3236                        $parent = new Block();
3237                        $parent->selectors = $parentSelectors;
3238
3239                        foreach ($mixin->children as $k => $child) {
3240                            if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
3241                                $mixin->children[$k][1]->parent = $parent;
3242                            }
3243                        }
3244                    }
3245                }
3246
3247                // clone the stored content to not have its scope spoiled by a further call to the same mixin
3248                // i.e., recursive @include of the same mixin
3249                if (isset($content)) {
3250                    $copyContent = clone $content;
3251                    $copyContent->scope = clone $callingScope;
3252
3253                    $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
3254                } else {
3255                    $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
3256                }
3257
3258                // save the "using" argument list for applying it to when "@content" is invoked
3259                if (isset($argUsing)) {
3260                    $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
3261                } else {
3262                    $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
3263                }
3264
3265                if (isset($mixin->args)) {
3266                    $this->applyArguments($mixin->args, $argValues);
3267                }
3268
3269                $this->env->marker = 'mixin';
3270
3271                if (! empty($mixin->parentEnv)) {
3272                    $this->env->declarationScopeParent = $mixin->parentEnv;
3273                } else {
3274                    throw $this->error("@mixin $name() without parentEnv");
3275                }
3276
3277                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
3278
3279                $this->popEnv();
3280                break;
3281
3282            case Type::T_MIXIN_CONTENT:
3283                $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
3284                $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
3285                $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
3286                $argContent = $child[1];
3287
3288                if (! $content) {
3289                    break;
3290                }
3291
3292                $storeEnv = $this->storeEnv;
3293                $varsUsing = [];
3294
3295                if (isset($argUsing) && isset($argContent)) {
3296                    // Get the arguments provided for the content with the names provided in the "using" argument list
3297                    $this->storeEnv = null;
3298                    $varsUsing = $this->applyArguments($argUsing, $argContent, false);
3299                }
3300
3301                // restore the scope from the @content
3302                $this->storeEnv = $content->scope;
3303
3304                // append the vars from using if any
3305                foreach ($varsUsing as $name => $val) {
3306                    $this->set($name, $val, true, $this->storeEnv);
3307                }
3308
3309                $this->compileChildrenNoReturn($content->children, $out);
3310
3311                $this->storeEnv = $storeEnv;
3312                break;
3313
3314            case Type::T_DEBUG:
3315                list(, $value) = $child;
3316
3317                $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3318                $line  = $this->sourceLine;
3319                $value = $this->compileDebugValue($value);
3320
3321                $this->logger->debug("$fname:$line DEBUG: $value");
3322                break;
3323
3324            case Type::T_WARN:
3325                list(, $value) = $child;
3326
3327                $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3328                $line  = $this->sourceLine;
3329                $value = $this->compileDebugValue($value);
3330
3331                $this->logger->warn("$value\n         on line $line of $fname");
3332                break;
3333
3334            case Type::T_ERROR:
3335                list(, $value) = $child;
3336
3337                $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3338                $line  = $this->sourceLine;
3339                $value = $this->compileValue($this->reduce($value, true));
3340
3341                throw $this->error("File $fname on line $line ERROR: $value\n");
3342
3343            default:
3344                throw $this->error("unknown child type: $child[0]");
3345        }
3346    }
3347
3348    /**
3349     * Reduce expression to string
3350     *
3351     * @param array $exp
3352     * @param bool $keepParens
3353     *
3354     * @return array
3355     */
3356    protected function expToString($exp, $keepParens = false)
3357    {
3358        list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3359
3360        $content = [];
3361
3362        if ($keepParens && $inParens) {
3363            $content[] = '(';
3364        }
3365
3366        $content[] = $this->reduce($left);
3367
3368        if ($whiteLeft) {
3369            $content[] = ' ';
3370        }
3371
3372        $content[] = $op;
3373
3374        if ($whiteRight) {
3375            $content[] = ' ';
3376        }
3377
3378        $content[] = $this->reduce($right);
3379
3380        if ($keepParens && $inParens) {
3381            $content[] = ')';
3382        }
3383
3384        return [Type::T_STRING, '', $content];
3385    }
3386
3387    /**
3388     * Is truthy?
3389     *
3390     * @param array|Number $value
3391     *
3392     * @return boolean
3393     */
3394    public function isTruthy($value)
3395    {
3396        return $value !== static::$false && $value !== static::$null;
3397    }
3398
3399    /**
3400     * Is the value a direct relationship combinator?
3401     *
3402     * @param string $value
3403     *
3404     * @return boolean
3405     */
3406    protected function isImmediateRelationshipCombinator($value)
3407    {
3408        return $value === '>' || $value === '+' || $value === '~';
3409    }
3410
3411    /**
3412     * Should $value cause its operand to eval
3413     *
3414     * @param array $value
3415     *
3416     * @return boolean
3417     */
3418    protected function shouldEval($value)
3419    {
3420        switch ($value[0]) {
3421            case Type::T_EXPRESSION:
3422                if ($value[1] === '/') {
3423                    return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
3424                }
3425
3426                // fall-thru
3427            case Type::T_VARIABLE:
3428            case Type::T_FUNCTION_CALL:
3429                return true;
3430        }
3431
3432        return false;
3433    }
3434
3435    /**
3436     * Reduce value
3437     *
3438     * @param array|Number $value
3439     * @param boolean $inExp
3440     *
3441     * @return array|Number
3442     */
3443    protected function reduce($value, $inExp = false)
3444    {
3445        if ($value instanceof Number) {
3446            return $value;
3447        }
3448
3449        switch ($value[0]) {
3450            case Type::T_EXPRESSION:
3451                list(, $op, $left, $right, $inParens) = $value;
3452
3453                $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
3454                $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
3455
3456                $left = $this->reduce($left, true);
3457
3458                if ($op !== 'and' && $op !== 'or') {
3459                    $right = $this->reduce($right, true);
3460                }
3461
3462                // special case: looks like css shorthand
3463                if (
3464                    $opName == 'div' && ! $inParens && ! $inExp &&
3465                    (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
3466                    ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3467                ) {
3468                    return $this->expToString($value);
3469                }
3470
3471                $left  = $this->coerceForExpression($left);
3472                $right = $this->coerceForExpression($right);
3473                $ltype = $left[0];
3474                $rtype = $right[0];
3475
3476                $ucOpName = ucfirst($opName);
3477                $ucLType  = ucfirst($ltype);
3478                $ucRType  = ucfirst($rtype);
3479
3480                // this tries:
3481                // 1. op[op name][left type][right type]
3482                // 2. op[left type][right type] (passing the op as first arg
3483                // 3. op[op name]
3484                $fn = "op${ucOpName}${ucLType}${ucRType}";
3485
3486                if (
3487                    \is_callable([$this, $fn]) ||
3488                    (($fn = "op${ucLType}${ucRType}") &&
3489                        \is_callable([$this, $fn]) &&
3490                        $passOp = true) ||
3491                    (($fn = "op${ucOpName}") &&
3492                        \is_callable([$this, $fn]) &&
3493                        $genOp = true)
3494                ) {
3495                    $shouldEval = $inParens || $inExp;
3496
3497                    if (isset($passOp)) {
3498                        $out = $this->$fn($op, $left, $right, $shouldEval);
3499                    } else {
3500                        $out = $this->$fn($left, $right, $shouldEval);
3501                    }
3502
3503                    if (isset($out)) {
3504                        return $out;
3505                    }
3506                }
3507
3508                return $this->expToString($value);
3509
3510            case Type::T_UNARY:
3511                list(, $op, $exp, $inParens) = $value;
3512
3513                $inExp = $inExp || $this->shouldEval($exp);
3514                $exp = $this->reduce($exp);
3515
3516                if ($exp instanceof Number) {
3517                    switch ($op) {
3518                        case '+':
3519                            return $exp;
3520
3521                        case '-':
3522                            return $exp->unaryMinus();
3523                    }
3524                }
3525
3526                if ($op === 'not') {
3527                    if ($inExp || $inParens) {
3528                        if ($exp === static::$false || $exp === static::$null) {
3529                            return static::$true;
3530                        }
3531
3532                        return static::$false;
3533                    }
3534
3535                    $op = $op . ' ';
3536                }
3537
3538                return [Type::T_STRING, '', [$op, $exp]];
3539
3540            case Type::T_VARIABLE:
3541                return $this->reduce($this->get($value[1]));
3542
3543            case Type::T_LIST:
3544                foreach ($value[2] as &$item) {
3545                    $item = $this->reduce($item);
3546                }
3547                unset($item);
3548
3549                if (isset($value[3]) && \is_array($value[3])) {
3550                    foreach ($value[3] as &$item) {
3551                        $item = $this->reduce($item);
3552                    }
3553                    unset($item);
3554                }
3555
3556                return $value;
3557
3558            case Type::T_MAP:
3559                foreach ($value[1] as &$item) {
3560                    $item = $this->reduce($item);
3561                }
3562
3563                foreach ($value[2] as &$item) {
3564                    $item = $this->reduce($item);
3565                }
3566
3567                return $value;
3568
3569            case Type::T_STRING:
3570                foreach ($value[2] as &$item) {
3571                    if (\is_array($item) || $item instanceof Number) {
3572                        $item = $this->reduce($item);
3573                    }
3574                }
3575
3576                return $value;
3577
3578            case Type::T_INTERPOLATE:
3579                $value[1] = $this->reduce($value[1]);
3580
3581                if ($inExp) {
3582                    return [Type::T_KEYWORD, $this->compileValue($value, false)];
3583                }
3584
3585                return $value;
3586
3587            case Type::T_FUNCTION_CALL:
3588                return $this->fncall($value[1], $value[2]);
3589
3590            case Type::T_SELF:
3591                $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
3592                $selfSelector = $this->multiplySelectors($this->env, $selfParent);
3593                $selfSelector = $this->collapseSelectorsAsList($selfSelector);
3594
3595                return $selfSelector;
3596
3597            default:
3598                return $value;
3599        }
3600    }
3601
3602    /**
3603     * Function caller
3604     *
3605     * @param string|array $functionReference
3606     * @param array        $argValues
3607     *
3608     * @return array|Number
3609     */
3610    protected function fncall($functionReference, $argValues)
3611    {
3612        // a string means this is a static hard reference coming from the parsing
3613        if (is_string($functionReference)) {
3614            $name = $functionReference;
3615
3616            $functionReference = $this->getFunctionReference($name);
3617            if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3618                $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
3619            }
3620        }
3621
3622        // a function type means we just want a plain css function call
3623        if ($functionReference[0] === Type::T_FUNCTION) {
3624            // for CSS functions, simply flatten the arguments into a list
3625            $listArgs = [];
3626
3627            foreach ((array) $argValues as $arg) {
3628                if (empty($arg[0]) || count($argValues) === 1) {
3629                    $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
3630                }
3631            }
3632
3633            return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
3634        }
3635
3636        if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3637            return static::$defaultValue;
3638        }
3639
3640
3641        switch ($functionReference[1]) {
3642            // SCSS @function
3643            case 'scss':
3644                return $this->callScssFunction($functionReference[3], $argValues);
3645
3646            // native PHP functions
3647            case 'user':
3648            case 'native':
3649                list(,,$name, $fn, $prototype) = $functionReference;
3650
3651                // special cases of css valid functions min/max
3652                $name = strtolower($name);
3653                if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) {
3654                    $cssFunction = $this->cssValidArg(
3655                        [Type::T_FUNCTION_CALL, $name, $argValues],
3656                        ['min', 'max', 'calc', 'env', 'var']
3657                    );
3658                    if ($cssFunction !== false) {
3659                        return $cssFunction;
3660                    }
3661                }
3662                $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
3663
3664                if (! isset($returnValue)) {
3665                    return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
3666                }
3667
3668                return $returnValue;
3669
3670            default:
3671                return static::$defaultValue;
3672        }
3673    }
3674
3675    /**
3676     * @param array|Number $arg
3677     * @param string[]     $allowed_function
3678     * @param bool         $inFunction
3679     *
3680     * @return array|Number|false
3681     */
3682    protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3683    {
3684        if ($arg instanceof Number) {
3685            return $this->stringifyFncallArgs($arg);
3686        }
3687
3688        switch ($arg[0]) {
3689            case Type::T_INTERPOLATE:
3690                return [Type::T_KEYWORD, $this->CompileValue($arg)];
3691
3692            case Type::T_FUNCTION:
3693                if (! \in_array($arg[1], $allowed_function)) {
3694                    return false;
3695                }
3696                if ($arg[2][0] === Type::T_LIST) {
3697                    foreach ($arg[2][2] as $k => $subarg) {
3698                        $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
3699                        if ($arg[2][2][$k] === false) {
3700                            return false;
3701                        }
3702                    }
3703                }
3704                return $arg;
3705
3706            case Type::T_FUNCTION_CALL:
3707                if (! \in_array($arg[1], $allowed_function)) {
3708                    return false;
3709                }
3710                $cssArgs = [];
3711                foreach ($arg[2] as $argValue) {
3712                    if ($argValue === static::$null) {
3713                        return false;
3714                    }
3715                    $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3716                    if (empty($argValue[0]) && $cssArg !== false) {
3717                        $cssArgs[] = [$argValue[0], $cssArg];
3718                    } else {
3719                        return false;
3720                    }
3721                }
3722
3723                return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
3724
3725            case Type::T_STRING:
3726            case Type::T_KEYWORD:
3727                if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
3728                    return false;
3729                }
3730                return $this->stringifyFncallArgs($arg);
3731
3732            case Type::T_LIST:
3733                if (!$inFunction) {
3734                    return false;
3735                }
3736                if (empty($arg['enclosing']) and $arg[1] === '') {
3737                    foreach ($arg[2] as $k => $subarg) {
3738                        $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
3739                        if ($arg[2][$k] === false) {
3740                            return false;
3741                        }
3742                    }
3743                    $arg[0] = Type::T_STRING;
3744                    return $arg;
3745                }
3746                return false;
3747
3748            case Type::T_EXPRESSION:
3749                if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
3750                    return false;
3751                }
3752                $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
3753                $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
3754                if ($arg[2] === false || $arg[3] === false) {
3755                    return false;
3756                }
3757                return $this->expToString($arg, true);
3758
3759            case Type::T_VARIABLE:
3760            case Type::T_SELF:
3761            default:
3762                return false;
3763        }
3764    }
3765
3766
3767    /**
3768     * Reformat fncall arguments to proper css function output
3769     *
3770     * @param array|Number $arg
3771     *
3772     * @return array|Number
3773     */
3774    protected function stringifyFncallArgs($arg)
3775    {
3776        if ($arg instanceof Number) {
3777            return $arg;
3778        }
3779
3780        switch ($arg[0]) {
3781            case Type::T_LIST:
3782                foreach ($arg[2] as $k => $v) {
3783                    $arg[2][$k] = $this->stringifyFncallArgs($v);
3784                }
3785                break;
3786
3787            case Type::T_EXPRESSION:
3788                if ($arg[1] === '/') {
3789                    $arg[2] = $this->stringifyFncallArgs($arg[2]);
3790                    $arg[3] = $this->stringifyFncallArgs($arg[3]);
3791                    $arg[5] = $arg[6] = false; // no space around /
3792                    $arg = $this->expToString($arg);
3793                }
3794                break;
3795
3796            case Type::T_FUNCTION_CALL:
3797                $name = strtolower($arg[1]);
3798
3799                if (in_array($name, ['max', 'min', 'calc'])) {
3800                    $args = $arg[2];
3801                    $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
3802                }
3803                break;
3804        }
3805
3806        return $arg;
3807    }
3808
3809    /**
3810     * Find a function reference
3811     * @param string $name
3812     * @param bool $safeCopy
3813     * @return array
3814     */
3815    protected function getFunctionReference($name, $safeCopy = false)
3816    {
3817        // SCSS @function
3818        if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3819            if ($safeCopy) {
3820                $func = clone $func;
3821            }
3822
3823            return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
3824        }
3825
3826        // native PHP functions
3827
3828        // try to find a native lib function
3829        $normalizedName = $this->normalizeName($name);
3830
3831        if (isset($this->userFunctions[$normalizedName])) {
3832            // see if we can find a user function
3833            list($f, $prototype) = $this->userFunctions[$normalizedName];
3834
3835            return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
3836        }
3837
3838        $lowercasedName = strtolower($normalizedName);
3839
3840        // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase
3841        // to avoid the deprecation warning about the wrong case being used.
3842        if ($lowercasedName === 'min' || $lowercasedName === 'max') {
3843            $normalizedName = $lowercasedName;
3844        }
3845
3846        if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
3847            $libName   = $f[1];
3848            $prototype = isset(static::$$libName) ? static::$$libName : null;
3849
3850            // All core functions have a prototype defined. Not finding the
3851            // prototype can mean 2 things:
3852            // - the function comes from a child class (deprecated just after)
3853            // - the function was found with a different case, which relates to calling the
3854            //   wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`),
3855            //   because PHP method names are case-insensitive while property names are
3856            //   case-sensitive.
3857            if ($prototype === null || strtolower($normalizedName) !== $normalizedName) {
3858                $r = new \ReflectionMethod($this, $libName);
3859                $actualLibName = $r->name;
3860
3861                if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) {
3862                    $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3));
3863                    assert($kebabCaseName !== null);
3864                    $originalName = strtolower($kebabCaseName);
3865                    $warning = "Calling built-in functions with a non-standard name is deprecated since Scssphp 1.8.0 and will not work anymore in 2.0 (they will be treated as CSS function calls instead).\nUse \"$originalName\" instead of \"$name\".";
3866                    @trigger_error($warning, E_USER_DEPRECATED);
3867                    $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3868                    $line  = $this->sourceLine;
3869                    Warn::deprecation("$warning\n         on line $line of $fname");
3870
3871                    // Use the actual function definition
3872                    $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null;
3873                    $f[1] = $libName = $actualLibName;
3874                }
3875            }
3876
3877            if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) {
3878                $r = new \ReflectionMethod($this, $libName);
3879                $declaringClass = $r->getDeclaringClass()->name;
3880
3881                $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__;
3882
3883                if ($needsWarning) {
3884                    if (method_exists(__CLASS__, $libName)) {
3885                        @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED);
3886                    } else {
3887                        @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED);
3888                    }
3889                }
3890            }
3891
3892            return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3893        }
3894
3895        return static::$null;
3896    }
3897
3898
3899    /**
3900     * Normalize name
3901     *
3902     * @param string $name
3903     *
3904     * @return string
3905     */
3906    protected function normalizeName($name)
3907    {
3908        return str_replace('-', '_', $name);
3909    }
3910
3911    /**
3912     * Normalize value
3913     *
3914     * @internal
3915     *
3916     * @param array|Number $value
3917     *
3918     * @return array|Number
3919     */
3920    public function normalizeValue($value)
3921    {
3922        $value = $this->coerceForExpression($this->reduce($value));
3923
3924        if ($value instanceof Number) {
3925            return $value;
3926        }
3927
3928        switch ($value[0]) {
3929            case Type::T_LIST:
3930                $value = $this->extractInterpolation($value);
3931
3932                if ($value[0] !== Type::T_LIST) {
3933                    return [Type::T_KEYWORD, $this->compileValue($value)];
3934                }
3935
3936                foreach ($value[2] as $key => $item) {
3937                    $value[2][$key] = $this->normalizeValue($item);
3938                }
3939
3940                if (! empty($value['enclosing'])) {
3941                    unset($value['enclosing']);
3942                }
3943
3944                return $value;
3945
3946            case Type::T_STRING:
3947                return [$value[0], '"', [$this->compileStringContent($value)]];
3948
3949            case Type::T_INTERPOLATE:
3950                return [Type::T_KEYWORD, $this->compileValue($value)];
3951
3952            default:
3953                return $value;
3954        }
3955    }
3956
3957    /**
3958     * Add numbers
3959     *
3960     * @param Number $left
3961     * @param Number $right
3962     *
3963     * @return Number
3964     */
3965    protected function opAddNumberNumber(Number $left, Number $right)
3966    {
3967        return $left->plus($right);
3968    }
3969
3970    /**
3971     * Multiply numbers
3972     *
3973     * @param Number $left
3974     * @param Number $right
3975     *
3976     * @return Number
3977     */
3978    protected function opMulNumberNumber(Number $left, Number $right)
3979    {
3980        return $left->times($right);
3981    }
3982
3983    /**
3984     * Subtract numbers
3985     *
3986     * @param Number $left
3987     * @param Number $right
3988     *
3989     * @return Number
3990     */
3991    protected function opSubNumberNumber(Number $left, Number $right)
3992    {
3993        return $left->minus($right);
3994    }
3995
3996    /**
3997     * Divide numbers
3998     *
3999     * @param Number $left
4000     * @param Number $right
4001     *
4002     * @return Number
4003     */
4004    protected function opDivNumberNumber(Number $left, Number $right)
4005    {
4006        return $left->dividedBy($right);
4007    }
4008
4009    /**
4010     * Mod numbers
4011     *
4012     * @param Number $left
4013     * @param Number $right
4014     *
4015     * @return Number
4016     */
4017    protected function opModNumberNumber(Number $left, Number $right)
4018    {
4019        return $left->modulo($right);
4020    }
4021
4022    /**
4023     * Add strings
4024     *
4025     * @param array $left
4026     * @param array $right
4027     *
4028     * @return array|null
4029     */
4030    protected function opAdd($left, $right)
4031    {
4032        if ($strLeft = $this->coerceString($left)) {
4033            if ($right[0] === Type::T_STRING) {
4034                $right[1] = '';
4035            }
4036
4037            $strLeft[2][] = $right;
4038
4039            return $strLeft;
4040        }
4041
4042        if ($strRight = $this->coerceString($right)) {
4043            if ($left[0] === Type::T_STRING) {
4044                $left[1] = '';
4045            }
4046
4047            array_unshift($strRight[2], $left);
4048
4049            return $strRight;
4050        }
4051
4052        return null;
4053    }
4054
4055    /**
4056     * Boolean and
4057     *
4058     * @param array|Number $left
4059     * @param array|Number  $right
4060     * @param boolean $shouldEval
4061     *
4062     * @return array|Number|null
4063     */
4064    protected function opAnd($left, $right, $shouldEval)
4065    {
4066        $truthy = ($left === static::$null || $right === static::$null) ||
4067                  ($left === static::$false || $left === static::$true) &&
4068                  ($right === static::$false || $right === static::$true);
4069
4070        if (! $shouldEval) {
4071            if (! $truthy) {
4072                return null;
4073            }
4074        }
4075
4076        if ($left !== static::$false && $left !== static::$null) {
4077            return $this->reduce($right, true);
4078        }
4079
4080        return $left;
4081    }
4082
4083    /**
4084     * Boolean or
4085     *
4086     * @param array|Number $left
4087     * @param array|Number $right
4088     * @param boolean $shouldEval
4089     *
4090     * @return array|Number|null
4091     */
4092    protected function opOr($left, $right, $shouldEval)
4093    {
4094        $truthy = ($left === static::$null || $right === static::$null) ||
4095                  ($left === static::$false || $left === static::$true) &&
4096                  ($right === static::$false || $right === static::$true);
4097
4098        if (! $shouldEval) {
4099            if (! $truthy) {
4100                return null;
4101            }
4102        }
4103
4104        if ($left !== static::$false && $left !== static::$null) {
4105            return $left;
4106        }
4107
4108        return $this->reduce($right, true);
4109    }
4110
4111    /**
4112     * Compare colors
4113     *
4114     * @param string $op
4115     * @param array  $left
4116     * @param array  $right
4117     *
4118     * @return array
4119     */
4120    protected function opColorColor($op, $left, $right)
4121    {
4122        if ($op !== '==' && $op !== '!=') {
4123            $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
4124                . "Consider using Sass's color functions instead.";
4125            $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
4126            $line  = $this->sourceLine;
4127
4128            Warn::deprecation("$warning\n         on line $line of $fname");
4129        }
4130
4131        $out = [Type::T_COLOR];
4132
4133        foreach ([1, 2, 3] as $i) {
4134            $lval = isset($left[$i]) ? $left[$i] : 0;
4135            $rval = isset($right[$i]) ? $right[$i] : 0;
4136
4137            switch ($op) {
4138                case '+':
4139                    $out[] = $lval + $rval;
4140                    break;
4141
4142                case '-':
4143                    $out[] = $lval - $rval;
4144                    break;
4145
4146                case '*':
4147                    $out[] = $lval * $rval;
4148                    break;
4149
4150                case '%':
4151                    if ($rval == 0) {
4152                        throw $this->error("color: Can't take modulo by zero");
4153                    }
4154
4155                    $out[] = $lval % $rval;
4156                    break;
4157
4158                case '/':
4159                    if ($rval == 0) {
4160                        throw $this->error("color: Can't divide by zero");
4161                    }
4162
4163                    $out[] = (int) ($lval / $rval);
4164                    break;
4165
4166                case '==':
4167                    return $this->opEq($left, $right);
4168
4169                case '!=':
4170                    return $this->opNeq($left, $right);
4171
4172                default:
4173                    throw $this->error("color: unknown op $op");
4174            }
4175        }
4176
4177        if (isset($left[4])) {
4178            $out[4] = $left[4];
4179        } elseif (isset($right[4])) {
4180            $out[4] = $right[4];
4181        }
4182
4183        return $this->fixColor($out);
4184    }
4185
4186    /**
4187     * Compare color and number
4188     *
4189     * @param string $op
4190     * @param array  $left
4191     * @param Number  $right
4192     *
4193     * @return array
4194     */
4195    protected function opColorNumber($op, $left, Number $right)
4196    {
4197        if ($op === '==') {
4198            return static::$false;
4199        }
4200
4201        if ($op === '!=') {
4202            return static::$true;
4203        }
4204
4205        $value = $right->getDimension();
4206
4207        return $this->opColorColor(
4208            $op,
4209            $left,
4210            [Type::T_COLOR, $value, $value, $value]
4211        );
4212    }
4213
4214    /**
4215     * Compare number and color
4216     *
4217     * @param string $op
4218     * @param Number  $left
4219     * @param array  $right
4220     *
4221     * @return array
4222     */
4223    protected function opNumberColor($op, Number $left, $right)
4224    {
4225        if ($op === '==') {
4226            return static::$false;
4227        }
4228
4229        if ($op === '!=') {
4230            return static::$true;
4231        }
4232
4233        $value = $left->getDimension();
4234
4235        return $this->opColorColor(
4236            $op,
4237            [Type::T_COLOR, $value, $value, $value],
4238            $right
4239        );
4240    }
4241
4242    /**
4243     * Compare number1 == number2
4244     *
4245     * @param array|Number $left
4246     * @param array|Number $right
4247     *
4248     * @return array
4249     */
4250    protected function opEq($left, $right)
4251    {
4252        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4253            $lStr[1] = '';
4254            $rStr[1] = '';
4255
4256            $left = $this->compileValue($lStr);
4257            $right = $this->compileValue($rStr);
4258        }
4259
4260        return $this->toBool($left === $right);
4261    }
4262
4263    /**
4264     * Compare number1 != number2
4265     *
4266     * @param array|Number $left
4267     * @param array|Number $right
4268     *
4269     * @return array
4270     */
4271    protected function opNeq($left, $right)
4272    {
4273        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4274            $lStr[1] = '';
4275            $rStr[1] = '';
4276
4277            $left = $this->compileValue($lStr);
4278            $right = $this->compileValue($rStr);
4279        }
4280
4281        return $this->toBool($left !== $right);
4282    }
4283
4284    /**
4285     * Compare number1 == number2
4286     *
4287     * @param Number $left
4288     * @param Number $right
4289     *
4290     * @return array
4291     */
4292    protected function opEqNumberNumber(Number $left, Number $right)
4293    {
4294        return $this->toBool($left->equals($right));
4295    }
4296
4297    /**
4298     * Compare number1 != number2
4299     *
4300     * @param Number $left
4301     * @param Number $right
4302     *
4303     * @return array
4304     */
4305    protected function opNeqNumberNumber(Number $left, Number $right)
4306    {
4307        return $this->toBool(!$left->equals($right));
4308    }
4309
4310    /**
4311     * Compare number1 >= number2
4312     *
4313     * @param Number $left
4314     * @param Number $right
4315     *
4316     * @return array
4317     */
4318    protected function opGteNumberNumber(Number $left, Number $right)
4319    {
4320        return $this->toBool($left->greaterThanOrEqual($right));
4321    }
4322
4323    /**
4324     * Compare number1 > number2
4325     *
4326     * @param Number $left
4327     * @param Number $right
4328     *
4329     * @return array
4330     */
4331    protected function opGtNumberNumber(Number $left, Number $right)
4332    {
4333        return $this->toBool($left->greaterThan($right));
4334    }
4335
4336    /**
4337     * Compare number1 <= number2
4338     *
4339     * @param Number $left
4340     * @param Number $right
4341     *
4342     * @return array
4343     */
4344    protected function opLteNumberNumber(Number $left, Number $right)
4345    {
4346        return $this->toBool($left->lessThanOrEqual($right));
4347    }
4348
4349    /**
4350     * Compare number1 < number2
4351     *
4352     * @param Number $left
4353     * @param Number $right
4354     *
4355     * @return array
4356     */
4357    protected function opLtNumberNumber(Number $left, Number $right)
4358    {
4359        return $this->toBool($left->lessThan($right));
4360    }
4361
4362    /**
4363     * Cast to boolean
4364     *
4365     * @api
4366     *
4367     * @param bool $thing
4368     *
4369     * @return array
4370     */
4371    public function toBool($thing)
4372    {
4373        return $thing ? static::$true : static::$false;
4374    }
4375
4376    /**
4377     * Escape non printable chars in strings output as in dart-sass
4378     *
4379     * @internal
4380     *
4381     * @param string $string
4382     * @param bool   $inKeyword
4383     *
4384     * @return string
4385     */
4386    public function escapeNonPrintableChars($string, $inKeyword = false)
4387    {
4388        static $replacement = [];
4389        if (empty($replacement[$inKeyword])) {
4390            for ($i = 0; $i < 32; $i++) {
4391                if ($i !== 9 || $inKeyword) {
4392                    $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
4393                }
4394            }
4395        }
4396        $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
4397        // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
4398        if (strpos($string, chr(0)) !== false) {
4399            if (substr($string, -1) === chr(0)) {
4400                $string = substr($string, 0, -1);
4401            }
4402            $string = str_replace(
4403                [chr(0) . '\\',chr(0) . ' '],
4404                [ '\\', ' '],
4405                $string
4406            );
4407            if (strpos($string, chr(0)) !== false) {
4408                $parts = explode(chr(0), $string);
4409                $string = array_shift($parts);
4410                while (count($parts)) {
4411                    $next = array_shift($parts);
4412                    if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
4413                        $string .= " ";
4414                    }
4415                    $string .= $next;
4416                }
4417            }
4418        }
4419
4420        return $string;
4421    }
4422
4423    /**
4424     * Compiles a primitive value into a CSS property value.
4425     *
4426     * Values in scssphp are typed by being wrapped in arrays, their format is
4427     * typically:
4428     *
4429     *     array(type, contents [, additional_contents]*)
4430     *
4431     * The input is expected to be reduced. This function will not work on
4432     * things like expressions and variables.
4433     *
4434     * @api
4435     *
4436     * @param array|Number $value
4437     * @param bool         $quote
4438     *
4439     * @return string
4440     */
4441    public function compileValue($value, $quote = true)
4442    {
4443        $value = $this->reduce($value);
4444
4445        if ($value instanceof Number) {
4446            return $value->output($this);
4447        }
4448
4449        switch ($value[0]) {
4450            case Type::T_KEYWORD:
4451                return $this->escapeNonPrintableChars($value[1], true);
4452
4453            case Type::T_COLOR:
4454                // [1] - red component (either number for a %)
4455                // [2] - green component
4456                // [3] - blue component
4457                // [4] - optional alpha component
4458                list(, $r, $g, $b) = $value;
4459
4460                $r = $this->compileRGBAValue($r);
4461                $g = $this->compileRGBAValue($g);
4462                $b = $this->compileRGBAValue($b);
4463
4464                if (\count($value) === 5) {
4465                    $alpha = $this->compileRGBAValue($value[4], true);
4466
4467                    if (! is_numeric($alpha) || $alpha < 1) {
4468                        $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
4469
4470                        if (! \is_null($colorName)) {
4471                            return $colorName;
4472                        }
4473
4474                        if (is_numeric($alpha)) {
4475                            $a = new Number($alpha, '');
4476                        } else {
4477                            $a = $alpha;
4478                        }
4479
4480                        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
4481                    }
4482                }
4483
4484                if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
4485                    return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
4486                }
4487
4488                $colorName = Colors::RGBaToColorName($r, $g, $b);
4489
4490                if (! \is_null($colorName)) {
4491                    return $colorName;
4492                }
4493
4494                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
4495
4496                // Converting hex color to short notation (e.g. #003399 to #039)
4497                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
4498                    $h = '#' . $h[1] . $h[3] . $h[5];
4499                }
4500
4501                return $h;
4502
4503            case Type::T_STRING:
4504                $content = $this->compileStringContent($value, $quote);
4505
4506                if ($value[1] && $quote) {
4507                    $content = str_replace('\\', '\\\\', $content);
4508
4509                    $content = $this->escapeNonPrintableChars($content);
4510
4511                    // force double quote as string quote for the output in certain cases
4512                    if (
4513                        $value[1] === "'" &&
4514                        (strpos($content, '"') === false or strpos($content, "'") !== false) &&
4515                        strpbrk($content, '{}\\\'') !== false
4516                    ) {
4517                        $value[1] = '"';
4518                    } elseif (
4519                        $value[1] === '"' &&
4520                        (strpos($content, '"') !== false and strpos($content, "'") === false)
4521                    ) {
4522                        $value[1] = "'";
4523                    }
4524
4525                    $content = str_replace($value[1], '\\' . $value[1], $content);
4526                }
4527
4528                return $value[1] . $content . $value[1];
4529
4530            case Type::T_FUNCTION:
4531                $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : '';
4532
4533                return "$value[1]($args)";
4534
4535            case Type::T_FUNCTION_REFERENCE:
4536                $name = ! empty($value[2]) ? $value[2] : '';
4537
4538                return "get-function(\"$name\")";
4539
4540            case Type::T_LIST:
4541                $value = $this->extractInterpolation($value);
4542
4543                if ($value[0] !== Type::T_LIST) {
4544                    return $this->compileValue($value, $quote);
4545                }
4546
4547                list(, $delim, $items) = $value;
4548                $pre = $post = '';
4549
4550                if (! empty($value['enclosing'])) {
4551                    switch ($value['enclosing']) {
4552                        case 'parent':
4553                            //$pre = '(';
4554                            //$post = ')';
4555                            break;
4556                        case 'forced_parent':
4557                            $pre = '(';
4558                            $post = ')';
4559                            break;
4560                        case 'bracket':
4561                        case 'forced_bracket':
4562                            $pre = '[';
4563                            $post = ']';
4564                            break;
4565                    }
4566                }
4567
4568                $prefix_value = '';
4569
4570                if ($delim !== ' ') {
4571                    $prefix_value = ' ';
4572                }
4573
4574                $filtered = [];
4575
4576                $same_string_quote = null;
4577                foreach ($items as $item) {
4578                    if (\is_null($same_string_quote)) {
4579                        $same_string_quote = false;
4580                        if ($item[0] === Type::T_STRING) {
4581                            $same_string_quote = $item[1];
4582                            foreach ($items as $ii) {
4583                                if ($ii[0] !== Type::T_STRING) {
4584                                    $same_string_quote = false;
4585                                    break;
4586                                }
4587                            }
4588                        }
4589                    }
4590                    if ($item[0] === Type::T_NULL) {
4591                        continue;
4592                    }
4593                    if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
4594                        $item[1] = $same_string_quote;
4595                    }
4596
4597                    $compiled = $this->compileValue($item, $quote);
4598
4599                    if ($prefix_value && \strlen($compiled)) {
4600                        $compiled = $prefix_value . $compiled;
4601                    }
4602
4603                    $filtered[] = $compiled;
4604                }
4605
4606                return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post;
4607
4608            case Type::T_MAP:
4609                $keys     = $value[1];
4610                $values   = $value[2];
4611                $filtered = [];
4612
4613                for ($i = 0, $s = \count($keys); $i < $s; $i++) {
4614                    $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote);
4615                }
4616
4617                array_walk($filtered, function (&$value, $key) {
4618                    $value = $key . ': ' . $value;
4619                });
4620
4621                return '(' . implode(', ', $filtered) . ')';
4622
4623            case Type::T_INTERPOLATED:
4624                // node created by extractInterpolation
4625                list(, $interpolate, $left, $right) = $value;
4626                list(,, $whiteLeft, $whiteRight) = $interpolate;
4627
4628                $delim = $left[1];
4629
4630                if ($delim && $delim !== ' ' && ! $whiteLeft) {
4631                    $delim .= ' ';
4632                }
4633
4634                $left = \count($left[2]) > 0
4635                    ?  $this->compileValue($left, $quote) . $delim . $whiteLeft
4636                    : '';
4637
4638                $delim = $right[1];
4639
4640                if ($delim && $delim !== ' ') {
4641                    $delim .= ' ';
4642                }
4643
4644                $right = \count($right[2]) > 0 ?
4645                    $whiteRight . $delim . $this->compileValue($right, $quote) : '';
4646
4647                return $left . $this->compileValue($interpolate, $quote) . $right;
4648
4649            case Type::T_INTERPOLATE:
4650                // strip quotes if it's a string
4651                $reduced = $this->reduce($value[1]);
4652
4653                if ($reduced instanceof Number) {
4654                    return $this->compileValue($reduced, $quote);
4655                }
4656
4657                switch ($reduced[0]) {
4658                    case Type::T_LIST:
4659                        $reduced = $this->extractInterpolation($reduced);
4660
4661                        if ($reduced[0] !== Type::T_LIST) {
4662                            break;
4663                        }
4664
4665                        list(, $delim, $items) = $reduced;
4666
4667                        if ($delim !== ' ') {
4668                            $delim .= ' ';
4669                        }
4670
4671                        $filtered = [];
4672
4673                        foreach ($items as $item) {
4674                            if ($item[0] === Type::T_NULL) {
4675                                continue;
4676                            }
4677
4678                            if ($item[0] === Type::T_STRING) {
4679                                $filtered[] = $this->compileStringContent($item, $quote);
4680                            } elseif ($item[0] === Type::T_KEYWORD) {
4681                                $filtered[] = $item[1];
4682                            } else {
4683                                $filtered[] = $this->compileValue($item, $quote);
4684                            }
4685                        }
4686
4687                        $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
4688                        break;
4689
4690                    case Type::T_STRING:
4691                        $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
4692                        break;
4693
4694                    case Type::T_NULL:
4695                        $reduced = [Type::T_KEYWORD, ''];
4696                }
4697
4698                return $this->compileValue($reduced, $quote);
4699
4700            case Type::T_NULL:
4701                return 'null';
4702
4703            case Type::T_COMMENT:
4704                return $this->compileCommentValue($value);
4705
4706            default:
4707                throw $this->error('unknown value type: ' . json_encode($value));
4708        }
4709    }
4710
4711    /**
4712     * @param array|Number $value
4713     *
4714     * @return string
4715     */
4716    protected function compileDebugValue($value)
4717    {
4718        $value = $this->reduce($value, true);
4719
4720        if ($value instanceof Number) {
4721            return $this->compileValue($value);
4722        }
4723
4724        switch ($value[0]) {
4725            case Type::T_STRING:
4726                return $this->compileStringContent($value);
4727
4728            default:
4729                return $this->compileValue($value);
4730        }
4731    }
4732
4733    /**
4734     * Flatten list
4735     *
4736     * @param array $list
4737     *
4738     * @return string
4739     *
4740     * @deprecated
4741     */
4742    protected function flattenList($list)
4743    {
4744        @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
4745
4746        return $this->compileValue($list);
4747    }
4748
4749    /**
4750     * Gets the text of a Sass string
4751     *
4752     * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
4753     * to ensure that the value is indeed a string.
4754     *
4755     * @param array $value
4756     *
4757     * @return string
4758     */
4759    public function getStringText(array $value)
4760    {
4761        if ($value[0] !== Type::T_STRING) {
4762            throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
4763        }
4764
4765        return $this->compileStringContent($value);
4766    }
4767
4768    /**
4769     * Compile string content
4770     *
4771     * @param array $string
4772     * @param bool  $quote
4773     *
4774     * @return string
4775     */
4776    protected function compileStringContent($string, $quote = true)
4777    {
4778        $parts = [];
4779
4780        foreach ($string[2] as $part) {
4781            if (\is_array($part) || $part instanceof Number) {
4782                $parts[] = $this->compileValue($part, $quote);
4783            } else {
4784                $parts[] = $part;
4785            }
4786        }
4787
4788        return implode($parts);
4789    }
4790
4791    /**
4792     * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4793     *
4794     * @param array $list
4795     *
4796     * @return array
4797     */
4798    protected function extractInterpolation($list)
4799    {
4800        $items = $list[2];
4801
4802        foreach ($items as $i => $item) {
4803            if ($item[0] === Type::T_INTERPOLATE) {
4804                $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
4805                $after  = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
4806
4807                return [Type::T_INTERPOLATED, $item, $before, $after];
4808            }
4809        }
4810
4811        return $list;
4812    }
4813
4814    /**
4815     * Find the final set of selectors
4816     *
4817     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4818     * @param \ScssPhp\ScssPhp\Block                $selfParent
4819     *
4820     * @return array
4821     */
4822    protected function multiplySelectors(Environment $env, $selfParent = null)
4823    {
4824        $envs            = $this->compactEnv($env);
4825        $selectors       = [];
4826        $parentSelectors = [[]];
4827
4828        $selfParentSelectors = null;
4829
4830        if (! \is_null($selfParent) && $selfParent->selectors) {
4831            $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
4832        }
4833
4834        while ($env = array_pop($envs)) {
4835            if (empty($env->selectors)) {
4836                continue;
4837            }
4838
4839            $selectors = $env->selectors;
4840
4841            do {
4842                $stillHasSelf  = false;
4843                $prevSelectors = $selectors;
4844                $selectors     = [];
4845
4846                foreach ($parentSelectors as $parent) {
4847                    foreach ($prevSelectors as $selector) {
4848                        if ($selfParentSelectors) {
4849                            foreach ($selfParentSelectors as $selfParent) {
4850                                // if no '&' in the selector, each call will give same result, only add once
4851                                $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
4852                                $selectors[serialize($s)] = $s;
4853                            }
4854                        } else {
4855                            $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
4856                            $selectors[serialize($s)] = $s;
4857                        }
4858                    }
4859                }
4860            } while ($stillHasSelf);
4861
4862            $parentSelectors = $selectors;
4863        }
4864
4865        $selectors = array_values($selectors);
4866
4867        // case we are just starting a at-root : nothing to multiply but parentSelectors
4868        if (! $selectors && $selfParentSelectors) {
4869            $selectors = $selfParentSelectors;
4870        }
4871
4872        return $selectors;
4873    }
4874
4875    /**
4876     * Join selectors; looks for & to replace, or append parent before child
4877     *
4878     * @param array   $parent
4879     * @param array   $child
4880     * @param boolean $stillHasSelf
4881     * @param array   $selfParentSelectors
4882
4883     * @return array
4884     */
4885    protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4886    {
4887        $setSelf = false;
4888        $out = [];
4889
4890        foreach ($child as $part) {
4891            $newPart = [];
4892
4893            foreach ($part as $p) {
4894                // only replace & once and should be recalled to be able to make combinations
4895                if ($p === static::$selfSelector && $setSelf) {
4896                    $stillHasSelf = true;
4897                }
4898
4899                if ($p === static::$selfSelector && ! $setSelf) {
4900                    $setSelf = true;
4901
4902                    if (\is_null($selfParentSelectors)) {
4903                        $selfParentSelectors = $parent;
4904                    }
4905
4906                    foreach ($selfParentSelectors as $i => $parentPart) {
4907                        if ($i > 0) {
4908                            $out[] = $newPart;
4909                            $newPart = [];
4910                        }
4911
4912                        foreach ($parentPart as $pp) {
4913                            if (\is_array($pp)) {
4914                                $flatten = [];
4915
4916                                array_walk_recursive($pp, function ($a) use (&$flatten) {
4917                                    $flatten[] = $a;
4918                                });
4919
4920                                $pp = implode($flatten);
4921                            }
4922
4923                            $newPart[] = $pp;
4924                        }
4925                    }
4926                } else {
4927                    $newPart[] = $p;
4928                }
4929            }
4930
4931            $out[] = $newPart;
4932        }
4933
4934        return $setSelf ? $out : array_merge($parent, $child);
4935    }
4936
4937    /**
4938     * Multiply media
4939     *
4940     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4941     * @param array                                 $childQueries
4942     *
4943     * @return array
4944     */
4945    protected function multiplyMedia(Environment $env = null, $childQueries = null)
4946    {
4947        if (
4948            ! isset($env) ||
4949            ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
4950        ) {
4951            return $childQueries;
4952        }
4953
4954        // plain old block, skip
4955        if (empty($env->block->type)) {
4956            return $this->multiplyMedia($env->parent, $childQueries);
4957        }
4958
4959        $parentQueries = isset($env->block->queryList)
4960            ? $env->block->queryList
4961            : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
4962
4963        $store = [$this->env, $this->storeEnv];
4964
4965        $this->env      = $env;
4966        $this->storeEnv = null;
4967        $parentQueries  = $this->evaluateMediaQuery($parentQueries);
4968
4969        list($this->env, $this->storeEnv) = $store;
4970
4971        if (\is_null($childQueries)) {
4972            $childQueries = $parentQueries;
4973        } else {
4974            $originalQueries = $childQueries;
4975            $childQueries = [];
4976
4977            foreach ($parentQueries as $parentQuery) {
4978                foreach ($originalQueries as $childQuery) {
4979                    $childQueries[] = array_merge(
4980                        $parentQuery,
4981                        [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
4982                        $childQuery
4983                    );
4984                }
4985            }
4986        }
4987
4988        return $this->multiplyMedia($env->parent, $childQueries);
4989    }
4990
4991    /**
4992     * Convert env linked list to stack
4993     *
4994     * @param Environment $env
4995     *
4996     * @return Environment[]
4997     *
4998     * @phpstan-return non-empty-array<Environment>
4999     */
5000    protected function compactEnv(Environment $env)
5001    {
5002        for ($envs = []; $env; $env = $env->parent) {
5003            $envs[] = $env;
5004        }
5005
5006        return $envs;
5007    }
5008
5009    /**
5010     * Convert env stack to singly linked list
5011     *
5012     * @param Environment[] $envs
5013     *
5014     * @return Environment
5015     *
5016     * @phpstan-param  non-empty-array<Environment> $envs
5017     */
5018    protected function extractEnv($envs)
5019    {
5020        for ($env = null; $e = array_pop($envs);) {
5021            $e->parent = $env;
5022            $env = $e;
5023        }
5024
5025        return $env;
5026    }
5027
5028    /**
5029     * Push environment
5030     *
5031     * @param \ScssPhp\ScssPhp\Block $block
5032     *
5033     * @return \ScssPhp\ScssPhp\Compiler\Environment
5034     */
5035    protected function pushEnv(Block $block = null)
5036    {
5037        $env = new Environment();
5038        $env->parent = $this->env;
5039        $env->parentStore = $this->storeEnv;
5040        $env->store  = [];
5041        $env->block  = $block;
5042        $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
5043
5044        $this->env = $env;
5045        $this->storeEnv = null;
5046
5047        return $env;
5048    }
5049
5050    /**
5051     * Pop environment
5052     *
5053     * @return void
5054     */
5055    protected function popEnv()
5056    {
5057        $this->storeEnv = $this->env->parentStore;
5058        $this->env = $this->env->parent;
5059    }
5060
5061    /**
5062     * Propagate vars from a just poped Env (used in @each and @for)
5063     *
5064     * @param array         $store
5065     * @param null|string[] $excludedVars
5066     *
5067     * @return void
5068     */
5069    protected function backPropagateEnv($store, $excludedVars = null)
5070    {
5071        foreach ($store as $key => $value) {
5072            if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
5073                $this->set($key, $value, true);
5074            }
5075        }
5076    }
5077
5078    /**
5079     * Get store environment
5080     *
5081     * @return \ScssPhp\ScssPhp\Compiler\Environment
5082     */
5083    protected function getStoreEnv()
5084    {
5085        return isset($this->storeEnv) ? $this->storeEnv : $this->env;
5086    }
5087
5088    /**
5089     * Set variable
5090     *
5091     * @param string                                $name
5092     * @param mixed                                 $value
5093     * @param boolean                               $shadow
5094     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5095     * @param mixed                                 $valueUnreduced
5096     *
5097     * @return void
5098     */
5099    protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
5100    {
5101        $name = $this->normalizeName($name);
5102
5103        if (! isset($env)) {
5104            $env = $this->getStoreEnv();
5105        }
5106
5107        if ($shadow) {
5108            $this->setRaw($name, $value, $env, $valueUnreduced);
5109        } else {
5110            $this->setExisting($name, $value, $env, $valueUnreduced);
5111        }
5112    }
5113
5114    /**
5115     * Set existing variable
5116     *
5117     * @param string                                $name
5118     * @param mixed                                 $value
5119     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5120     * @param mixed                                 $valueUnreduced
5121     *
5122     * @return void
5123     */
5124    protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
5125    {
5126        $storeEnv = $env;
5127        $specialContentKey = static::$namespaces['special'] . 'content';
5128
5129        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
5130
5131        $maxDepth = 10000;
5132
5133        for (;;) {
5134            if ($maxDepth-- <= 0) {
5135                break;
5136            }
5137
5138            if (\array_key_exists($name, $env->store)) {
5139                break;
5140            }
5141
5142            if (! $hasNamespace && isset($env->marker)) {
5143                if (! empty($env->store[$specialContentKey])) {
5144                    $env = $env->store[$specialContentKey]->scope;
5145                    continue;
5146                }
5147
5148                if (! empty($env->declarationScopeParent)) {
5149                    $env = $env->declarationScopeParent;
5150                    continue;
5151                } else {
5152                    $env = $storeEnv;
5153                    break;
5154                }
5155            }
5156
5157            if (isset($env->parentStore)) {
5158                $env = $env->parentStore;
5159            } elseif (isset($env->parent)) {
5160                $env = $env->parent;
5161            } else {
5162                $env = $storeEnv;
5163                break;
5164            }
5165        }
5166
5167        $env->store[$name] = $value;
5168
5169        if ($valueUnreduced) {
5170            $env->storeUnreduced[$name] = $valueUnreduced;
5171        }
5172    }
5173
5174    /**
5175     * Set raw variable
5176     *
5177     * @param string                                $name
5178     * @param mixed                                 $value
5179     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5180     * @param mixed                                 $valueUnreduced
5181     *
5182     * @return void
5183     */
5184    protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
5185    {
5186        $env->store[$name] = $value;
5187
5188        if ($valueUnreduced) {
5189            $env->storeUnreduced[$name] = $valueUnreduced;
5190        }
5191    }
5192
5193    /**
5194     * Get variable
5195     *
5196     * @internal
5197     *
5198     * @param string                                $name
5199     * @param boolean                               $shouldThrow
5200     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5201     * @param boolean                               $unreduced
5202     *
5203     * @return mixed|null
5204     */
5205    public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
5206    {
5207        $normalizedName = $this->normalizeName($name);
5208        $specialContentKey = static::$namespaces['special'] . 'content';
5209
5210        if (! isset($env)) {
5211            $env = $this->getStoreEnv();
5212        }
5213
5214        $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
5215
5216        $maxDepth = 10000;
5217
5218        for (;;) {
5219            if ($maxDepth-- <= 0) {
5220                break;
5221            }
5222
5223            if (\array_key_exists($normalizedName, $env->store)) {
5224                if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
5225                    return $env->storeUnreduced[$normalizedName];
5226                }
5227
5228                return $env->store[$normalizedName];
5229            }
5230
5231            if (! $hasNamespace && isset($env->marker)) {
5232                if (! empty($env->store[$specialContentKey])) {
5233                    $env = $env->store[$specialContentKey]->scope;
5234                    continue;
5235                }
5236
5237                if (! empty($env->declarationScopeParent)) {
5238                    $env = $env->declarationScopeParent;
5239                } else {
5240                    $env = $this->rootEnv;
5241                }
5242                continue;
5243            }
5244
5245            if (isset($env->parentStore)) {
5246                $env = $env->parentStore;
5247            } elseif (isset($env->parent)) {
5248                $env = $env->parent;
5249            } else {
5250                break;
5251            }
5252        }
5253
5254        if ($shouldThrow) {
5255            throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
5256        }
5257
5258        // found nothing
5259        return null;
5260    }
5261
5262    /**
5263     * Has variable?
5264     *
5265     * @param string                                $name
5266     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5267     *
5268     * @return boolean
5269     */
5270    protected function has($name, Environment $env = null)
5271    {
5272        return ! \is_null($this->get($name, false, $env));
5273    }
5274
5275    /**
5276     * Inject variables
5277     *
5278     * @param array $args
5279     *
5280     * @return void
5281     */
5282    protected function injectVariables(array $args)
5283    {
5284        if (empty($args)) {
5285            return;
5286        }
5287
5288        $parser = $this->parserFactory(__METHOD__);
5289
5290        foreach ($args as $name => $strValue) {
5291            if ($name[0] === '$') {
5292                $name = substr($name, 1);
5293            }
5294
5295            if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) {
5296                $value = $this->coerceValue($strValue);
5297            }
5298
5299            $this->set($name, $value);
5300        }
5301    }
5302
5303    /**
5304     * Replaces variables.
5305     *
5306     * @param array<string, mixed> $variables
5307     *
5308     * @return void
5309     */
5310    public function replaceVariables(array $variables)
5311    {
5312        $this->registeredVars = [];
5313        $this->addVariables($variables);
5314    }
5315
5316    /**
5317     * Replaces variables.
5318     *
5319     * @param array<string, mixed> $variables
5320     *
5321     * @return void
5322     */
5323    public function addVariables(array $variables)
5324    {
5325        $triggerWarning = false;
5326
5327        foreach ($variables as $name => $value) {
5328            if (!$value instanceof Number && !\is_array($value)) {
5329                $triggerWarning = true;
5330            }
5331
5332            $this->registeredVars[$name] = $value;
5333        }
5334
5335        if ($triggerWarning) {
5336            @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED);
5337        }
5338    }
5339
5340    /**
5341     * Set variables
5342     *
5343     * @api
5344     *
5345     * @param array $variables
5346     *
5347     * @return void
5348     *
5349     * @deprecated Use "addVariables" or "replaceVariables" instead.
5350     */
5351    public function setVariables(array $variables)
5352    {
5353        @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.');
5354
5355        $this->addVariables($variables);
5356    }
5357
5358    /**
5359     * Unset variable
5360     *
5361     * @api
5362     *
5363     * @param string $name
5364     *
5365     * @return void
5366     */
5367    public function unsetVariable($name)
5368    {
5369        unset($this->registeredVars[$name]);
5370    }
5371
5372    /**
5373     * Returns list of variables
5374     *
5375     * @api
5376     *
5377     * @return array
5378     */
5379    public function getVariables()
5380    {
5381        return $this->registeredVars;
5382    }
5383
5384    /**
5385     * Adds to list of parsed files
5386     *
5387     * @internal
5388     *
5389     * @param string|null $path
5390     *
5391     * @return void
5392     */
5393    public function addParsedFile($path)
5394    {
5395        if (! \is_null($path) && is_file($path)) {
5396            $this->parsedFiles[realpath($path)] = filemtime($path);
5397        }
5398    }
5399
5400    /**
5401     * Returns list of parsed files
5402     *
5403     * @deprecated
5404     * @return array<string, int>
5405     */
5406    public function getParsedFiles()
5407    {
5408        @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED);
5409        return $this->parsedFiles;
5410    }
5411
5412    /**
5413     * Add import path
5414     *
5415     * @api
5416     *
5417     * @param string|callable $path
5418     *
5419     * @return void
5420     */
5421    public function addImportPath($path)
5422    {
5423        if (! \in_array($path, $this->importPaths)) {
5424            $this->importPaths[] = $path;
5425        }
5426    }
5427
5428    /**
5429     * Set import paths
5430     *
5431     * @api
5432     *
5433     * @param string|array<string|callable> $path
5434     *
5435     * @return void
5436     */
5437    public function setImportPaths($path)
5438    {
5439        $paths = (array) $path;
5440        $actualImportPaths = array_filter($paths, function ($path) {
5441            return $path !== '';
5442        });
5443
5444        $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths);
5445
5446        if ($this->legacyCwdImportPath) {
5447            @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
5448        }
5449
5450        $this->importPaths = $actualImportPaths;
5451    }
5452
5453    /**
5454     * Set number precision
5455     *
5456     * @api
5457     *
5458     * @param integer $numberPrecision
5459     *
5460     * @return void
5461     *
5462     * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
5463     */
5464    public function setNumberPrecision($numberPrecision)
5465    {
5466        @trigger_error('The number precision is not configurable anymore. '
5467            . 'The default is enough for all browsers.', E_USER_DEPRECATED);
5468    }
5469
5470    /**
5471     * Sets the output style.
5472     *
5473     * @api
5474     *
5475     * @param string $style One of the OutputStyle constants
5476     *
5477     * @return void
5478     *
5479     * @phpstan-param OutputStyle::* $style
5480     */
5481    public function setOutputStyle($style)
5482    {
5483        switch ($style) {
5484            case OutputStyle::EXPANDED:
5485                $this->formatter = Expanded::class;
5486                break;
5487
5488            case OutputStyle::COMPRESSED:
5489                $this->formatter = Compressed::class;
5490                break;
5491
5492            default:
5493                throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5494        }
5495    }
5496
5497    /**
5498     * Set formatter
5499     *
5500     * @api
5501     *
5502     * @param string $formatterName
5503     *
5504     * @return void
5505     *
5506     * @deprecated Use {@see setOutputStyle} instead.
5507     */
5508    public function setFormatter($formatterName)
5509    {
5510        if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) {
5511            @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED);
5512        }
5513        @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED);
5514
5515        $this->formatter = $formatterName;
5516    }
5517
5518    /**
5519     * Set line number style
5520     *
5521     * @api
5522     *
5523     * @param string $lineNumberStyle
5524     *
5525     * @return void
5526     *
5527     * @deprecated The line number output is not supported anymore. Use source maps instead.
5528     */
5529    public function setLineNumberStyle($lineNumberStyle)
5530    {
5531        @trigger_error('The line number output is not supported anymore. '
5532                       . 'Use source maps instead.', E_USER_DEPRECATED);
5533    }
5534
5535    /**
5536     * Configures the handling of non-ASCII outputs.
5537     *
5538     * If $charset is `true`, this will include a `@charset` declaration or a
5539     * UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII
5540     * characters. Otherwise, it will never include a `@charset` declaration or a
5541     * byte-order mark.
5542     *
5543     * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
5544     *
5545     * @param bool $charset
5546     *
5547     * @return void
5548     */
5549    public function setCharset($charset)
5550    {
5551        $this->charset = $charset;
5552    }
5553
5554    /**
5555     * Enable/disable source maps
5556     *
5557     * @api
5558     *
5559     * @param integer $sourceMap
5560     *
5561     * @return void
5562     *
5563     * @phpstan-param self::SOURCE_MAP_* $sourceMap
5564     */
5565    public function setSourceMap($sourceMap)
5566    {
5567        $this->sourceMap = $sourceMap;
5568    }
5569
5570    /**
5571     * Set source map options
5572     *
5573     * @api
5574     *
5575     * @param array $sourceMapOptions
5576     *
5577     * @phpstan-param  array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions
5578     *
5579     * @return void
5580     */
5581    public function setSourceMapOptions($sourceMapOptions)
5582    {
5583        $this->sourceMapOptions = $sourceMapOptions;
5584    }
5585
5586    /**
5587     * Register function
5588     *
5589     * @api
5590     *
5591     * @param string        $name
5592     * @param callable      $callback
5593     * @param string[]|null $argumentDeclaration
5594     *
5595     * @return void
5596     */
5597    public function registerFunction($name, $callback, $argumentDeclaration = null)
5598    {
5599        if (self::isNativeFunction($name)) {
5600            @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED);
5601        }
5602
5603        if ($argumentDeclaration === null) {
5604            @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED);
5605        }
5606
5607        $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration];
5608    }
5609
5610    /**
5611     * Unregister function
5612     *
5613     * @api
5614     *
5615     * @param string $name
5616     *
5617     * @return void
5618     */
5619    public function unregisterFunction($name)
5620    {
5621        unset($this->userFunctions[$this->normalizeName($name)]);
5622    }
5623
5624    /**
5625     * Add feature
5626     *
5627     * @api
5628     *
5629     * @param string $name
5630     *
5631     * @return void
5632     *
5633     * @deprecated Registering additional features is deprecated.
5634     */
5635    public function addFeature($name)
5636    {
5637        @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED);
5638
5639        $this->registeredFeatures[$name] = true;
5640    }
5641
5642    /**
5643     * Import file
5644     *
5645     * @param string                                 $path
5646     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
5647     *
5648     * @return void
5649     */
5650    protected function importFile($path, OutputBlock $out)
5651    {
5652        $this->pushCallStack('import ' . $this->getPrettyPath($path));
5653        // see if tree is cached
5654        $realPath = realpath($path);
5655
5656        if (substr($path, -5) === '.sass') {
5657            $this->sourceIndex = \count($this->sourceNames);
5658            $this->sourceNames[] = $path;
5659            $this->sourceLine = 1;
5660            $this->sourceColumn = 1;
5661
5662            throw $this->error('The Sass indented syntax is not implemented.');
5663        }
5664
5665        if (isset($this->importCache[$realPath])) {
5666            $this->handleImportLoop($realPath);
5667
5668            $tree = $this->importCache[$realPath];
5669        } else {
5670            $code   = file_get_contents($path);
5671            $parser = $this->parserFactory($path);
5672            $tree   = $parser->parse($code);
5673
5674            $this->importCache[$realPath] = $tree;
5675        }
5676
5677        $currentDirectory = $this->currentDirectory;
5678        $this->currentDirectory = dirname($path);
5679
5680        $this->compileChildrenNoReturn($tree->children, $out);
5681        $this->currentDirectory = $currentDirectory;
5682        $this->popCallStack();
5683    }
5684
5685    /**
5686     * Save the imported files with their resolving path context
5687     *
5688     * @param string|null $currentDirectory
5689     * @param string      $path
5690     * @param string      $filePath
5691     *
5692     * @return void
5693     */
5694    private function registerImport($currentDirectory, $path, $filePath)
5695    {
5696        $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath];
5697    }
5698
5699    /**
5700     * Detects whether the import is a CSS import.
5701     *
5702     * For legacy reasons, custom importers are called for those, allowing them
5703     * to replace them with an actual Sass import. However this behavior is
5704     * deprecated. Custom importers are expected to return null when they receive
5705     * a CSS import.
5706     *
5707     * @param string $url
5708     *
5709     * @return bool
5710     */
5711    public static function isCssImport($url)
5712    {
5713        return 1 === preg_match('~\.css$|^https?://|^//~', $url);
5714    }
5715
5716    /**
5717     * Return the file path for an import url if it exists
5718     *
5719     * @internal
5720     *
5721     * @param string      $url
5722     * @param string|null $currentDir
5723     *
5724     * @return string|null
5725     */
5726    public function findImport($url, $currentDir = null)
5727    {
5728        // Vanilla css and external requests. These are not meant to be Sass imports.
5729        // Callback importers are still called for BC.
5730        if (self::isCssImport($url)) {
5731            foreach ($this->importPaths as $dir) {
5732                if (\is_string($dir)) {
5733                    continue;
5734                }
5735
5736                if (\is_callable($dir)) {
5737                    // check custom callback for import path
5738                    $file = \call_user_func($dir, $url);
5739
5740                    if (! \is_null($file)) {
5741                        if (\is_array($dir)) {
5742                            $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1];
5743                        } elseif ($dir instanceof \Closure) {
5744                            $r = new \ReflectionFunction($dir);
5745                            if (false !== strpos($r->name, '{closure}')) {
5746                                $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine());
5747                            } elseif ($class = $r->getClosureScopeClass()) {
5748                                $callableDescription = $class->name.'::'.$r->name;
5749                            } else {
5750                                $callableDescription = $r->name;
5751                            }
5752                        } elseif (\is_object($dir)) {
5753                            $callableDescription = \get_class($dir) . '::__invoke';
5754                        } else {
5755                            $callableDescription = 'callable'; // Fallback if we don't have a dedicated description
5756                        }
5757                        @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED);
5758
5759                        return $file;
5760                    }
5761                }
5762            }
5763            return null;
5764        }
5765
5766        if (!\is_null($currentDir)) {
5767            $relativePath = $this->resolveImportPath($url, $currentDir);
5768
5769            if (!\is_null($relativePath)) {
5770                return $relativePath;
5771            }
5772        }
5773
5774        foreach ($this->importPaths as $dir) {
5775            if (\is_string($dir)) {
5776                $path = $this->resolveImportPath($url, $dir);
5777
5778                if (!\is_null($path)) {
5779                    return $path;
5780                }
5781            } elseif (\is_callable($dir)) {
5782                // check custom callback for import path
5783                $file = \call_user_func($dir, $url);
5784
5785                if (! \is_null($file)) {
5786                    return $file;
5787                }
5788            }
5789        }
5790
5791        if ($this->legacyCwdImportPath) {
5792            $path = $this->resolveImportPath($url, getcwd());
5793
5794            if (!\is_null($path)) {
5795                @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
5796
5797                return $path;
5798            }
5799        }
5800
5801        throw $this->error("`$url` file not found for @import");
5802    }
5803
5804    /**
5805     * @param string $url
5806     * @param string $baseDir
5807     *
5808     * @return string|null
5809     */
5810    private function resolveImportPath($url, $baseDir)
5811    {
5812        $path = Path::join($baseDir, $url);
5813
5814        $hasExtension = preg_match('/.s[ac]ss$/', $url);
5815
5816        if ($hasExtension) {
5817            return $this->checkImportPathConflicts($this->tryImportPath($path));
5818        }
5819
5820        $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path));
5821
5822        if (!\is_null($result)) {
5823            return $result;
5824        }
5825
5826        return $this->tryImportPathAsDirectory($path);
5827    }
5828
5829    /**
5830     * @param string[] $paths
5831     *
5832     * @return string|null
5833     */
5834    private function checkImportPathConflicts(array $paths)
5835    {
5836        if (\count($paths) === 0) {
5837            return null;
5838        }
5839
5840        if (\count($paths) === 1) {
5841            return $paths[0];
5842        }
5843
5844        $formattedPrettyPaths = [];
5845
5846        foreach ($paths as $path) {
5847            $formattedPrettyPaths[] = '  ' . $this->getPrettyPath($path);
5848        }
5849
5850        throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
5851    }
5852
5853    /**
5854     * @param string $path
5855     *
5856     * @return string[]
5857     */
5858    private function tryImportPathWithExtensions($path)
5859    {
5860        $result = array_merge(
5861            $this->tryImportPath($path.'.sass'),
5862            $this->tryImportPath($path.'.scss')
5863        );
5864
5865        if ($result) {
5866            return $result;
5867        }
5868
5869        return $this->tryImportPath($path.'.css');
5870    }
5871
5872    /**
5873     * @param string $path
5874     *
5875     * @return string[]
5876     */
5877    private function tryImportPath($path)
5878    {
5879        $partial = dirname($path).'/_'.basename($path);
5880
5881        $candidates = [];
5882
5883        if (is_file($partial)) {
5884            $candidates[] = $partial;
5885        }
5886
5887        if (is_file($path)) {
5888            $candidates[] = $path;
5889        }
5890
5891        return $candidates;
5892    }
5893
5894    /**
5895     * @param string $path
5896     *
5897     * @return string|null
5898     */
5899    private function tryImportPathAsDirectory($path)
5900    {
5901        if (!is_dir($path)) {
5902            return null;
5903        }
5904
5905        return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5906    }
5907
5908    /**
5909     * @param string|null $path
5910     *
5911     * @return string
5912     */
5913    private function getPrettyPath($path)
5914    {
5915        if ($path === null) {
5916            return '(unknown file)';
5917        }
5918
5919        $normalizedPath = $path;
5920        $normalizedRootDirectory = $this->rootDirectory.'/';
5921
5922        if (\DIRECTORY_SEPARATOR === '\\') {
5923            $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
5924            $normalizedPath = str_replace('\\', '/', $path);
5925        }
5926
5927        if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
5928            return substr($path, \strlen($normalizedRootDirectory));
5929        }
5930
5931        return $path;
5932    }
5933
5934    /**
5935     * Set encoding
5936     *
5937     * @api
5938     *
5939     * @param string|null $encoding
5940     *
5941     * @return void
5942     *
5943     * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated.
5944     */
5945    public function setEncoding($encoding)
5946    {
5947        if (!$encoding || strtolower($encoding) === 'utf-8') {
5948            @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
5949        } else {
5950            @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED);
5951        }
5952
5953        $this->encoding = $encoding;
5954    }
5955
5956    /**
5957     * Ignore errors?
5958     *
5959     * @api
5960     *
5961     * @param boolean $ignoreErrors
5962     *
5963     * @return \ScssPhp\ScssPhp\Compiler
5964     *
5965     * @deprecated Ignoring Sass errors is not longer supported.
5966     */
5967    public function setIgnoreErrors($ignoreErrors)
5968    {
5969        @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
5970
5971        return $this;
5972    }
5973
5974    /**
5975     * Get source position
5976     *
5977     * @api
5978     *
5979     * @return array
5980     *
5981     * @deprecated
5982     */
5983    public function getSourcePosition()
5984    {
5985        @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
5986
5987        $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
5988
5989        return [$sourceFile, $this->sourceLine, $this->sourceColumn];
5990    }
5991
5992    /**
5993     * Throw error (exception)
5994     *
5995     * @api
5996     *
5997     * @param string $msg Message with optional sprintf()-style vararg parameters
5998     *
5999     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
6000     *
6001     * @deprecated use "error" and throw the exception in the caller instead.
6002     */
6003    public function throwError($msg)
6004    {
6005        @trigger_error(
6006            'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
6007            E_USER_DEPRECATED
6008        );
6009
6010        throw $this->error(...func_get_args());
6011    }
6012
6013    /**
6014     * Build an error (exception)
6015     *
6016     * @internal
6017     *
6018     * @param string $msg Message with optional sprintf()-style vararg parameters
6019     *
6020     * @return CompilerException
6021     */
6022    public function error($msg, ...$args)
6023    {
6024        if ($args) {
6025            $msg = sprintf($msg, ...$args);
6026        }
6027
6028        if (! $this->ignoreCallStackMessage) {
6029            $msg = $this->addLocationToMessage($msg);
6030        }
6031
6032        return new CompilerException($msg);
6033    }
6034
6035    /**
6036     * @param string $msg
6037     *
6038     * @return string
6039     */
6040    private function addLocationToMessage($msg)
6041    {
6042        $line   = $this->sourceLine;
6043        $column = $this->sourceColumn;
6044
6045        $loc = isset($this->sourceNames[$this->sourceIndex])
6046            ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
6047            : "line: $line, column: $column";
6048
6049        $msg = "$msg: $loc";
6050
6051        $callStackMsg = $this->callStackMessage();
6052
6053        if ($callStackMsg) {
6054            $msg .= "\nCall Stack:\n" . $callStackMsg;
6055        }
6056
6057        return $msg;
6058    }
6059
6060    /**
6061     * @param string $functionName
6062     * @param array $ExpectedArgs
6063     * @param int $nbActual
6064     * @return CompilerException
6065     *
6066     * @deprecated
6067     */
6068    public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
6069    {
6070        @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
6071
6072        $nbExpected = \count($ExpectedArgs);
6073
6074        if ($nbActual > $nbExpected) {
6075            return $this->error(
6076                'Error: Only %d arguments allowed in %s(), but %d were passed.',
6077                $nbExpected,
6078                $functionName,
6079                $nbActual
6080            );
6081        } else {
6082            $missing = [];
6083
6084            while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
6085                array_unshift($missing, array_pop($ExpectedArgs));
6086            }
6087
6088            return $this->error(
6089                'Error: %s() argument%s %s missing.',
6090                $functionName,
6091                count($missing) > 1 ? 's' : '',
6092                implode(', ', $missing)
6093            );
6094        }
6095    }
6096
6097    /**
6098     * Beautify call stack for output
6099     *
6100     * @param boolean  $all
6101     * @param int|null $limit
6102     *
6103     * @return string
6104     */
6105    protected function callStackMessage($all = false, $limit = null)
6106    {
6107        $callStackMsg = [];
6108        $ncall = 0;
6109
6110        if ($this->callStack) {
6111            foreach (array_reverse($this->callStack) as $call) {
6112                if ($all || (isset($call['n']) && $call['n'])) {
6113                    $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
6114                    $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6115                          ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6116                          : '(unknown file)');
6117                    $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
6118
6119                    $callStackMsg[] = $msg;
6120
6121                    if (! \is_null($limit) && $ncall > $limit) {
6122                        break;
6123                    }
6124                }
6125            }
6126        }
6127
6128        return implode("\n", $callStackMsg);
6129    }
6130
6131    /**
6132     * Handle import loop
6133     *
6134     * @param string $name
6135     *
6136     * @throws \Exception
6137     */
6138    protected function handleImportLoop($name)
6139    {
6140        for ($env = $this->env; $env; $env = $env->parent) {
6141            if (! $env->block) {
6142                continue;
6143            }
6144
6145            $file = $this->sourceNames[$env->block->sourceIndex];
6146
6147            if ($file === null) {
6148                continue;
6149            }
6150
6151            if (realpath($file) === $name) {
6152                throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
6153            }
6154        }
6155    }
6156
6157    /**
6158     * Call SCSS @function
6159     *
6160     * @param Object $func
6161     * @param array  $argValues
6162     *
6163     * @return array|Number
6164     */
6165    protected function callScssFunction($func, $argValues)
6166    {
6167        if (! $func) {
6168            return static::$defaultValue;
6169        }
6170        $name = $func->name;
6171
6172        $this->pushEnv();
6173
6174        // set the args
6175        if (isset($func->args)) {
6176            $this->applyArguments($func->args, $argValues);
6177        }
6178
6179        // throw away lines and children
6180        $tmp = new OutputBlock();
6181        $tmp->lines    = [];
6182        $tmp->children = [];
6183
6184        $this->env->marker = 'function';
6185
6186        if (! empty($func->parentEnv)) {
6187            $this->env->declarationScopeParent = $func->parentEnv;
6188        } else {
6189            throw $this->error("@function $name() without parentEnv");
6190        }
6191
6192        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
6193
6194        $this->popEnv();
6195
6196        return ! isset($ret) ? static::$defaultValue : $ret;
6197    }
6198
6199    /**
6200     * Call built-in and registered (PHP) functions
6201     *
6202     * @param string $name
6203     * @param callable $function
6204     * @param array  $prototype
6205     * @param array  $args
6206     *
6207     * @return array|Number|null
6208     */
6209    protected function callNativeFunction($name, $function, $prototype, $args)
6210    {
6211        $libName = (is_array($function) ? end($function) : null);
6212        $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
6213
6214        if (\is_null($sorted_kwargs)) {
6215            return null;
6216        }
6217        @list($sorted, $kwargs) = $sorted_kwargs;
6218
6219        if ($name !== 'if') {
6220            foreach ($sorted as &$val) {
6221                if ($val !== null) {
6222                    $val = $this->reduce($val, true);
6223                }
6224            }
6225        }
6226
6227        $returnValue = \call_user_func($function, $sorted, $kwargs);
6228
6229        if (! isset($returnValue)) {
6230            return null;
6231        }
6232
6233        if (\is_array($returnValue) || $returnValue instanceof Number) {
6234            return $returnValue;
6235        }
6236
6237        @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED);
6238
6239        return $this->coerceValue($returnValue);
6240    }
6241
6242    /**
6243     * Get built-in function
6244     *
6245     * @param string $name Normalized name
6246     *
6247     * @return array
6248     */
6249    protected function getBuiltinFunction($name)
6250    {
6251        $libName = self::normalizeNativeFunctionName($name);
6252        return [$this, $libName];
6253    }
6254
6255    /**
6256     * Normalize native function name
6257     *
6258     * @internal
6259     *
6260     * @param string $name
6261     *
6262     * @return string
6263     */
6264    public static function normalizeNativeFunctionName($name)
6265    {
6266        $name = str_replace("-", "_", $name);
6267        $libName = 'lib' . preg_replace_callback(
6268            '/_(.)/',
6269            function ($m) {
6270                return ucfirst($m[1]);
6271            },
6272            ucfirst($name)
6273        );
6274        return $libName;
6275    }
6276
6277    /**
6278     * Check if a function is a native built-in scss function, for css parsing
6279     *
6280     * @internal
6281     *
6282     * @param string $name
6283     *
6284     * @return bool
6285     */
6286    public static function isNativeFunction($name)
6287    {
6288        return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
6289    }
6290
6291    /**
6292     * Sorts keyword arguments
6293     *
6294     * @param string $functionName
6295     * @param array|null  $prototypes
6296     * @param array  $args
6297     *
6298     * @return array|null
6299     */
6300    protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
6301    {
6302        static $parser = null;
6303
6304        if (! isset($prototypes)) {
6305            $keyArgs = [];
6306            $posArgs = [];
6307
6308            if (\is_array($args) && \count($args) && \end($args) === static::$null) {
6309                array_pop($args);
6310            }
6311
6312            // separate positional and keyword arguments
6313            foreach ($args as $arg) {
6314                list($key, $value) = $arg;
6315
6316                if (empty($key) or empty($key[1])) {
6317                    $posArgs[] = empty($arg[2]) ? $value : $arg;
6318                } else {
6319                    $keyArgs[$key[1]] = $value;
6320                }
6321            }
6322
6323            return [$posArgs, $keyArgs];
6324        }
6325
6326        // specific cases ?
6327        if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
6328            // notation 100 127 255 / 0 is in fact a simple list of 4 values
6329            foreach ($args as $k => $arg) {
6330                if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
6331                    $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]);
6332                }
6333            }
6334        }
6335
6336        list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false);
6337
6338        if (! \is_array(reset($prototypes))) {
6339            $prototypes = [$prototypes];
6340        }
6341
6342        $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes);
6343        assert(!empty($parsedPrototypes));
6344        $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names);
6345
6346        $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat);
6347
6348        $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator);
6349
6350        $finalArgs = [];
6351        $keyArgs = [];
6352
6353        foreach ($matchedPrototype['arguments'] as $argument) {
6354            list($normalizedName, $originalName, $default) = $argument;
6355
6356            if (isset($vars[$normalizedName])) {
6357                $value = $vars[$normalizedName];
6358            } else {
6359                $value = $default;
6360            }
6361
6362            // special null value as default: translate to real null here
6363            if ($value === [Type::T_KEYWORD, 'null']) {
6364                $value = null;
6365            }
6366
6367            $finalArgs[] = $value;
6368            $keyArgs[$originalName] = $value;
6369        }
6370
6371        if ($matchedPrototype['rest_argument'] !== null) {
6372            $value = $vars[$matchedPrototype['rest_argument']];
6373
6374            $finalArgs[] = $value;
6375            $keyArgs[$matchedPrototype['rest_argument']] = $value;
6376        }
6377
6378        return [$finalArgs, $keyArgs];
6379    }
6380
6381    /**
6382     * Parses a function prototype to the internal representation of arguments.
6383     *
6384     * The input is an array of strings describing each argument, as supported
6385     * in {@see registerFunction}. Argument names don't include the `$`.
6386     * The output contains the list of positional argument, with their normalized
6387     * name (underscores are replaced by dashes), their original name (to be used
6388     * in case of error reporting) and their default value. The output also contains
6389     * the normalized name of the rest argument, or null if the function prototype
6390     * is not variadic.
6391     *
6392     * @param string[] $prototype
6393     *
6394     * @return array
6395     * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6396     */
6397    private function parseFunctionPrototype(array $prototype)
6398    {
6399        static $parser = null;
6400
6401        $arguments = [];
6402        $restArgument = null;
6403
6404        foreach ($prototype as $p) {
6405            if (null !== $restArgument) {
6406                throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.');
6407            }
6408
6409            $default = null;
6410            $p = explode(':', $p, 2);
6411            $name = str_replace('_', '-', $p[0]);
6412
6413            if (isset($p[1])) {
6414                $defaultSource = trim($p[1]);
6415
6416                if ($defaultSource === 'null') {
6417                    // differentiate this null from the static::$null
6418                    $default = [Type::T_KEYWORD, 'null'];
6419                } else {
6420                    if (\is_null($parser)) {
6421                        $parser = $this->parserFactory(__METHOD__);
6422                    }
6423
6424                    $parser->parseValue($defaultSource, $default);
6425                }
6426            }
6427
6428            if (substr($name, -3) === '...') {
6429                $restArgument = substr($name, 0, -3);
6430            } else {
6431                $arguments[] = [$name, $p[0], $default];
6432            }
6433        }
6434
6435        return [
6436            'arguments' => $arguments,
6437            'rest_argument' => $restArgument,
6438        ];
6439    }
6440
6441    /**
6442     * Returns the function prototype for the given positional and named arguments.
6443     *
6444     * If no exact match is found, finds the closest approximation. Note that this
6445     * doesn't guarantee that $positional and $names are valid for the returned
6446     * prototype.
6447     *
6448     * @param array[]               $prototypes
6449     * @param int                   $positional
6450     * @param array<string, string> $names A set of names, as both keys and values
6451     *
6452     * @return array
6453     *
6454     * @phpstan-param non-empty-list<array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}> $prototypes
6455     * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6456     */
6457    private function selectFunctionPrototype(array $prototypes, $positional, array $names)
6458    {
6459        $fuzzyMatch = null;
6460        $minMismatchDistance = null;
6461
6462        foreach ($prototypes as $prototype) {
6463            // Ideally, find an exact match.
6464            if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
6465                return $prototype;
6466            }
6467
6468            $mismatchDistance = \count($prototype['arguments']) - $positional;
6469
6470            if ($minMismatchDistance !== null) {
6471                if (abs($mismatchDistance) > abs($minMismatchDistance)) {
6472                    continue;
6473                }
6474
6475                // If two overloads have the same mismatch distance, favor the overload
6476                // that has more arguments.
6477                if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
6478                    continue;
6479                }
6480            }
6481
6482            $minMismatchDistance = $mismatchDistance;
6483            $fuzzyMatch = $prototype;
6484        }
6485
6486        return $fuzzyMatch;
6487    }
6488
6489    /**
6490     * Checks whether the argument invocation matches the callable prototype.
6491     *
6492     * The rules are similar to {@see verifyPrototype}. The boolean return value
6493     * avoids the overhead of building and catching exceptions when the reason of
6494     * not matching the prototype does not need to be known.
6495     *
6496     * @param array                 $prototype
6497     * @param int                   $positional
6498     * @param array<string, string> $names
6499     *
6500     * @return bool
6501     *
6502     * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6503     */
6504    private function checkPrototypeMatches(array $prototype, $positional, array $names)
6505    {
6506        $nameUsed = 0;
6507
6508        foreach ($prototype['arguments'] as $i => $argument) {
6509            list ($name, $originalName, $default) = $argument;
6510
6511            if ($i < $positional) {
6512                if (isset($names[$name])) {
6513                    return false;
6514                }
6515            } elseif (isset($names[$name])) {
6516                $nameUsed++;
6517            } elseif ($default === null) {
6518                return false;
6519            }
6520        }
6521
6522        if ($prototype['rest_argument'] !== null) {
6523            return true;
6524        }
6525
6526        if ($positional > \count($prototype['arguments'])) {
6527            return false;
6528        }
6529
6530        if ($nameUsed < \count($names)) {
6531            return false;
6532        }
6533
6534        return true;
6535    }
6536
6537    /**
6538     * Verifies that the argument invocation is valid for the callable prototype.
6539     *
6540     * @param array                 $prototype
6541     * @param int                   $positional
6542     * @param array<string, string> $names
6543     * @param bool                  $hasSplat
6544     *
6545     * @return void
6546     *
6547     * @throws SassScriptException
6548     *
6549     * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6550     */
6551    private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat)
6552    {
6553        $nameUsed = 0;
6554
6555        foreach ($prototype['arguments'] as $i => $argument) {
6556            list ($name, $originalName, $default) = $argument;
6557
6558            if ($i < $positional) {
6559                if (isset($names[$name])) {
6560                    throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName));
6561                }
6562            } elseif (isset($names[$name])) {
6563                $nameUsed++;
6564            } elseif ($default === null) {
6565                throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
6566            }
6567        }
6568
6569        if ($prototype['rest_argument'] !== null) {
6570            return;
6571        }
6572
6573        if ($positional > \count($prototype['arguments'])) {
6574            $message = sprintf(
6575                'Only %d %sargument%s allowed, but %d %s passed.',
6576                \count($prototype['arguments']),
6577                empty($names) ? '' : 'positional ',
6578                \count($prototype['arguments']) === 1 ? '' : 's',
6579                $positional,
6580                $positional === 1 ? 'was' : 'were'
6581            );
6582            if (!$hasSplat) {
6583                throw new SassScriptException($message);
6584            }
6585
6586            $message = $this->addLocationToMessage($message);
6587            $message .= "\nThis will be an error in future versions of Sass.";
6588            $this->logger->warn($message, true);
6589        }
6590
6591        if ($nameUsed < \count($names)) {
6592            $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0)));
6593            $lastName = array_pop($unknownNames);
6594            $message = sprintf(
6595                'No argument%s named $%s%s.',
6596                $unknownNames ? 's' : '',
6597                $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
6598                $lastName
6599            );
6600            throw new SassScriptException($message);
6601        }
6602    }
6603
6604    /**
6605     * Evaluates the argument from the invocation.
6606     *
6607     * This returns several things about this invocation:
6608     * - the list of positional arguments
6609     * - the map of named arguments, indexed by normalized names
6610     * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access)
6611     * - the separator used by the list using the splat operator, if any
6612     * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting.
6613     *
6614     * @param array[] $args
6615     * @param bool    $reduce Whether arguments should be reduced to their value
6616     *
6617     * @return array
6618     *
6619     * @throws SassScriptException
6620     *
6621     * @phpstan-return array{0: list<array|Number>, 1: array<string, array|Number>, 2: array<string, string>, 3: string|null, 4: bool}
6622     */
6623    private function evaluateArguments(array $args, $reduce = true)
6624    {
6625        // this represents trailing commas
6626        if (\count($args) && end($args) === static::$null) {
6627            array_pop($args);
6628        }
6629
6630        $splatSeparator = null;
6631        $keywordArgs = [];
6632        $names = [];
6633        $positionalArgs = [];
6634        $hasKeywordArgument = false;
6635        $hasSplat = false;
6636
6637        foreach ($args as $arg) {
6638            if (!empty($arg[0])) {
6639                $hasKeywordArgument = true;
6640
6641                assert(\is_string($arg[0][1]));
6642                $name = str_replace('_', '-', $arg[0][1]);
6643
6644                if (isset($keywordArgs[$name])) {
6645                    throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1]));
6646                }
6647
6648                $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]);
6649                $names[$name] = $name;
6650            } elseif (! empty($arg[2])) {
6651                // $arg[2] means a var followed by ... in the arg ($list... )
6652                $val = $this->reduce($arg[1], true);
6653                $hasSplat = true;
6654
6655                if ($val[0] === Type::T_LIST) {
6656                    foreach ($val[2] as $item) {
6657                        if (\is_null($splatSeparator)) {
6658                            $splatSeparator = $val[1];
6659                        }
6660
6661                        $positionalArgs[] = $this->maybeReduce($reduce, $item);
6662                    }
6663
6664                    if (isset($val[3]) && \is_array($val[3])) {
6665                        foreach ($val[3] as $name => $item) {
6666                            assert(\is_string($name));
6667
6668                            $normalizedName = str_replace('_', '-', $name);
6669
6670                            if (isset($keywordArgs[$normalizedName])) {
6671                                throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
6672                            }
6673
6674                            $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6675                            $names[$normalizedName] = $normalizedName;
6676                            $hasKeywordArgument = true;
6677                        }
6678                    }
6679                } elseif ($val[0] === Type::T_MAP) {
6680                    foreach ($val[1] as $i => $name) {
6681                        $name = $this->compileStringContent($this->coerceString($name));
6682                        $item = $val[2][$i];
6683
6684                        if (! is_numeric($name)) {
6685                            $normalizedName = str_replace('_', '-', $name);
6686
6687                            if (isset($keywordArgs[$normalizedName])) {
6688                                throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
6689                            }
6690
6691                            $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6692                            $names[$normalizedName] = $normalizedName;
6693                            $hasKeywordArgument = true;
6694                        } else {
6695                            if (\is_null($splatSeparator)) {
6696                                $splatSeparator = $val[1];
6697                            }
6698
6699                            $positionalArgs[] = $this->maybeReduce($reduce, $item);
6700                        }
6701                    }
6702                } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list
6703                    $positionalArgs[] = $this->maybeReduce($reduce, $val);
6704                }
6705            } elseif ($hasKeywordArgument) {
6706                throw new SassScriptException('Positional arguments must come before keyword arguments.');
6707            } else {
6708                $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]);
6709            }
6710        }
6711
6712        return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat];
6713    }
6714
6715    /**
6716     * @param bool         $reduce
6717     * @param array|Number $value
6718     *
6719     * @return array|Number
6720     */
6721    private function maybeReduce($reduce, $value)
6722    {
6723        if ($reduce) {
6724            return $this->reduce($value, true);
6725        }
6726
6727        return $value;
6728    }
6729
6730    /**
6731     * Apply argument values per definition
6732     *
6733     * @param array[]    $argDef
6734     * @param array|null $argValues
6735     * @param boolean $storeInEnv
6736     * @param boolean $reduce
6737     *   only used if $storeInEnv = false
6738     *
6739     * @return array<string, array|Number>
6740     *
6741     * @phpstan-param list<array{0: string, 1: array|Number|null, 2: bool}> $argDef
6742     *
6743     * @throws \Exception
6744     */
6745    protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
6746    {
6747        $output = [];
6748
6749        if (\is_null($argValues)) {
6750            $argValues = [];
6751        }
6752
6753        if ($storeInEnv) {
6754            $storeEnv = $this->getStoreEnv();
6755
6756            $env = new Environment();
6757            $env->store = $storeEnv->store;
6758        }
6759
6760        $prototype = ['arguments' => [], 'rest_argument' => null];
6761        $originalRestArgumentName = null;
6762
6763        foreach ($argDef as $i => $arg) {
6764            list($name, $default, $isVariable) = $arg;
6765            $normalizedName = str_replace('_', '-', $name);
6766
6767            if ($isVariable) {
6768                $originalRestArgumentName = $name;
6769                $prototype['rest_argument'] = $normalizedName;
6770            } else {
6771                $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null];
6772            }
6773        }
6774
6775        list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce);
6776
6777        $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat);
6778
6779        $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator);
6780
6781        foreach ($prototype['arguments'] as $argument) {
6782            list($normalizedName, $name) = $argument;
6783
6784            if (!isset($vars[$normalizedName])) {
6785                continue;
6786            }
6787
6788            $val = $vars[$normalizedName];
6789
6790            if ($storeInEnv) {
6791                $this->set($name, $this->reduce($val, true), true, $env);
6792            } else {
6793                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6794            }
6795        }
6796
6797        if ($prototype['rest_argument'] !== null) {
6798            assert($originalRestArgumentName !== null);
6799            $name = $originalRestArgumentName;
6800            $val = $vars[$prototype['rest_argument']];
6801
6802            if ($storeInEnv) {
6803                $this->set($name, $this->reduce($val, true), true, $env);
6804            } else {
6805                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6806            }
6807        }
6808
6809        if ($storeInEnv) {
6810            $storeEnv->store = $env->store;
6811        }
6812
6813        foreach ($prototype['arguments'] as $argument) {
6814            list($normalizedName, $name, $default) = $argument;
6815
6816            if (isset($vars[$normalizedName])) {
6817                continue;
6818            }
6819            assert($default !== null);
6820
6821            if ($storeInEnv) {
6822                $this->set($name, $this->reduce($default, true), true);
6823            } else {
6824                $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
6825            }
6826        }
6827
6828        return $output;
6829    }
6830
6831    /**
6832     * Apply argument values per definition.
6833     *
6834     * This method assumes that the arguments are valid for the provided prototype.
6835     * The validation with {@see verifyPrototype} must have been run before calling
6836     * it.
6837     * Arguments are returned as a map from the normalized argument names to the
6838     * value. Additional arguments are collected in a sass argument list available
6839     * under the name of the rest argument in the result.
6840     *
6841     * Defaults are not applied as they are resolved in a different environment.
6842     *
6843     * @param array                       $prototype
6844     * @param array<array|Number>         $positionalArgs
6845     * @param array<string, array|Number> $namedArgs
6846     * @param string|null                 $splatSeparator
6847     *
6848     * @return array<string, array|Number>
6849     *
6850     * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6851     */
6852    private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator)
6853    {
6854        $output = [];
6855        $minLength = min(\count($positionalArgs), \count($prototype['arguments']));
6856
6857        for ($i = 0; $i < $minLength; $i++) {
6858            list($name) = $prototype['arguments'][$i];
6859            $val = $positionalArgs[$i];
6860
6861            $output[$name] = $val;
6862        }
6863
6864        $restNamed = $namedArgs;
6865
6866        for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) {
6867            $argument = $prototype['arguments'][$i];
6868            list($name) = $argument;
6869
6870            if (isset($namedArgs[$name])) {
6871                $val = $namedArgs[$name];
6872                unset($restNamed[$name]);
6873            } else {
6874                continue;
6875            }
6876
6877            $output[$name] = $val;
6878        }
6879
6880        if ($prototype['rest_argument'] !== null) {
6881            $name = $prototype['rest_argument'];
6882            $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments'])));
6883
6884            $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed];
6885
6886            $output[$name] = $val;
6887        }
6888
6889        return $output;
6890    }
6891
6892    /**
6893     * Coerce a php value into a scss one
6894     *
6895     * @param mixed $value
6896     *
6897     * @return array|Number
6898     */
6899    protected function coerceValue($value)
6900    {
6901        if (\is_array($value) || $value instanceof Number) {
6902            return $value;
6903        }
6904
6905        if (\is_bool($value)) {
6906            return $this->toBool($value);
6907        }
6908
6909        if (\is_null($value)) {
6910            return static::$null;
6911        }
6912
6913        if (is_numeric($value)) {
6914            return new Number($value, '');
6915        }
6916
6917        if ($value === '') {
6918            return static::$emptyString;
6919        }
6920
6921        $value = [Type::T_KEYWORD, $value];
6922        $color = $this->coerceColor($value);
6923
6924        if ($color) {
6925            return $color;
6926        }
6927
6928        return $value;
6929    }
6930
6931    /**
6932     * Coerce something to map
6933     *
6934     * @param array|Number $item
6935     *
6936     * @return array|Number
6937     */
6938    protected function coerceMap($item)
6939    {
6940        if ($item[0] === Type::T_MAP) {
6941            return $item;
6942        }
6943
6944        if (
6945            $item[0] === Type::T_LIST &&
6946            $item[2] === []
6947        ) {
6948            return static::$emptyMap;
6949        }
6950
6951        return $item;
6952    }
6953
6954    /**
6955     * Coerce something to list
6956     *
6957     * @param array|Number $item
6958     * @param string       $delim
6959     * @param boolean      $removeTrailingNull
6960     *
6961     * @return array
6962     */
6963    protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
6964    {
6965        if ($item instanceof Number) {
6966            return [Type::T_LIST, $delim, [$item]];
6967        }
6968
6969        if ($item[0] === Type::T_LIST) {
6970            // remove trailing null from the list
6971            if ($removeTrailingNull && end($item[2]) === static::$null) {
6972                array_pop($item[2]);
6973            }
6974
6975            return $item;
6976        }
6977
6978        if ($item[0] === Type::T_MAP) {
6979            $keys = $item[1];
6980            $values = $item[2];
6981            $list = [];
6982
6983            for ($i = 0, $s = \count($keys); $i < $s; $i++) {
6984                $key = $keys[$i];
6985                $value = $values[$i];
6986
6987                switch ($key[0]) {
6988                    case Type::T_LIST:
6989                    case Type::T_MAP:
6990                    case Type::T_STRING:
6991                    case Type::T_NULL:
6992                        break;
6993
6994                    default:
6995                        $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
6996                        break;
6997                }
6998
6999                $list[] = [
7000                    Type::T_LIST,
7001                    '',
7002                    [$key, $value]
7003                ];
7004            }
7005
7006            return [Type::T_LIST, ',', $list];
7007        }
7008
7009        return [Type::T_LIST, $delim, [$item]];
7010    }
7011
7012    /**
7013     * Coerce color for expression
7014     *
7015     * @param array|Number $value
7016     *
7017     * @return array|Number
7018     */
7019    protected function coerceForExpression($value)
7020    {
7021        if ($color = $this->coerceColor($value)) {
7022            return $color;
7023        }
7024
7025        return $value;
7026    }
7027
7028    /**
7029     * Coerce value to color
7030     *
7031     * @param array|Number $value
7032     * @param bool         $inRGBFunction
7033     *
7034     * @return array|null
7035     */
7036    protected function coerceColor($value, $inRGBFunction = false)
7037    {
7038        if ($value instanceof Number) {
7039            return null;
7040        }
7041
7042        switch ($value[0]) {
7043            case Type::T_COLOR:
7044                for ($i = 1; $i <= 3; $i++) {
7045                    if (! is_numeric($value[$i])) {
7046                        $cv = $this->compileRGBAValue($value[$i]);
7047
7048                        if (! is_numeric($cv)) {
7049                            return null;
7050                        }
7051
7052                        $value[$i] = $cv;
7053                    }
7054
7055                    if (isset($value[4])) {
7056                        if (! is_numeric($value[4])) {
7057                            $cv = $this->compileRGBAValue($value[4], true);
7058
7059                            if (! is_numeric($cv)) {
7060                                return null;
7061                            }
7062
7063                            $value[4] = $cv;
7064                        }
7065                    }
7066                }
7067
7068                return $value;
7069
7070            case Type::T_LIST:
7071                if ($inRGBFunction) {
7072                    if (\count($value[2]) == 3 || \count($value[2]) == 4) {
7073                        $color = $value[2];
7074                        array_unshift($color, Type::T_COLOR);
7075
7076                        return $this->coerceColor($color);
7077                    }
7078                }
7079
7080                return null;
7081
7082            case Type::T_KEYWORD:
7083                if (! \is_string($value[1])) {
7084                    return null;
7085                }
7086
7087                $name = strtolower($value[1]);
7088
7089                // hexa color?
7090                if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
7091                    $nofValues = \strlen($m[1]);
7092
7093                    if (\in_array($nofValues, [3, 4, 6, 8])) {
7094                        $nbChannels = 3;
7095                        $color      = [];
7096                        $num        = hexdec($m[1]);
7097
7098                        switch ($nofValues) {
7099                            case 4:
7100                                $nbChannels = 4;
7101                                // then continuing with the case 3:
7102                            case 3:
7103                                for ($i = 0; $i < $nbChannels; $i++) {
7104                                    $t = $num & 0xf;
7105                                    array_unshift($color, $t << 4 | $t);
7106                                    $num >>= 4;
7107                                }
7108
7109                                break;
7110
7111                            case 8:
7112                                $nbChannels = 4;
7113                                // then continuing with the case 6:
7114                            case 6:
7115                                for ($i = 0; $i < $nbChannels; $i++) {
7116                                    array_unshift($color, $num & 0xff);
7117                                    $num >>= 8;
7118                                }
7119
7120                                break;
7121                        }
7122
7123                        if ($nbChannels === 4) {
7124                            if ($color[3] === 255) {
7125                                $color[3] = 1; // fully opaque
7126                            } else {
7127                                $color[3] = round($color[3] / 255, Number::PRECISION);
7128                            }
7129                        }
7130
7131                        array_unshift($color, Type::T_COLOR);
7132
7133                        return $color;
7134                    }
7135                }
7136
7137                if ($rgba = Colors::colorNameToRGBa($name)) {
7138                    return isset($rgba[3])
7139                        ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
7140                        : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
7141                }
7142
7143                return null;
7144        }
7145
7146        return null;
7147    }
7148
7149    /**
7150     * @param integer|Number $value
7151     * @param boolean        $isAlpha
7152     *
7153     * @return integer|mixed
7154     */
7155    protected function compileRGBAValue($value, $isAlpha = false)
7156    {
7157        if ($isAlpha) {
7158            return $this->compileColorPartValue($value, 0, 1, false);
7159        }
7160
7161        return $this->compileColorPartValue($value, 0, 255, true);
7162    }
7163
7164    /**
7165     * @param mixed         $value
7166     * @param integer|float $min
7167     * @param integer|float $max
7168     * @param boolean       $isInt
7169     *
7170     * @return integer|mixed
7171     */
7172    protected function compileColorPartValue($value, $min, $max, $isInt = true)
7173    {
7174        if (! is_numeric($value)) {
7175            if (\is_array($value)) {
7176                $reduced = $this->reduce($value);
7177
7178                if ($reduced instanceof Number) {
7179                    $value = $reduced;
7180                }
7181            }
7182
7183            if ($value instanceof Number) {
7184                if ($value->unitless()) {
7185                    $num = $value->getDimension();
7186                } elseif ($value->hasUnit('%')) {
7187                    $num = $max * $value->getDimension() / 100;
7188                } else {
7189                    throw $this->error('Expected %s to have no units or "%%".', $value);
7190                }
7191
7192                $value = $num;
7193            } elseif (\is_array($value)) {
7194                $value = $this->compileValue($value);
7195            }
7196        }
7197
7198        if (is_numeric($value)) {
7199            if ($isInt) {
7200                $value = round($value);
7201            }
7202
7203            $value = min($max, max($min, $value));
7204
7205            return $value;
7206        }
7207
7208        return $value;
7209    }
7210
7211    /**
7212     * Coerce value to string
7213     *
7214     * @param array|Number $value
7215     *
7216     * @return array
7217     */
7218    protected function coerceString($value)
7219    {
7220        if ($value[0] === Type::T_STRING) {
7221            return $value;
7222        }
7223
7224        return [Type::T_STRING, '', [$this->compileValue($value)]];
7225    }
7226
7227    /**
7228     * Assert value is a string
7229     *
7230     * This method deals with internal implementation details of the value
7231     * representation where unquoted strings can sometimes be stored under
7232     * other types.
7233     * The returned value is always using the T_STRING type.
7234     *
7235     * @api
7236     *
7237     * @param array|Number $value
7238     * @param string|null  $varName
7239     *
7240     * @return array
7241     *
7242     * @throws SassScriptException
7243     */
7244    public function assertString($value, $varName = null)
7245    {
7246        // case of url(...) parsed a a function
7247        if ($value[0] === Type::T_FUNCTION) {
7248            $value = $this->coerceString($value);
7249        }
7250
7251        if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
7252            $value = $this->compileValue($value);
7253            throw SassScriptException::forArgument("$value is not a string.", $varName);
7254        }
7255
7256        return $this->coerceString($value);
7257    }
7258
7259    /**
7260     * Coerce value to a percentage
7261     *
7262     * @param array|Number $value
7263     *
7264     * @return integer|float
7265     *
7266     * @deprecated
7267     */
7268    protected function coercePercent($value)
7269    {
7270        @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED);
7271
7272        if ($value instanceof Number) {
7273            if ($value->hasUnit('%')) {
7274                return $value->getDimension() / 100;
7275            }
7276
7277            return $value->getDimension();
7278        }
7279
7280        return 0;
7281    }
7282
7283    /**
7284     * Assert value is a map
7285     *
7286     * @api
7287     *
7288     * @param array|Number $value
7289     * @param string|null  $varName
7290     *
7291     * @return array
7292     *
7293     * @throws SassScriptException
7294     */
7295    public function assertMap($value, $varName = null)
7296    {
7297        $value = $this->coerceMap($value);
7298
7299        if ($value[0] !== Type::T_MAP) {
7300            $value = $this->compileValue($value);
7301
7302            throw SassScriptException::forArgument("$value is not a map.", $varName);
7303        }
7304
7305        return $value;
7306    }
7307
7308    /**
7309     * Assert value is a list
7310     *
7311     * @api
7312     *
7313     * @param array|Number $value
7314     *
7315     * @return array
7316     *
7317     * @throws \Exception
7318     */
7319    public function assertList($value)
7320    {
7321        if ($value[0] !== Type::T_LIST) {
7322            throw $this->error('expecting list, %s received', $value[0]);
7323        }
7324
7325        return $value;
7326    }
7327
7328    /**
7329     * Gets the keywords of an argument list.
7330     *
7331     * Keys in the returned array are normalized names (underscores are replaced with dashes)
7332     * without the leading `$`.
7333     * Calling this helper with anything that an argument list received for a rest argument
7334     * of the function argument declaration is not supported.
7335     *
7336     * @param array|Number $value
7337     *
7338     * @return array<string, array|Number>
7339     */
7340    public function getArgumentListKeywords($value)
7341    {
7342        if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
7343            throw new \InvalidArgumentException('The argument is not a sass argument list.');
7344        }
7345
7346        return $value[3];
7347    }
7348
7349    /**
7350     * Assert value is a color
7351     *
7352     * @api
7353     *
7354     * @param array|Number $value
7355     * @param string|null  $varName
7356     *
7357     * @return array
7358     *
7359     * @throws SassScriptException
7360     */
7361    public function assertColor($value, $varName = null)
7362    {
7363        if ($color = $this->coerceColor($value)) {
7364            return $color;
7365        }
7366
7367        $value = $this->compileValue($value);
7368
7369        throw SassScriptException::forArgument("$value is not a color.", $varName);
7370    }
7371
7372    /**
7373     * Assert value is a number
7374     *
7375     * @api
7376     *
7377     * @param array|Number $value
7378     * @param string|null  $varName
7379     *
7380     * @return Number
7381     *
7382     * @throws SassScriptException
7383     */
7384    public function assertNumber($value, $varName = null)
7385    {
7386        if (!$value instanceof Number) {
7387            $value = $this->compileValue($value);
7388            throw SassScriptException::forArgument("$value is not a number.", $varName);
7389        }
7390
7391        return $value;
7392    }
7393
7394    /**
7395     * Assert value is a integer
7396     *
7397     * @api
7398     *
7399     * @param array|Number $value
7400     * @param string|null  $varName
7401     *
7402     * @return integer
7403     *
7404     * @throws SassScriptException
7405     */
7406    public function assertInteger($value, $varName = null)
7407    {
7408        $value = $this->assertNumber($value, $varName)->getDimension();
7409        if (round($value - \intval($value), Number::PRECISION) > 0) {
7410            throw SassScriptException::forArgument("$value is not an integer.", $varName);
7411        }
7412
7413        return intval($value);
7414    }
7415
7416    /**
7417     * Extract the  ... / alpha on the last argument of channel arg
7418     * in color functions
7419     *
7420     * @param array $args
7421     * @return array
7422     */
7423    private function extractSlashAlphaInColorFunction($args)
7424    {
7425        $last = end($args);
7426        if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') {
7427            array_pop($args);
7428            $args[] = $last[2];
7429            $args[] = $last[3];
7430        }
7431        return $args;
7432    }
7433
7434
7435    /**
7436     * Make sure a color's components don't go out of bounds
7437     *
7438     * @param array $c
7439     *
7440     * @return array
7441     */
7442    protected function fixColor($c)
7443    {
7444        foreach ([1, 2, 3] as $i) {
7445            if ($c[$i] < 0) {
7446                $c[$i] = 0;
7447            }
7448
7449            if ($c[$i] > 255) {
7450                $c[$i] = 255;
7451            }
7452
7453            if (!\is_int($c[$i])) {
7454                $c[$i] = round($c[$i]);
7455            }
7456        }
7457
7458        return $c;
7459    }
7460
7461    /**
7462     * Convert RGB to HSL
7463     *
7464     * @internal
7465     *
7466     * @param integer $red
7467     * @param integer $green
7468     * @param integer $blue
7469     *
7470     * @return array
7471     */
7472    public function toHSL($red, $green, $blue)
7473    {
7474        $min = min($red, $green, $blue);
7475        $max = max($red, $green, $blue);
7476
7477        $l = $min + $max;
7478        $d = $max - $min;
7479
7480        if ((int) $d === 0) {
7481            $h = $s = 0;
7482        } else {
7483            if ($l < 255) {
7484                $s = $d / $l;
7485            } else {
7486                $s = $d / (510 - $l);
7487            }
7488
7489            if ($red == $max) {
7490                $h = 60 * ($green - $blue) / $d;
7491            } elseif ($green == $max) {
7492                $h = 60 * ($blue - $red) / $d + 120;
7493            } elseif ($blue == $max) {
7494                $h = 60 * ($red - $green) / $d + 240;
7495            }
7496        }
7497
7498        return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1];
7499    }
7500
7501    /**
7502     * Hue to RGB helper
7503     *
7504     * @param float $m1
7505     * @param float $m2
7506     * @param float $h
7507     *
7508     * @return float
7509     */
7510    protected function hueToRGB($m1, $m2, $h)
7511    {
7512        if ($h < 0) {
7513            $h += 1;
7514        } elseif ($h > 1) {
7515            $h -= 1;
7516        }
7517
7518        if ($h * 6 < 1) {
7519            return $m1 + ($m2 - $m1) * $h * 6;
7520        }
7521
7522        if ($h * 2 < 1) {
7523            return $m2;
7524        }
7525
7526        if ($h * 3 < 2) {
7527            return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
7528        }
7529
7530        return $m1;
7531    }
7532
7533    /**
7534     * Convert HSL to RGB
7535     *
7536     * @internal
7537     *
7538     * @param int|float $hue        H from 0 to 360
7539     * @param int|float $saturation S from 0 to 100
7540     * @param int|float $lightness  L from 0 to 100
7541     *
7542     * @return array
7543     */
7544    public function toRGB($hue, $saturation, $lightness)
7545    {
7546        if ($hue < 0) {
7547            $hue += 360;
7548        }
7549
7550        $h = $hue / 360;
7551        $s = min(100, max(0, $saturation)) / 100;
7552        $l = min(100, max(0, $lightness)) / 100;
7553
7554        $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
7555        $m1 = $l * 2 - $m2;
7556
7557        $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
7558        $g = $this->hueToRGB($m1, $m2, $h) * 255;
7559        $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
7560
7561        $out = [Type::T_COLOR, $r, $g, $b];
7562
7563        return $out;
7564    }
7565
7566    /**
7567     * Convert HWB to RGB
7568     * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
7569     *
7570     * @api
7571     *
7572     * @param integer $hue        H from 0 to 360
7573     * @param integer $whiteness  W from 0 to 100
7574     * @param integer $blackness  B from 0 to 100
7575     *
7576     * @return array
7577     */
7578    private function HWBtoRGB($hue, $whiteness, $blackness)
7579    {
7580        $w = min(100, max(0, $whiteness)) / 100;
7581        $b = min(100, max(0, $blackness)) / 100;
7582
7583        $sum = $w + $b;
7584        if ($sum > 1.0) {
7585            $w = $w / $sum;
7586            $b = $b / $sum;
7587        }
7588        $b = min(1.0 - $w, $b);
7589
7590        $rgb = $this->toRGB($hue, 100, 50);
7591        for($i = 1; $i < 4; $i++) {
7592          $rgb[$i] *= (1.0 - $w - $b);
7593          $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001);
7594        }
7595
7596        return $rgb;
7597    }
7598
7599    /**
7600     * Convert RGB to HWB
7601     *
7602     * @api
7603     *
7604     * @param integer $red
7605     * @param integer $green
7606     * @param integer $blue
7607     *
7608     * @return array
7609     */
7610    private function RGBtoHWB($red, $green, $blue)
7611    {
7612        $min = min($red, $green, $blue);
7613        $max = max($red, $green, $blue);
7614
7615        $d = $max - $min;
7616
7617        if ((int) $d === 0) {
7618            $h = 0;
7619        } else {
7620
7621            if ($red == $max) {
7622                $h = 60 * ($green - $blue) / $d;
7623            } elseif ($green == $max) {
7624                $h = 60 * ($blue - $red) / $d + 120;
7625            } elseif ($blue == $max) {
7626                $h = 60 * ($red - $green) / $d + 240;
7627            }
7628        }
7629
7630        return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100];
7631    }
7632
7633
7634    // Built in functions
7635
7636    protected static $libCall = ['function', 'args...'];
7637    protected function libCall($args)
7638    {
7639        $functionReference = $args[0];
7640
7641        if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
7642            $name = $this->compileStringContent($this->coerceString($functionReference));
7643            $warning = "Passing a string to call() is deprecated and will be illegal\n"
7644                . "in Sass 4.0. Use call(function-reference($name)) instead.";
7645            Warn::deprecation($warning);
7646            $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]);
7647        }
7648
7649        if ($functionReference === static::$null) {
7650            return static::$null;
7651        }
7652
7653        if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
7654            throw $this->error('Function reference expected, got ' . $functionReference[0]);
7655        }
7656
7657        $callArgs = [
7658            [null, $args[1], true]
7659        ];
7660
7661        return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
7662    }
7663
7664
7665    protected static $libGetFunction = [
7666        ['name'],
7667        ['name', 'css']
7668    ];
7669    protected function libGetFunction($args)
7670    {
7671        $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
7672        $isCss = false;
7673
7674        if (count($args)) {
7675            $isCss = array_shift($args);
7676            $isCss = (($isCss === static::$true) ? true : false);
7677        }
7678
7679        if ($isCss) {
7680            return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
7681        }
7682
7683        return $this->getFunctionReference($name, true);
7684    }
7685
7686    protected static $libIf = ['condition', 'if-true', 'if-false:'];
7687    protected function libIf($args)
7688    {
7689        list($cond, $t, $f) = $args;
7690
7691        if (! $this->isTruthy($this->reduce($cond, true))) {
7692            return $this->reduce($f, true);
7693        }
7694
7695        return $this->reduce($t, true);
7696    }
7697
7698    protected static $libIndex = ['list', 'value'];
7699    protected function libIndex($args)
7700    {
7701        list($list, $value) = $args;
7702
7703        if (
7704            $list[0] === Type::T_MAP ||
7705            $list[0] === Type::T_STRING ||
7706            $list[0] === Type::T_KEYWORD ||
7707            $list[0] === Type::T_INTERPOLATE
7708        ) {
7709            $list = $this->coerceList($list, ' ');
7710        }
7711
7712        if ($list[0] !== Type::T_LIST) {
7713            return static::$null;
7714        }
7715
7716        // Numbers are represented with value objects, for which the PHP equality operator does not
7717        // match the Sass rules (and we cannot overload it). As they are the only type of values
7718        // represented with a value object for now, they require a special case.
7719        if ($value instanceof Number) {
7720            $key = 0;
7721            foreach ($list[2] as $item) {
7722                $key++;
7723                $itemValue = $this->normalizeValue($item);
7724
7725                if ($itemValue instanceof Number && $value->equals($itemValue)) {
7726                    return new Number($key, '');
7727                }
7728            }
7729            return static::$null;
7730        }
7731
7732        $values = [];
7733
7734
7735        foreach ($list[2] as $item) {
7736            $values[] = $this->normalizeValue($item);
7737        }
7738
7739        $key = array_search($this->normalizeValue($value), $values);
7740
7741        return false === $key ? static::$null : new Number($key + 1, '');
7742    }
7743
7744    protected static $libRgb = [
7745        ['color'],
7746        ['color', 'alpha'],
7747        ['channels'],
7748        ['red', 'green', 'blue'],
7749        ['red', 'green', 'blue', 'alpha'] ];
7750    protected function libRgb($args, $kwargs, $funcName = 'rgb')
7751    {
7752        switch (\count($args)) {
7753            case 1:
7754                if (! $color = $this->coerceColor($args[0], true)) {
7755                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
7756                }
7757                break;
7758
7759            case 3:
7760                $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
7761
7762                if (! $color = $this->coerceColor($color)) {
7763                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
7764                }
7765
7766                return $color;
7767
7768            case 2:
7769                if ($color = $this->coerceColor($args[0], true)) {
7770                    $alpha = $this->compileRGBAValue($args[1], true);
7771
7772                    if (is_numeric($alpha)) {
7773                        $color[4] = $alpha;
7774                    } else {
7775                        $color = [Type::T_STRING, '',
7776                            [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
7777                    }
7778                } else {
7779                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']];
7780                }
7781                break;
7782
7783            case 4:
7784            default:
7785                $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
7786
7787                if (! $color = $this->coerceColor($color)) {
7788                    $color = [Type::T_STRING, '',
7789                        [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
7790                }
7791                break;
7792        }
7793
7794        return $color;
7795    }
7796
7797    protected static $libRgba = [
7798        ['color'],
7799        ['color', 'alpha'],
7800        ['channels'],
7801        ['red', 'green', 'blue'],
7802        ['red', 'green', 'blue', 'alpha'] ];
7803    protected function libRgba($args, $kwargs)
7804    {
7805        return $this->libRgb($args, $kwargs, 'rgba');
7806    }
7807
7808    /**
7809     * Helper function for adjust_color, change_color, and scale_color
7810     *
7811     * @param array<array|Number> $args
7812     * @param string $operation
7813     * @param callable $fn
7814     *
7815     * @return array
7816     *
7817     * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn
7818     */
7819    protected function alterColor(array $args, $operation, $fn)
7820    {
7821        $color = $this->assertColor($args[0], 'color');
7822
7823        if ($args[1][2]) {
7824            throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
7825        }
7826
7827        $kwargs = $this->getArgumentListKeywords($args[1]);
7828
7829        $scale = $operation === 'scale';
7830        $change = $operation === 'change';
7831
7832        /**
7833         * @param string $name
7834         * @param float|int $max
7835         * @param bool $checkPercent
7836         * @param bool $assertPercent
7837         *
7838         * @return float|int|null
7839         */
7840        $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) {
7841            if (!isset($kwargs[$name])) {
7842                return null;
7843            }
7844
7845            $number = $this->assertNumber($kwargs[$name], $name);
7846            unset($kwargs[$name]);
7847
7848            if (!$scale && $checkPercent) {
7849                if (!$number->hasUnit('%')) {
7850                    $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated.");
7851                    $this->logger->warn($warning->getMessage(), true);
7852                }
7853            }
7854
7855            if ($scale || $assertPercent) {
7856                $number->assertUnit('%', $name);
7857            }
7858
7859            if ($scale) {
7860                $max = 100;
7861            }
7862
7863            return $number->valueInRange($change ? 0 : -$max, $max, $name);
7864        };
7865
7866        $alpha = $getParam('alpha', 1);
7867        $red = $getParam('red', 255);
7868        $green = $getParam('green', 255);
7869        $blue = $getParam('blue', 255);
7870
7871        if ($scale || !isset($kwargs['hue'])) {
7872            $hue = null;
7873        } else {
7874            $hueNumber = $this->assertNumber($kwargs['hue'], 'hue');
7875            unset($kwargs['hue']);
7876            $hue = $hueNumber->getDimension();
7877        }
7878        $saturation = $getParam('saturation', 100, true);
7879        $lightness = $getParam('lightness', 100, true);
7880        $whiteness = $getParam('whiteness', 100, false, true);
7881        $blackness = $getParam('blackness', 100, false, true);
7882
7883        if (!empty($kwargs)) {
7884            $unknownNames = array_keys($kwargs);
7885            $lastName = array_pop($unknownNames);
7886            $message = sprintf(
7887                'No argument%s named $%s%s.',
7888                $unknownNames ? 's' : '',
7889                $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
7890                $lastName
7891            );
7892            throw new SassScriptException($message);
7893        }
7894
7895        $hasRgb = $red !== null || $green !== null || $blue !== null;
7896        $hasSL = $saturation !== null || $lightness !== null;
7897        $hasWB = $whiteness !== null || $blackness !== null;
7898        $found = false;
7899
7900        if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
7901            throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL'));
7902        }
7903
7904        if ($hasWB && $hasSL) {
7905            throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
7906        }
7907
7908        if ($hasRgb) {
7909            $color[1] = round($fn($color[1], $red, 255));
7910            $color[2] = round($fn($color[2], $green, 255));
7911            $color[3] = round($fn($color[3], $blue, 255));
7912        } elseif ($hasWB) {
7913            $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
7914            if ($hue !== null) {
7915                $hwb[1] = $change ? $hue : $hwb[1] + $hue;
7916            }
7917            $hwb[2] = $fn($hwb[2], $whiteness, 100);
7918            $hwb[3] = $fn($hwb[3], $blackness, 100);
7919
7920            $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]);
7921
7922            if (isset($color[4])) {
7923                $rgb[4] = $color[4];
7924            }
7925
7926            $color = $rgb;
7927        } elseif ($hue !== null || $hasSL) {
7928            $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7929
7930            if ($hue !== null) {
7931                $hsl[1] = $change ? $hue : $hsl[1] + $hue;
7932            }
7933            $hsl[2] = $fn($hsl[2], $saturation, 100);
7934            $hsl[3] = $fn($hsl[3], $lightness, 100);
7935
7936            $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
7937
7938            if (isset($color[4])) {
7939                $rgb[4] = $color[4];
7940            }
7941
7942            $color = $rgb;
7943        }
7944
7945        if ($alpha !== null) {
7946            $existingAlpha = isset($color[4]) ? $color[4] : 1;
7947            $color[4] = $fn($existingAlpha, $alpha, 1);
7948        }
7949
7950        return $color;
7951    }
7952
7953    protected static $libAdjustColor = ['color', 'kwargs...'];
7954    protected function libAdjustColor($args)
7955    {
7956        return $this->alterColor($args, 'adjust', function ($base, $alter, $max) {
7957            if ($alter === null) {
7958                return $base;
7959            }
7960
7961            $new = $base + $alter;
7962
7963            if ($new < 0) {
7964                return 0;
7965            }
7966
7967            if ($new > $max) {
7968                return $max;
7969            }
7970
7971            return $new;
7972        });
7973    }
7974
7975    protected static $libChangeColor = ['color', 'kwargs...'];
7976    protected function libChangeColor($args)
7977    {
7978        return $this->alterColor($args,'change', function ($base, $alter, $max) {
7979            if ($alter === null) {
7980                return $base;
7981            }
7982
7983            return $alter;
7984        });
7985    }
7986
7987    protected static $libScaleColor = ['color', 'kwargs...'];
7988    protected function libScaleColor($args)
7989    {
7990        return $this->alterColor($args, 'scale', function ($base, $scale, $max) {
7991            if ($scale === null) {
7992                return $base;
7993            }
7994
7995            $scale = $scale / 100;
7996
7997            if ($scale < 0) {
7998                return $base * $scale + $base;
7999            }
8000
8001            return ($max - $base) * $scale + $base;
8002        });
8003    }
8004
8005    protected static $libIeHexStr = ['color'];
8006    protected function libIeHexStr($args)
8007    {
8008        $color = $this->coerceColor($args[0]);
8009
8010        if (\is_null($color)) {
8011            throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color');
8012        }
8013
8014        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
8015
8016        return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
8017    }
8018
8019    protected static $libRed = ['color'];
8020    protected function libRed($args)
8021    {
8022        $color = $this->coerceColor($args[0]);
8023
8024        if (\is_null($color)) {
8025            throw $this->error('Error: argument `$color` of `red($color)` must be a color');
8026        }
8027
8028        return new Number((int) $color[1], '');
8029    }
8030
8031    protected static $libGreen = ['color'];
8032    protected function libGreen($args)
8033    {
8034        $color = $this->coerceColor($args[0]);
8035
8036        if (\is_null($color)) {
8037            throw $this->error('Error: argument `$color` of `green($color)` must be a color');
8038        }
8039
8040        return new Number((int) $color[2], '');
8041    }
8042
8043    protected static $libBlue = ['color'];
8044    protected function libBlue($args)
8045    {
8046        $color = $this->coerceColor($args[0]);
8047
8048        if (\is_null($color)) {
8049            throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
8050        }
8051
8052        return new Number((int) $color[3], '');
8053    }
8054
8055    protected static $libAlpha = ['color'];
8056    protected function libAlpha($args)
8057    {
8058        if ($color = $this->coerceColor($args[0])) {
8059            return new Number(isset($color[4]) ? $color[4] : 1, '');
8060        }
8061
8062        // this might be the IE function, so return value unchanged
8063        return null;
8064    }
8065
8066    protected static $libOpacity = ['color'];
8067    protected function libOpacity($args)
8068    {
8069        $value = $args[0];
8070
8071        if ($value instanceof Number) {
8072            return null;
8073        }
8074
8075        return $this->libAlpha($args);
8076    }
8077
8078    // mix two colors
8079    protected static $libMix = [
8080        ['color1', 'color2', 'weight:50%'],
8081        ['color-1', 'color-2', 'weight:50%']
8082        ];
8083    protected function libMix($args)
8084    {
8085        list($first, $second, $weight) = $args;
8086
8087        $first = $this->assertColor($first, 'color1');
8088        $second = $this->assertColor($second, 'color2');
8089        $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100;
8090
8091        $firstAlpha = isset($first[4]) ? $first[4] : 1;
8092        $secondAlpha = isset($second[4]) ? $second[4] : 1;
8093
8094        $normalizedWeight = $weightScale * 2 - 1;
8095        $alphaDistance = $firstAlpha - $secondAlpha;
8096
8097        $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance);
8098        $weight1 = ($combinedWeight + 1) / 2.0;
8099        $weight2 = 1.0 - $weight1;
8100
8101        $new = [Type::T_COLOR,
8102            $weight1 * $first[1] + $weight2 * $second[1],
8103            $weight1 * $first[2] + $weight2 * $second[2],
8104            $weight1 * $first[3] + $weight2 * $second[3],
8105        ];
8106
8107        if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
8108            $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale);
8109        }
8110
8111        return $this->fixColor($new);
8112    }
8113
8114    protected static $libHsl = [
8115        ['channels'],
8116        ['hue', 'saturation'],
8117        ['hue', 'saturation', 'lightness'],
8118        ['hue', 'saturation', 'lightness', 'alpha'] ];
8119    protected function libHsl($args, $kwargs, $funcName = 'hsl')
8120    {
8121        $args_to_check = $args;
8122
8123        if (\count($args) == 1) {
8124            if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
8125                return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
8126            }
8127
8128            $args = $args[0][2];
8129            $args_to_check = $kwargs['channels'][2];
8130        }
8131
8132        if (\count($args) === 2) {
8133            // if var() is used as an argument, return as a css function
8134            foreach ($args as $arg) {
8135                if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) {
8136                    return null;
8137                }
8138            }
8139
8140            throw new SassScriptException('Missing argument $lightness.');
8141        }
8142
8143        foreach ($kwargs as $k => $arg) {
8144            if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8145                return null;
8146            }
8147        }
8148
8149        foreach ($args_to_check as $k => $arg) {
8150            if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8151                if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8152                    return null;
8153                }
8154
8155                $args[$k] = $this->stringifyFncallArgs($arg);
8156            }
8157
8158            if (
8159                $k >= 2 && count($args) === 4 &&
8160                in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
8161                in_array($arg[1], ['calc','env'])
8162            ) {
8163                return null;
8164            }
8165        }
8166
8167        $hue = $this->reduce($args[0]);
8168        $saturation = $this->reduce($args[1]);
8169        $lightness = $this->reduce($args[2]);
8170        $alpha = null;
8171
8172        if (\count($args) === 4) {
8173            $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
8174
8175            if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
8176                return [Type::T_STRING, '',
8177                    [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
8178            }
8179        } else {
8180            if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
8181                return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
8182            }
8183        }
8184
8185        $hueValue = fmod($hue->getDimension(), 360);
8186
8187        while ($hueValue < 0) {
8188            $hueValue += 360;
8189        }
8190
8191        $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
8192
8193        if (! \is_null($alpha)) {
8194            $color[4] = $alpha;
8195        }
8196
8197        return $color;
8198    }
8199
8200    protected static $libHsla = [
8201            ['channels'],
8202            ['hue', 'saturation'],
8203            ['hue', 'saturation', 'lightness'],
8204            ['hue', 'saturation', 'lightness', 'alpha']];
8205    protected function libHsla($args, $kwargs)
8206    {
8207        return $this->libHsl($args, $kwargs, 'hsla');
8208    }
8209
8210    protected static $libHue = ['color'];
8211    protected function libHue($args)
8212    {
8213        $color = $this->assertColor($args[0], 'color');
8214        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8215
8216        return new Number($hsl[1], 'deg');
8217    }
8218
8219    protected static $libSaturation = ['color'];
8220    protected function libSaturation($args)
8221    {
8222        $color = $this->assertColor($args[0], 'color');
8223        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8224
8225        return new Number($hsl[2], '%');
8226    }
8227
8228    protected static $libLightness = ['color'];
8229    protected function libLightness($args)
8230    {
8231        $color = $this->assertColor($args[0], 'color');
8232        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8233
8234        return new Number($hsl[3], '%');
8235    }
8236
8237    /*
8238     * Todo : a integrer dans le futur module color
8239    protected static $libHwb = [
8240        ['channels'],
8241        ['hue', 'whiteness', 'blackness'],
8242        ['hue', 'whiteness', 'blackness', 'alpha'] ];
8243    protected function libHwb($args, $kwargs, $funcName = 'hwb')
8244    {
8245        $args_to_check = $args;
8246
8247        if (\count($args) == 1) {
8248            if ($args[0][0] !== Type::T_LIST) {
8249                throw $this->error("Missing elements \$whiteness and \$blackness");
8250            }
8251
8252            if (\trim($args[0][1])) {
8253                throw $this->error("\$channels must be a space-separated list.");
8254            }
8255
8256            if (! empty($args[0]['enclosing'])) {
8257                throw $this->error("\$channels must be an unbracketed list.");
8258            }
8259
8260            $args = $args[0][2];
8261            if (\count($args) > 3) {
8262                throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed");
8263            }
8264
8265            $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]);
8266            if (\count($args_to_check) !== \count($kwargs['channels'][2])) {
8267                $args = $args_to_check;
8268            }
8269        }
8270
8271        if (\count($args_to_check) < 2) {
8272            throw $this->error("Missing elements \$whiteness and \$blackness");
8273        }
8274        if (\count($args_to_check) < 3) {
8275            throw $this->error("Missing element \$blackness");
8276        }
8277        if (\count($args_to_check) > 4) {
8278            throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed");
8279        }
8280
8281        foreach ($kwargs as $k => $arg) {
8282            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8283                return null;
8284            }
8285        }
8286
8287        foreach ($args_to_check as $k => $arg) {
8288            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8289                if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8290                    return null;
8291                }
8292
8293                $args[$k] = $this->stringifyFncallArgs($arg);
8294            }
8295
8296            if (
8297                $k >= 2 && count($args) === 4 &&
8298                in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
8299                in_array($arg[1], ['calc','env'])
8300            ) {
8301                return null;
8302            }
8303        }
8304
8305        $hue = $this->reduce($args[0]);
8306        $whiteness = $this->reduce($args[1]);
8307        $blackness = $this->reduce($args[2]);
8308        $alpha = null;
8309
8310        if (\count($args) === 4) {
8311            $alpha = $this->compileColorPartValue($args[3], 0, 1, false);
8312
8313            if (! \is_numeric($alpha)) {
8314                $val = $this->compileValue($args[3]);
8315                throw $this->error("\$alpha: $val is not a number");
8316            }
8317        }
8318
8319        $this->assertNumber($hue, 'hue');
8320        $this->assertUnit($whiteness, ['%'], 'whiteness');
8321        $this->assertUnit($blackness, ['%'], 'blackness');
8322
8323        $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness");
8324        $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness");
8325
8326        $w = $whiteness->getDimension();
8327        $b = $blackness->getDimension();
8328
8329        $hueValue = $hue->getDimension() % 360;
8330
8331        while ($hueValue < 0) {
8332            $hueValue += 360;
8333        }
8334
8335        $color = $this->HWBtoRGB($hueValue, $w, $b);
8336
8337        if (! \is_null($alpha)) {
8338            $color[4] = $alpha;
8339        }
8340
8341        return $color;
8342    }
8343
8344    protected static $libWhiteness = ['color'];
8345    protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') {
8346
8347        $color = $this->assertColor($args[0]);
8348        $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8349
8350        return new Number($hwb[2], '%');
8351    }
8352
8353    protected static $libBlackness = ['color'];
8354    protected function libBlackness($args, $kwargs, $funcName = 'blackness') {
8355
8356        $color = $this->assertColor($args[0]);
8357        $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8358
8359        return new Number($hwb[3], '%');
8360    }
8361    */
8362
8363    protected function adjustHsl($color, $idx, $amount)
8364    {
8365        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8366        $hsl[$idx] += $amount;
8367
8368        if ($idx !== 1) {
8369            // Clamp the saturation and lightness
8370            $hsl[$idx] = min(max(0, $hsl[$idx]), 100);
8371        }
8372
8373        $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
8374
8375        if (isset($color[4])) {
8376            $out[4] = $color[4];
8377        }
8378
8379        return $out;
8380    }
8381
8382    protected static $libAdjustHue = ['color', 'degrees'];
8383    protected function libAdjustHue($args)
8384    {
8385        $color = $this->assertColor($args[0], 'color');
8386        $degrees = $this->assertNumber($args[1], 'degrees')->getDimension();
8387
8388        return $this->adjustHsl($color, 1, $degrees);
8389    }
8390
8391    protected static $libLighten = ['color', 'amount'];
8392    protected function libLighten($args)
8393    {
8394        $color = $this->assertColor($args[0], 'color');
8395        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8396
8397        return $this->adjustHsl($color, 3, $amount);
8398    }
8399
8400    protected static $libDarken = ['color', 'amount'];
8401    protected function libDarken($args)
8402    {
8403        $color = $this->assertColor($args[0], 'color');
8404        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8405
8406        return $this->adjustHsl($color, 3, -$amount);
8407    }
8408
8409    protected static $libSaturate = [['color', 'amount'], ['amount']];
8410    protected function libSaturate($args)
8411    {
8412        $value = $args[0];
8413
8414        if (count($args) === 1) {
8415            $this->assertNumber($args[0], 'amount');
8416
8417            return null;
8418        }
8419
8420        $color = $this->assertColor($args[0], 'color');
8421        $amount = $this->assertNumber($args[1], 'amount');
8422
8423        return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount'));
8424    }
8425
8426    protected static $libDesaturate = ['color', 'amount'];
8427    protected function libDesaturate($args)
8428    {
8429        $color = $this->assertColor($args[0], 'color');
8430        $amount = $this->assertNumber($args[1], 'amount');
8431
8432        return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount'));
8433    }
8434
8435    protected static $libGrayscale = ['color'];
8436    protected function libGrayscale($args)
8437    {
8438        $value = $args[0];
8439
8440        if ($value instanceof Number) {
8441            return null;
8442        }
8443
8444        return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100);
8445    }
8446
8447    protected static $libComplement = ['color'];
8448    protected function libComplement($args)
8449    {
8450        return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180);
8451    }
8452
8453    protected static $libInvert = ['color', 'weight:100%'];
8454    protected function libInvert($args)
8455    {
8456        $value = $args[0];
8457
8458        $weight = $this->assertNumber($args[1], 'weight');
8459
8460        if ($value instanceof Number) {
8461            if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) {
8462                throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.');
8463            }
8464
8465            return null;
8466        }
8467
8468        $color = $this->assertColor($value, 'color');
8469        $inverted = $color;
8470        $inverted[1] = 255 - $inverted[1];
8471        $inverted[2] = 255 - $inverted[2];
8472        $inverted[3] = 255 - $inverted[3];
8473
8474        return $this->libMix([$inverted, $color, $weight]);
8475    }
8476
8477    // increases opacity by amount
8478    protected static $libOpacify = ['color', 'amount'];
8479    protected function libOpacify($args)
8480    {
8481        $color = $this->assertColor($args[0], 'color');
8482        $amount = $this->assertNumber($args[1], 'amount');
8483
8484        $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount');
8485        $color[4] = min(1, max(0, $color[4]));
8486
8487        return $color;
8488    }
8489
8490    protected static $libFadeIn = ['color', 'amount'];
8491    protected function libFadeIn($args)
8492    {
8493        return $this->libOpacify($args);
8494    }
8495
8496    // decreases opacity by amount
8497    protected static $libTransparentize = ['color', 'amount'];
8498    protected function libTransparentize($args)
8499    {
8500        $color = $this->assertColor($args[0], 'color');
8501        $amount = $this->assertNumber($args[1], 'amount');
8502
8503        $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount');
8504        $color[4] = min(1, max(0, $color[4]));
8505
8506        return $color;
8507    }
8508
8509    protected static $libFadeOut = ['color', 'amount'];
8510    protected function libFadeOut($args)
8511    {
8512        return $this->libTransparentize($args);
8513    }
8514
8515    protected static $libUnquote = ['string'];
8516    protected function libUnquote($args)
8517    {
8518        try {
8519            $str = $this->assertString($args[0], 'string');
8520        } catch (SassScriptException $e) {
8521            $value = $this->compileValue($args[0]);
8522            $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
8523            $line  = $this->sourceLine;
8524
8525            $message = "Passing $value, a non-string value, to unquote()
8526will be an error in future versions of Sass.\n         on line $line of $fname";
8527
8528            $this->logger->warn($message, true);
8529
8530            return $args[0];
8531        }
8532
8533        $str[1] = '';
8534
8535        return $str;
8536    }
8537
8538    protected static $libQuote = ['string'];
8539    protected function libQuote($args)
8540    {
8541        $value = $this->assertString($args[0], 'string');
8542
8543        $value[1] = '"';
8544
8545        return $value;
8546    }
8547
8548    protected static $libPercentage = ['number'];
8549    protected function libPercentage($args)
8550    {
8551        $num = $this->assertNumber($args[0], 'number');
8552        $num->assertNoUnits('number');
8553
8554        return new Number($num->getDimension() * 100, '%');
8555    }
8556
8557    protected static $libRound = ['number'];
8558    protected function libRound($args)
8559    {
8560        $num = $this->assertNumber($args[0], 'number');
8561
8562        return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8563    }
8564
8565    protected static $libFloor = ['number'];
8566    protected function libFloor($args)
8567    {
8568        $num = $this->assertNumber($args[0], 'number');
8569
8570        return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8571    }
8572
8573    protected static $libCeil = ['number'];
8574    protected function libCeil($args)
8575    {
8576        $num = $this->assertNumber($args[0], 'number');
8577
8578        return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8579    }
8580
8581    protected static $libAbs = ['number'];
8582    protected function libAbs($args)
8583    {
8584        $num = $this->assertNumber($args[0], 'number');
8585
8586        return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8587    }
8588
8589    protected static $libMin = ['numbers...'];
8590    protected function libMin($args)
8591    {
8592        /**
8593         * @var Number|null
8594         */
8595        $min = null;
8596
8597        foreach ($args[0][2] as $arg) {
8598            $number = $this->assertNumber($arg);
8599
8600            if (\is_null($min) || $min->greaterThan($number)) {
8601                $min = $number;
8602            }
8603        }
8604
8605        if (!\is_null($min)) {
8606            return $min;
8607        }
8608
8609        throw $this->error('At least one argument must be passed.');
8610    }
8611
8612    protected static $libMax = ['numbers...'];
8613    protected function libMax($args)
8614    {
8615        /**
8616         * @var Number|null
8617         */
8618        $max = null;
8619
8620        foreach ($args[0][2] as $arg) {
8621            $number = $this->assertNumber($arg);
8622
8623            if (\is_null($max) || $max->lessThan($number)) {
8624                $max = $number;
8625            }
8626        }
8627
8628        if (!\is_null($max)) {
8629            return $max;
8630        }
8631
8632        throw $this->error('At least one argument must be passed.');
8633    }
8634
8635    protected static $libLength = ['list'];
8636    protected function libLength($args)
8637    {
8638        $list = $this->coerceList($args[0], ',', true);
8639
8640        return new Number(\count($list[2]), '');
8641    }
8642
8643    protected static $libListSeparator = ['list'];
8644    protected function libListSeparator($args)
8645    {
8646        if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
8647            return [Type::T_KEYWORD, 'space'];
8648        }
8649
8650        $list = $this->coerceList($args[0]);
8651
8652        if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
8653            return [Type::T_KEYWORD, 'space'];
8654        }
8655
8656        if ($list[1] === ',') {
8657            return [Type::T_KEYWORD, 'comma'];
8658        }
8659
8660        return [Type::T_KEYWORD, 'space'];
8661    }
8662
8663    protected static $libNth = ['list', 'n'];
8664    protected function libNth($args)
8665    {
8666        $list = $this->coerceList($args[0], ',', false);
8667        $n = $this->assertNumber($args[1])->getDimension();
8668
8669        if ($n > 0) {
8670            $n--;
8671        } elseif ($n < 0) {
8672            $n += \count($list[2]);
8673        }
8674
8675        return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
8676    }
8677
8678    protected static $libSetNth = ['list', 'n', 'value'];
8679    protected function libSetNth($args)
8680    {
8681        $list = $this->coerceList($args[0]);
8682        $n = $this->assertNumber($args[1])->getDimension();
8683
8684        if ($n > 0) {
8685            $n--;
8686        } elseif ($n < 0) {
8687            $n += \count($list[2]);
8688        }
8689
8690        if (! isset($list[2][$n])) {
8691            throw $this->error('Invalid argument for "n"');
8692        }
8693
8694        $list[2][$n] = $args[2];
8695
8696        return $list;
8697    }
8698
8699    protected static $libMapGet = ['map', 'key'];
8700    protected function libMapGet($args)
8701    {
8702        $map = $this->assertMap($args[0], 'map');
8703        $key = $args[1];
8704
8705        if (! \is_null($key)) {
8706            $key = $this->compileStringContent($this->coerceString($key));
8707
8708            for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8709                if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
8710                    return $map[2][$i];
8711                }
8712            }
8713        }
8714
8715        return static::$null;
8716    }
8717
8718    protected static $libMapKeys = ['map'];
8719    protected function libMapKeys($args)
8720    {
8721        $map = $this->assertMap($args[0], 'map');
8722        $keys = $map[1];
8723
8724        return [Type::T_LIST, ',', $keys];
8725    }
8726
8727    protected static $libMapValues = ['map'];
8728    protected function libMapValues($args)
8729    {
8730        $map = $this->assertMap($args[0], 'map');
8731        $values = $map[2];
8732
8733        return [Type::T_LIST, ',', $values];
8734    }
8735
8736    protected static $libMapRemove = [
8737        ['map'],
8738        ['map', 'key', 'keys...'],
8739    ];
8740    protected function libMapRemove($args)
8741    {
8742        $map = $this->assertMap($args[0], 'map');
8743
8744        if (\count($args) === 1) {
8745            return $map;
8746        }
8747
8748        $keys = [];
8749        $keys[] = $this->compileStringContent($this->coerceString($args[1]));
8750
8751        foreach ($args[2][2] as $key) {
8752            $keys[] = $this->compileStringContent($this->coerceString($key));
8753        }
8754
8755        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8756            if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
8757                array_splice($map[1], $i, 1);
8758                array_splice($map[2], $i, 1);
8759            }
8760        }
8761
8762        return $map;
8763    }
8764
8765    protected static $libMapHasKey = ['map', 'key'];
8766    protected function libMapHasKey($args)
8767    {
8768        $map = $this->assertMap($args[0], 'map');
8769
8770        return $this->toBool($this->mapHasKey($map, $args[1]));
8771    }
8772
8773    /**
8774     * @param array|Number $keyValue
8775     *
8776     * @return bool
8777     */
8778    private function mapHasKey(array $map, $keyValue)
8779    {
8780        $key = $this->compileStringContent($this->coerceString($keyValue));
8781
8782        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8783            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
8784                return true;
8785            }
8786        }
8787
8788        return false;
8789    }
8790
8791    protected static $libMapMerge = [
8792        ['map1', 'map2'],
8793        ['map-1', 'map-2']
8794    ];
8795    protected function libMapMerge($args)
8796    {
8797        $map1 = $this->assertMap($args[0], 'map1');
8798        $map2 = $this->assertMap($args[1], 'map2');
8799
8800        foreach ($map2[1] as $i2 => $key2) {
8801            $key = $this->compileStringContent($this->coerceString($key2));
8802
8803            foreach ($map1[1] as $i1 => $key1) {
8804                if ($key === $this->compileStringContent($this->coerceString($key1))) {
8805                    $map1[2][$i1] = $map2[2][$i2];
8806                    continue 2;
8807                }
8808            }
8809
8810            $map1[1][] = $map2[1][$i2];
8811            $map1[2][] = $map2[2][$i2];
8812        }
8813
8814        return $map1;
8815    }
8816
8817    protected static $libKeywords = ['args'];
8818    protected function libKeywords($args)
8819    {
8820        $value = $args[0];
8821
8822        if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
8823            $compiledValue = $this->compileValue($value);
8824
8825            throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args');
8826        }
8827
8828        $keys = [];
8829        $values = [];
8830
8831        foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
8832            $keys[] = [Type::T_KEYWORD, $name];
8833            $values[] = $arg;
8834        }
8835
8836        return [Type::T_MAP, $keys, $values];
8837    }
8838
8839    protected static $libIsBracketed = ['list'];
8840    protected function libIsBracketed($args)
8841    {
8842        $list = $args[0];
8843        $this->coerceList($list, ' ');
8844
8845        if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
8846            return self::$true;
8847        }
8848
8849        return self::$false;
8850    }
8851
8852    /**
8853     * @param array $list1
8854     * @param array|Number|null $sep
8855     *
8856     * @return string
8857     * @throws CompilerException
8858     */
8859    protected function listSeparatorForJoin($list1, $sep)
8860    {
8861        if (! isset($sep)) {
8862            return $list1[1];
8863        }
8864
8865        switch ($this->compileValue($sep)) {
8866            case 'comma':
8867                return ',';
8868
8869            case 'space':
8870                return ' ';
8871
8872            default:
8873                return $list1[1];
8874        }
8875    }
8876
8877    protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
8878    protected function libJoin($args)
8879    {
8880        list($list1, $list2, $sep, $bracketed) = $args;
8881
8882        $list1 = $this->coerceList($list1, ' ', true);
8883        $list2 = $this->coerceList($list2, ' ', true);
8884        $sep   = $this->listSeparatorForJoin($list1, $sep);
8885
8886        if ($bracketed === static::$true) {
8887            $bracketed = true;
8888        } elseif ($bracketed === static::$false) {
8889            $bracketed = false;
8890        } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
8891            $bracketed = 'auto';
8892        } elseif ($bracketed === static::$null) {
8893            $bracketed = false;
8894        } else {
8895            $bracketed = $this->compileValue($bracketed);
8896            $bracketed = ! ! $bracketed;
8897
8898            if ($bracketed === true) {
8899                $bracketed = true;
8900            }
8901        }
8902
8903        if ($bracketed === 'auto') {
8904            $bracketed = false;
8905
8906            if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
8907                $bracketed = true;
8908            }
8909        }
8910
8911        $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
8912
8913        if (isset($list1['enclosing'])) {
8914            $res['enlcosing'] = $list1['enclosing'];
8915        }
8916
8917        if ($bracketed) {
8918            $res['enclosing'] = 'bracket';
8919        }
8920
8921        return $res;
8922    }
8923
8924    protected static $libAppend = ['list', 'val', 'separator:null'];
8925    protected function libAppend($args)
8926    {
8927        list($list1, $value, $sep) = $args;
8928
8929        $list1 = $this->coerceList($list1, ' ', true);
8930        $sep   = $this->listSeparatorForJoin($list1, $sep);
8931        $res   = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
8932
8933        if (isset($list1['enclosing'])) {
8934            $res['enclosing'] = $list1['enclosing'];
8935        }
8936
8937        return $res;
8938    }
8939
8940    protected static $libZip = ['lists...'];
8941    protected function libZip($args)
8942    {
8943        $argLists = [];
8944        foreach ($args[0][2] as $arg) {
8945            $argLists[] = $this->coerceList($arg);
8946        }
8947
8948        $lists = [];
8949        $firstList = array_shift($argLists);
8950
8951        $result = [Type::T_LIST, ',', $lists];
8952        if (! \is_null($firstList)) {
8953            foreach ($firstList[2] as $key => $item) {
8954                $list = [Type::T_LIST, '', [$item]];
8955
8956                foreach ($argLists as $arg) {
8957                    if (isset($arg[2][$key])) {
8958                        $list[2][] = $arg[2][$key];
8959                    } else {
8960                        break 2;
8961                    }
8962                }
8963
8964                $lists[] = $list;
8965            }
8966
8967            $result[2] = $lists;
8968        } else {
8969            $result['enclosing'] = 'parent';
8970        }
8971
8972        return $result;
8973    }
8974
8975    protected static $libTypeOf = ['value'];
8976    protected function libTypeOf($args)
8977    {
8978        $value = $args[0];
8979
8980        return [Type::T_KEYWORD, $this->getTypeOf($value)];
8981    }
8982
8983    /**
8984     * @param array|Number $value
8985     *
8986     * @return string
8987     */
8988    private function getTypeOf($value)
8989    {
8990        switch ($value[0]) {
8991            case Type::T_KEYWORD:
8992                if ($value === static::$true || $value === static::$false) {
8993                    return 'bool';
8994                }
8995
8996                if ($this->coerceColor($value)) {
8997                    return 'color';
8998                }
8999
9000                // fall-thru
9001            case Type::T_FUNCTION:
9002                return 'string';
9003
9004            case Type::T_FUNCTION_REFERENCE:
9005                return 'function';
9006
9007            case Type::T_LIST:
9008                if (isset($value[3]) && \is_array($value[3])) {
9009                    return 'arglist';
9010                }
9011
9012                // fall-thru
9013            default:
9014                return $value[0];
9015        }
9016    }
9017
9018    protected static $libUnit = ['number'];
9019    protected function libUnit($args)
9020    {
9021        $num = $this->assertNumber($args[0], 'number');
9022
9023        return [Type::T_STRING, '"', [$num->unitStr()]];
9024    }
9025
9026    protected static $libUnitless = ['number'];
9027    protected function libUnitless($args)
9028    {
9029        $value = $this->assertNumber($args[0], 'number');
9030
9031        return $this->toBool($value->unitless());
9032    }
9033
9034    protected static $libComparable = [
9035        ['number1', 'number2'],
9036        ['number-1', 'number-2']
9037    ];
9038    protected function libComparable($args)
9039    {
9040        list($number1, $number2) = $args;
9041
9042        if (
9043            ! $number1 instanceof Number ||
9044            ! $number2 instanceof Number
9045        ) {
9046            throw $this->error('Invalid argument(s) for "comparable"');
9047        }
9048
9049        return $this->toBool($number1->isComparableTo($number2));
9050    }
9051
9052    protected static $libStrIndex = ['string', 'substring'];
9053    protected function libStrIndex($args)
9054    {
9055        $string = $this->assertString($args[0], 'string');
9056        $stringContent = $this->compileStringContent($string);
9057
9058        $substring = $this->assertString($args[1], 'substring');
9059        $substringContent = $this->compileStringContent($substring);
9060
9061        if (! \strlen($substringContent)) {
9062            $result = 0;
9063        } else {
9064            $result = Util::mbStrpos($stringContent, $substringContent);
9065        }
9066
9067        return $result === false ? static::$null : new Number($result + 1, '');
9068    }
9069
9070    protected static $libStrInsert = ['string', 'insert', 'index'];
9071    protected function libStrInsert($args)
9072    {
9073        $string = $this->assertString($args[0], 'string');
9074        $stringContent = $this->compileStringContent($string);
9075
9076        $insert = $this->assertString($args[1], 'insert');
9077        $insertContent = $this->compileStringContent($insert);
9078
9079        $index = $this->assertInteger($args[2], 'index');
9080        if ($index > 0) {
9081            $index = $index - 1;
9082        }
9083        if ($index < 0) {
9084            $index = Util::mbStrlen($stringContent) + 1 + $index;
9085        }
9086
9087        $string[2] = [
9088            Util::mbSubstr($stringContent, 0, $index),
9089            $insertContent,
9090            Util::mbSubstr($stringContent, $index)
9091        ];
9092
9093        return $string;
9094    }
9095
9096    protected static $libStrLength = ['string'];
9097    protected function libStrLength($args)
9098    {
9099        $string = $this->assertString($args[0], 'string');
9100        $stringContent = $this->compileStringContent($string);
9101
9102        return new Number(Util::mbStrlen($stringContent), '');
9103    }
9104
9105    protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
9106    protected function libStrSlice($args)
9107    {
9108        $string = $this->assertString($args[0], 'string');
9109        $stringContent = $this->compileStringContent($string);
9110
9111        $start = $this->assertNumber($args[1], 'start-at');
9112        $start->assertNoUnits('start-at');
9113        $startInt = $this->assertInteger($start, 'start-at');
9114        $end = $this->assertNumber($args[2], 'end-at');
9115        $end->assertNoUnits('end-at');
9116        $endInt = $this->assertInteger($end, 'end-at');
9117
9118        if ($endInt === 0) {
9119            return [Type::T_STRING, $string[1], []];
9120        }
9121
9122        if ($startInt > 0) {
9123            $startInt--;
9124        }
9125
9126        if ($endInt < 0) {
9127            $endInt = Util::mbStrlen($stringContent) + $endInt;
9128        } else {
9129            $endInt--;
9130        }
9131
9132        if ($endInt < $startInt) {
9133            return [Type::T_STRING, $string[1], []];
9134        }
9135
9136        $length = $endInt - $startInt + 1; // The end of the slice is inclusive
9137
9138        $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)];
9139
9140        return $string;
9141    }
9142
9143    protected static $libToLowerCase = ['string'];
9144    protected function libToLowerCase($args)
9145    {
9146        $string = $this->assertString($args[0], 'string');
9147        $stringContent = $this->compileStringContent($string);
9148
9149        $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
9150
9151        return $string;
9152    }
9153
9154    protected static $libToUpperCase = ['string'];
9155    protected function libToUpperCase($args)
9156    {
9157        $string = $this->assertString($args[0], 'string');
9158        $stringContent = $this->compileStringContent($string);
9159
9160        $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
9161
9162        return $string;
9163    }
9164
9165    /**
9166     * Apply a filter on a string content, only on ascii chars
9167     * let extended chars untouched
9168     *
9169     * @param string $stringContent
9170     * @param callable $filter
9171     * @return string
9172     */
9173    protected function stringTransformAsciiOnly($stringContent, $filter)
9174    {
9175        $mblength = Util::mbStrlen($stringContent);
9176        if ($mblength === strlen($stringContent)) {
9177            return $filter($stringContent);
9178        }
9179        $filteredString = "";
9180        for ($i = 0; $i < $mblength; $i++) {
9181            $char = Util::mbSubstr($stringContent, $i, 1);
9182            if (strlen($char) > 1) {
9183                $filteredString .= $char;
9184            } else {
9185                $filteredString .= $filter($char);
9186            }
9187        }
9188
9189        return $filteredString;
9190    }
9191
9192    protected static $libFeatureExists = ['feature'];
9193    protected function libFeatureExists($args)
9194    {
9195        $string = $this->assertString($args[0], 'feature');
9196        $name = $this->compileStringContent($string);
9197
9198        return $this->toBool(
9199            \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
9200        );
9201    }
9202
9203    protected static $libFunctionExists = ['name'];
9204    protected function libFunctionExists($args)
9205    {
9206        $string = $this->assertString($args[0], 'name');
9207        $name = $this->compileStringContent($string);
9208
9209        // user defined functions
9210        if ($this->has(static::$namespaces['function'] . $name)) {
9211            return self::$true;
9212        }
9213
9214        $name = $this->normalizeName($name);
9215
9216        if (isset($this->userFunctions[$name])) {
9217            return self::$true;
9218        }
9219
9220        // built-in functions
9221        $f = $this->getBuiltinFunction($name);
9222
9223        return $this->toBool(\is_callable($f));
9224    }
9225
9226    protected static $libGlobalVariableExists = ['name'];
9227    protected function libGlobalVariableExists($args)
9228    {
9229        $string = $this->assertString($args[0], 'name');
9230        $name = $this->compileStringContent($string);
9231
9232        return $this->toBool($this->has($name, $this->rootEnv));
9233    }
9234
9235    protected static $libMixinExists = ['name'];
9236    protected function libMixinExists($args)
9237    {
9238        $string = $this->assertString($args[0], 'name');
9239        $name = $this->compileStringContent($string);
9240
9241        return $this->toBool($this->has(static::$namespaces['mixin'] . $name));
9242    }
9243
9244    protected static $libVariableExists = ['name'];
9245    protected function libVariableExists($args)
9246    {
9247        $string = $this->assertString($args[0], 'name');
9248        $name = $this->compileStringContent($string);
9249
9250        return $this->toBool($this->has($name));
9251    }
9252
9253    protected static $libCounter = ['args...'];
9254    /**
9255     * Workaround IE7's content counter bug.
9256     *
9257     * @param array $args
9258     *
9259     * @return array
9260     */
9261    protected function libCounter($args)
9262    {
9263        $list = array_map([$this, 'compileValue'], $args[0][2]);
9264
9265        return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
9266    }
9267
9268    protected static $libRandom = ['limit:null'];
9269    protected function libRandom($args)
9270    {
9271        if (isset($args[0]) && $args[0] !== static::$null) {
9272            $n = $this->assertInteger($args[0], 'limit');
9273
9274            if ($n < 1) {
9275                throw new SassScriptException("\$limit: Must be greater than 0, was $n.");
9276            }
9277
9278            return new Number(mt_rand(1, $n), '');
9279        }
9280
9281        $max = mt_getrandmax();
9282        return new Number(mt_rand(0, $max - 1) / $max, '');
9283    }
9284
9285    protected static $libUniqueId = [];
9286    protected function libUniqueId()
9287    {
9288        static $id;
9289
9290        if (! isset($id)) {
9291            $id = PHP_INT_SIZE === 4
9292                ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
9293                : mt_rand(0, pow(36, 8));
9294        }
9295
9296        $id += mt_rand(0, 10) + 1;
9297
9298        return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
9299    }
9300
9301    /**
9302     * @param array|Number $value
9303     * @param bool         $force_enclosing_display
9304     *
9305     * @return array
9306     */
9307    protected function inspectFormatValue($value, $force_enclosing_display = false)
9308    {
9309        if ($value === static::$null) {
9310            $value = [Type::T_KEYWORD, 'null'];
9311        }
9312
9313        $stringValue = [$value];
9314
9315        if ($value instanceof Number) {
9316            return [Type::T_STRING, '', $stringValue];
9317        }
9318
9319        if ($value[0] === Type::T_LIST) {
9320            if (end($value[2]) === static::$null) {
9321                array_pop($value[2]);
9322                $value[2][] = [Type::T_STRING, '', ['']];
9323                $force_enclosing_display = true;
9324            }
9325
9326            if (
9327                ! empty($value['enclosing']) &&
9328                ($force_enclosing_display ||
9329                    ($value['enclosing'] === 'bracket') ||
9330                    ! \count($value[2]))
9331            ) {
9332                $value['enclosing'] = 'forced_' . $value['enclosing'];
9333                $force_enclosing_display = true;
9334            }
9335
9336            foreach ($value[2] as $k => $listelement) {
9337                $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
9338            }
9339
9340            $stringValue = [$value];
9341        }
9342
9343        return [Type::T_STRING, '', $stringValue];
9344    }
9345
9346    protected static $libInspect = ['value'];
9347    protected function libInspect($args)
9348    {
9349        $value = $args[0];
9350
9351        return $this->inspectFormatValue($value);
9352    }
9353
9354    /**
9355     * Preprocess selector args
9356     *
9357     * @param array       $arg
9358     * @param string|null $varname
9359     * @param bool        $allowParent
9360     *
9361     * @return array
9362     */
9363    protected function getSelectorArg($arg, $varname = null, $allowParent = false)
9364    {
9365        static $parser = null;
9366
9367        if (\is_null($parser)) {
9368            $parser = $this->parserFactory(__METHOD__);
9369        }
9370
9371        if (! $this->checkSelectorArgType($arg)) {
9372            $var_value = $this->compileValue($arg);
9373            throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname);
9374        }
9375
9376
9377        if ($arg[0] === Type::T_STRING) {
9378            $arg[1] = '';
9379        }
9380        $arg = $this->compileValue($arg);
9381
9382        $parsedSelector = [];
9383
9384        if ($parser->parseSelector($arg, $parsedSelector, true)) {
9385            $selector = $this->evalSelectors($parsedSelector);
9386            $gluedSelector = $this->glueFunctionSelectors($selector);
9387
9388            if (! $allowParent) {
9389                foreach ($gluedSelector as $selector) {
9390                    foreach ($selector as $s) {
9391                        if (in_array(static::$selfSelector, $s)) {
9392                            throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname);
9393                        }
9394                    }
9395                }
9396            }
9397
9398            return $gluedSelector;
9399        }
9400
9401        throw SassScriptException::forArgument("expected more input, invalid selector.", $varname);
9402    }
9403
9404    /**
9405     * Check variable type for getSelectorArg() function
9406     * @param array $arg
9407     * @param int $maxDepth
9408     * @return bool
9409     */
9410    protected function checkSelectorArgType($arg, $maxDepth = 2)
9411    {
9412        if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
9413            foreach ($arg[2] as $elt) {
9414                if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
9415                    return false;
9416                }
9417            }
9418            return true;
9419        }
9420        if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
9421            return false;
9422        }
9423        return true;
9424    }
9425
9426    /**
9427     * Postprocess selector to output in right format
9428     *
9429     * @param array $selectors
9430     *
9431     * @return array
9432     */
9433    protected function formatOutputSelector($selectors)
9434    {
9435        $selectors = $this->collapseSelectorsAsList($selectors);
9436
9437        return $selectors;
9438    }
9439
9440    protected static $libIsSuperselector = ['super', 'sub'];
9441    protected function libIsSuperselector($args)
9442    {
9443        list($super, $sub) = $args;
9444
9445        $super = $this->getSelectorArg($super, 'super');
9446        $sub = $this->getSelectorArg($sub, 'sub');
9447
9448        return $this->toBool($this->isSuperSelector($super, $sub));
9449    }
9450
9451    /**
9452     * Test a $super selector again $sub
9453     *
9454     * @param array $super
9455     * @param array $sub
9456     *
9457     * @return boolean
9458     */
9459    protected function isSuperSelector($super, $sub)
9460    {
9461        // one and only one selector for each arg
9462        if (! $super) {
9463            throw $this->error('Invalid super selector for isSuperSelector()');
9464        }
9465
9466        if (! $sub) {
9467            throw $this->error('Invalid sub selector for isSuperSelector()');
9468        }
9469
9470        if (count($sub) > 1) {
9471            foreach ($sub as $s) {
9472                if (! $this->isSuperSelector($super, [$s])) {
9473                    return false;
9474                }
9475            }
9476            return true;
9477        }
9478
9479        if (count($super) > 1) {
9480            foreach ($super as $s) {
9481                if ($this->isSuperSelector([$s], $sub)) {
9482                    return true;
9483                }
9484            }
9485            return false;
9486        }
9487
9488        $super = reset($super);
9489        $sub = reset($sub);
9490
9491        $i = 0;
9492        $nextMustMatch = false;
9493
9494        foreach ($super as $node) {
9495            $compound = '';
9496
9497            array_walk_recursive(
9498                $node,
9499                function ($value, $key) use (&$compound) {
9500                    $compound .= $value;
9501                }
9502            );
9503
9504            if ($this->isImmediateRelationshipCombinator($compound)) {
9505                if ($node !== $sub[$i]) {
9506                    return false;
9507                }
9508
9509                $nextMustMatch = true;
9510                $i++;
9511            } else {
9512                while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
9513                    if ($nextMustMatch) {
9514                        return false;
9515                    }
9516
9517                    $i++;
9518                }
9519
9520                if ($i >= \count($sub)) {
9521                    return false;
9522                }
9523
9524                $nextMustMatch = false;
9525                $i++;
9526            }
9527        }
9528
9529        return true;
9530    }
9531
9532    /**
9533     * Test a part of super selector again a part of sub selector
9534     *
9535     * @param array $superParts
9536     * @param array $subParts
9537     *
9538     * @return boolean
9539     */
9540    protected function isSuperPart($superParts, $subParts)
9541    {
9542        $i = 0;
9543
9544        foreach ($superParts as $superPart) {
9545            while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
9546                $i++;
9547            }
9548
9549            if ($i >= \count($subParts)) {
9550                return false;
9551            }
9552
9553            $i++;
9554        }
9555
9556        return true;
9557    }
9558
9559    protected static $libSelectorAppend = ['selector...'];
9560    protected function libSelectorAppend($args)
9561    {
9562        // get the selector... list
9563        $args = reset($args);
9564        $args = $args[2];
9565
9566        if (\count($args) < 1) {
9567            throw $this->error('selector-append() needs at least 1 argument');
9568        }
9569
9570        $selectors = [];
9571        foreach ($args as $arg) {
9572            $selectors[] = $this->getSelectorArg($arg, 'selector');
9573        }
9574
9575        return $this->formatOutputSelector($this->selectorAppend($selectors));
9576    }
9577
9578    /**
9579     * Append parts of the last selector in the list to the previous, recursively
9580     *
9581     * @param array $selectors
9582     *
9583     * @return array
9584     *
9585     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
9586     */
9587    protected function selectorAppend($selectors)
9588    {
9589        $lastSelectors = array_pop($selectors);
9590
9591        if (! $lastSelectors) {
9592            throw $this->error('Invalid selector list in selector-append()');
9593        }
9594
9595        while (\count($selectors)) {
9596            $previousSelectors = array_pop($selectors);
9597
9598            if (! $previousSelectors) {
9599                throw $this->error('Invalid selector list in selector-append()');
9600            }
9601
9602            // do the trick, happening $lastSelector to $previousSelector
9603            $appended = [];
9604
9605            foreach ($lastSelectors as $lastSelector) {
9606                $previous = $previousSelectors;
9607
9608                foreach ($lastSelector as $lastSelectorParts) {
9609                    foreach ($lastSelectorParts as $lastSelectorPart) {
9610                        foreach ($previous as $i => $previousSelector) {
9611                            foreach ($previousSelector as $j => $previousSelectorParts) {
9612                                $previous[$i][$j][] = $lastSelectorPart;
9613                            }
9614                        }
9615                    }
9616                }
9617
9618                foreach ($previous as $ps) {
9619                    $appended[] = $ps;
9620                }
9621            }
9622
9623            $lastSelectors = $appended;
9624        }
9625
9626        return $lastSelectors;
9627    }
9628
9629    protected static $libSelectorExtend = [
9630        ['selector', 'extendee', 'extender'],
9631        ['selectors', 'extendee', 'extender']
9632    ];
9633    protected function libSelectorExtend($args)
9634    {
9635        list($selectors, $extendee, $extender) = $args;
9636
9637        $selectors = $this->getSelectorArg($selectors, 'selector');
9638        $extendee  = $this->getSelectorArg($extendee, 'extendee');
9639        $extender  = $this->getSelectorArg($extender, 'extender');
9640
9641        if (! $selectors || ! $extendee || ! $extender) {
9642            throw $this->error('selector-extend() invalid arguments');
9643        }
9644
9645        $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
9646
9647        return $this->formatOutputSelector($extended);
9648    }
9649
9650    protected static $libSelectorReplace = [
9651        ['selector', 'original', 'replacement'],
9652        ['selectors', 'original', 'replacement']
9653    ];
9654    protected function libSelectorReplace($args)
9655    {
9656        list($selectors, $original, $replacement) = $args;
9657
9658        $selectors   = $this->getSelectorArg($selectors, 'selector');
9659        $original    = $this->getSelectorArg($original, 'original');
9660        $replacement = $this->getSelectorArg($replacement, 'replacement');
9661
9662        if (! $selectors || ! $original || ! $replacement) {
9663            throw $this->error('selector-replace() invalid arguments');
9664        }
9665
9666        $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
9667
9668        return $this->formatOutputSelector($replaced);
9669    }
9670
9671    /**
9672     * Extend/replace in selectors
9673     * used by selector-extend and selector-replace that use the same logic
9674     *
9675     * @param array   $selectors
9676     * @param array   $extendee
9677     * @param array   $extender
9678     * @param boolean $replace
9679     *
9680     * @return array
9681     */
9682    protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
9683    {
9684        $saveExtends = $this->extends;
9685        $saveExtendsMap = $this->extendsMap;
9686
9687        $this->extends = [];
9688        $this->extendsMap = [];
9689
9690        foreach ($extendee as $es) {
9691            if (\count($es) !== 1) {
9692                throw $this->error('Can\'t extend complex selector.');
9693            }
9694
9695            // only use the first one
9696            $this->pushExtends(reset($es), $extender, null);
9697        }
9698
9699        $extended = [];
9700
9701        foreach ($selectors as $selector) {
9702            if (! $replace) {
9703                $extended[] = $selector;
9704            }
9705
9706            $n = \count($extended);
9707
9708            $this->matchExtends($selector, $extended);
9709
9710            // if didnt match, keep the original selector if we are in a replace operation
9711            if ($replace && \count($extended) === $n) {
9712                $extended[] = $selector;
9713            }
9714        }
9715
9716        $this->extends = $saveExtends;
9717        $this->extendsMap = $saveExtendsMap;
9718
9719        return $extended;
9720    }
9721
9722    protected static $libSelectorNest = ['selector...'];
9723    protected function libSelectorNest($args)
9724    {
9725        // get the selector... list
9726        $args = reset($args);
9727        $args = $args[2];
9728
9729        if (\count($args) < 1) {
9730            throw $this->error('selector-nest() needs at least 1 argument');
9731        }
9732
9733        $selectorsMap = [];
9734        foreach ($args as $arg) {
9735            $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
9736        }
9737
9738        $envs = [];
9739
9740        foreach ($selectorsMap as $selectors) {
9741            $env = new Environment();
9742            $env->selectors = $selectors;
9743
9744            $envs[] = $env;
9745        }
9746
9747        $envs            = array_reverse($envs);
9748        $env             = $this->extractEnv($envs);
9749        $outputSelectors = $this->multiplySelectors($env);
9750
9751        return $this->formatOutputSelector($outputSelectors);
9752    }
9753
9754    protected static $libSelectorParse = [
9755        ['selector'],
9756        ['selectors']
9757    ];
9758    protected function libSelectorParse($args)
9759    {
9760        $selectors = reset($args);
9761        $selectors = $this->getSelectorArg($selectors, 'selector');
9762
9763        return $this->formatOutputSelector($selectors);
9764    }
9765
9766    protected static $libSelectorUnify = ['selectors1', 'selectors2'];
9767    protected function libSelectorUnify($args)
9768    {
9769        list($selectors1, $selectors2) = $args;
9770
9771        $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
9772        $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
9773
9774        if (! $selectors1 || ! $selectors2) {
9775            throw $this->error('selector-unify() invalid arguments');
9776        }
9777
9778        // only consider the first compound of each
9779        $compound1 = reset($selectors1);
9780        $compound2 = reset($selectors2);
9781
9782        // unify them and that's it
9783        $unified = $this->unifyCompoundSelectors($compound1, $compound2);
9784
9785        return $this->formatOutputSelector($unified);
9786    }
9787
9788    /**
9789     * The selector-unify magic as its best
9790     * (at least works as expected on test cases)
9791     *
9792     * @param array $compound1
9793     * @param array $compound2
9794     *
9795     * @return array
9796     */
9797    protected function unifyCompoundSelectors($compound1, $compound2)
9798    {
9799        if (! \count($compound1)) {
9800            return $compound2;
9801        }
9802
9803        if (! \count($compound2)) {
9804            return $compound1;
9805        }
9806
9807        // check that last part are compatible
9808        $lastPart1 = array_pop($compound1);
9809        $lastPart2 = array_pop($compound2);
9810        $last      = $this->mergeParts($lastPart1, $lastPart2);
9811
9812        if (! $last) {
9813            return [[]];
9814        }
9815
9816        $unifiedCompound = [$last];
9817        $unifiedSelectors = [$unifiedCompound];
9818
9819        // do the rest
9820        while (\count($compound1) || \count($compound2)) {
9821            $part1 = end($compound1);
9822            $part2 = end($compound2);
9823
9824            if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
9825                list($compound2, $part2, $after2) = $match2;
9826
9827                if ($after2) {
9828                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
9829                }
9830
9831                $c = $this->mergeParts($part1, $part2);
9832                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
9833
9834                $part1 = $part2 = null;
9835
9836                array_pop($compound1);
9837            }
9838
9839            if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
9840                list($compound1, $part1, $after1) = $match1;
9841
9842                if ($after1) {
9843                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
9844                }
9845
9846                $c = $this->mergeParts($part2, $part1);
9847                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
9848
9849                $part1 = $part2 = null;
9850
9851                array_pop($compound2);
9852            }
9853
9854            $new = [];
9855
9856            if ($part1 && $part2) {
9857                array_pop($compound1);
9858                array_pop($compound2);
9859
9860                $s   = $this->prependSelectors($unifiedSelectors, [$part2]);
9861                $new = array_merge($new, $this->prependSelectors($s, [$part1]));
9862                $s   = $this->prependSelectors($unifiedSelectors, [$part1]);
9863                $new = array_merge($new, $this->prependSelectors($s, [$part2]));
9864            } elseif ($part1) {
9865                array_pop($compound1);
9866
9867                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
9868            } elseif ($part2) {
9869                array_pop($compound2);
9870
9871                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
9872            }
9873
9874            if ($new) {
9875                $unifiedSelectors = $new;
9876            }
9877        }
9878
9879        return $unifiedSelectors;
9880    }
9881
9882    /**
9883     * Prepend each selector from $selectors with $parts
9884     *
9885     * @param array $selectors
9886     * @param array $parts
9887     *
9888     * @return array
9889     */
9890    protected function prependSelectors($selectors, $parts)
9891    {
9892        $new = [];
9893
9894        foreach ($selectors as $compoundSelector) {
9895            array_unshift($compoundSelector, $parts);
9896
9897            $new[] = $compoundSelector;
9898        }
9899
9900        return $new;
9901    }
9902
9903    /**
9904     * Try to find a matching part in a compound:
9905     * - with same html tag name
9906     * - with some class or id or something in common
9907     *
9908     * @param array $part
9909     * @param array $compound
9910     *
9911     * @return array|false
9912     */
9913    protected function matchPartInCompound($part, $compound)
9914    {
9915        $partTag = $this->findTagName($part);
9916        $before  = $compound;
9917        $after   = [];
9918
9919        // try to find a match by tag name first
9920        while (\count($before)) {
9921            $p = array_pop($before);
9922
9923            if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
9924                return [$before, $p, $after];
9925            }
9926
9927            $after[] = $p;
9928        }
9929
9930        // try again matching a non empty intersection and a compatible tagname
9931        $before = $compound;
9932        $after = [];
9933
9934        while (\count($before)) {
9935            $p = array_pop($before);
9936
9937            if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
9938                if (\count(array_intersect($part, $p))) {
9939                    return [$before, $p, $after];
9940                }
9941            }
9942
9943            $after[] = $p;
9944        }
9945
9946        return false;
9947    }
9948
9949    /**
9950     * Merge two part list taking care that
9951     * - the html tag is coming first - if any
9952     * - the :something are coming last
9953     *
9954     * @param array $parts1
9955     * @param array $parts2
9956     *
9957     * @return array
9958     */
9959    protected function mergeParts($parts1, $parts2)
9960    {
9961        $tag1 = $this->findTagName($parts1);
9962        $tag2 = $this->findTagName($parts2);
9963        $tag  = $this->checkCompatibleTags($tag1, $tag2);
9964
9965        // not compatible tags
9966        if ($tag === false) {
9967            return [];
9968        }
9969
9970        if ($tag) {
9971            if ($tag1) {
9972                $parts1 = array_diff($parts1, [$tag1]);
9973            }
9974
9975            if ($tag2) {
9976                $parts2 = array_diff($parts2, [$tag2]);
9977            }
9978        }
9979
9980        $mergedParts = array_merge($parts1, $parts2);
9981        $mergedOrderedParts = [];
9982
9983        foreach ($mergedParts as $part) {
9984            if (strpos($part, ':') === 0) {
9985                $mergedOrderedParts[] = $part;
9986            }
9987        }
9988
9989        $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
9990        $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
9991
9992        if ($tag) {
9993            array_unshift($mergedParts, $tag);
9994        }
9995
9996        return $mergedParts;
9997    }
9998
9999    /**
10000     * Check the compatibility between two tag names:
10001     * if both are defined they should be identical or one has to be '*'
10002     *
10003     * @param string $tag1
10004     * @param string $tag2
10005     *
10006     * @return array|false
10007     */
10008    protected function checkCompatibleTags($tag1, $tag2)
10009    {
10010        $tags = [$tag1, $tag2];
10011        $tags = array_unique($tags);
10012        $tags = array_filter($tags);
10013
10014        if (\count($tags) > 1) {
10015            $tags = array_diff($tags, ['*']);
10016        }
10017
10018        // not compatible nodes
10019        if (\count($tags) > 1) {
10020            return false;
10021        }
10022
10023        return $tags;
10024    }
10025
10026    /**
10027     * Find the html tag name in a selector parts list
10028     *
10029     * @param string[] $parts
10030     *
10031     * @return string
10032     */
10033    protected function findTagName($parts)
10034    {
10035        foreach ($parts as $part) {
10036            if (! preg_match('/^[\[.:#%_-]/', $part)) {
10037                return $part;
10038            }
10039        }
10040
10041        return '';
10042    }
10043
10044    protected static $libSimpleSelectors = ['selector'];
10045    protected function libSimpleSelectors($args)
10046    {
10047        $selector = reset($args);
10048        $selector = $this->getSelectorArg($selector, 'selector');
10049
10050        // remove selectors list layer, keeping the first one
10051        $selector = reset($selector);
10052
10053        // remove parts list layer, keeping the first part
10054        $part = reset($selector);
10055
10056        $listParts = [];
10057
10058        foreach ($part as $p) {
10059            $listParts[] = [Type::T_STRING, '', [$p]];
10060        }
10061
10062        return [Type::T_LIST, ',', $listParts];
10063    }
10064
10065    protected static $libScssphpGlob = ['pattern'];
10066    protected function libScssphpGlob($args)
10067    {
10068        @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED);
10069
10070        $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true);
10071
10072        $string = $this->assertString($args[0], 'pattern');
10073        $pattern = $this->compileStringContent($string);
10074        $matches = glob($pattern);
10075        $listParts = [];
10076
10077        foreach ($matches as $match) {
10078            if (! is_file($match)) {
10079                continue;
10080            }
10081
10082            $listParts[] = [Type::T_STRING, '"', [$match]];
10083        }
10084
10085        return [Type::T_LIST, ',', $listParts];
10086    }
10087}
10088