Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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