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