Merge branch 'MDL-76525-MOODLE_400_STABLE' of https://github.com/PhMemmel/moodle...
[moodle.git] / lib / scssphp / Compiler.php
blobb6ef02727a81c7f9368e4a4b7aa3de0dd23ecdd8
1 <?php
3 /**
4 * SCSSPHP
6 * @copyright 2012-2020 Leaf Corcoran
8 * @license http://opensource.org/licenses/MIT MIT
10 * @link http://scssphp.github.io/scssphp
13 namespace ScssPhp\ScssPhp;
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;
42 /**
43 * The scss compiler and parser.
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.
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.
54 * The `compile` function of `Compiler` is the entry point.
56 * In summary:
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.
63 * The `Parser` class is only concerned with parsing its input.
65 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
66 * handling things like indentation.
69 /**
70 * SCSS compiler
72 * @author Leaf Corcoran <leafot@gmail.com>
74 * @final Extending the Compiler is deprecated
76 class Compiler
78 /**
79 * @deprecated
81 const LINE_COMMENTS = 1;
82 /**
83 * @deprecated
85 const DEBUG_INFO = 2;
87 /**
88 * @deprecated
90 const WITH_RULE = 1;
91 /**
92 * @deprecated
94 const WITH_MEDIA = 2;
95 /**
96 * @deprecated
98 const WITH_SUPPORTS = 4;
99 /**
100 * @deprecated
102 const WITH_ALL = 7;
104 const SOURCE_MAP_NONE = 0;
105 const SOURCE_MAP_INLINE = 1;
106 const SOURCE_MAP_FILE = 2;
109 * @var array<string, string>
111 protected static $operatorNames = [
112 '+' => 'add',
113 '-' => 'sub',
114 '*' => 'mul',
115 '/' => 'div',
116 '%' => 'mod',
118 '==' => 'eq',
119 '!=' => 'neq',
120 '<' => 'lt',
121 '>' => 'gt',
123 '<=' => 'lte',
124 '>=' => 'gte',
128 * @var array<string, string>
130 protected static $namespaces = [
131 'special' => '%',
132 'mixin' => '@',
133 'function' => '^',
136 public static $true = [Type::T_KEYWORD, 'true'];
137 public static $false = [Type::T_KEYWORD, 'false'];
138 /** @deprecated */
139 public static $NaN = [Type::T_KEYWORD, 'NaN'];
140 /** @deprecated */
141 public static $Infinity = [Type::T_KEYWORD, 'Infinity'];
142 public static $null = [Type::T_NULL];
143 public static $nullString = [Type::T_STRING, '', []];
144 public static $defaultValue = [Type::T_KEYWORD, ''];
145 public static $selfSelector = [Type::T_SELF];
146 public static $emptyList = [Type::T_LIST, '', []];
147 public static $emptyMap = [Type::T_MAP, [], []];
148 public static $emptyString = [Type::T_STRING, '"', []];
149 public static $with = [Type::T_KEYWORD, 'with'];
150 public static $without = [Type::T_KEYWORD, 'without'];
151 private static $emptyArgumentList = [Type::T_LIST, '', [], []];
154 * @var array<int, string|callable>
156 protected $importPaths = [];
158 * @var array<string, Block>
160 protected $importCache = [];
163 * @var string[]
165 protected $importedFiles = [];
168 * @var array
169 * @phpstan-var array<string, array{0: callable, 1: array|null}>
171 protected $userFunctions = [];
173 * @var array<string, mixed>
175 protected $registeredVars = [];
177 * @var array<string, bool>
179 protected $registeredFeatures = [
180 'extend-selector-pseudoclass' => false,
181 'at-error' => true,
182 'units-level-3' => true,
183 'global-variable-shadowing' => false,
187 * @var string|null
189 protected $encoding = null;
191 * @var null
192 * @deprecated
194 protected $lineNumberStyle = null;
197 * @var int|SourceMapGenerator
198 * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
200 protected $sourceMap = self::SOURCE_MAP_NONE;
203 * @var array
204 * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
206 protected $sourceMapOptions = [];
209 * @var bool
211 private $charset = true;
214 * @var string|\ScssPhp\ScssPhp\Formatter
216 protected $formatter = Expanded::class;
219 * @var Environment
221 protected $rootEnv;
223 * @var OutputBlock|null
225 protected $rootBlock;
228 * @var \ScssPhp\ScssPhp\Compiler\Environment
230 protected $env;
232 * @var OutputBlock|null
234 protected $scope;
236 * @var Environment|null
238 protected $storeEnv;
240 * @var bool|null
242 * @deprecated
244 protected $charsetSeen;
246 * @var array<int, string|null>
248 protected $sourceNames;
251 * @var Cache|null
253 protected $cache;
256 * @var bool
258 protected $cacheCheckImportResolutions = false;
261 * @var int
263 protected $indentLevel;
265 * @var array[]
267 protected $extends;
269 * @var array<string, int[]>
271 protected $extendsMap;
274 * @var array<string, int>
276 protected $parsedFiles = [];
279 * @var Parser|null
281 protected $parser;
283 * @var int|null
285 protected $sourceIndex;
287 * @var int|null
289 protected $sourceLine;
291 * @var int|null
293 protected $sourceColumn;
295 * @var bool|null
297 protected $shouldEvaluate;
299 * @var null
300 * @deprecated
302 protected $ignoreErrors;
304 * @var bool
306 protected $ignoreCallStackMessage = false;
309 * @var array[]
311 protected $callStack = [];
314 * @var array
315 * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
317 private $resolvedImports = [];
320 * The directory of the currently processed file
322 * @var string|null
324 private $currentDirectory;
327 * The directory of the input file
329 * @var string
331 private $rootDirectory;
334 * @var bool
336 private $legacyCwdImportPath = true;
339 * @var LoggerInterface
341 private $logger;
344 * @var array<string, bool>
346 private $warnedChildFunctions = [];
349 * Constructor
351 * @param array|null $cacheOptions
352 * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions
354 public function __construct($cacheOptions = null)
356 $this->sourceNames = [];
358 if ($cacheOptions) {
359 $this->cache = new Cache($cacheOptions);
360 if (!empty($cacheOptions['checkImportResolutions'])) {
361 $this->cacheCheckImportResolutions = true;
365 $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
369 * Get compiler options
371 * @return array<string, mixed>
373 * @internal
375 public function getCompileOptions()
377 $options = [
378 'importPaths' => $this->importPaths,
379 'registeredVars' => $this->registeredVars,
380 'registeredFeatures' => $this->registeredFeatures,
381 'encoding' => $this->encoding,
382 'sourceMap' => serialize($this->sourceMap),
383 'sourceMapOptions' => $this->sourceMapOptions,
384 'formatter' => $this->formatter,
385 'legacyImportPath' => $this->legacyCwdImportPath,
388 return $options;
392 * Sets an alternative logger.
394 * Changing the logger in the middle of the compilation is not
395 * supported and will result in an undefined behavior.
397 * @param LoggerInterface $logger
399 * @return void
401 public function setLogger(LoggerInterface $logger)
403 $this->logger = $logger;
407 * Set an alternative error output stream, for testing purpose only
409 * @param resource $handle
411 * @return void
413 * @deprecated Use {@see setLogger} instead
415 public function setErrorOuput($handle)
417 @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED);
419 $this->logger = new StreamLogger($handle);
423 * Compile scss
425 * @param string $code
426 * @param string|null $path
428 * @return string
430 * @throws SassException when the source fails to compile
432 * @deprecated Use {@see compileString} instead.
434 public function compile($code, $path = null)
436 @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED);
438 $result = $this->compileString($code, $path);
440 $sourceMap = $result->getSourceMap();
442 if ($sourceMap !== null) {
443 if ($this->sourceMap instanceof SourceMapGenerator) {
444 $this->sourceMap->saveMap($sourceMap);
445 } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) {
446 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
447 $sourceMapGenerator->saveMap($sourceMap);
451 return $result->getCss();
455 * Compile scss
457 * @param string $source
458 * @param string|null $path
460 * @return CompilationResult
462 * @throws SassException when the source fails to compile
464 public function compileString($source, $path = null)
466 if ($this->cache) {
467 $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source);
468 $compileOptions = $this->getCompileOptions();
469 $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions);
471 if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) {
472 return $cachedResult->getResult();
476 $this->indentLevel = -1;
477 $this->extends = [];
478 $this->extendsMap = [];
479 $this->sourceIndex = null;
480 $this->sourceLine = null;
481 $this->sourceColumn = null;
482 $this->env = null;
483 $this->scope = null;
484 $this->storeEnv = null;
485 $this->shouldEvaluate = null;
486 $this->ignoreCallStackMessage = false;
487 $this->parsedFiles = [];
488 $this->importedFiles = [];
489 $this->resolvedImports = [];
491 if (!\is_null($path) && is_file($path)) {
492 $path = realpath($path) ?: $path;
493 $this->currentDirectory = dirname($path);
494 $this->rootDirectory = $this->currentDirectory;
495 } else {
496 $this->currentDirectory = null;
497 $this->rootDirectory = getcwd();
500 try {
501 $this->parser = $this->parserFactory($path);
502 $tree = $this->parser->parse($source);
503 $this->parser = null;
505 $this->formatter = new $this->formatter();
506 $this->rootBlock = null;
507 $this->rootEnv = $this->pushEnv($tree);
509 $warnCallback = function ($message, $deprecation) {
510 $this->logger->warn($message, $deprecation);
512 $previousWarnCallback = Warn::setCallback($warnCallback);
514 try {
515 $this->injectVariables($this->registeredVars);
516 $this->compileRoot($tree);
517 $this->popEnv();
518 } finally {
519 Warn::setCallback($previousWarnCallback);
522 $sourceMapGenerator = null;
524 if ($this->sourceMap) {
525 if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
526 $sourceMapGenerator = $this->sourceMap;
527 $this->sourceMap = self::SOURCE_MAP_FILE;
528 } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
529 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
533 $out = $this->formatter->format($this->scope, $sourceMapGenerator);
535 $prefix = '';
537 if ($this->charset && strlen($out) !== Util::mbStrlen($out)) {
538 $prefix = '@charset "UTF-8";' . "\n";
539 $out = $prefix . $out;
542 $sourceMap = null;
544 if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
545 $sourceMap = $sourceMapGenerator->generateJson($prefix);
546 $sourceMapUrl = null;
548 switch ($this->sourceMap) {
549 case self::SOURCE_MAP_INLINE:
550 $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
551 break;
553 case self::SOURCE_MAP_FILE:
554 if (isset($this->sourceMapOptions['sourceMapURL'])) {
555 $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
557 break;
560 if ($sourceMapUrl !== null) {
561 $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
564 } catch (SassScriptException $e) {
565 throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
568 $includedFiles = [];
570 foreach ($this->resolvedImports as $resolvedImport) {
571 $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath'];
574 $result = new CompilationResult($out, $sourceMap, array_values($includedFiles));
576 if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
577 $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions);
580 // Reset state to free memory
581 // TODO in 2.0, reset parsedFiles as well when the getter is removed.
582 $this->resolvedImports = [];
583 $this->importedFiles = [];
585 return $result;
589 * @param CachedResult $result
591 * @return bool
593 private function isFreshCachedResult(CachedResult $result)
595 // check if any dependency file changed since the result was compiled
596 foreach ($result->getParsedFiles() as $file => $mtime) {
597 if (! is_file($file) || filemtime($file) !== $mtime) {
598 return false;
602 if ($this->cacheCheckImportResolutions) {
603 $resolvedImports = [];
605 foreach ($result->getResolvedImports() as $import) {
606 $currentDir = $import['currentDir'];
607 $path = $import['path'];
608 // store the check across all the results in memory to avoid multiple findImport() on the same path
609 // with same context.
610 // this is happening in a same hit with multiple compilations (especially with big frameworks)
611 if (empty($resolvedImports[$currentDir][$path])) {
612 $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir);
615 if ($resolvedImports[$currentDir][$path] !== $import['filePath']) {
616 return false;
621 return true;
625 * Instantiate parser
627 * @param string|null $path
629 * @return \ScssPhp\ScssPhp\Parser
631 protected function parserFactory($path)
633 // https://sass-lang.com/documentation/at-rules/import
634 // CSS files imported by Sass don’t allow any special Sass features.
635 // In order to make sure authors don’t accidentally write Sass in their CSS,
636 // all Sass features that aren’t also valid CSS will produce errors.
637 // Otherwise, the CSS will be rendered as-is. It can even be extended!
638 $cssOnly = false;
640 if ($path !== null && substr($path, -4) === '.css') {
641 $cssOnly = true;
644 $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger);
646 $this->sourceNames[] = $path;
647 $this->addParsedFile($path);
649 return $parser;
653 * Is self extend?
655 * @param array $target
656 * @param array $origin
658 * @return bool
660 protected function isSelfExtend($target, $origin)
662 foreach ($origin as $sel) {
663 if (\in_array($target, $sel)) {
664 return true;
668 return false;
672 * Push extends
674 * @param array $target
675 * @param array $origin
676 * @param array|null $block
678 * @return void
680 protected function pushExtends($target, $origin, $block)
682 $i = \count($this->extends);
683 $this->extends[] = [$target, $origin, $block];
685 foreach ($target as $part) {
686 if (isset($this->extendsMap[$part])) {
687 $this->extendsMap[$part][] = $i;
688 } else {
689 $this->extendsMap[$part] = [$i];
695 * Make output block
697 * @param string|null $type
698 * @param string[]|null $selectors
700 * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
702 protected function makeOutputBlock($type, $selectors = null)
704 $out = new OutputBlock();
705 $out->type = $type;
706 $out->lines = [];
707 $out->children = [];
708 $out->parent = $this->scope;
709 $out->selectors = $selectors;
710 $out->depth = $this->env->depth;
712 if ($this->env->block instanceof Block) {
713 $out->sourceName = $this->env->block->sourceName;
714 $out->sourceLine = $this->env->block->sourceLine;
715 $out->sourceColumn = $this->env->block->sourceColumn;
716 } else {
717 $out->sourceName = null;
718 $out->sourceLine = null;
719 $out->sourceColumn = null;
722 return $out;
726 * Compile root
728 * @param \ScssPhp\ScssPhp\Block $rootBlock
730 * @return void
732 protected function compileRoot(Block $rootBlock)
734 $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
736 $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
737 $this->flattenSelectors($this->scope);
738 $this->missingSelectors();
742 * Report missing selectors
744 * @return void
746 protected function missingSelectors()
748 foreach ($this->extends as $extend) {
749 if (isset($extend[3])) {
750 continue;
753 list($target, $origin, $block) = $extend;
755 // ignore if !optional
756 if ($block[2]) {
757 continue;
760 $target = implode(' ', $target);
761 $origin = $this->collapseSelectors($origin);
763 $this->sourceLine = $block[Parser::SOURCE_LINE];
764 throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
769 * Flatten selectors
771 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
772 * @param string $parentKey
774 * @return void
776 protected function flattenSelectors(OutputBlock $block, $parentKey = null)
778 if ($block->selectors) {
779 $selectors = [];
781 foreach ($block->selectors as $s) {
782 $selectors[] = $s;
784 if (! \is_array($s)) {
785 continue;
788 // check extends
789 if (! empty($this->extendsMap)) {
790 $this->matchExtends($s, $selectors);
792 // remove duplicates
793 array_walk($selectors, function (&$value) {
794 $value = serialize($value);
797 $selectors = array_unique($selectors);
799 array_walk($selectors, function (&$value) {
800 $value = unserialize($value);
805 $block->selectors = [];
806 $placeholderSelector = false;
808 foreach ($selectors as $selector) {
809 if ($this->hasSelectorPlaceholder($selector)) {
810 $placeholderSelector = true;
811 continue;
814 $block->selectors[] = $this->compileSelector($selector);
817 if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
818 unset($block->parent->children[$parentKey]);
820 return;
824 foreach ($block->children as $key => $child) {
825 $this->flattenSelectors($child, $key);
830 * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts
832 * @param array $parts
834 * @return array
836 protected function glueFunctionSelectors($parts)
838 $new = [];
840 foreach ($parts as $part) {
841 if (\is_array($part)) {
842 $part = $this->glueFunctionSelectors($part);
843 $new[] = $part;
844 } else {
845 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
846 // and need to be joined to this
847 if (
848 \count($new) && \is_string($new[\count($new) - 1]) &&
849 \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
851 while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
852 $part = array_pop($new) . $part;
854 $new[\count($new) - 1] .= $part;
855 } else {
856 $new[] = $part;
861 return $new;
865 * Match extends
867 * @param array $selector
868 * @param array $out
869 * @param int $from
870 * @param bool $initial
872 * @return void
874 protected function matchExtends($selector, &$out, $from = 0, $initial = true)
876 static $partsPile = [];
877 $selector = $this->glueFunctionSelectors($selector);
879 if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
880 return;
883 $outRecurs = [];
885 foreach ($selector as $i => $part) {
886 if ($i < $from) {
887 continue;
890 // check that we are not building an infinite loop of extensions
891 // if the new part is just including a previous part don't try to extend anymore
892 if (\count($part) > 1) {
893 foreach ($partsPile as $previousPart) {
894 if (! \count(array_diff($previousPart, $part))) {
895 continue 2;
900 $partsPile[] = $part;
902 if ($this->matchExtendsSingle($part, $origin, $initial)) {
903 $after = \array_slice($selector, $i + 1);
904 $before = \array_slice($selector, 0, $i);
905 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
907 foreach ($origin as $new) {
908 $k = 0;
910 // remove shared parts
911 if (\count($new) > 1) {
912 while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
913 $k++;
917 if (\count($nonBreakableBefore) && $k === \count($new)) {
918 $k--;
921 $replacement = [];
922 $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
924 for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
925 $slice = [];
927 foreach ($tempReplacement[$l] as $chunk) {
928 if (! \in_array($chunk, $slice)) {
929 $slice[] = $chunk;
933 array_unshift($replacement, $slice);
935 if (! $this->isImmediateRelationshipCombinator(end($slice))) {
936 break;
940 $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
942 // Merge shared direct relationships.
943 $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
945 $result = array_merge(
946 $before,
947 $mergedBefore,
948 $replacement,
949 $after
952 if ($result === $selector) {
953 continue;
956 $this->pushOrMergeExtentedSelector($out, $result);
958 // recursively check for more matches
959 $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
961 if (\count($origin) > 1) {
962 $this->matchExtends($result, $out, $startRecurseFrom, false);
963 } else {
964 $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
967 // selector sequence merging
968 if (! empty($before) && \count($new) > 1) {
969 $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
970 $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
972 list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
974 $result2 = array_merge(
975 $preSharedParts,
976 $betweenSharedParts,
977 $postSharedParts,
978 $nonBreakabl2,
979 $nonBreakableBefore,
980 $replacement,
981 $after
984 $this->pushOrMergeExtentedSelector($out, $result2);
988 array_pop($partsPile);
991 while (\count($outRecurs)) {
992 $result = array_shift($outRecurs);
993 $this->pushOrMergeExtentedSelector($out, $result);
998 * Test a part for being a pseudo selector
1000 * @param string $part
1001 * @param array $matches
1003 * @return bool
1005 protected function isPseudoSelector($part, &$matches)
1007 if (
1008 strpos($part, ':') === 0 &&
1009 preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
1011 return true;
1014 return false;
1018 * Push extended selector except if
1019 * - this is a pseudo selector
1020 * - same as previous
1021 * - in a white list
1022 * in this case we merge the pseudo selector content
1024 * @param array $out
1025 * @param array $extended
1027 * @return void
1029 protected function pushOrMergeExtentedSelector(&$out, $extended)
1031 if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
1032 $single = reset($extended);
1033 $part = reset($single);
1035 if (
1036 $this->isPseudoSelector($part, $matchesExtended) &&
1037 \in_array($matchesExtended[1], [ 'slotted' ])
1039 $prev = end($out);
1040 $prev = $this->glueFunctionSelectors($prev);
1042 if (\count($prev) === 1 && \count(reset($prev)) === 1) {
1043 $single = reset($prev);
1044 $part = reset($single);
1046 if (
1047 $this->isPseudoSelector($part, $matchesPrev) &&
1048 $matchesPrev[1] === $matchesExtended[1]
1050 $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
1051 $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
1052 $extended = implode($matchesExtended[1] . '(', $extended);
1053 $extended = [ [ $extended ]];
1054 array_pop($out);
1059 $out[] = $extended;
1063 * Match extends single
1065 * @param array $rawSingle
1066 * @param array $outOrigin
1067 * @param bool $initial
1069 * @return bool
1071 protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
1073 $counts = [];
1074 $single = [];
1076 // simple usual cases, no need to do the whole trick
1077 if (\in_array($rawSingle, [['>'],['+'],['~']])) {
1078 return false;
1081 foreach ($rawSingle as $part) {
1082 // matches Number
1083 if (! \is_string($part)) {
1084 return false;
1087 if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
1088 $single[\count($single) - 1] .= $part;
1089 } else {
1090 $single[] = $part;
1094 $extendingDecoratedTag = false;
1096 if (\count($single) > 1) {
1097 $matches = null;
1098 $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
1101 $outOrigin = [];
1102 $found = false;
1104 foreach ($single as $k => $part) {
1105 if (isset($this->extendsMap[$part])) {
1106 foreach ($this->extendsMap[$part] as $idx) {
1107 $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
1111 if (
1112 $initial &&
1113 $this->isPseudoSelector($part, $matches) &&
1114 ! \in_array($matches[1], [ 'not' ])
1116 $buffer = $matches[2];
1117 $parser = $this->parserFactory(__METHOD__);
1119 if ($parser->parseSelector($buffer, $subSelectors, false)) {
1120 foreach ($subSelectors as $ksub => $subSelector) {
1121 $subExtended = [];
1122 $this->matchExtends($subSelector, $subExtended, 0, false);
1124 if ($subExtended) {
1125 $subSelectorsExtended = $subSelectors;
1126 $subSelectorsExtended[$ksub] = $subExtended;
1128 foreach ($subSelectorsExtended as $ksse => $sse) {
1129 $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
1132 $subSelectorsExtended = implode(', ', $subSelectorsExtended);
1133 $singleExtended = $single;
1134 $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
1135 $outOrigin[] = [ $singleExtended ];
1136 $found = true;
1143 foreach ($counts as $idx => $count) {
1144 list($target, $origin, /* $block */) = $this->extends[$idx];
1146 $origin = $this->glueFunctionSelectors($origin);
1148 // check count
1149 if ($count !== \count($target)) {
1150 continue;
1153 $this->extends[$idx][3] = true;
1155 $rem = array_diff($single, $target);
1157 foreach ($origin as $j => $new) {
1158 // prevent infinite loop when target extends itself
1159 if ($this->isSelfExtend($single, $origin) && ! $initial) {
1160 return false;
1163 $replacement = end($new);
1165 // Extending a decorated tag with another tag is not possible.
1166 if (
1167 $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
1168 preg_match('/^[a-z0-9]+$/i', $replacement[0])
1170 unset($origin[$j]);
1171 continue;
1174 $combined = $this->combineSelectorSingle($replacement, $rem);
1176 if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
1177 $origin[$j][\count($origin[$j]) - 1] = $combined;
1181 $outOrigin = array_merge($outOrigin, $origin);
1183 $found = true;
1186 return $found;
1190 * Extract a relationship from the fragment.
1192 * When extracting the last portion of a selector we will be left with a
1193 * fragment which may end with a direction relationship combinator. This
1194 * method will extract the relationship fragment and return it along side
1195 * the rest.
1197 * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
1199 * @return array The selector without the relationship fragment if any, the relationship fragment.
1201 protected function extractRelationshipFromFragment(array $fragment)
1203 $parents = [];
1204 $children = [];
1206 $j = $i = \count($fragment);
1208 for (;;) {
1209 $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
1210 $parents = \array_slice($fragment, 0, $j);
1211 $slice = end($parents);
1213 if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
1214 break;
1217 $j -= 2;
1220 return [$parents, $children];
1224 * Combine selector single
1226 * @param array $base
1227 * @param array $other
1229 * @return array
1231 protected function combineSelectorSingle($base, $other)
1233 $tag = [];
1234 $out = [];
1235 $wasTag = false;
1236 $pseudo = [];
1238 while (\count($other) && strpos(end($other), ':') === 0) {
1239 array_unshift($pseudo, array_pop($other));
1242 foreach ([array_reverse($base), array_reverse($other)] as $single) {
1243 $rang = count($single);
1245 foreach ($single as $part) {
1246 if (preg_match('/^[\[:]/', $part)) {
1247 $out[] = $part;
1248 $wasTag = false;
1249 } elseif (preg_match('/^[\.#]/', $part)) {
1250 array_unshift($out, $part);
1251 $wasTag = false;
1252 } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
1253 $tag[] = $part;
1254 $wasTag = true;
1255 } elseif ($wasTag) {
1256 $tag[\count($tag) - 1] .= $part;
1257 } else {
1258 array_unshift($out, $part);
1260 $rang--;
1264 if (\count($tag)) {
1265 array_unshift($out, $tag[0]);
1268 while (\count($pseudo)) {
1269 $out[] = array_shift($pseudo);
1272 return $out;
1276 * Compile media
1278 * @param \ScssPhp\ScssPhp\Block $media
1280 * @return void
1282 protected function compileMedia(Block $media)
1284 assert($media instanceof MediaBlock);
1285 $this->pushEnv($media);
1287 $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
1289 if (! empty($mediaQueries)) {
1290 $previousScope = $this->scope;
1291 $parentScope = $this->mediaParent($this->scope);
1293 foreach ($mediaQueries as $mediaQuery) {
1294 $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
1296 $parentScope->children[] = $this->scope;
1297 $parentScope = $this->scope;
1300 // top level properties in a media cause it to be wrapped
1301 $needsWrap = false;
1303 foreach ($media->children as $child) {
1304 $type = $child[0];
1306 if (
1307 $type !== Type::T_BLOCK &&
1308 $type !== Type::T_MEDIA &&
1309 $type !== Type::T_DIRECTIVE &&
1310 $type !== Type::T_IMPORT
1312 $needsWrap = true;
1313 break;
1317 if ($needsWrap) {
1318 $wrapped = new Block();
1319 $wrapped->sourceName = $media->sourceName;
1320 $wrapped->sourceIndex = $media->sourceIndex;
1321 $wrapped->sourceLine = $media->sourceLine;
1322 $wrapped->sourceColumn = $media->sourceColumn;
1323 $wrapped->selectors = [];
1324 $wrapped->comments = [];
1325 $wrapped->parent = $media;
1326 $wrapped->children = $media->children;
1328 $media->children = [[Type::T_BLOCK, $wrapped]];
1331 $this->compileChildrenNoReturn($media->children, $this->scope);
1333 $this->scope = $previousScope;
1336 $this->popEnv();
1340 * Media parent
1342 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1344 * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1346 protected function mediaParent(OutputBlock $scope)
1348 while (! empty($scope->parent)) {
1349 if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
1350 break;
1353 $scope = $scope->parent;
1356 return $scope;
1360 * Compile directive
1362 * @param DirectiveBlock|array $directive
1363 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1365 * @return void
1367 protected function compileDirective($directive, OutputBlock $out)
1369 if (\is_array($directive)) {
1370 $directiveName = $this->compileDirectiveName($directive[0]);
1371 $s = '@' . $directiveName;
1373 if (! empty($directive[1])) {
1374 $s .= ' ' . $this->compileValue($directive[1]);
1376 // sass-spec compliance on newline after directives, a bit tricky :/
1377 $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
1378 if (\is_array($directive[0]) && empty($directive[1])) {
1379 $appendNewLine = "\n";
1382 if (empty($directive[3])) {
1383 $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
1384 } else {
1385 $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
1387 } else {
1388 $directive->name = $this->compileDirectiveName($directive->name);
1389 $s = '@' . $directive->name;
1391 if (! empty($directive->value)) {
1392 $s .= ' ' . $this->compileValue($directive->value);
1395 if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1396 $this->compileKeyframeBlock($directive, [$s]);
1397 } else {
1398 $this->compileNestedBlock($directive, [$s]);
1404 * directive names can include some interpolation
1406 * @param string|array $directiveName
1407 * @return string
1408 * @throws CompilerException
1410 protected function compileDirectiveName($directiveName)
1412 if (is_string($directiveName)) {
1413 return $directiveName;
1416 return $this->compileValue($directiveName);
1420 * Compile at-root
1422 * @param \ScssPhp\ScssPhp\Block $block
1424 * @return void
1426 protected function compileAtRoot(Block $block)
1428 assert($block instanceof AtRootBlock);
1429 $env = $this->pushEnv($block);
1430 $envs = $this->compactEnv($env);
1431 list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1433 // wrap inline selector
1434 if ($block->selector) {
1435 $wrapped = new Block();
1436 $wrapped->sourceName = $block->sourceName;
1437 $wrapped->sourceIndex = $block->sourceIndex;
1438 $wrapped->sourceLine = $block->sourceLine;
1439 $wrapped->sourceColumn = $block->sourceColumn;
1440 $wrapped->selectors = $block->selector;
1441 $wrapped->comments = [];
1442 $wrapped->parent = $block;
1443 $wrapped->children = $block->children;
1444 $wrapped->selfParent = $block->selfParent;
1446 $block->children = [[Type::T_BLOCK, $wrapped]];
1447 $block->selector = null;
1450 $selfParent = $block->selfParent;
1451 assert($selfParent !== null, 'at-root blocks must have a selfParent set.');
1453 if (
1454 ! $selfParent->selectors &&
1455 isset($block->parent) && $block->parent &&
1456 isset($block->parent->selectors) && $block->parent->selectors
1458 $selfParent = $block->parent;
1461 $this->env = $this->filterWithWithout($envs, $with, $without);
1463 $saveScope = $this->scope;
1464 $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1466 // propagate selfParent to the children where they still can be useful
1467 $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1469 $this->scope = $this->completeScope($this->scope, $saveScope);
1470 $this->scope = $saveScope;
1471 $this->env = $this->extractEnv($envs);
1473 $this->popEnv();
1477 * Filter at-root scope depending on with/without option
1479 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1480 * @param array $with
1481 * @param array $without
1483 * @return OutputBlock
1485 protected function filterScopeWithWithout($scope, $with, $without)
1487 $filteredScopes = [];
1488 $childStash = [];
1490 if ($scope->type === Type::T_ROOT) {
1491 return $scope;
1494 // start from the root
1495 while ($scope->parent && $scope->parent->type !== Type::T_ROOT) {
1496 array_unshift($childStash, $scope);
1497 $scope = $scope->parent;
1500 for (;;) {
1501 if (! $scope) {
1502 break;
1505 if ($this->isWith($scope, $with, $without)) {
1506 $s = clone $scope;
1507 $s->children = [];
1508 $s->lines = [];
1509 $s->parent = null;
1511 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1512 $s->selectors = [];
1515 $filteredScopes[] = $s;
1518 if (\count($childStash)) {
1519 $scope = array_shift($childStash);
1520 } elseif ($scope->children) {
1521 $scope = end($scope->children);
1522 } else {
1523 $scope = null;
1527 if (! \count($filteredScopes)) {
1528 return $this->rootBlock;
1531 $newScope = array_shift($filteredScopes);
1532 $newScope->parent = $this->rootBlock;
1534 $this->rootBlock->children[] = $newScope;
1536 $p = &$newScope;
1538 while (\count($filteredScopes)) {
1539 $s = array_shift($filteredScopes);
1540 $s->parent = $p;
1541 $p->children[] = $s;
1542 $newScope = &$p->children[0];
1543 $p = &$p->children[0];
1546 return $newScope;
1550 * found missing selector from a at-root compilation in the previous scope
1551 * (if at-root is just enclosing a property, the selector is in the parent tree)
1553 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1554 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1556 * @return OutputBlock
1558 protected function completeScope($scope, $previousScope)
1560 if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
1561 $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1564 if ($scope->children) {
1565 foreach ($scope->children as $k => $c) {
1566 $scope->children[$k] = $this->completeScope($c, $previousScope);
1570 return $scope;
1574 * Find a selector by the depth node in the scope
1576 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1577 * @param int $depth
1579 * @return array
1581 protected function findScopeSelectors($scope, $depth)
1583 if ($scope->depth === $depth && $scope->selectors) {
1584 return $scope->selectors;
1587 if ($scope->children) {
1588 foreach (array_reverse($scope->children) as $c) {
1589 if ($s = $this->findScopeSelectors($c, $depth)) {
1590 return $s;
1595 return [];
1599 * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1601 * @param array|null $withCondition
1603 * @return array
1605 * @phpstan-return array{array<string, bool>, array<string, bool>}
1607 protected function compileWith($withCondition)
1609 // just compile what we have in 2 lists
1610 $with = [];
1611 $without = ['rule' => true];
1613 if ($withCondition) {
1614 if ($withCondition[0] === Type::T_INTERPOLATE) {
1615 $w = $this->compileValue($withCondition);
1617 $buffer = "($w)";
1618 $parser = $this->parserFactory(__METHOD__);
1620 if ($parser->parseValue($buffer, $reParsedWith)) {
1621 $withCondition = $reParsedWith;
1625 $withConfig = $this->mapGet($withCondition, static::$with);
1626 if ($withConfig !== null) {
1627 $without = []; // cancel the default
1628 $list = $this->coerceList($withConfig);
1630 foreach ($list[2] as $item) {
1631 $keyword = $this->compileStringContent($this->coerceString($item));
1633 $with[$keyword] = true;
1637 $withoutConfig = $this->mapGet($withCondition, static::$without);
1638 if ($withoutConfig !== null) {
1639 $without = []; // cancel the default
1640 $list = $this->coerceList($withoutConfig);
1642 foreach ($list[2] as $item) {
1643 $keyword = $this->compileStringContent($this->coerceString($item));
1645 $without[$keyword] = true;
1650 return [$with, $without];
1654 * Filter env stack
1656 * @param Environment[] $envs
1657 * @param array $with
1658 * @param array $without
1660 * @return Environment
1662 * @phpstan-param non-empty-array<Environment> $envs
1664 protected function filterWithWithout($envs, $with, $without)
1666 $filtered = [];
1668 foreach ($envs as $e) {
1669 if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1670 $ec = clone $e;
1671 $ec->block = null;
1672 $ec->selectors = [];
1674 $filtered[] = $ec;
1675 } else {
1676 $filtered[] = $e;
1680 return $this->extractEnv($filtered);
1684 * Filter WITH rules
1686 * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1687 * @param array $with
1688 * @param array $without
1690 * @return bool
1692 protected function isWith($block, $with, $without)
1694 if (isset($block->type)) {
1695 if ($block->type === Type::T_MEDIA) {
1696 return $this->testWithWithout('media', $with, $without);
1699 if ($block->type === Type::T_DIRECTIVE) {
1700 assert($block instanceof DirectiveBlock || $block instanceof OutputBlock);
1701 if (isset($block->name)) {
1702 return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
1703 } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1704 return $this->testWithWithout($m[1], $with, $without);
1705 } else {
1706 return $this->testWithWithout('???', $with, $without);
1709 } elseif (isset($block->selectors)) {
1710 // a selector starting with number is a keyframe rule
1711 if (\count($block->selectors)) {
1712 $s = reset($block->selectors);
1714 while (\is_array($s)) {
1715 $s = reset($s);
1718 if (\is_object($s) && $s instanceof Number) {
1719 return $this->testWithWithout('keyframes', $with, $without);
1723 return $this->testWithWithout('rule', $with, $without);
1726 return true;
1730 * Test a single type of block against with/without lists
1732 * @param string $what
1733 * @param array $with
1734 * @param array $without
1736 * @return bool
1737 * true if the block should be kept, false to reject
1739 protected function testWithWithout($what, $with, $without)
1741 // if without, reject only if in the list (or 'all' is in the list)
1742 if (\count($without)) {
1743 return (isset($without[$what]) || isset($without['all'])) ? false : true;
1746 // otherwise reject all what is not in the with list
1747 return (isset($with[$what]) || isset($with['all'])) ? true : false;
1752 * Compile keyframe block
1754 * @param \ScssPhp\ScssPhp\Block $block
1755 * @param string[] $selectors
1757 * @return void
1759 protected function compileKeyframeBlock(Block $block, $selectors)
1761 $env = $this->pushEnv($block);
1763 $envs = $this->compactEnv($env);
1765 $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1766 return ! isset($e->block->selectors);
1767 }));
1769 $this->scope = $this->makeOutputBlock($block->type, $selectors);
1770 $this->scope->depth = 1;
1771 $this->scope->parent->children[] = $this->scope;
1773 $this->compileChildrenNoReturn($block->children, $this->scope);
1775 $this->scope = $this->scope->parent;
1776 $this->env = $this->extractEnv($envs);
1778 $this->popEnv();
1782 * Compile nested properties lines
1784 * @param \ScssPhp\ScssPhp\Block $block
1785 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1787 * @return void
1789 protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1791 assert($block instanceof NestedPropertyBlock);
1792 $prefix = $this->compileValue($block->prefix) . '-';
1794 $nested = $this->makeOutputBlock($block->type);
1795 $nested->parent = $out;
1797 if ($block->hasValue) {
1798 $nested->depth = $out->depth + 1;
1801 $out->children[] = $nested;
1803 foreach ($block->children as $child) {
1804 switch ($child[0]) {
1805 case Type::T_ASSIGN:
1806 array_unshift($child[1][2], $prefix);
1807 break;
1809 case Type::T_NESTED_PROPERTY:
1810 assert($child[1] instanceof NestedPropertyBlock);
1811 array_unshift($child[1]->prefix[2], $prefix);
1812 break;
1815 $this->compileChild($child, $nested);
1820 * Compile nested block
1822 * @param \ScssPhp\ScssPhp\Block $block
1823 * @param string[] $selectors
1825 * @return void
1827 protected function compileNestedBlock(Block $block, $selectors)
1829 $this->pushEnv($block);
1831 $this->scope = $this->makeOutputBlock($block->type, $selectors);
1832 $this->scope->parent->children[] = $this->scope;
1834 // wrap assign children in a block
1835 // except for @font-face
1836 if (!$block instanceof DirectiveBlock || $this->compileDirectiveName($block->name) !== 'font-face') {
1837 // need wrapping?
1838 $needWrapping = false;
1840 foreach ($block->children as $child) {
1841 if ($child[0] === Type::T_ASSIGN) {
1842 $needWrapping = true;
1843 break;
1847 if ($needWrapping) {
1848 $wrapped = new Block();
1849 $wrapped->sourceName = $block->sourceName;
1850 $wrapped->sourceIndex = $block->sourceIndex;
1851 $wrapped->sourceLine = $block->sourceLine;
1852 $wrapped->sourceColumn = $block->sourceColumn;
1853 $wrapped->selectors = [];
1854 $wrapped->comments = [];
1855 $wrapped->parent = $block;
1856 $wrapped->children = $block->children;
1857 $wrapped->selfParent = $block->selfParent;
1859 $block->children = [[Type::T_BLOCK, $wrapped]];
1863 $this->compileChildrenNoReturn($block->children, $this->scope);
1865 $this->scope = $this->scope->parent;
1867 $this->popEnv();
1871 * Recursively compiles a block.
1873 * A block is analogous to a CSS block in most cases. A single SCSS document
1874 * is encapsulated in a block when parsed, but it does not have parent tags
1875 * so all of its children appear on the root level when compiled.
1877 * Blocks are made up of selectors and children.
1879 * The children of a block are just all the blocks that are defined within.
1881 * Compiling the block involves pushing a fresh environment on the stack,
1882 * and iterating through the props, compiling each one.
1884 * @see Compiler::compileChild()
1886 * @param \ScssPhp\ScssPhp\Block $block
1888 * @return void
1890 protected function compileBlock(Block $block)
1892 $env = $this->pushEnv($block);
1893 $env->selectors = $this->evalSelectors($block->selectors);
1895 $out = $this->makeOutputBlock(null);
1897 $this->scope->children[] = $out;
1899 if (\count($block->children)) {
1900 $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1902 // propagate selfParent to the children where they still can be useful
1903 $selfParentSelectors = null;
1905 if (isset($block->selfParent->selectors)) {
1906 $selfParentSelectors = $block->selfParent->selectors;
1907 $block->selfParent->selectors = $out->selectors;
1910 $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1912 // and revert for the following children of the same block
1913 if ($selfParentSelectors) {
1914 $block->selfParent->selectors = $selfParentSelectors;
1918 $this->popEnv();
1923 * Compile the value of a comment that can have interpolation
1925 * @param array $value
1926 * @param bool $pushEnv
1928 * @return string
1930 protected function compileCommentValue($value, $pushEnv = false)
1932 $c = $value[1];
1934 if (isset($value[2])) {
1935 if ($pushEnv) {
1936 $this->pushEnv();
1939 try {
1940 $c = $this->compileValue($value[2]);
1941 } catch (SassScriptException $e) {
1942 $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true);
1943 // ignore error in comment compilation which are only interpolation
1944 } catch (SassException $e) {
1945 $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true);
1946 // ignore error in comment compilation which are only interpolation
1949 if ($pushEnv) {
1950 $this->popEnv();
1954 return $c;
1958 * Compile root level comment
1960 * @param array $block
1962 * @return void
1964 protected function compileComment($block)
1966 $out = $this->makeOutputBlock(Type::T_COMMENT);
1967 $out->lines[] = $this->compileCommentValue($block, true);
1969 $this->scope->children[] = $out;
1973 * Evaluate selectors
1975 * @param array $selectors
1977 * @return array
1979 protected function evalSelectors($selectors)
1981 $this->shouldEvaluate = false;
1983 $selectors = array_map([$this, 'evalSelector'], $selectors);
1985 // after evaluating interpolates, we might need a second pass
1986 if ($this->shouldEvaluate) {
1987 $selectors = $this->replaceSelfSelector($selectors, '&');
1988 $buffer = $this->collapseSelectors($selectors);
1989 $parser = $this->parserFactory(__METHOD__);
1991 try {
1992 $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1993 } catch (ParserException $e) {
1994 throw $this->error($e->getMessage());
1997 if ($isValid) {
1998 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
2002 return $selectors;
2006 * Evaluate selector
2008 * @param array $selector
2010 * @return array
2012 protected function evalSelector($selector)
2014 return array_map([$this, 'evalSelectorPart'], $selector);
2018 * Evaluate selector part; replaces all the interpolates, stripping quotes
2020 * @param array $part
2022 * @return array
2024 protected function evalSelectorPart($part)
2026 foreach ($part as &$p) {
2027 if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
2028 $p = $this->compileValue($p);
2030 // force re-evaluation if self char or non standard char
2031 if (preg_match(',[^\w-],', $p)) {
2032 $this->shouldEvaluate = true;
2034 } elseif (
2035 \is_string($p) && \strlen($p) >= 2 &&
2036 ($first = $p[0]) && ($first === '"' || $first === "'") &&
2037 substr($p, -1) === $first
2039 $p = substr($p, 1, -1);
2043 return $this->flattenSelectorSingle($part);
2047 * Collapse selectors
2049 * @param array $selectors
2051 * @return string
2053 protected function collapseSelectors($selectors)
2055 $parts = [];
2057 foreach ($selectors as $selector) {
2058 $output = [];
2060 foreach ($selector as $node) {
2061 $compound = '';
2063 array_walk_recursive(
2064 $node,
2065 function ($value, $key) use (&$compound) {
2066 $compound .= $value;
2070 $output[] = $compound;
2073 $parts[] = implode(' ', $output);
2076 return implode(', ', $parts);
2080 * Collapse selectors
2082 * @param array $selectors
2084 * @return array
2086 private function collapseSelectorsAsList($selectors)
2088 $parts = [];
2090 foreach ($selectors as $selector) {
2091 $output = [];
2092 $glueNext = false;
2094 foreach ($selector as $node) {
2095 $compound = '';
2097 array_walk_recursive(
2098 $node,
2099 function ($value, $key) use (&$compound) {
2100 $compound .= $value;
2104 if ($this->isImmediateRelationshipCombinator($compound)) {
2105 if (\count($output)) {
2106 $output[\count($output) - 1] .= ' ' . $compound;
2107 } else {
2108 $output[] = $compound;
2111 $glueNext = true;
2112 } elseif ($glueNext) {
2113 $output[\count($output) - 1] .= ' ' . $compound;
2114 $glueNext = false;
2115 } else {
2116 $output[] = $compound;
2120 foreach ($output as &$o) {
2121 $o = [Type::T_STRING, '', [$o]];
2124 $parts[] = [Type::T_LIST, ' ', $output];
2127 return [Type::T_LIST, ',', $parts];
2131 * Parse down the selector and revert [self] to "&" before a reparsing
2133 * @param array $selectors
2134 * @param string|null $replace
2136 * @return array
2138 protected function replaceSelfSelector($selectors, $replace = null)
2140 foreach ($selectors as &$part) {
2141 if (\is_array($part)) {
2142 if ($part === [Type::T_SELF]) {
2143 if (\is_null($replace)) {
2144 $replace = $this->reduce([Type::T_SELF]);
2145 $replace = $this->compileValue($replace);
2147 $part = $replace;
2148 } else {
2149 $part = $this->replaceSelfSelector($part, $replace);
2154 return $selectors;
2158 * Flatten selector single; joins together .classes and #ids
2160 * @param array $single
2162 * @return array
2164 protected function flattenSelectorSingle($single)
2166 $joined = [];
2168 foreach ($single as $part) {
2169 if (
2170 empty($joined) ||
2171 ! \is_string($part) ||
2172 preg_match('/[\[.:#%]/', $part)
2174 $joined[] = $part;
2175 continue;
2178 if (\is_array(end($joined))) {
2179 $joined[] = $part;
2180 } else {
2181 $joined[\count($joined) - 1] .= $part;
2185 return $joined;
2189 * Compile selector to string; self(&) should have been replaced by now
2191 * @param string|array $selector
2193 * @return string
2195 protected function compileSelector($selector)
2197 if (! \is_array($selector)) {
2198 return $selector; // media and the like
2201 return implode(
2202 ' ',
2203 array_map(
2204 [$this, 'compileSelectorPart'],
2205 $selector
2211 * Compile selector part
2213 * @param array $piece
2215 * @return string
2217 protected function compileSelectorPart($piece)
2219 foreach ($piece as &$p) {
2220 if (! \is_array($p)) {
2221 continue;
2224 switch ($p[0]) {
2225 case Type::T_SELF:
2226 $p = '&';
2227 break;
2229 default:
2230 $p = $this->compileValue($p);
2231 break;
2235 return implode($piece);
2239 * Has selector placeholder?
2241 * @param array $selector
2243 * @return bool
2245 protected function hasSelectorPlaceholder($selector)
2247 if (! \is_array($selector)) {
2248 return false;
2251 foreach ($selector as $parts) {
2252 foreach ($parts as $part) {
2253 if (\strlen($part) && '%' === $part[0]) {
2254 return true;
2259 return false;
2263 * @param string $name
2265 * @return void
2267 protected function pushCallStack($name = '')
2269 $this->callStack[] = [
2270 'n' => $name,
2271 Parser::SOURCE_INDEX => $this->sourceIndex,
2272 Parser::SOURCE_LINE => $this->sourceLine,
2273 Parser::SOURCE_COLUMN => $this->sourceColumn
2276 // infinite calling loop
2277 if (\count($this->callStack) > 25000) {
2278 // not displayed but you can var_dump it to deep debug
2279 $msg = $this->callStackMessage(true, 100);
2280 $msg = 'Infinite calling loop';
2282 throw $this->error($msg);
2287 * @return void
2289 protected function popCallStack()
2291 array_pop($this->callStack);
2295 * Compile children and return result
2297 * @param array $stms
2298 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2299 * @param string $traceName
2301 * @return array|Number|null
2303 protected function compileChildren($stms, OutputBlock $out, $traceName = '')
2305 $this->pushCallStack($traceName);
2307 foreach ($stms as $stm) {
2308 $ret = $this->compileChild($stm, $out);
2310 if (isset($ret)) {
2311 $this->popCallStack();
2313 return $ret;
2317 $this->popCallStack();
2319 return null;
2323 * Compile children and throw exception if unexpected `@return`
2325 * @param array $stms
2326 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2327 * @param \ScssPhp\ScssPhp\Block $selfParent
2328 * @param string $traceName
2330 * @return void
2332 * @throws \Exception
2334 protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
2336 $this->pushCallStack($traceName);
2338 foreach ($stms as $stm) {
2339 if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
2340 $stm[1]->selfParent = $selfParent;
2341 $ret = $this->compileChild($stm, $out);
2342 $stm[1]->selfParent = null;
2343 } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) {
2344 $stm['selfParent'] = $selfParent;
2345 $ret = $this->compileChild($stm, $out);
2346 unset($stm['selfParent']);
2347 } else {
2348 $ret = $this->compileChild($stm, $out);
2351 if (isset($ret)) {
2352 throw $this->error('@return may only be used within a function');
2356 $this->popCallStack();
2361 * evaluate media query : compile internal value keeping the structure unchanged
2363 * @param array $queryList
2365 * @return array
2367 protected function evaluateMediaQuery($queryList)
2369 static $parser = null;
2371 $outQueryList = [];
2373 foreach ($queryList as $kql => $query) {
2374 $shouldReparse = false;
2376 foreach ($query as $kq => $q) {
2377 for ($i = 1; $i < \count($q); $i++) {
2378 $value = $this->compileValue($q[$i]);
2380 // the parser had no mean to know if media type or expression if it was an interpolation
2381 // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
2382 if (
2383 $q[0] == Type::T_MEDIA_TYPE &&
2384 (strpos($value, '(') !== false ||
2385 strpos($value, ')') !== false ||
2386 strpos($value, ':') !== false ||
2387 strpos($value, ',') !== false)
2389 $shouldReparse = true;
2392 $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
2396 if ($shouldReparse) {
2397 if (\is_null($parser)) {
2398 $parser = $this->parserFactory(__METHOD__);
2401 $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2402 $queryString = reset($queryString);
2404 if (strpos($queryString, '@media ') === 0) {
2405 $queryString = substr($queryString, 7);
2406 $queries = [];
2408 if ($parser->parseMediaQueryList($queryString, $queries)) {
2409 $queries = $this->evaluateMediaQuery($queries[2]);
2411 while (\count($queries)) {
2412 $outQueryList[] = array_shift($queries);
2415 continue;
2420 $outQueryList[] = $queryList[$kql];
2423 return $outQueryList;
2427 * Compile media query
2429 * @param array $queryList
2431 * @return string[]
2433 protected function compileMediaQuery($queryList)
2435 $start = '@media ';
2436 $default = trim($start);
2437 $out = [];
2438 $current = '';
2440 foreach ($queryList as $query) {
2441 $type = null;
2442 $parts = [];
2444 $mediaTypeOnly = true;
2446 foreach ($query as $q) {
2447 if ($q[0] !== Type::T_MEDIA_TYPE) {
2448 $mediaTypeOnly = false;
2449 break;
2453 foreach ($query as $q) {
2454 switch ($q[0]) {
2455 case Type::T_MEDIA_TYPE:
2456 $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
2458 // combining not and anything else than media type is too risky and should be avoided
2459 if (! $mediaTypeOnly) {
2460 if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
2461 if ($type) {
2462 array_unshift($parts, implode(' ', array_filter($type)));
2465 if (! empty($parts)) {
2466 if (\strlen($current)) {
2467 $current .= $this->formatter->tagSeparator;
2470 $current .= implode(' and ', $parts);
2473 if ($current) {
2474 $out[] = $start . $current;
2477 $current = '';
2478 $type = null;
2479 $parts = [];
2483 if ($newType === ['all'] && $default) {
2484 $default = $start . 'all';
2487 // all can be safely ignored and mixed with whatever else
2488 if ($newType !== ['all']) {
2489 if ($type) {
2490 $type = $this->mergeMediaTypes($type, $newType);
2492 if (empty($type)) {
2493 // merge failed : ignore this query that is not valid, skip to the next one
2494 $parts = [];
2495 $default = ''; // if everything fail, no @media at all
2496 continue 3;
2498 } else {
2499 $type = $newType;
2502 break;
2504 case Type::T_MEDIA_EXPRESSION:
2505 if (isset($q[2])) {
2506 $parts[] = '('
2507 . $this->compileValue($q[1])
2508 . $this->formatter->assignSeparator
2509 . $this->compileValue($q[2])
2510 . ')';
2511 } else {
2512 $parts[] = '('
2513 . $this->compileValue($q[1])
2514 . ')';
2516 break;
2518 case Type::T_MEDIA_VALUE:
2519 $parts[] = $this->compileValue($q[1]);
2520 break;
2524 if ($type) {
2525 array_unshift($parts, implode(' ', array_filter($type)));
2528 if (! empty($parts)) {
2529 if (\strlen($current)) {
2530 $current .= $this->formatter->tagSeparator;
2533 $current .= implode(' and ', $parts);
2537 if ($current) {
2538 $out[] = $start . $current;
2541 // no @media type except all, and no conflict?
2542 if (! $out && $default) {
2543 $out[] = $default;
2546 return $out;
2550 * Merge direct relationships between selectors
2552 * @param array $selectors1
2553 * @param array $selectors2
2555 * @return array
2557 protected function mergeDirectRelationships($selectors1, $selectors2)
2559 if (empty($selectors1) || empty($selectors2)) {
2560 return array_merge($selectors1, $selectors2);
2563 $part1 = end($selectors1);
2564 $part2 = end($selectors2);
2566 if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2567 return array_merge($selectors1, $selectors2);
2570 $merged = [];
2572 do {
2573 $part1 = array_pop($selectors1);
2574 $part2 = array_pop($selectors2);
2576 if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2577 if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2578 array_unshift($merged, [$part1[0] . $part2[0]]);
2579 $merged = array_merge($selectors1, $selectors2, $merged);
2580 } else {
2581 $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2584 break;
2587 array_unshift($merged, $part1);
2588 } while (! empty($selectors1) && ! empty($selectors2));
2590 return $merged;
2594 * Merge media types
2596 * @param array $type1
2597 * @param array $type2
2599 * @return array|null
2601 protected function mergeMediaTypes($type1, $type2)
2603 if (empty($type1)) {
2604 return $type2;
2607 if (empty($type2)) {
2608 return $type1;
2611 if (\count($type1) > 1) {
2612 $m1 = strtolower($type1[0]);
2613 $t1 = strtolower($type1[1]);
2614 } else {
2615 $m1 = '';
2616 $t1 = strtolower($type1[0]);
2619 if (\count($type2) > 1) {
2620 $m2 = strtolower($type2[0]);
2621 $t2 = strtolower($type2[1]);
2622 } else {
2623 $m2 = '';
2624 $t2 = strtolower($type2[0]);
2627 if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2628 if ($t1 === $t2) {
2629 return null;
2632 return [
2633 $m1 === Type::T_NOT ? $m2 : $m1,
2634 $m1 === Type::T_NOT ? $t2 : $t1,
2638 if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2639 // CSS has no way of representing "neither screen nor print"
2640 if ($t1 !== $t2) {
2641 return null;
2644 return [Type::T_NOT, $t1];
2647 if ($t1 !== $t2) {
2648 return null;
2651 // t1 == t2, neither m1 nor m2 are "not"
2652 return [empty($m1) ? $m2 : $m1, $t1];
2656 * Compile import; returns true if the value was something that could be imported
2658 * @param array $rawPath
2659 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2660 * @param bool $once
2662 * @return bool
2664 protected function compileImport($rawPath, OutputBlock $out, $once = false)
2666 if ($rawPath[0] === Type::T_STRING) {
2667 $path = $this->compileStringContent($rawPath);
2669 if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) {
2670 $this->registerImport($this->currentDirectory, $path, $filePath);
2672 if (! $once || ! \in_array($filePath, $this->importedFiles)) {
2673 $this->importFile($filePath, $out);
2674 $this->importedFiles[] = $filePath;
2677 return true;
2680 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2682 return false;
2685 if ($rawPath[0] === Type::T_LIST) {
2686 // handle a list of strings
2687 if (\count($rawPath[2]) === 0) {
2688 return false;
2691 foreach ($rawPath[2] as $path) {
2692 if ($path[0] !== Type::T_STRING) {
2693 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2695 return false;
2699 foreach ($rawPath[2] as $path) {
2700 $this->compileImport($path, $out, $once);
2703 return true;
2706 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2708 return false;
2712 * @param array $rawPath
2713 * @return string
2714 * @throws CompilerException
2716 protected function compileImportPath($rawPath)
2718 $path = $this->compileValue($rawPath);
2720 // case url() without quotes : suppress \r \n remaining in the path
2721 // if this is a real string there can not be CR or LF char
2722 if (strpos($path, 'url(') === 0) {
2723 $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2724 } else {
2725 // if this is a file name in a string, spaces should be escaped
2726 $path = $this->reduce($rawPath);
2727 $path = $this->escapeImportPathString($path);
2728 $path = $this->compileValue($path);
2731 return $path;
2735 * @param array $path
2736 * @return array
2737 * @throws CompilerException
2739 protected function escapeImportPathString($path)
2741 switch ($path[0]) {
2742 case Type::T_LIST:
2743 foreach ($path[2] as $k => $v) {
2744 $path[2][$k] = $this->escapeImportPathString($v);
2746 break;
2747 case Type::T_STRING:
2748 if ($path[1]) {
2749 $path = $this->compileValue($path);
2750 $path = str_replace(' ', '\\ ', $path);
2751 $path = [Type::T_KEYWORD, $path];
2753 break;
2756 return $path;
2760 * Append a root directive like @import or @charset as near as the possible from the source code
2761 * (keeping before comments, @import and @charset coming before in the source code)
2763 * @param string $line
2764 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2765 * @param array $allowed
2767 * @return void
2769 protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2771 $root = $out;
2773 while ($root->parent) {
2774 $root = $root->parent;
2777 $i = 0;
2779 while ($i < \count($root->children)) {
2780 if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2781 break;
2784 $i++;
2787 // remove incompatible children from the bottom of the list
2788 $saveChildren = [];
2790 while ($i < \count($root->children)) {
2791 $saveChildren[] = array_pop($root->children);
2794 // insert the directive as a comment
2795 $child = $this->makeOutputBlock(Type::T_COMMENT);
2796 $child->lines[] = $line;
2797 $child->sourceName = $this->sourceNames[$this->sourceIndex] ?: '(stdin)';
2798 $child->sourceLine = $this->sourceLine;
2799 $child->sourceColumn = $this->sourceColumn;
2801 $root->children[] = $child;
2803 // repush children
2804 while (\count($saveChildren)) {
2805 $root->children[] = array_pop($saveChildren);
2810 * Append lines to the current output block:
2811 * directly to the block or through a child if necessary
2813 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2814 * @param string $type
2815 * @param string $line
2817 * @return void
2819 protected function appendOutputLine(OutputBlock $out, $type, $line)
2821 $outWrite = &$out;
2823 // check if it's a flat output or not
2824 if (\count($out->children)) {
2825 $lastChild = &$out->children[\count($out->children) - 1];
2827 if (
2828 $lastChild->depth === $out->depth &&
2829 \is_null($lastChild->selectors) &&
2830 ! \count($lastChild->children)
2832 $outWrite = $lastChild;
2833 } else {
2834 $nextLines = $this->makeOutputBlock($type);
2835 $nextLines->parent = $out;
2836 $nextLines->depth = $out->depth;
2838 $out->children[] = $nextLines;
2839 $outWrite = &$nextLines;
2843 $outWrite->lines[] = $line;
2847 * Compile child; returns a value to halt execution
2849 * @param array $child
2850 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2852 * @return array|Number|null
2854 protected function compileChild($child, OutputBlock $out)
2856 if (isset($child[Parser::SOURCE_LINE])) {
2857 $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2858 $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2859 $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2860 } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
2861 $this->sourceIndex = $child[1]->sourceIndex;
2862 $this->sourceLine = $child[1]->sourceLine;
2863 $this->sourceColumn = $child[1]->sourceColumn;
2864 } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2865 $this->sourceLine = $out->sourceLine;
2866 $sourceIndex = array_search($out->sourceName, $this->sourceNames);
2867 $this->sourceColumn = $out->sourceColumn;
2869 if ($sourceIndex === false) {
2870 $sourceIndex = null;
2872 $this->sourceIndex = $sourceIndex;
2875 switch ($child[0]) {
2876 case Type::T_SCSSPHP_IMPORT_ONCE:
2877 $rawPath = $this->reduce($child[1]);
2879 $this->compileImport($rawPath, $out, true);
2880 break;
2882 case Type::T_IMPORT:
2883 $rawPath = $this->reduce($child[1]);
2885 $this->compileImport($rawPath, $out);
2886 break;
2888 case Type::T_DIRECTIVE:
2889 $this->compileDirective($child[1], $out);
2890 break;
2892 case Type::T_AT_ROOT:
2893 $this->compileAtRoot($child[1]);
2894 break;
2896 case Type::T_MEDIA:
2897 $this->compileMedia($child[1]);
2898 break;
2900 case Type::T_BLOCK:
2901 $this->compileBlock($child[1]);
2902 break;
2904 case Type::T_CHARSET:
2905 break;
2907 case Type::T_CUSTOM_PROPERTY:
2908 list(, $name, $value) = $child;
2909 $compiledName = $this->compileValue($name);
2911 // if the value reduces to null from something else then
2912 // the property should be discarded
2913 if ($value[0] !== Type::T_NULL) {
2914 $value = $this->reduce($value);
2916 if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2917 break;
2921 $compiledValue = $this->compileValue($value);
2923 $line = $this->formatter->customProperty(
2924 $compiledName,
2925 $compiledValue
2928 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2929 break;
2931 case Type::T_ASSIGN:
2932 list(, $name, $value) = $child;
2934 if ($name[0] === Type::T_VARIABLE) {
2935 $flags = isset($child[3]) ? $child[3] : [];
2936 $isDefault = \in_array('!default', $flags);
2937 $isGlobal = \in_array('!global', $flags);
2939 if ($isGlobal) {
2940 $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2941 break;
2944 $shouldSet = $isDefault &&
2945 (\is_null($result = $this->get($name[1], false)) ||
2946 $result === static::$null);
2948 if (! $isDefault || $shouldSet) {
2949 $this->set($name[1], $this->reduce($value), true, null, $value);
2951 break;
2954 $compiledName = $this->compileValue($name);
2956 // handle shorthand syntaxes : size / line-height...
2957 if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2958 if ($value[0] === Type::T_VARIABLE) {
2959 // if the font value comes from variable, the content is already reduced
2960 // (i.e., formulas were already calculated), so we need the original unreduced value
2961 $value = $this->get($value[1], true, null, true);
2964 $shorthandValue=&$value;
2966 $shorthandDividerNeedsUnit = false;
2967 $maxListElements = null;
2968 $maxShorthandDividers = 1;
2970 switch ($compiledName) {
2971 case 'border-radius':
2972 $maxListElements = 4;
2973 $shorthandDividerNeedsUnit = true;
2974 break;
2977 if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
2978 // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2979 // we need to handle the first list element
2980 $shorthandValue=&$value[2][0];
2983 if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2984 $revert = true;
2986 if ($shorthandDividerNeedsUnit) {
2987 $divider = $shorthandValue[3];
2989 if (\is_array($divider)) {
2990 $divider = $this->reduce($divider, true);
2993 if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
2994 $revert = false;
2998 if ($revert) {
2999 $shorthandValue = $this->expToString($shorthandValue);
3001 } elseif ($shorthandValue[0] === Type::T_LIST) {
3002 foreach ($shorthandValue[2] as &$item) {
3003 if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
3004 if ($maxShorthandDividers > 0) {
3005 $revert = true;
3007 // if the list of values is too long, this has to be a shorthand,
3008 // otherwise it could be a real division
3009 if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
3010 if ($shorthandDividerNeedsUnit) {
3011 $divider = $item[3];
3013 if (\is_array($divider)) {
3014 $divider = $this->reduce($divider, true);
3017 if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
3018 $revert = false;
3023 if ($revert) {
3024 $item = $this->expToString($item);
3025 $maxShorthandDividers--;
3033 // if the value reduces to null from something else then
3034 // the property should be discarded
3035 if ($value[0] !== Type::T_NULL) {
3036 $value = $this->reduce($value);
3038 if ($value[0] === Type::T_NULL || $value === static::$nullString) {
3039 break;
3043 $compiledValue = $this->compileValue($value);
3045 // ignore empty value
3046 if (\strlen($compiledValue)) {
3047 $line = $this->formatter->property(
3048 $compiledName,
3049 $compiledValue
3051 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
3053 break;
3055 case Type::T_COMMENT:
3056 if ($out->type === Type::T_ROOT) {
3057 $this->compileComment($child);
3058 break;
3061 $line = $this->compileCommentValue($child, true);
3062 $this->appendOutputLine($out, Type::T_COMMENT, $line);
3063 break;
3065 case Type::T_MIXIN:
3066 case Type::T_FUNCTION:
3067 list(, $block) = $child;
3068 assert($block instanceof CallableBlock);
3069 // the block need to be able to go up to it's parent env to resolve vars
3070 $block->parentEnv = $this->getStoreEnv();
3071 $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
3072 break;
3074 case Type::T_EXTEND:
3075 foreach ($child[1] as $sel) {
3076 $replacedSel = $this->replaceSelfSelector($sel);
3078 if ($replacedSel !== $sel) {
3079 throw $this->error('Parent selectors aren\'t allowed here.');
3082 $results = $this->evalSelectors([$sel]);
3084 foreach ($results as $result) {
3085 if (\count($result) !== 1) {
3086 throw $this->error('complex selectors may not be extended.');
3089 // only use the first one
3090 $result = $result[0];
3091 $selectors = $out->selectors;
3093 if (! $selectors && isset($child['selfParent'])) {
3094 $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
3097 if (\count($result) > 1) {
3098 $replacement = implode(', ', $result);
3099 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3100 $line = $this->sourceLine;
3102 $message = <<<EOL
3103 on line $line of $fname:
3104 Compound selectors may no longer be extended.
3105 Consider `@extend $replacement` instead.
3106 See http://bit.ly/ExtendCompound for details.
3107 EOL;
3109 $this->logger->warn($message);
3112 $this->pushExtends($result, $selectors, $child);
3115 break;
3117 case Type::T_IF:
3118 list(, $if) = $child;
3119 assert($if instanceof IfBlock);
3121 if ($this->isTruthy($this->reduce($if->cond, true))) {
3122 return $this->compileChildren($if->children, $out);
3125 foreach ($if->cases as $case) {
3126 if (
3127 $case instanceof ElseBlock ||
3128 $case instanceof ElseifBlock && $this->isTruthy($this->reduce($case->cond))
3130 return $this->compileChildren($case->children, $out);
3133 break;
3135 case Type::T_EACH:
3136 list(, $each) = $child;
3137 assert($each instanceof EachBlock);
3139 $list = $this->coerceList($this->reduce($each->list), ',', true);
3141 $this->pushEnv();
3143 foreach ($list[2] as $item) {
3144 if (\count($each->vars) === 1) {
3145 $this->set($each->vars[0], $item, true);
3146 } else {
3147 list(,, $values) = $this->coerceList($item);
3149 foreach ($each->vars as $i => $var) {
3150 $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
3154 $ret = $this->compileChildren($each->children, $out);
3156 if ($ret) {
3157 $store = $this->env->store;
3158 $this->popEnv();
3159 $this->backPropagateEnv($store, $each->vars);
3161 return $ret;
3164 $store = $this->env->store;
3165 $this->popEnv();
3166 $this->backPropagateEnv($store, $each->vars);
3168 break;
3170 case Type::T_WHILE:
3171 list(, $while) = $child;
3172 assert($while instanceof WhileBlock);
3174 while ($this->isTruthy($this->reduce($while->cond, true))) {
3175 $ret = $this->compileChildren($while->children, $out);
3177 if ($ret) {
3178 return $ret;
3181 break;
3183 case Type::T_FOR:
3184 list(, $for) = $child;
3185 assert($for instanceof ForBlock);
3187 $startNumber = $this->assertNumber($this->reduce($for->start, true));
3188 $endNumber = $this->assertNumber($this->reduce($for->end, true));
3190 $start = $this->assertInteger($startNumber);
3192 $numeratorUnits = $startNumber->getNumeratorUnits();
3193 $denominatorUnits = $startNumber->getDenominatorUnits();
3195 $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits));
3197 $d = $start < $end ? 1 : -1;
3199 $this->pushEnv();
3201 for (;;) {
3202 if (
3203 (! $for->until && $start - $d == $end) ||
3204 ($for->until && $start == $end)
3206 break;
3209 $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
3210 $start += $d;
3212 $ret = $this->compileChildren($for->children, $out);
3214 if ($ret) {
3215 $store = $this->env->store;
3216 $this->popEnv();
3217 $this->backPropagateEnv($store, [$for->var]);
3219 return $ret;
3223 $store = $this->env->store;
3224 $this->popEnv();
3225 $this->backPropagateEnv($store, [$for->var]);
3227 break;
3229 case Type::T_RETURN:
3230 return $this->reduce($child[1], true);
3232 case Type::T_NESTED_PROPERTY:
3233 $this->compileNestedPropertiesBlock($child[1], $out);
3234 break;
3236 case Type::T_INCLUDE:
3237 // including a mixin
3238 list(, $name, $argValues, $content, $argUsing) = $child;
3240 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
3242 if (! $mixin) {
3243 throw $this->error("Undefined mixin $name");
3246 assert($mixin instanceof CallableBlock);
3248 $callingScope = $this->getStoreEnv();
3250 // push scope, apply args
3251 $this->pushEnv();
3252 $this->env->depth--;
3254 // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
3255 // and assign this fake parent to childs
3256 $selfParent = null;
3258 if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
3259 $selfParent = $child['selfParent'];
3260 } else {
3261 $parentSelectors = $this->multiplySelectors($this->env);
3263 if ($parentSelectors) {
3264 $parent = new Block();
3265 $parent->selectors = $parentSelectors;
3267 foreach ($mixin->children as $k => $child) {
3268 if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
3269 $mixin->children[$k][1]->parent = $parent;
3275 // clone the stored content to not have its scope spoiled by a further call to the same mixin
3276 // i.e., recursive @include of the same mixin
3277 if (isset($content)) {
3278 $copyContent = clone $content;
3279 $copyContent->scope = clone $callingScope;
3281 $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
3282 } else {
3283 $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
3286 // save the "using" argument list for applying it to when "@content" is invoked
3287 if (isset($argUsing)) {
3288 $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
3289 } else {
3290 $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
3293 if (isset($mixin->args)) {
3294 $this->applyArguments($mixin->args, $argValues);
3297 $this->env->marker = 'mixin';
3299 if (! empty($mixin->parentEnv)) {
3300 $this->env->declarationScopeParent = $mixin->parentEnv;
3301 } else {
3302 throw $this->error("@mixin $name() without parentEnv");
3305 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
3307 $this->popEnv();
3308 break;
3310 case Type::T_MIXIN_CONTENT:
3311 $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
3312 $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
3313 $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env);
3314 $argContent = $child[1];
3316 if (! $content) {
3317 break;
3320 $storeEnv = $this->storeEnv;
3321 $varsUsing = [];
3323 if (isset($argUsing) && isset($argContent)) {
3324 // Get the arguments provided for the content with the names provided in the "using" argument list
3325 $this->storeEnv = null;
3326 $varsUsing = $this->applyArguments($argUsing, $argContent, false);
3329 // restore the scope from the @content
3330 $this->storeEnv = $content->scope;
3332 // append the vars from using if any
3333 foreach ($varsUsing as $name => $val) {
3334 $this->set($name, $val, true, $this->storeEnv);
3337 $this->compileChildrenNoReturn($content->children, $out);
3339 $this->storeEnv = $storeEnv;
3340 break;
3342 case Type::T_DEBUG:
3343 list(, $value) = $child;
3345 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3346 $line = $this->sourceLine;
3347 $value = $this->compileDebugValue($value);
3349 $this->logger->debug("$fname:$line DEBUG: $value");
3350 break;
3352 case Type::T_WARN:
3353 list(, $value) = $child;
3355 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3356 $line = $this->sourceLine;
3357 $value = $this->compileDebugValue($value);
3359 $this->logger->warn("$value\n on line $line of $fname");
3360 break;
3362 case Type::T_ERROR:
3363 list(, $value) = $child;
3365 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3366 $line = $this->sourceLine;
3367 $value = $this->compileValue($this->reduce($value, true));
3369 throw $this->error("File $fname on line $line ERROR: $value\n");
3371 default:
3372 throw $this->error("unknown child type: $child[0]");
3377 * Reduce expression to string
3379 * @param array $exp
3380 * @param bool $keepParens
3382 * @return array
3384 protected function expToString($exp, $keepParens = false)
3386 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3388 $content = [];
3390 if ($keepParens && $inParens) {
3391 $content[] = '(';
3394 $content[] = $this->reduce($left);
3396 if ($whiteLeft) {
3397 $content[] = ' ';
3400 $content[] = $op;
3402 if ($whiteRight) {
3403 $content[] = ' ';
3406 $content[] = $this->reduce($right);
3408 if ($keepParens && $inParens) {
3409 $content[] = ')';
3412 return [Type::T_STRING, '', $content];
3416 * Is truthy?
3418 * @param array|Number $value
3420 * @return bool
3422 public function isTruthy($value)
3424 return $value !== static::$false && $value !== static::$null;
3428 * Is the value a direct relationship combinator?
3430 * @param string $value
3432 * @return bool
3434 protected function isImmediateRelationshipCombinator($value)
3436 return $value === '>' || $value === '+' || $value === '~';
3440 * Should $value cause its operand to eval
3442 * @param array $value
3444 * @return bool
3446 protected function shouldEval($value)
3448 switch ($value[0]) {
3449 case Type::T_EXPRESSION:
3450 if ($value[1] === '/') {
3451 return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
3454 // fall-thru
3455 case Type::T_VARIABLE:
3456 case Type::T_FUNCTION_CALL:
3457 return true;
3460 return false;
3464 * Reduce value
3466 * @param array|Number $value
3467 * @param bool $inExp
3469 * @return array|Number
3471 protected function reduce($value, $inExp = false)
3473 if ($value instanceof Number) {
3474 return $value;
3477 switch ($value[0]) {
3478 case Type::T_EXPRESSION:
3479 list(, $op, $left, $right, $inParens) = $value;
3481 $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
3482 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
3484 $left = $this->reduce($left, true);
3486 if ($op !== 'and' && $op !== 'or') {
3487 $right = $this->reduce($right, true);
3490 // special case: looks like css shorthand
3491 if (
3492 $opName == 'div' && ! $inParens && ! $inExp &&
3493 (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
3494 ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3496 return $this->expToString($value);
3499 $left = $this->coerceForExpression($left);
3500 $right = $this->coerceForExpression($right);
3501 $ltype = $left[0];
3502 $rtype = $right[0];
3504 $ucOpName = ucfirst($opName);
3505 $ucLType = ucfirst($ltype);
3506 $ucRType = ucfirst($rtype);
3508 // this tries:
3509 // 1. op[op name][left type][right type]
3510 // 2. op[left type][right type] (passing the op as first arg
3511 // 3. op[op name]
3512 $fn = "op${ucOpName}${ucLType}${ucRType}";
3514 if (
3515 \is_callable([$this, $fn]) ||
3516 (($fn = "op${ucLType}${ucRType}") &&
3517 \is_callable([$this, $fn]) &&
3518 $passOp = true) ||
3519 (($fn = "op${ucOpName}") &&
3520 \is_callable([$this, $fn]) &&
3521 $genOp = true)
3523 $shouldEval = $inParens || $inExp;
3525 if (isset($passOp)) {
3526 $out = $this->$fn($op, $left, $right, $shouldEval);
3527 } else {
3528 $out = $this->$fn($left, $right, $shouldEval);
3531 if (isset($out)) {
3532 return $out;
3536 return $this->expToString($value);
3538 case Type::T_UNARY:
3539 list(, $op, $exp, $inParens) = $value;
3541 $inExp = $inExp || $this->shouldEval($exp);
3542 $exp = $this->reduce($exp);
3544 if ($exp instanceof Number) {
3545 switch ($op) {
3546 case '+':
3547 return $exp;
3549 case '-':
3550 return $exp->unaryMinus();
3554 if ($op === 'not') {
3555 if ($inExp || $inParens) {
3556 if ($exp === static::$false || $exp === static::$null) {
3557 return static::$true;
3560 return static::$false;
3563 $op = $op . ' ';
3566 return [Type::T_STRING, '', [$op, $exp]];
3568 case Type::T_VARIABLE:
3569 return $this->reduce($this->get($value[1]));
3571 case Type::T_LIST:
3572 foreach ($value[2] as &$item) {
3573 $item = $this->reduce($item);
3575 unset($item);
3577 if (isset($value[3]) && \is_array($value[3])) {
3578 foreach ($value[3] as &$item) {
3579 $item = $this->reduce($item);
3581 unset($item);
3584 return $value;
3586 case Type::T_MAP:
3587 foreach ($value[1] as &$item) {
3588 $item = $this->reduce($item);
3591 foreach ($value[2] as &$item) {
3592 $item = $this->reduce($item);
3595 return $value;
3597 case Type::T_STRING:
3598 foreach ($value[2] as &$item) {
3599 if (\is_array($item) || $item instanceof Number) {
3600 $item = $this->reduce($item);
3604 return $value;
3606 case Type::T_INTERPOLATE:
3607 $value[1] = $this->reduce($value[1]);
3609 if ($inExp) {
3610 return [Type::T_KEYWORD, $this->compileValue($value, false)];
3613 return $value;
3615 case Type::T_FUNCTION_CALL:
3616 return $this->fncall($value[1], $value[2]);
3618 case Type::T_SELF:
3619 $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
3620 $selfSelector = $this->multiplySelectors($this->env, $selfParent);
3621 $selfSelector = $this->collapseSelectorsAsList($selfSelector);
3623 return $selfSelector;
3625 default:
3626 return $value;
3631 * Function caller
3633 * @param string|array $functionReference
3634 * @param array $argValues
3636 * @return array|Number
3638 protected function fncall($functionReference, $argValues)
3640 // a string means this is a static hard reference coming from the parsing
3641 if (is_string($functionReference)) {
3642 $name = $functionReference;
3644 $functionReference = $this->getFunctionReference($name);
3645 if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3646 $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
3650 // a function type means we just want a plain css function call
3651 if ($functionReference[0] === Type::T_FUNCTION) {
3652 // for CSS functions, simply flatten the arguments into a list
3653 $listArgs = [];
3655 foreach ((array) $argValues as $arg) {
3656 if (empty($arg[0]) || count($argValues) === 1) {
3657 $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
3661 return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
3664 if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3665 return static::$defaultValue;
3669 switch ($functionReference[1]) {
3670 // SCSS @function
3671 case 'scss':
3672 return $this->callScssFunction($functionReference[3], $argValues);
3674 // native PHP functions
3675 case 'user':
3676 case 'native':
3677 list(,,$name, $fn, $prototype) = $functionReference;
3679 // special cases of css valid functions min/max
3680 $name = strtolower($name);
3681 if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) {
3682 $cssFunction = $this->cssValidArg(
3683 [Type::T_FUNCTION_CALL, $name, $argValues],
3684 ['min', 'max', 'calc', 'env', 'var']
3686 if ($cssFunction !== false) {
3687 return $cssFunction;
3690 $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
3692 if (! isset($returnValue)) {
3693 return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
3696 return $returnValue;
3698 default:
3699 return static::$defaultValue;
3704 * @param array|Number $arg
3705 * @param string[] $allowed_function
3706 * @param bool $inFunction
3708 * @return array|Number|false
3710 protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3712 if ($arg instanceof Number) {
3713 return $this->stringifyFncallArgs($arg);
3716 switch ($arg[0]) {
3717 case Type::T_INTERPOLATE:
3718 return [Type::T_KEYWORD, $this->CompileValue($arg)];
3720 case Type::T_FUNCTION:
3721 if (! \in_array($arg[1], $allowed_function)) {
3722 return false;
3724 if ($arg[2][0] === Type::T_LIST) {
3725 foreach ($arg[2][2] as $k => $subarg) {
3726 $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
3727 if ($arg[2][2][$k] === false) {
3728 return false;
3732 return $arg;
3734 case Type::T_FUNCTION_CALL:
3735 if (! \in_array($arg[1], $allowed_function)) {
3736 return false;
3738 $cssArgs = [];
3739 foreach ($arg[2] as $argValue) {
3740 if ($argValue === static::$null) {
3741 return false;
3743 $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3744 if (empty($argValue[0]) && $cssArg !== false) {
3745 $cssArgs[] = [$argValue[0], $cssArg];
3746 } else {
3747 return false;
3751 return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
3753 case Type::T_STRING:
3754 case Type::T_KEYWORD:
3755 if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
3756 return false;
3758 return $this->stringifyFncallArgs($arg);
3760 case Type::T_LIST:
3761 if (!$inFunction) {
3762 return false;
3764 if (empty($arg['enclosing']) and $arg[1] === '') {
3765 foreach ($arg[2] as $k => $subarg) {
3766 $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
3767 if ($arg[2][$k] === false) {
3768 return false;
3771 $arg[0] = Type::T_STRING;
3772 return $arg;
3774 return false;
3776 case Type::T_EXPRESSION:
3777 if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
3778 return false;
3780 $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
3781 $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
3782 if ($arg[2] === false || $arg[3] === false) {
3783 return false;
3785 return $this->expToString($arg, true);
3787 case Type::T_VARIABLE:
3788 case Type::T_SELF:
3789 default:
3790 return false;
3796 * Reformat fncall arguments to proper css function output
3798 * @param array|Number $arg
3800 * @return array|Number
3802 protected function stringifyFncallArgs($arg)
3804 if ($arg instanceof Number) {
3805 return $arg;
3808 switch ($arg[0]) {
3809 case Type::T_LIST:
3810 foreach ($arg[2] as $k => $v) {
3811 $arg[2][$k] = $this->stringifyFncallArgs($v);
3813 break;
3815 case Type::T_EXPRESSION:
3816 if ($arg[1] === '/') {
3817 $arg[2] = $this->stringifyFncallArgs($arg[2]);
3818 $arg[3] = $this->stringifyFncallArgs($arg[3]);
3819 $arg[5] = $arg[6] = false; // no space around /
3820 $arg = $this->expToString($arg);
3822 break;
3824 case Type::T_FUNCTION_CALL:
3825 $name = strtolower($arg[1]);
3827 if (in_array($name, ['max', 'min', 'calc'])) {
3828 $args = $arg[2];
3829 $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
3831 break;
3834 return $arg;
3838 * Find a function reference
3839 * @param string $name
3840 * @param bool $safeCopy
3841 * @return array
3843 protected function getFunctionReference($name, $safeCopy = false)
3845 // SCSS @function
3846 if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3847 if ($safeCopy) {
3848 $func = clone $func;
3851 return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
3854 // native PHP functions
3856 // try to find a native lib function
3857 $normalizedName = $this->normalizeName($name);
3859 if (isset($this->userFunctions[$normalizedName])) {
3860 // see if we can find a user function
3861 list($f, $prototype) = $this->userFunctions[$normalizedName];
3863 return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
3866 $lowercasedName = strtolower($normalizedName);
3868 // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase
3869 // to avoid the deprecation warning about the wrong case being used.
3870 if ($lowercasedName === 'min' || $lowercasedName === 'max') {
3871 $normalizedName = $lowercasedName;
3874 if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
3875 $libName = $f[1];
3876 $prototype = isset(static::$$libName) ? static::$$libName : null;
3878 // All core functions have a prototype defined. Not finding the
3879 // prototype can mean 2 things:
3880 // - the function comes from a child class (deprecated just after)
3881 // - the function was found with a different case, which relates to calling the
3882 // wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`),
3883 // because PHP method names are case-insensitive while property names are
3884 // case-sensitive.
3885 if ($prototype === null || strtolower($normalizedName) !== $normalizedName) {
3886 $r = new \ReflectionMethod($this, $libName);
3887 $actualLibName = $r->name;
3889 if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) {
3890 $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3));
3891 assert($kebabCaseName !== null);
3892 $originalName = strtolower($kebabCaseName);
3893 $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\".";
3894 @trigger_error($warning, E_USER_DEPRECATED);
3895 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
3896 $line = $this->sourceLine;
3897 Warn::deprecation("$warning\n on line $line of $fname");
3899 // Use the actual function definition
3900 $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null;
3901 $f[1] = $libName = $actualLibName;
3905 if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) {
3906 $r = new \ReflectionMethod($this, $libName);
3907 $declaringClass = $r->getDeclaringClass()->name;
3909 $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__;
3911 if ($needsWarning) {
3912 if (method_exists(__CLASS__, $libName)) {
3913 @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);
3914 } else {
3915 @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);
3920 return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3923 return static::$null;
3928 * Normalize name
3930 * @param string $name
3932 * @return string
3934 protected function normalizeName($name)
3936 return str_replace('-', '_', $name);
3940 * Normalize value
3942 * @internal
3944 * @param array|Number $value
3946 * @return array|Number
3948 public function normalizeValue($value)
3950 $value = $this->coerceForExpression($this->reduce($value));
3952 if ($value instanceof Number) {
3953 return $value;
3956 switch ($value[0]) {
3957 case Type::T_LIST:
3958 $value = $this->extractInterpolation($value);
3960 if ($value[0] !== Type::T_LIST) {
3961 return [Type::T_KEYWORD, $this->compileValue($value)];
3964 foreach ($value[2] as $key => $item) {
3965 $value[2][$key] = $this->normalizeValue($item);
3968 if (! empty($value['enclosing'])) {
3969 unset($value['enclosing']);
3972 if ($value[1] === '' && count($value[2]) > 1) {
3973 $value[1] = ' ';
3976 return $value;
3978 case Type::T_STRING:
3979 return [$value[0], '"', [$this->compileStringContent($value)]];
3981 case Type::T_INTERPOLATE:
3982 return [Type::T_KEYWORD, $this->compileValue($value)];
3984 default:
3985 return $value;
3990 * Add numbers
3992 * @param Number $left
3993 * @param Number $right
3995 * @return Number
3997 protected function opAddNumberNumber(Number $left, Number $right)
3999 return $left->plus($right);
4003 * Multiply numbers
4005 * @param Number $left
4006 * @param Number $right
4008 * @return Number
4010 protected function opMulNumberNumber(Number $left, Number $right)
4012 return $left->times($right);
4016 * Subtract numbers
4018 * @param Number $left
4019 * @param Number $right
4021 * @return Number
4023 protected function opSubNumberNumber(Number $left, Number $right)
4025 return $left->minus($right);
4029 * Divide numbers
4031 * @param Number $left
4032 * @param Number $right
4034 * @return Number
4036 protected function opDivNumberNumber(Number $left, Number $right)
4038 return $left->dividedBy($right);
4042 * Mod numbers
4044 * @param Number $left
4045 * @param Number $right
4047 * @return Number
4049 protected function opModNumberNumber(Number $left, Number $right)
4051 return $left->modulo($right);
4055 * Add strings
4057 * @param array $left
4058 * @param array $right
4060 * @return array|null
4062 protected function opAdd($left, $right)
4064 if ($strLeft = $this->coerceString($left)) {
4065 if ($right[0] === Type::T_STRING) {
4066 $right[1] = '';
4069 $strLeft[2][] = $right;
4071 return $strLeft;
4074 if ($strRight = $this->coerceString($right)) {
4075 if ($left[0] === Type::T_STRING) {
4076 $left[1] = '';
4079 array_unshift($strRight[2], $left);
4081 return $strRight;
4084 return null;
4088 * Boolean and
4090 * @param array|Number $left
4091 * @param array|Number $right
4092 * @param bool $shouldEval
4094 * @return array|Number|null
4096 protected function opAnd($left, $right, $shouldEval)
4098 $truthy = ($left === static::$null || $right === static::$null) ||
4099 ($left === static::$false || $left === static::$true) &&
4100 ($right === static::$false || $right === static::$true);
4102 if (! $shouldEval) {
4103 if (! $truthy) {
4104 return null;
4108 if ($left !== static::$false && $left !== static::$null) {
4109 return $this->reduce($right, true);
4112 return $left;
4116 * Boolean or
4118 * @param array|Number $left
4119 * @param array|Number $right
4120 * @param bool $shouldEval
4122 * @return array|Number|null
4124 protected function opOr($left, $right, $shouldEval)
4126 $truthy = ($left === static::$null || $right === static::$null) ||
4127 ($left === static::$false || $left === static::$true) &&
4128 ($right === static::$false || $right === static::$true);
4130 if (! $shouldEval) {
4131 if (! $truthy) {
4132 return null;
4136 if ($left !== static::$false && $left !== static::$null) {
4137 return $left;
4140 return $this->reduce($right, true);
4144 * Compare colors
4146 * @param string $op
4147 * @param array $left
4148 * @param array $right
4150 * @return array
4152 protected function opColorColor($op, $left, $right)
4154 if ($op !== '==' && $op !== '!=') {
4155 $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
4156 . "Consider using Sass's color functions instead.";
4157 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
4158 $line = $this->sourceLine;
4160 Warn::deprecation("$warning\n on line $line of $fname");
4163 $out = [Type::T_COLOR];
4165 foreach ([1, 2, 3] as $i) {
4166 $lval = isset($left[$i]) ? $left[$i] : 0;
4167 $rval = isset($right[$i]) ? $right[$i] : 0;
4169 switch ($op) {
4170 case '+':
4171 $out[] = $lval + $rval;
4172 break;
4174 case '-':
4175 $out[] = $lval - $rval;
4176 break;
4178 case '*':
4179 $out[] = $lval * $rval;
4180 break;
4182 case '%':
4183 if ($rval == 0) {
4184 throw $this->error("color: Can't take modulo by zero");
4187 $out[] = $lval % $rval;
4188 break;
4190 case '/':
4191 if ($rval == 0) {
4192 throw $this->error("color: Can't divide by zero");
4195 $out[] = (int) ($lval / $rval);
4196 break;
4198 case '==':
4199 return $this->opEq($left, $right);
4201 case '!=':
4202 return $this->opNeq($left, $right);
4204 default:
4205 throw $this->error("color: unknown op $op");
4209 if (isset($left[4])) {
4210 $out[4] = $left[4];
4211 } elseif (isset($right[4])) {
4212 $out[4] = $right[4];
4215 return $this->fixColor($out);
4219 * Compare color and number
4221 * @param string $op
4222 * @param array $left
4223 * @param Number $right
4225 * @return array
4227 protected function opColorNumber($op, $left, Number $right)
4229 if ($op === '==') {
4230 return static::$false;
4233 if ($op === '!=') {
4234 return static::$true;
4237 $value = $right->getDimension();
4239 return $this->opColorColor(
4240 $op,
4241 $left,
4242 [Type::T_COLOR, $value, $value, $value]
4247 * Compare number and color
4249 * @param string $op
4250 * @param Number $left
4251 * @param array $right
4253 * @return array
4255 protected function opNumberColor($op, Number $left, $right)
4257 if ($op === '==') {
4258 return static::$false;
4261 if ($op === '!=') {
4262 return static::$true;
4265 $value = $left->getDimension();
4267 return $this->opColorColor(
4268 $op,
4269 [Type::T_COLOR, $value, $value, $value],
4270 $right
4275 * Compare number1 == number2
4277 * @param array|Number $left
4278 * @param array|Number $right
4280 * @return array
4282 protected function opEq($left, $right)
4284 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4285 $lStr[1] = '';
4286 $rStr[1] = '';
4288 $left = $this->compileValue($lStr);
4289 $right = $this->compileValue($rStr);
4292 return $this->toBool($left === $right);
4296 * Compare number1 != number2
4298 * @param array|Number $left
4299 * @param array|Number $right
4301 * @return array
4303 protected function opNeq($left, $right)
4305 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4306 $lStr[1] = '';
4307 $rStr[1] = '';
4309 $left = $this->compileValue($lStr);
4310 $right = $this->compileValue($rStr);
4313 return $this->toBool($left !== $right);
4317 * Compare number1 == number2
4319 * @param Number $left
4320 * @param Number $right
4322 * @return array
4324 protected function opEqNumberNumber(Number $left, Number $right)
4326 return $this->toBool($left->equals($right));
4330 * Compare number1 != number2
4332 * @param Number $left
4333 * @param Number $right
4335 * @return array
4337 protected function opNeqNumberNumber(Number $left, Number $right)
4339 return $this->toBool(!$left->equals($right));
4343 * Compare number1 >= number2
4345 * @param Number $left
4346 * @param Number $right
4348 * @return array
4350 protected function opGteNumberNumber(Number $left, Number $right)
4352 return $this->toBool($left->greaterThanOrEqual($right));
4356 * Compare number1 > number2
4358 * @param Number $left
4359 * @param Number $right
4361 * @return array
4363 protected function opGtNumberNumber(Number $left, Number $right)
4365 return $this->toBool($left->greaterThan($right));
4369 * Compare number1 <= number2
4371 * @param Number $left
4372 * @param Number $right
4374 * @return array
4376 protected function opLteNumberNumber(Number $left, Number $right)
4378 return $this->toBool($left->lessThanOrEqual($right));
4382 * Compare number1 < number2
4384 * @param Number $left
4385 * @param Number $right
4387 * @return array
4389 protected function opLtNumberNumber(Number $left, Number $right)
4391 return $this->toBool($left->lessThan($right));
4395 * Cast to boolean
4397 * @api
4399 * @param bool $thing
4401 * @return array
4403 public function toBool($thing)
4405 return $thing ? static::$true : static::$false;
4409 * Escape non printable chars in strings output as in dart-sass
4411 * @internal
4413 * @param string $string
4414 * @param bool $inKeyword
4416 * @return string
4418 public function escapeNonPrintableChars($string, $inKeyword = false)
4420 static $replacement = [];
4421 if (empty($replacement[$inKeyword])) {
4422 for ($i = 0; $i < 32; $i++) {
4423 if ($i !== 9 || $inKeyword) {
4424 $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
4428 $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
4429 // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
4430 if (strpos($string, chr(0)) !== false) {
4431 if (substr($string, -1) === chr(0)) {
4432 $string = substr($string, 0, -1);
4434 $string = str_replace(
4435 [chr(0) . '\\',chr(0) . ' '],
4436 [ '\\', ' '],
4437 $string
4439 if (strpos($string, chr(0)) !== false) {
4440 $parts = explode(chr(0), $string);
4441 $string = array_shift($parts);
4442 while (count($parts)) {
4443 $next = array_shift($parts);
4444 if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
4445 $string .= " ";
4447 $string .= $next;
4452 return $string;
4456 * Compiles a primitive value into a CSS property value.
4458 * Values in scssphp are typed by being wrapped in arrays, their format is
4459 * typically:
4461 * array(type, contents [, additional_contents]*)
4463 * The input is expected to be reduced. This function will not work on
4464 * things like expressions and variables.
4466 * @api
4468 * @param array|Number $value
4469 * @param bool $quote
4471 * @return string
4473 public function compileValue($value, $quote = true)
4475 $value = $this->reduce($value);
4477 if ($value instanceof Number) {
4478 return $value->output($this);
4481 switch ($value[0]) {
4482 case Type::T_KEYWORD:
4483 return $this->escapeNonPrintableChars($value[1], true);
4485 case Type::T_COLOR:
4486 // [1] - red component (either number for a %)
4487 // [2] - green component
4488 // [3] - blue component
4489 // [4] - optional alpha component
4490 list(, $r, $g, $b) = $value;
4492 $r = $this->compileRGBAValue($r);
4493 $g = $this->compileRGBAValue($g);
4494 $b = $this->compileRGBAValue($b);
4496 if (\count($value) === 5) {
4497 $alpha = $this->compileRGBAValue($value[4], true);
4499 if (! is_numeric($alpha) || $alpha < 1) {
4500 $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
4502 if (! \is_null($colorName)) {
4503 return $colorName;
4506 if (is_numeric($alpha)) {
4507 $a = new Number($alpha, '');
4508 } else {
4509 $a = $alpha;
4512 return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
4516 if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
4517 return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
4520 $colorName = Colors::RGBaToColorName($r, $g, $b);
4522 if (! \is_null($colorName)) {
4523 return $colorName;
4526 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
4528 // Converting hex color to short notation (e.g. #003399 to #039)
4529 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
4530 $h = '#' . $h[1] . $h[3] . $h[5];
4533 return $h;
4535 case Type::T_STRING:
4536 $content = $this->compileStringContent($value, $quote);
4538 if ($value[1] && $quote) {
4539 $content = str_replace('\\', '\\\\', $content);
4541 $content = $this->escapeNonPrintableChars($content);
4543 // force double quote as string quote for the output in certain cases
4544 if (
4545 $value[1] === "'" &&
4546 (strpos($content, '"') === false or strpos($content, "'") !== false)
4548 $value[1] = '"';
4549 } elseif (
4550 $value[1] === '"' &&
4551 (strpos($content, '"') !== false and strpos($content, "'") === false)
4553 $value[1] = "'";
4556 $content = str_replace($value[1], '\\' . $value[1], $content);
4559 return $value[1] . $content . $value[1];
4561 case Type::T_FUNCTION:
4562 $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : '';
4564 return "$value[1]($args)";
4566 case Type::T_FUNCTION_REFERENCE:
4567 $name = ! empty($value[2]) ? $value[2] : '';
4569 return "get-function(\"$name\")";
4571 case Type::T_LIST:
4572 $value = $this->extractInterpolation($value);
4574 if ($value[0] !== Type::T_LIST) {
4575 return $this->compileValue($value, $quote);
4578 list(, $delim, $items) = $value;
4579 $pre = $post = '';
4581 if (! empty($value['enclosing'])) {
4582 switch ($value['enclosing']) {
4583 case 'parent':
4584 //$pre = '(';
4585 //$post = ')';
4586 break;
4587 case 'forced_parent':
4588 $pre = '(';
4589 $post = ')';
4590 break;
4591 case 'bracket':
4592 case 'forced_bracket':
4593 $pre = '[';
4594 $post = ']';
4595 break;
4599 $separator = $delim === '/' ? ' /' : $delim;
4601 $prefix_value = '';
4603 if ($delim !== ' ') {
4604 $prefix_value = ' ';
4607 $filtered = [];
4609 $same_string_quote = null;
4610 foreach ($items as $item) {
4611 if (\is_null($same_string_quote)) {
4612 $same_string_quote = false;
4613 if ($item[0] === Type::T_STRING) {
4614 $same_string_quote = $item[1];
4615 foreach ($items as $ii) {
4616 if ($ii[0] !== Type::T_STRING) {
4617 $same_string_quote = false;
4618 break;
4623 if ($item[0] === Type::T_NULL) {
4624 continue;
4626 if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
4627 $item[1] = $same_string_quote;
4630 $compiled = $this->compileValue($item, $quote);
4632 if ($prefix_value && \strlen($compiled)) {
4633 $compiled = $prefix_value . $compiled;
4636 $filtered[] = $compiled;
4639 return $pre . substr(implode($separator, $filtered), \strlen($prefix_value)) . $post;
4641 case Type::T_MAP:
4642 $keys = $value[1];
4643 $values = $value[2];
4644 $filtered = [];
4646 for ($i = 0, $s = \count($keys); $i < $s; $i++) {
4647 $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote);
4650 array_walk($filtered, function (&$value, $key) {
4651 $value = $key . ': ' . $value;
4654 return '(' . implode(', ', $filtered) . ')';
4656 case Type::T_INTERPOLATED:
4657 // node created by extractInterpolation
4658 list(, $interpolate, $left, $right) = $value;
4659 list(,, $whiteLeft, $whiteRight) = $interpolate;
4661 $delim = $left[1];
4663 if ($delim && $delim !== ' ' && ! $whiteLeft) {
4664 $delim .= ' ';
4667 $left = \count($left[2]) > 0
4668 ? $this->compileValue($left, $quote) . $delim . $whiteLeft
4669 : '';
4671 $delim = $right[1];
4673 if ($delim && $delim !== ' ') {
4674 $delim .= ' ';
4677 $right = \count($right[2]) > 0 ?
4678 $whiteRight . $delim . $this->compileValue($right, $quote) : '';
4680 return $left . $this->compileValue($interpolate, $quote) . $right;
4682 case Type::T_INTERPOLATE:
4683 // strip quotes if it's a string
4684 $reduced = $this->reduce($value[1]);
4686 if ($reduced instanceof Number) {
4687 return $this->compileValue($reduced, $quote);
4690 switch ($reduced[0]) {
4691 case Type::T_LIST:
4692 $reduced = $this->extractInterpolation($reduced);
4694 if ($reduced[0] !== Type::T_LIST) {
4695 break;
4698 list(, $delim, $items) = $reduced;
4700 if ($delim !== ' ') {
4701 $delim .= ' ';
4704 $filtered = [];
4706 foreach ($items as $item) {
4707 if ($item[0] === Type::T_NULL) {
4708 continue;
4711 if ($item[0] === Type::T_STRING) {
4712 $filtered[] = $this->compileStringContent($item, $quote);
4713 } elseif ($item[0] === Type::T_KEYWORD) {
4714 $filtered[] = $item[1];
4715 } else {
4716 $filtered[] = $this->compileValue($item, $quote);
4720 $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
4721 break;
4723 case Type::T_STRING:
4724 $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
4725 break;
4727 case Type::T_NULL:
4728 $reduced = [Type::T_KEYWORD, ''];
4731 return $this->compileValue($reduced, $quote);
4733 case Type::T_NULL:
4734 return 'null';
4736 case Type::T_COMMENT:
4737 return $this->compileCommentValue($value);
4739 default:
4740 throw $this->error('unknown value type: ' . json_encode($value));
4745 * @param array|Number $value
4747 * @return string
4749 protected function compileDebugValue($value)
4751 $value = $this->reduce($value, true);
4753 if ($value instanceof Number) {
4754 return $this->compileValue($value);
4757 switch ($value[0]) {
4758 case Type::T_STRING:
4759 return $this->compileStringContent($value);
4761 default:
4762 return $this->compileValue($value);
4767 * Flatten list
4769 * @param array $list
4771 * @return string
4773 * @deprecated
4775 protected function flattenList($list)
4777 @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
4779 return $this->compileValue($list);
4783 * Gets the text of a Sass string
4785 * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
4786 * to ensure that the value is indeed a string.
4788 * @param array $value
4790 * @return string
4792 public function getStringText(array $value)
4794 if ($value[0] !== Type::T_STRING) {
4795 throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
4798 return $this->compileStringContent($value);
4802 * Compile string content
4804 * @param array $string
4805 * @param bool $quote
4807 * @return string
4809 protected function compileStringContent($string, $quote = true)
4811 $parts = [];
4813 foreach ($string[2] as $part) {
4814 if (\is_array($part) || $part instanceof Number) {
4815 $parts[] = $this->compileValue($part, $quote);
4816 } else {
4817 $parts[] = $part;
4821 return implode($parts);
4825 * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4827 * @param array $list
4829 * @return array
4831 protected function extractInterpolation($list)
4833 $items = $list[2];
4835 foreach ($items as $i => $item) {
4836 if ($item[0] === Type::T_INTERPOLATE) {
4837 $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
4838 $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
4840 return [Type::T_INTERPOLATED, $item, $before, $after];
4844 return $list;
4848 * Find the final set of selectors
4850 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4851 * @param \ScssPhp\ScssPhp\Block $selfParent
4853 * @return array
4855 protected function multiplySelectors(Environment $env, $selfParent = null)
4857 $envs = $this->compactEnv($env);
4858 $selectors = [];
4859 $parentSelectors = [[]];
4861 $selfParentSelectors = null;
4863 if (! \is_null($selfParent) && $selfParent->selectors) {
4864 $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
4867 while ($env = array_pop($envs)) {
4868 if (empty($env->selectors)) {
4869 continue;
4872 $selectors = $env->selectors;
4874 do {
4875 $stillHasSelf = false;
4876 $prevSelectors = $selectors;
4877 $selectors = [];
4879 foreach ($parentSelectors as $parent) {
4880 foreach ($prevSelectors as $selector) {
4881 if ($selfParentSelectors) {
4882 foreach ($selfParentSelectors as $selfParent) {
4883 // if no '&' in the selector, each call will give same result, only add once
4884 $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
4885 $selectors[serialize($s)] = $s;
4887 } else {
4888 $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
4889 $selectors[serialize($s)] = $s;
4893 } while ($stillHasSelf);
4895 $parentSelectors = $selectors;
4898 $selectors = array_values($selectors);
4900 // case we are just starting a at-root : nothing to multiply but parentSelectors
4901 if (! $selectors && $selfParentSelectors) {
4902 $selectors = $selfParentSelectors;
4905 return $selectors;
4909 * Join selectors; looks for & to replace, or append parent before child
4911 * @param array $parent
4912 * @param array $child
4913 * @param bool $stillHasSelf
4914 * @param array $selfParentSelectors
4916 * @return array
4918 protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4920 $setSelf = false;
4921 $out = [];
4923 foreach ($child as $part) {
4924 $newPart = [];
4926 foreach ($part as $p) {
4927 // only replace & once and should be recalled to be able to make combinations
4928 if ($p === static::$selfSelector && $setSelf) {
4929 $stillHasSelf = true;
4932 if ($p === static::$selfSelector && ! $setSelf) {
4933 $setSelf = true;
4935 if (\is_null($selfParentSelectors)) {
4936 $selfParentSelectors = $parent;
4939 foreach ($selfParentSelectors as $i => $parentPart) {
4940 if ($i > 0) {
4941 $out[] = $newPart;
4942 $newPart = [];
4945 foreach ($parentPart as $pp) {
4946 if (\is_array($pp)) {
4947 $flatten = [];
4949 array_walk_recursive($pp, function ($a) use (&$flatten) {
4950 $flatten[] = $a;
4953 $pp = implode($flatten);
4956 $newPart[] = $pp;
4959 } else {
4960 $newPart[] = $p;
4964 $out[] = $newPart;
4967 return $setSelf ? $out : array_merge($parent, $child);
4971 * Multiply media
4973 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4974 * @param array $childQueries
4976 * @return array
4978 protected function multiplyMedia(Environment $env = null, $childQueries = null)
4980 if (
4981 ! isset($env) ||
4982 ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
4984 return $childQueries;
4987 // plain old block, skip
4988 if (empty($env->block->type)) {
4989 return $this->multiplyMedia($env->parent, $childQueries);
4992 assert($env->block instanceof MediaBlock);
4994 $parentQueries = isset($env->block->queryList)
4995 ? $env->block->queryList
4996 : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
4998 $store = [$this->env, $this->storeEnv];
5000 $this->env = $env;
5001 $this->storeEnv = null;
5002 $parentQueries = $this->evaluateMediaQuery($parentQueries);
5004 list($this->env, $this->storeEnv) = $store;
5006 if (\is_null($childQueries)) {
5007 $childQueries = $parentQueries;
5008 } else {
5009 $originalQueries = $childQueries;
5010 $childQueries = [];
5012 foreach ($parentQueries as $parentQuery) {
5013 foreach ($originalQueries as $childQuery) {
5014 $childQueries[] = array_merge(
5015 $parentQuery,
5016 [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
5017 $childQuery
5023 return $this->multiplyMedia($env->parent, $childQueries);
5027 * Convert env linked list to stack
5029 * @param Environment $env
5031 * @return Environment[]
5033 * @phpstan-return non-empty-array<Environment>
5035 protected function compactEnv(Environment $env)
5037 for ($envs = []; $env; $env = $env->parent) {
5038 $envs[] = $env;
5041 return $envs;
5045 * Convert env stack to singly linked list
5047 * @param Environment[] $envs
5049 * @return Environment
5051 * @phpstan-param non-empty-array<Environment> $envs
5053 protected function extractEnv($envs)
5055 for ($env = null; $e = array_pop($envs);) {
5056 $e->parent = $env;
5057 $env = $e;
5060 return $env;
5064 * Push environment
5066 * @param \ScssPhp\ScssPhp\Block $block
5068 * @return \ScssPhp\ScssPhp\Compiler\Environment
5070 protected function pushEnv(Block $block = null)
5072 $env = new Environment();
5073 $env->parent = $this->env;
5074 $env->parentStore = $this->storeEnv;
5075 $env->store = [];
5076 $env->block = $block;
5077 $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
5079 $this->env = $env;
5080 $this->storeEnv = null;
5082 return $env;
5086 * Pop environment
5088 * @return void
5090 protected function popEnv()
5092 $this->storeEnv = $this->env->parentStore;
5093 $this->env = $this->env->parent;
5097 * Propagate vars from a just poped Env (used in @each and @for)
5099 * @param array $store
5100 * @param null|string[] $excludedVars
5102 * @return void
5104 protected function backPropagateEnv($store, $excludedVars = null)
5106 foreach ($store as $key => $value) {
5107 if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
5108 $this->set($key, $value, true);
5114 * Get store environment
5116 * @return \ScssPhp\ScssPhp\Compiler\Environment
5118 protected function getStoreEnv()
5120 return isset($this->storeEnv) ? $this->storeEnv : $this->env;
5124 * Set variable
5126 * @param string $name
5127 * @param mixed $value
5128 * @param bool $shadow
5129 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5130 * @param mixed $valueUnreduced
5132 * @return void
5134 protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
5136 $name = $this->normalizeName($name);
5138 if (! isset($env)) {
5139 $env = $this->getStoreEnv();
5142 if ($shadow) {
5143 $this->setRaw($name, $value, $env, $valueUnreduced);
5144 } else {
5145 $this->setExisting($name, $value, $env, $valueUnreduced);
5150 * Set existing variable
5152 * @param string $name
5153 * @param mixed $value
5154 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5155 * @param mixed $valueUnreduced
5157 * @return void
5159 protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
5161 $storeEnv = $env;
5162 $specialContentKey = static::$namespaces['special'] . 'content';
5164 $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
5166 $maxDepth = 10000;
5168 for (;;) {
5169 if ($maxDepth-- <= 0) {
5170 break;
5173 if (\array_key_exists($name, $env->store)) {
5174 break;
5177 if (! $hasNamespace && isset($env->marker)) {
5178 if (! empty($env->store[$specialContentKey])) {
5179 $env = $env->store[$specialContentKey]->scope;
5180 continue;
5183 if (! empty($env->declarationScopeParent)) {
5184 $env = $env->declarationScopeParent;
5185 continue;
5186 } else {
5187 $env = $storeEnv;
5188 break;
5192 if (isset($env->parentStore)) {
5193 $env = $env->parentStore;
5194 } elseif (isset($env->parent)) {
5195 $env = $env->parent;
5196 } else {
5197 $env = $storeEnv;
5198 break;
5202 $env->store[$name] = $value;
5204 if ($valueUnreduced) {
5205 $env->storeUnreduced[$name] = $valueUnreduced;
5210 * Set raw variable
5212 * @param string $name
5213 * @param mixed $value
5214 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5215 * @param mixed $valueUnreduced
5217 * @return void
5219 protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
5221 $env->store[$name] = $value;
5223 if ($valueUnreduced) {
5224 $env->storeUnreduced[$name] = $valueUnreduced;
5229 * Get variable
5231 * @internal
5233 * @param string $name
5234 * @param bool $shouldThrow
5235 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5236 * @param bool $unreduced
5238 * @return mixed|null
5240 public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
5242 $normalizedName = $this->normalizeName($name);
5243 $specialContentKey = static::$namespaces['special'] . 'content';
5245 if (! isset($env)) {
5246 $env = $this->getStoreEnv();
5249 $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
5251 $maxDepth = 10000;
5253 for (;;) {
5254 if ($maxDepth-- <= 0) {
5255 break;
5258 if (\array_key_exists($normalizedName, $env->store)) {
5259 if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
5260 return $env->storeUnreduced[$normalizedName];
5263 return $env->store[$normalizedName];
5266 if (! $hasNamespace && isset($env->marker)) {
5267 if (! empty($env->store[$specialContentKey])) {
5268 $env = $env->store[$specialContentKey]->scope;
5269 continue;
5272 if (! empty($env->declarationScopeParent)) {
5273 $env = $env->declarationScopeParent;
5274 } else {
5275 $env = $this->rootEnv;
5277 continue;
5280 if (isset($env->parentStore)) {
5281 $env = $env->parentStore;
5282 } elseif (isset($env->parent)) {
5283 $env = $env->parent;
5284 } else {
5285 break;
5289 if ($shouldThrow) {
5290 throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
5293 // found nothing
5294 return null;
5298 * Has variable?
5300 * @param string $name
5301 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5303 * @return bool
5305 protected function has($name, Environment $env = null)
5307 return ! \is_null($this->get($name, false, $env));
5311 * Inject variables
5313 * @param array $args
5315 * @return void
5317 protected function injectVariables(array $args)
5319 if (empty($args)) {
5320 return;
5323 $parser = $this->parserFactory(__METHOD__);
5325 foreach ($args as $name => $strValue) {
5326 if ($name[0] === '$') {
5327 $name = substr($name, 1);
5330 if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) {
5331 $value = $this->coerceValue($strValue);
5334 $this->set($name, $value);
5339 * Replaces variables.
5341 * @param array<string, mixed> $variables
5343 * @return void
5345 public function replaceVariables(array $variables)
5347 $this->registeredVars = [];
5348 $this->addVariables($variables);
5352 * Replaces variables.
5354 * @param array<string, mixed> $variables
5356 * @return void
5358 public function addVariables(array $variables)
5360 $triggerWarning = false;
5362 foreach ($variables as $name => $value) {
5363 if (!$value instanceof Number && !\is_array($value)) {
5364 $triggerWarning = true;
5367 $this->registeredVars[$name] = $value;
5370 if ($triggerWarning) {
5371 @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);
5376 * Set variables
5378 * @api
5380 * @param array $variables
5382 * @return void
5384 * @deprecated Use "addVariables" or "replaceVariables" instead.
5386 public function setVariables(array $variables)
5388 @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.');
5390 $this->addVariables($variables);
5394 * Unset variable
5396 * @api
5398 * @param string $name
5400 * @return void
5402 public function unsetVariable($name)
5404 unset($this->registeredVars[$name]);
5408 * Returns list of variables
5410 * @api
5412 * @return array
5414 public function getVariables()
5416 return $this->registeredVars;
5420 * Adds to list of parsed files
5422 * @internal
5424 * @param string|null $path
5426 * @return void
5428 public function addParsedFile($path)
5430 if (! \is_null($path) && is_file($path)) {
5431 $this->parsedFiles[realpath($path)] = filemtime($path);
5436 * Returns list of parsed files
5438 * @deprecated
5439 * @return array<string, int>
5441 public function getParsedFiles()
5443 @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);
5444 return $this->parsedFiles;
5448 * Add import path
5450 * @api
5452 * @param string|callable $path
5454 * @return void
5456 public function addImportPath($path)
5458 if (! \in_array($path, $this->importPaths)) {
5459 $this->importPaths[] = $path;
5464 * Set import paths
5466 * @api
5468 * @param string|array<string|callable> $path
5470 * @return void
5472 public function setImportPaths($path)
5474 $paths = (array) $path;
5475 $actualImportPaths = array_filter($paths, function ($path) {
5476 return $path !== '';
5479 $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths);
5481 if ($this->legacyCwdImportPath) {
5482 @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);
5485 $this->importPaths = $actualImportPaths;
5489 * Set number precision
5491 * @api
5493 * @param int $numberPrecision
5495 * @return void
5497 * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
5499 public function setNumberPrecision($numberPrecision)
5501 @trigger_error('The number precision is not configurable anymore. '
5502 . 'The default is enough for all browsers.', E_USER_DEPRECATED);
5506 * Sets the output style.
5508 * @api
5510 * @param string $style One of the OutputStyle constants
5512 * @return void
5514 * @phpstan-param OutputStyle::* $style
5516 public function setOutputStyle($style)
5518 switch ($style) {
5519 case OutputStyle::EXPANDED:
5520 $this->formatter = Expanded::class;
5521 break;
5523 case OutputStyle::COMPRESSED:
5524 $this->formatter = Compressed::class;
5525 break;
5527 default:
5528 throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5533 * Set formatter
5535 * @api
5537 * @param string $formatterName
5539 * @return void
5541 * @deprecated Use {@see setOutputStyle} instead.
5543 public function setFormatter($formatterName)
5545 if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) {
5546 @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED);
5548 @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED);
5550 $this->formatter = $formatterName;
5554 * Set line number style
5556 * @api
5558 * @param string $lineNumberStyle
5560 * @return void
5562 * @deprecated The line number output is not supported anymore. Use source maps instead.
5564 public function setLineNumberStyle($lineNumberStyle)
5566 @trigger_error('The line number output is not supported anymore. '
5567 . 'Use source maps instead.', E_USER_DEPRECATED);
5571 * Configures the handling of non-ASCII outputs.
5573 * If $charset is `true`, this will include a `@charset` declaration or a
5574 * UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII
5575 * characters. Otherwise, it will never include a `@charset` declaration or a
5576 * byte-order mark.
5578 * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
5580 * @param bool $charset
5582 * @return void
5584 public function setCharset($charset)
5586 $this->charset = $charset;
5590 * Enable/disable source maps
5592 * @api
5594 * @param int $sourceMap
5596 * @return void
5598 * @phpstan-param self::SOURCE_MAP_* $sourceMap
5600 public function setSourceMap($sourceMap)
5602 $this->sourceMap = $sourceMap;
5606 * Set source map options
5608 * @api
5610 * @param array $sourceMapOptions
5612 * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions
5614 * @return void
5616 public function setSourceMapOptions($sourceMapOptions)
5618 $this->sourceMapOptions = $sourceMapOptions;
5622 * Register function
5624 * @api
5626 * @param string $name
5627 * @param callable $callback
5628 * @param string[]|null $argumentDeclaration
5630 * @return void
5632 public function registerFunction($name, $callback, $argumentDeclaration = null)
5634 if (self::isNativeFunction($name)) {
5635 @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);
5638 if ($argumentDeclaration === null) {
5639 @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);
5642 $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration];
5646 * Unregister function
5648 * @api
5650 * @param string $name
5652 * @return void
5654 public function unregisterFunction($name)
5656 unset($this->userFunctions[$this->normalizeName($name)]);
5660 * Add feature
5662 * @api
5664 * @param string $name
5666 * @return void
5668 * @deprecated Registering additional features is deprecated.
5670 public function addFeature($name)
5672 @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED);
5674 $this->registeredFeatures[$name] = true;
5678 * Import file
5680 * @param string $path
5681 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
5683 * @return void
5685 protected function importFile($path, OutputBlock $out)
5687 $this->pushCallStack('import ' . $this->getPrettyPath($path));
5688 // see if tree is cached
5689 $realPath = realpath($path);
5691 if (substr($path, -5) === '.sass') {
5692 $this->sourceIndex = \count($this->sourceNames);
5693 $this->sourceNames[] = $path;
5694 $this->sourceLine = 1;
5695 $this->sourceColumn = 1;
5697 throw $this->error('The Sass indented syntax is not implemented.');
5700 if (isset($this->importCache[$realPath])) {
5701 $this->handleImportLoop($realPath);
5703 $tree = $this->importCache[$realPath];
5704 } else {
5705 $code = file_get_contents($path);
5706 $parser = $this->parserFactory($path);
5707 $tree = $parser->parse($code);
5709 $this->importCache[$realPath] = $tree;
5712 $currentDirectory = $this->currentDirectory;
5713 $this->currentDirectory = dirname($path);
5715 $this->compileChildrenNoReturn($tree->children, $out);
5716 $this->currentDirectory = $currentDirectory;
5717 $this->popCallStack();
5721 * Save the imported files with their resolving path context
5723 * @param string|null $currentDirectory
5724 * @param string $path
5725 * @param string $filePath
5727 * @return void
5729 private function registerImport($currentDirectory, $path, $filePath)
5731 $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath];
5735 * Detects whether the import is a CSS import.
5737 * For legacy reasons, custom importers are called for those, allowing them
5738 * to replace them with an actual Sass import. However this behavior is
5739 * deprecated. Custom importers are expected to return null when they receive
5740 * a CSS import.
5742 * @param string $url
5744 * @return bool
5746 public static function isCssImport($url)
5748 return 1 === preg_match('~\.css$|^https?://|^//~', $url);
5752 * Return the file path for an import url if it exists
5754 * @internal
5756 * @param string $url
5757 * @param string|null $currentDir
5759 * @return string|null
5761 public function findImport($url, $currentDir = null)
5763 // Vanilla css and external requests. These are not meant to be Sass imports.
5764 // Callback importers are still called for BC.
5765 if (self::isCssImport($url)) {
5766 foreach ($this->importPaths as $dir) {
5767 if (\is_string($dir)) {
5768 continue;
5771 if (\is_callable($dir)) {
5772 // check custom callback for import path
5773 $file = \call_user_func($dir, $url);
5775 if (! \is_null($file)) {
5776 if (\is_array($dir)) {
5777 $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1];
5778 } elseif ($dir instanceof \Closure) {
5779 $r = new \ReflectionFunction($dir);
5780 if (false !== strpos($r->name, '{closure}')) {
5781 $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine());
5782 } elseif ($class = $r->getClosureScopeClass()) {
5783 $callableDescription = $class->name.'::'.$r->name;
5784 } else {
5785 $callableDescription = $r->name;
5787 } elseif (\is_object($dir)) {
5788 $callableDescription = \get_class($dir) . '::__invoke';
5789 } else {
5790 $callableDescription = 'callable'; // Fallback if we don't have a dedicated description
5792 @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);
5794 return $file;
5798 return null;
5801 if (!\is_null($currentDir)) {
5802 $relativePath = $this->resolveImportPath($url, $currentDir);
5804 if (!\is_null($relativePath)) {
5805 return $relativePath;
5809 foreach ($this->importPaths as $dir) {
5810 if (\is_string($dir)) {
5811 $path = $this->resolveImportPath($url, $dir);
5813 if (!\is_null($path)) {
5814 return $path;
5816 } elseif (\is_callable($dir)) {
5817 // check custom callback for import path
5818 $file = \call_user_func($dir, $url);
5820 if (! \is_null($file)) {
5821 return $file;
5826 if ($this->legacyCwdImportPath) {
5827 $path = $this->resolveImportPath($url, getcwd());
5829 if (!\is_null($path)) {
5830 @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);
5832 return $path;
5836 throw $this->error("`$url` file not found for @import");
5840 * @param string $url
5841 * @param string $baseDir
5843 * @return string|null
5845 private function resolveImportPath($url, $baseDir)
5847 $path = Path::join($baseDir, $url);
5849 $hasExtension = preg_match('/.s[ac]ss$/', $url);
5851 if ($hasExtension) {
5852 return $this->checkImportPathConflicts($this->tryImportPath($path));
5855 $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path));
5857 if (!\is_null($result)) {
5858 return $result;
5861 return $this->tryImportPathAsDirectory($path);
5865 * @param string[] $paths
5867 * @return string|null
5869 private function checkImportPathConflicts(array $paths)
5871 if (\count($paths) === 0) {
5872 return null;
5875 if (\count($paths) === 1) {
5876 return $paths[0];
5879 $formattedPrettyPaths = [];
5881 foreach ($paths as $path) {
5882 $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path);
5885 throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
5889 * @param string $path
5891 * @return string[]
5893 private function tryImportPathWithExtensions($path)
5895 $result = array_merge(
5896 $this->tryImportPath($path.'.sass'),
5897 $this->tryImportPath($path.'.scss')
5900 if ($result) {
5901 return $result;
5904 return $this->tryImportPath($path.'.css');
5908 * @param string $path
5910 * @return string[]
5912 private function tryImportPath($path)
5914 $partial = dirname($path).'/_'.basename($path);
5916 $candidates = [];
5918 if (is_file($partial)) {
5919 $candidates[] = $partial;
5922 if (is_file($path)) {
5923 $candidates[] = $path;
5926 return $candidates;
5930 * @param string $path
5932 * @return string|null
5934 private function tryImportPathAsDirectory($path)
5936 if (!is_dir($path)) {
5937 return null;
5940 return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5944 * @param string|null $path
5946 * @return string
5948 private function getPrettyPath($path)
5950 if ($path === null) {
5951 return '(unknown file)';
5954 $normalizedPath = $path;
5955 $normalizedRootDirectory = $this->rootDirectory.'/';
5957 if (\DIRECTORY_SEPARATOR === '\\') {
5958 $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
5959 $normalizedPath = str_replace('\\', '/', $path);
5962 if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
5963 return substr($path, \strlen($normalizedRootDirectory));
5966 return $path;
5970 * Set encoding
5972 * @api
5974 * @param string|null $encoding
5976 * @return void
5978 * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated.
5980 public function setEncoding($encoding)
5982 if (!$encoding || strtolower($encoding) === 'utf-8') {
5983 @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
5984 } else {
5985 @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);
5988 $this->encoding = $encoding;
5992 * Ignore errors?
5994 * @api
5996 * @param bool $ignoreErrors
5998 * @return \ScssPhp\ScssPhp\Compiler
6000 * @deprecated Ignoring Sass errors is not longer supported.
6002 public function setIgnoreErrors($ignoreErrors)
6004 @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
6006 return $this;
6010 * Get source position
6012 * @api
6014 * @return array
6016 * @deprecated
6018 public function getSourcePosition()
6020 @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
6022 $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
6024 return [$sourceFile, $this->sourceLine, $this->sourceColumn];
6028 * Throw error (exception)
6030 * @api
6032 * @param string $msg Message with optional sprintf()-style vararg parameters
6034 * @throws \ScssPhp\ScssPhp\Exception\CompilerException
6036 * @deprecated use "error" and throw the exception in the caller instead.
6038 public function throwError($msg)
6040 @trigger_error(
6041 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
6042 E_USER_DEPRECATED
6045 throw $this->error(...func_get_args());
6049 * Build an error (exception)
6051 * @internal
6053 * @param string $msg Message with optional sprintf()-style vararg parameters
6055 * @return CompilerException
6057 public function error($msg, ...$args)
6059 if ($args) {
6060 $msg = sprintf($msg, ...$args);
6063 if (! $this->ignoreCallStackMessage) {
6064 $msg = $this->addLocationToMessage($msg);
6067 return new CompilerException($msg);
6071 * @param string $msg
6073 * @return string
6075 private function addLocationToMessage($msg)
6077 $line = $this->sourceLine;
6078 $column = $this->sourceColumn;
6080 $loc = isset($this->sourceNames[$this->sourceIndex])
6081 ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
6082 : "line: $line, column: $column";
6084 $msg = "$msg: $loc";
6086 $callStackMsg = $this->callStackMessage();
6088 if ($callStackMsg) {
6089 $msg .= "\nCall Stack:\n" . $callStackMsg;
6092 return $msg;
6096 * @param string $functionName
6097 * @param array $ExpectedArgs
6098 * @param int $nbActual
6099 * @return CompilerException
6101 * @deprecated
6103 public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
6105 @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
6107 $nbExpected = \count($ExpectedArgs);
6109 if ($nbActual > $nbExpected) {
6110 return $this->error(
6111 'Error: Only %d arguments allowed in %s(), but %d were passed.',
6112 $nbExpected,
6113 $functionName,
6114 $nbActual
6116 } else {
6117 $missing = [];
6119 while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
6120 array_unshift($missing, array_pop($ExpectedArgs));
6123 return $this->error(
6124 'Error: %s() argument%s %s missing.',
6125 $functionName,
6126 count($missing) > 1 ? 's' : '',
6127 implode(', ', $missing)
6133 * Beautify call stack for output
6135 * @param bool $all
6136 * @param int|null $limit
6138 * @return string
6140 protected function callStackMessage($all = false, $limit = null)
6142 $callStackMsg = [];
6143 $ncall = 0;
6145 if ($this->callStack) {
6146 foreach (array_reverse($this->callStack) as $call) {
6147 if ($all || (isset($call['n']) && $call['n'])) {
6148 $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
6149 $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6150 ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]])
6151 : '(unknown file)');
6152 $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
6154 $callStackMsg[] = $msg;
6156 if (! \is_null($limit) && $ncall > $limit) {
6157 break;
6163 return implode("\n", $callStackMsg);
6167 * Handle import loop
6169 * @param string $name
6171 * @throws \Exception
6173 protected function handleImportLoop($name)
6175 for ($env = $this->env; $env; $env = $env->parent) {
6176 if (! $env->block) {
6177 continue;
6180 $file = $this->sourceNames[$env->block->sourceIndex];
6182 if ($file === null) {
6183 continue;
6186 if (realpath($file) === $name) {
6187 throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
6193 * Call SCSS @function
6195 * @param CallableBlock|null $func
6196 * @param array $argValues
6198 * @return array|Number
6200 protected function callScssFunction($func, $argValues)
6202 if (! $func) {
6203 return static::$defaultValue;
6205 $name = $func->name;
6207 $this->pushEnv();
6209 // set the args
6210 if (isset($func->args)) {
6211 $this->applyArguments($func->args, $argValues);
6214 // throw away lines and children
6215 $tmp = new OutputBlock();
6216 $tmp->lines = [];
6217 $tmp->children = [];
6219 $this->env->marker = 'function';
6221 if (! empty($func->parentEnv)) {
6222 $this->env->declarationScopeParent = $func->parentEnv;
6223 } else {
6224 throw $this->error("@function $name() without parentEnv");
6227 $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
6229 $this->popEnv();
6231 return ! isset($ret) ? static::$defaultValue : $ret;
6235 * Call built-in and registered (PHP) functions
6237 * @param string $name
6238 * @param callable $function
6239 * @param array $prototype
6240 * @param array $args
6242 * @return array|Number|null
6244 protected function callNativeFunction($name, $function, $prototype, $args)
6246 $libName = (is_array($function) ? end($function) : null);
6247 $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
6249 if (\is_null($sorted_kwargs)) {
6250 return null;
6252 @list($sorted, $kwargs) = $sorted_kwargs;
6254 if ($name !== 'if') {
6255 foreach ($sorted as &$val) {
6256 if ($val !== null) {
6257 $val = $this->reduce($val, true);
6262 $returnValue = \call_user_func($function, $sorted, $kwargs);
6264 if (! isset($returnValue)) {
6265 return null;
6268 if (\is_array($returnValue) || $returnValue instanceof Number) {
6269 return $returnValue;
6272 @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);
6274 return $this->coerceValue($returnValue);
6278 * Get built-in function
6280 * @param string $name Normalized name
6282 * @return array
6284 protected function getBuiltinFunction($name)
6286 $libName = self::normalizeNativeFunctionName($name);
6287 return [$this, $libName];
6291 * Normalize native function name
6293 * @internal
6295 * @param string $name
6297 * @return string
6299 public static function normalizeNativeFunctionName($name)
6301 $name = str_replace("-", "_", $name);
6302 $libName = 'lib' . preg_replace_callback(
6303 '/_(.)/',
6304 function ($m) {
6305 return ucfirst($m[1]);
6307 ucfirst($name)
6309 return $libName;
6313 * Check if a function is a native built-in scss function, for css parsing
6315 * @internal
6317 * @param string $name
6319 * @return bool
6321 public static function isNativeFunction($name)
6323 return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
6327 * Sorts keyword arguments
6329 * @param string $functionName
6330 * @param array|null $prototypes
6331 * @param array $args
6333 * @return array|null
6335 protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
6337 static $parser = null;
6339 if (! isset($prototypes)) {
6340 $keyArgs = [];
6341 $posArgs = [];
6343 if (\is_array($args) && \count($args) && \end($args) === static::$null) {
6344 array_pop($args);
6347 // separate positional and keyword arguments
6348 foreach ($args as $arg) {
6349 list($key, $value) = $arg;
6351 if (empty($key) or empty($key[1])) {
6352 $posArgs[] = empty($arg[2]) ? $value : $arg;
6353 } else {
6354 $keyArgs[$key[1]] = $value;
6358 return [$posArgs, $keyArgs];
6361 // specific cases ?
6362 if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
6363 // notation 100 127 255 / 0 is in fact a simple list of 4 values
6364 foreach ($args as $k => $arg) {
6365 if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
6366 $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]);
6371 list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false);
6373 if (! \is_array(reset($prototypes))) {
6374 $prototypes = [$prototypes];
6377 $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes);
6378 assert(!empty($parsedPrototypes));
6379 $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names);
6381 $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat);
6383 $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator);
6385 $finalArgs = [];
6386 $keyArgs = [];
6388 foreach ($matchedPrototype['arguments'] as $argument) {
6389 list($normalizedName, $originalName, $default) = $argument;
6391 if (isset($vars[$normalizedName])) {
6392 $value = $vars[$normalizedName];
6393 } else {
6394 $value = $default;
6397 // special null value as default: translate to real null here
6398 if ($value === [Type::T_KEYWORD, 'null']) {
6399 $value = null;
6402 $finalArgs[] = $value;
6403 $keyArgs[$originalName] = $value;
6406 if ($matchedPrototype['rest_argument'] !== null) {
6407 $value = $vars[$matchedPrototype['rest_argument']];
6409 $finalArgs[] = $value;
6410 $keyArgs[$matchedPrototype['rest_argument']] = $value;
6413 return [$finalArgs, $keyArgs];
6417 * Parses a function prototype to the internal representation of arguments.
6419 * The input is an array of strings describing each argument, as supported
6420 * in {@see registerFunction}. Argument names don't include the `$`.
6421 * The output contains the list of positional argument, with their normalized
6422 * name (underscores are replaced by dashes), their original name (to be used
6423 * in case of error reporting) and their default value. The output also contains
6424 * the normalized name of the rest argument, or null if the function prototype
6425 * is not variadic.
6427 * @param string[] $prototype
6429 * @return array
6430 * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6432 private function parseFunctionPrototype(array $prototype)
6434 static $parser = null;
6436 $arguments = [];
6437 $restArgument = null;
6439 foreach ($prototype as $p) {
6440 if (null !== $restArgument) {
6441 throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.');
6444 $default = null;
6445 $p = explode(':', $p, 2);
6446 $name = str_replace('_', '-', $p[0]);
6448 if (isset($p[1])) {
6449 $defaultSource = trim($p[1]);
6451 if ($defaultSource === 'null') {
6452 // differentiate this null from the static::$null
6453 $default = [Type::T_KEYWORD, 'null'];
6454 } else {
6455 if (\is_null($parser)) {
6456 $parser = $this->parserFactory(__METHOD__);
6459 $parser->parseValue($defaultSource, $default);
6463 if (substr($name, -3) === '...') {
6464 $restArgument = substr($name, 0, -3);
6465 } else {
6466 $arguments[] = [$name, $p[0], $default];
6470 return [
6471 'arguments' => $arguments,
6472 'rest_argument' => $restArgument,
6477 * Returns the function prototype for the given positional and named arguments.
6479 * If no exact match is found, finds the closest approximation. Note that this
6480 * doesn't guarantee that $positional and $names are valid for the returned
6481 * prototype.
6483 * @param array[] $prototypes
6484 * @param int $positional
6485 * @param array<string, string> $names A set of names, as both keys and values
6487 * @return array
6489 * @phpstan-param non-empty-list<array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}> $prototypes
6490 * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
6492 private function selectFunctionPrototype(array $prototypes, $positional, array $names)
6494 $fuzzyMatch = null;
6495 $minMismatchDistance = null;
6497 foreach ($prototypes as $prototype) {
6498 // Ideally, find an exact match.
6499 if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
6500 return $prototype;
6503 $mismatchDistance = \count($prototype['arguments']) - $positional;
6505 if ($minMismatchDistance !== null) {
6506 if (abs($mismatchDistance) > abs($minMismatchDistance)) {
6507 continue;
6510 // If two overloads have the same mismatch distance, favor the overload
6511 // that has more arguments.
6512 if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
6513 continue;
6517 $minMismatchDistance = $mismatchDistance;
6518 $fuzzyMatch = $prototype;
6521 return $fuzzyMatch;
6525 * Checks whether the argument invocation matches the callable prototype.
6527 * The rules are similar to {@see verifyPrototype}. The boolean return value
6528 * avoids the overhead of building and catching exceptions when the reason of
6529 * not matching the prototype does not need to be known.
6531 * @param array $prototype
6532 * @param int $positional
6533 * @param array<string, string> $names
6535 * @return bool
6537 * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6539 private function checkPrototypeMatches(array $prototype, $positional, array $names)
6541 $nameUsed = 0;
6543 foreach ($prototype['arguments'] as $i => $argument) {
6544 list ($name, $originalName, $default) = $argument;
6546 if ($i < $positional) {
6547 if (isset($names[$name])) {
6548 return false;
6550 } elseif (isset($names[$name])) {
6551 $nameUsed++;
6552 } elseif ($default === null) {
6553 return false;
6557 if ($prototype['rest_argument'] !== null) {
6558 return true;
6561 if ($positional > \count($prototype['arguments'])) {
6562 return false;
6565 if ($nameUsed < \count($names)) {
6566 return false;
6569 return true;
6573 * Verifies that the argument invocation is valid for the callable prototype.
6575 * @param array $prototype
6576 * @param int $positional
6577 * @param array<string, string> $names
6578 * @param bool $hasSplat
6580 * @return void
6582 * @throws SassScriptException
6584 * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6586 private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat)
6588 $nameUsed = 0;
6590 foreach ($prototype['arguments'] as $i => $argument) {
6591 list ($name, $originalName, $default) = $argument;
6593 if ($i < $positional) {
6594 if (isset($names[$name])) {
6595 throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName));
6597 } elseif (isset($names[$name])) {
6598 $nameUsed++;
6599 } elseif ($default === null) {
6600 throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
6604 if ($prototype['rest_argument'] !== null) {
6605 return;
6608 if ($positional > \count($prototype['arguments'])) {
6609 $message = sprintf(
6610 'Only %d %sargument%s allowed, but %d %s passed.',
6611 \count($prototype['arguments']),
6612 empty($names) ? '' : 'positional ',
6613 \count($prototype['arguments']) === 1 ? '' : 's',
6614 $positional,
6615 $positional === 1 ? 'was' : 'were'
6617 if (!$hasSplat) {
6618 throw new SassScriptException($message);
6621 $message = $this->addLocationToMessage($message);
6622 $message .= "\nThis will be an error in future versions of Sass.";
6623 $this->logger->warn($message, true);
6626 if ($nameUsed < \count($names)) {
6627 $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0)));
6628 $lastName = array_pop($unknownNames);
6629 $message = sprintf(
6630 'No argument%s named $%s%s.',
6631 $unknownNames ? 's' : '',
6632 $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
6633 $lastName
6635 throw new SassScriptException($message);
6640 * Evaluates the argument from the invocation.
6642 * This returns several things about this invocation:
6643 * - the list of positional arguments
6644 * - the map of named arguments, indexed by normalized names
6645 * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access)
6646 * - the separator used by the list using the splat operator, if any
6647 * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting.
6649 * @param array[] $args
6650 * @param bool $reduce Whether arguments should be reduced to their value
6652 * @return array
6654 * @throws SassScriptException
6656 * @phpstan-return array{0: list<array|Number>, 1: array<string, array|Number>, 2: array<string, string>, 3: string|null, 4: bool}
6658 private function evaluateArguments(array $args, $reduce = true)
6660 // this represents trailing commas
6661 if (\count($args) && end($args) === static::$null) {
6662 array_pop($args);
6665 $splatSeparator = null;
6666 $keywordArgs = [];
6667 $names = [];
6668 $positionalArgs = [];
6669 $hasKeywordArgument = false;
6670 $hasSplat = false;
6672 foreach ($args as $arg) {
6673 if (!empty($arg[0])) {
6674 $hasKeywordArgument = true;
6676 assert(\is_string($arg[0][1]));
6677 $name = str_replace('_', '-', $arg[0][1]);
6679 if (isset($keywordArgs[$name])) {
6680 throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1]));
6683 $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]);
6684 $names[$name] = $name;
6685 } elseif (! empty($arg[2])) {
6686 // $arg[2] means a var followed by ... in the arg ($list... )
6687 $val = $this->reduce($arg[1], true);
6688 $hasSplat = true;
6690 if ($val[0] === Type::T_LIST) {
6691 foreach ($val[2] as $item) {
6692 if (\is_null($splatSeparator)) {
6693 $splatSeparator = $val[1];
6696 $positionalArgs[] = $this->maybeReduce($reduce, $item);
6699 if (isset($val[3]) && \is_array($val[3])) {
6700 foreach ($val[3] as $name => $item) {
6701 assert(\is_string($name));
6703 $normalizedName = str_replace('_', '-', $name);
6705 if (isset($keywordArgs[$normalizedName])) {
6706 throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
6709 $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6710 $names[$normalizedName] = $normalizedName;
6711 $hasKeywordArgument = true;
6714 } elseif ($val[0] === Type::T_MAP) {
6715 foreach ($val[1] as $i => $name) {
6716 $name = $this->compileStringContent($this->coerceString($name));
6717 $item = $val[2][$i];
6719 if (! is_numeric($name)) {
6720 $normalizedName = str_replace('_', '-', $name);
6722 if (isset($keywordArgs[$normalizedName])) {
6723 throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
6726 $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
6727 $names[$normalizedName] = $normalizedName;
6728 $hasKeywordArgument = true;
6729 } else {
6730 if (\is_null($splatSeparator)) {
6731 $splatSeparator = $val[1];
6734 $positionalArgs[] = $this->maybeReduce($reduce, $item);
6737 } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list
6738 $positionalArgs[] = $this->maybeReduce($reduce, $val);
6740 } elseif ($hasKeywordArgument) {
6741 throw new SassScriptException('Positional arguments must come before keyword arguments.');
6742 } else {
6743 $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]);
6747 return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat];
6751 * @param bool $reduce
6752 * @param array|Number $value
6754 * @return array|Number
6756 private function maybeReduce($reduce, $value)
6758 if ($reduce) {
6759 return $this->reduce($value, true);
6762 return $value;
6766 * Apply argument values per definition
6768 * @param array[] $argDef
6769 * @param array|null $argValues
6770 * @param bool $storeInEnv
6771 * @param bool $reduce only used if $storeInEnv = false
6773 * @return array<string, array|Number>
6775 * @phpstan-param list<array{0: string, 1: array|Number|null, 2: bool}> $argDef
6777 * @throws \Exception
6779 protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
6781 $output = [];
6783 if (\is_null($argValues)) {
6784 $argValues = [];
6787 if ($storeInEnv) {
6788 $storeEnv = $this->getStoreEnv();
6790 $env = new Environment();
6791 $env->store = $storeEnv->store;
6794 $prototype = ['arguments' => [], 'rest_argument' => null];
6795 $originalRestArgumentName = null;
6797 foreach ($argDef as $i => $arg) {
6798 list($name, $default, $isVariable) = $arg;
6799 $normalizedName = str_replace('_', '-', $name);
6801 if ($isVariable) {
6802 $originalRestArgumentName = $name;
6803 $prototype['rest_argument'] = $normalizedName;
6804 } else {
6805 $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null];
6809 list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce);
6811 $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat);
6813 $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator);
6815 foreach ($prototype['arguments'] as $argument) {
6816 list($normalizedName, $name) = $argument;
6818 if (!isset($vars[$normalizedName])) {
6819 continue;
6822 $val = $vars[$normalizedName];
6824 if ($storeInEnv) {
6825 $this->set($name, $this->reduce($val, true), true, $env);
6826 } else {
6827 $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6831 if ($prototype['rest_argument'] !== null) {
6832 assert($originalRestArgumentName !== null);
6833 $name = $originalRestArgumentName;
6834 $val = $vars[$prototype['rest_argument']];
6836 if ($storeInEnv) {
6837 $this->set($name, $this->reduce($val, true), true, $env);
6838 } else {
6839 $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6843 if ($storeInEnv) {
6844 $storeEnv->store = $env->store;
6847 foreach ($prototype['arguments'] as $argument) {
6848 list($normalizedName, $name, $default) = $argument;
6850 if (isset($vars[$normalizedName])) {
6851 continue;
6853 assert($default !== null);
6855 if ($storeInEnv) {
6856 $this->set($name, $this->reduce($default, true), true);
6857 } else {
6858 $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
6862 return $output;
6866 * Apply argument values per definition.
6868 * This method assumes that the arguments are valid for the provided prototype.
6869 * The validation with {@see verifyPrototype} must have been run before calling
6870 * it.
6871 * Arguments are returned as a map from the normalized argument names to the
6872 * value. Additional arguments are collected in a sass argument list available
6873 * under the name of the rest argument in the result.
6875 * Defaults are not applied as they are resolved in a different environment.
6877 * @param array $prototype
6878 * @param array<array|Number> $positionalArgs
6879 * @param array<string, array|Number> $namedArgs
6880 * @param string|null $splatSeparator
6882 * @return array<string, array|Number>
6884 * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
6886 private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator)
6888 $output = [];
6889 $minLength = min(\count($positionalArgs), \count($prototype['arguments']));
6891 for ($i = 0; $i < $minLength; $i++) {
6892 list($name) = $prototype['arguments'][$i];
6893 $val = $positionalArgs[$i];
6895 $output[$name] = $val;
6898 $restNamed = $namedArgs;
6900 for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) {
6901 $argument = $prototype['arguments'][$i];
6902 list($name) = $argument;
6904 if (isset($namedArgs[$name])) {
6905 $val = $namedArgs[$name];
6906 unset($restNamed[$name]);
6907 } else {
6908 continue;
6911 $output[$name] = $val;
6914 if ($prototype['rest_argument'] !== null) {
6915 $name = $prototype['rest_argument'];
6916 $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments'])));
6918 $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed];
6920 $output[$name] = $val;
6923 return $output;
6927 * Coerce a php value into a scss one
6929 * @param mixed $value
6931 * @return array|Number
6933 protected function coerceValue($value)
6935 if (\is_array($value) || $value instanceof Number) {
6936 return $value;
6939 if (\is_bool($value)) {
6940 return $this->toBool($value);
6943 if (\is_null($value)) {
6944 return static::$null;
6947 if (is_numeric($value)) {
6948 return new Number($value, '');
6951 if ($value === '') {
6952 return static::$emptyString;
6955 $value = [Type::T_KEYWORD, $value];
6956 $color = $this->coerceColor($value);
6958 if ($color) {
6959 return $color;
6962 return $value;
6966 * Tries to convert an item to a Sass map
6968 * @param Number|array $item
6970 * @return array|null
6972 private function tryMap($item)
6974 if ($item instanceof Number) {
6975 return null;
6978 if ($item[0] === Type::T_MAP) {
6979 return $item;
6982 if (
6983 $item[0] === Type::T_LIST &&
6984 $item[2] === []
6986 return static::$emptyMap;
6989 return null;
6993 * Coerce something to map
6995 * @param array|Number $item
6997 * @return array|Number
6999 protected function coerceMap($item)
7001 $map = $this->tryMap($item);
7003 if ($map !== null) {
7004 return $map;
7007 return $item;
7011 * Coerce something to list
7013 * @param array|Number $item
7014 * @param string $delim
7015 * @param bool $removeTrailingNull
7017 * @return array
7019 protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
7021 if ($item instanceof Number) {
7022 return [Type::T_LIST, '', [$item]];
7025 if ($item[0] === Type::T_LIST) {
7026 // remove trailing null from the list
7027 if ($removeTrailingNull && end($item[2]) === static::$null) {
7028 array_pop($item[2]);
7031 return $item;
7034 if ($item[0] === Type::T_MAP) {
7035 $keys = $item[1];
7036 $values = $item[2];
7037 $list = [];
7039 for ($i = 0, $s = \count($keys); $i < $s; $i++) {
7040 $key = $keys[$i];
7041 $value = $values[$i];
7043 $list[] = [
7044 Type::T_LIST,
7045 ' ',
7046 [$key, $value]
7050 return [Type::T_LIST, $list ? ',' : '', $list];
7053 return [Type::T_LIST, '', [$item]];
7057 * Coerce color for expression
7059 * @param array|Number $value
7061 * @return array|Number
7063 protected function coerceForExpression($value)
7065 if ($color = $this->coerceColor($value)) {
7066 return $color;
7069 return $value;
7073 * Coerce value to color
7075 * @param array|Number $value
7076 * @param bool $inRGBFunction
7078 * @return array|null
7080 protected function coerceColor($value, $inRGBFunction = false)
7082 if ($value instanceof Number) {
7083 return null;
7086 switch ($value[0]) {
7087 case Type::T_COLOR:
7088 for ($i = 1; $i <= 3; $i++) {
7089 if (! is_numeric($value[$i])) {
7090 $cv = $this->compileRGBAValue($value[$i]);
7092 if (! is_numeric($cv)) {
7093 return null;
7096 $value[$i] = $cv;
7099 if (isset($value[4])) {
7100 if (! is_numeric($value[4])) {
7101 $cv = $this->compileRGBAValue($value[4], true);
7103 if (! is_numeric($cv)) {
7104 return null;
7107 $value[4] = $cv;
7112 return $value;
7114 case Type::T_LIST:
7115 if ($inRGBFunction) {
7116 if (\count($value[2]) == 3 || \count($value[2]) == 4) {
7117 $color = $value[2];
7118 array_unshift($color, Type::T_COLOR);
7120 return $this->coerceColor($color);
7124 return null;
7126 case Type::T_KEYWORD:
7127 if (! \is_string($value[1])) {
7128 return null;
7131 $name = strtolower($value[1]);
7133 // hexa color?
7134 if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
7135 $nofValues = \strlen($m[1]);
7137 if (\in_array($nofValues, [3, 4, 6, 8])) {
7138 $nbChannels = 3;
7139 $color = [];
7140 $num = hexdec($m[1]);
7142 switch ($nofValues) {
7143 case 4:
7144 $nbChannels = 4;
7145 // then continuing with the case 3:
7146 case 3:
7147 for ($i = 0; $i < $nbChannels; $i++) {
7148 $t = $num & 0xf;
7149 array_unshift($color, $t << 4 | $t);
7150 $num >>= 4;
7153 break;
7155 case 8:
7156 $nbChannels = 4;
7157 // then continuing with the case 6:
7158 case 6:
7159 for ($i = 0; $i < $nbChannels; $i++) {
7160 array_unshift($color, $num & 0xff);
7161 $num >>= 8;
7164 break;
7167 if ($nbChannels === 4) {
7168 if ($color[3] === 255) {
7169 $color[3] = 1; // fully opaque
7170 } else {
7171 $color[3] = round($color[3] / 255, Number::PRECISION);
7175 array_unshift($color, Type::T_COLOR);
7177 return $color;
7181 if ($rgba = Colors::colorNameToRGBa($name)) {
7182 return isset($rgba[3])
7183 ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
7184 : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
7187 return null;
7190 return null;
7194 * @param int|Number $value
7195 * @param bool $isAlpha
7197 * @return int|mixed
7199 protected function compileRGBAValue($value, $isAlpha = false)
7201 if ($isAlpha) {
7202 return $this->compileColorPartValue($value, 0, 1, false);
7205 return $this->compileColorPartValue($value, 0, 255, true);
7209 * @param mixed $value
7210 * @param int|float $min
7211 * @param int|float $max
7212 * @param bool $isInt
7214 * @return int|mixed
7216 protected function compileColorPartValue($value, $min, $max, $isInt = true)
7218 if (! is_numeric($value)) {
7219 if (\is_array($value)) {
7220 $reduced = $this->reduce($value);
7222 if ($reduced instanceof Number) {
7223 $value = $reduced;
7227 if ($value instanceof Number) {
7228 if ($value->unitless()) {
7229 $num = $value->getDimension();
7230 } elseif ($value->hasUnit('%')) {
7231 $num = $max * $value->getDimension() / 100;
7232 } else {
7233 throw $this->error('Expected %s to have no units or "%%".', $value);
7236 $value = $num;
7237 } elseif (\is_array($value)) {
7238 $value = $this->compileValue($value);
7242 if (is_numeric($value)) {
7243 if ($isInt) {
7244 $value = round($value);
7247 $value = min($max, max($min, $value));
7249 return $value;
7252 return $value;
7256 * Coerce value to string
7258 * @param array|Number $value
7260 * @return array
7262 protected function coerceString($value)
7264 if ($value[0] === Type::T_STRING) {
7265 return $value;
7268 return [Type::T_STRING, '', [$this->compileValue($value)]];
7272 * Assert value is a string
7274 * This method deals with internal implementation details of the value
7275 * representation where unquoted strings can sometimes be stored under
7276 * other types.
7277 * The returned value is always using the T_STRING type.
7279 * @api
7281 * @param array|Number $value
7282 * @param string|null $varName
7284 * @return array
7286 * @throws SassScriptException
7288 public function assertString($value, $varName = null)
7290 // case of url(...) parsed a a function
7291 if ($value[0] === Type::T_FUNCTION) {
7292 $value = $this->coerceString($value);
7295 if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
7296 $value = $this->compileValue($value);
7297 throw SassScriptException::forArgument("$value is not a string.", $varName);
7300 return $this->coerceString($value);
7304 * Coerce value to a percentage
7306 * @param array|Number $value
7308 * @return int|float
7310 * @deprecated
7312 protected function coercePercent($value)
7314 @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED);
7316 if ($value instanceof Number) {
7317 if ($value->hasUnit('%')) {
7318 return $value->getDimension() / 100;
7321 return $value->getDimension();
7324 return 0;
7328 * Assert value is a map
7330 * @api
7332 * @param array|Number $value
7333 * @param string|null $varName
7335 * @return array
7337 * @throws SassScriptException
7339 public function assertMap($value, $varName = null)
7341 $map = $this->tryMap($value);
7343 if ($map === null) {
7344 $value = $this->compileValue($value);
7346 throw SassScriptException::forArgument("$value is not a map.", $varName);
7349 return $map;
7353 * Assert value is a list
7355 * @api
7357 * @param array|Number $value
7359 * @return array
7361 * @throws \Exception
7363 public function assertList($value)
7365 if ($value[0] !== Type::T_LIST) {
7366 throw $this->error('expecting list, %s received', $value[0]);
7369 return $value;
7373 * Gets the keywords of an argument list.
7375 * Keys in the returned array are normalized names (underscores are replaced with dashes)
7376 * without the leading `$`.
7377 * Calling this helper with anything that an argument list received for a rest argument
7378 * of the function argument declaration is not supported.
7380 * @param array|Number $value
7382 * @return array<string, array|Number>
7384 public function getArgumentListKeywords($value)
7386 if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
7387 throw new \InvalidArgumentException('The argument is not a sass argument list.');
7390 return $value[3];
7394 * Assert value is a color
7396 * @api
7398 * @param array|Number $value
7399 * @param string|null $varName
7401 * @return array
7403 * @throws SassScriptException
7405 public function assertColor($value, $varName = null)
7407 if ($color = $this->coerceColor($value)) {
7408 return $color;
7411 $value = $this->compileValue($value);
7413 throw SassScriptException::forArgument("$value is not a color.", $varName);
7417 * Assert value is a number
7419 * @api
7421 * @param array|Number $value
7422 * @param string|null $varName
7424 * @return Number
7426 * @throws SassScriptException
7428 public function assertNumber($value, $varName = null)
7430 if (!$value instanceof Number) {
7431 $value = $this->compileValue($value);
7432 throw SassScriptException::forArgument("$value is not a number.", $varName);
7435 return $value;
7439 * Assert value is a integer
7441 * @api
7443 * @param array|Number $value
7444 * @param string|null $varName
7446 * @return int
7448 * @throws SassScriptException
7450 public function assertInteger($value, $varName = null)
7452 $value = $this->assertNumber($value, $varName)->getDimension();
7453 if (round($value - \intval($value), Number::PRECISION) > 0) {
7454 throw SassScriptException::forArgument("$value is not an integer.", $varName);
7457 return intval($value);
7461 * Extract the ... / alpha on the last argument of channel arg
7462 * in color functions
7464 * @param array $args
7465 * @return array
7467 private function extractSlashAlphaInColorFunction($args)
7469 $last = end($args);
7470 if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') {
7471 array_pop($args);
7472 $args[] = $last[2];
7473 $args[] = $last[3];
7475 return $args;
7480 * Make sure a color's components don't go out of bounds
7482 * @param array $c
7484 * @return array
7486 protected function fixColor($c)
7488 foreach ([1, 2, 3] as $i) {
7489 if ($c[$i] < 0) {
7490 $c[$i] = 0;
7493 if ($c[$i] > 255) {
7494 $c[$i] = 255;
7497 if (!\is_int($c[$i])) {
7498 $c[$i] = round($c[$i]);
7502 return $c;
7506 * Convert RGB to HSL
7508 * @internal
7510 * @param int $red
7511 * @param int $green
7512 * @param int $blue
7514 * @return array
7516 public function toHSL($red, $green, $blue)
7518 $min = min($red, $green, $blue);
7519 $max = max($red, $green, $blue);
7521 $l = $min + $max;
7522 $d = $max - $min;
7524 if ((int) $d === 0) {
7525 $h = $s = 0;
7526 } else {
7527 if ($l < 255) {
7528 $s = $d / $l;
7529 } else {
7530 $s = $d / (510 - $l);
7533 if ($red == $max) {
7534 $h = 60 * ($green - $blue) / $d;
7535 } elseif ($green == $max) {
7536 $h = 60 * ($blue - $red) / $d + 120;
7537 } elseif ($blue == $max) {
7538 $h = 60 * ($red - $green) / $d + 240;
7542 return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1];
7546 * Hue to RGB helper
7548 * @param float $m1
7549 * @param float $m2
7550 * @param float $h
7552 * @return float
7554 protected function hueToRGB($m1, $m2, $h)
7556 if ($h < 0) {
7557 $h += 1;
7558 } elseif ($h > 1) {
7559 $h -= 1;
7562 if ($h * 6 < 1) {
7563 return $m1 + ($m2 - $m1) * $h * 6;
7566 if ($h * 2 < 1) {
7567 return $m2;
7570 if ($h * 3 < 2) {
7571 return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
7574 return $m1;
7578 * Convert HSL to RGB
7580 * @internal
7582 * @param int|float $hue H from 0 to 360
7583 * @param int|float $saturation S from 0 to 100
7584 * @param int|float $lightness L from 0 to 100
7586 * @return array
7588 public function toRGB($hue, $saturation, $lightness)
7590 if ($hue < 0) {
7591 $hue += 360;
7594 $h = $hue / 360;
7595 $s = min(100, max(0, $saturation)) / 100;
7596 $l = min(100, max(0, $lightness)) / 100;
7598 $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
7599 $m1 = $l * 2 - $m2;
7601 $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
7602 $g = $this->hueToRGB($m1, $m2, $h) * 255;
7603 $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
7605 $out = [Type::T_COLOR, $r, $g, $b];
7607 return $out;
7611 * Convert HWB to RGB
7612 * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
7614 * @api
7616 * @param int $hue H from 0 to 360
7617 * @param int $whiteness W from 0 to 100
7618 * @param int $blackness B from 0 to 100
7620 * @return array
7622 private function HWBtoRGB($hue, $whiteness, $blackness)
7624 $w = min(100, max(0, $whiteness)) / 100;
7625 $b = min(100, max(0, $blackness)) / 100;
7627 $sum = $w + $b;
7628 if ($sum > 1.0) {
7629 $w = $w / $sum;
7630 $b = $b / $sum;
7632 $b = min(1.0 - $w, $b);
7634 $rgb = $this->toRGB($hue, 100, 50);
7635 for($i = 1; $i < 4; $i++) {
7636 $rgb[$i] *= (1.0 - $w - $b);
7637 $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001);
7640 return $rgb;
7644 * Convert RGB to HWB
7646 * @api
7648 * @param int $red
7649 * @param int $green
7650 * @param int $blue
7652 * @return array
7654 private function RGBtoHWB($red, $green, $blue)
7656 $min = min($red, $green, $blue);
7657 $max = max($red, $green, $blue);
7659 $d = $max - $min;
7661 if ((int) $d === 0) {
7662 $h = 0;
7663 } else {
7665 if ($red == $max) {
7666 $h = 60 * ($green - $blue) / $d;
7667 } elseif ($green == $max) {
7668 $h = 60 * ($blue - $red) / $d + 120;
7669 } elseif ($blue == $max) {
7670 $h = 60 * ($red - $green) / $d + 240;
7674 return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100];
7678 // Built in functions
7680 protected static $libCall = ['function', 'args...'];
7681 protected function libCall($args)
7683 $functionReference = $args[0];
7685 if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
7686 $name = $this->compileStringContent($this->coerceString($functionReference));
7687 $warning = "Passing a string to call() is deprecated and will be illegal\n"
7688 . "in Sass 4.0. Use call(function-reference($name)) instead.";
7689 Warn::deprecation($warning);
7690 $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]);
7693 if ($functionReference === static::$null) {
7694 return static::$null;
7697 if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
7698 throw $this->error('Function reference expected, got ' . $functionReference[0]);
7701 $callArgs = [
7702 [null, $args[1], true]
7705 return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
7709 protected static $libGetFunction = [
7710 ['name'],
7711 ['name', 'css']
7713 protected function libGetFunction($args)
7715 $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
7716 $isCss = false;
7718 if (count($args)) {
7719 $isCss = array_shift($args);
7720 $isCss = (($isCss === static::$true) ? true : false);
7723 if ($isCss) {
7724 return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
7727 return $this->getFunctionReference($name, true);
7730 protected static $libIf = ['condition', 'if-true', 'if-false:'];
7731 protected function libIf($args)
7733 list($cond, $t, $f) = $args;
7735 if (! $this->isTruthy($this->reduce($cond, true))) {
7736 return $this->reduce($f, true);
7739 return $this->reduce($t, true);
7742 protected static $libIndex = ['list', 'value'];
7743 protected function libIndex($args)
7745 list($list, $value) = $args;
7747 if (
7748 $list[0] === Type::T_MAP ||
7749 $list[0] === Type::T_STRING ||
7750 $list[0] === Type::T_KEYWORD ||
7751 $list[0] === Type::T_INTERPOLATE
7753 $list = $this->coerceList($list, ' ');
7756 if ($list[0] !== Type::T_LIST) {
7757 return static::$null;
7760 // Numbers are represented with value objects, for which the PHP equality operator does not
7761 // match the Sass rules (and we cannot overload it). As they are the only type of values
7762 // represented with a value object for now, they require a special case.
7763 if ($value instanceof Number) {
7764 $key = 0;
7765 foreach ($list[2] as $item) {
7766 $key++;
7767 $itemValue = $this->normalizeValue($item);
7769 if ($itemValue instanceof Number && $value->equals($itemValue)) {
7770 return new Number($key, '');
7773 return static::$null;
7776 $values = [];
7778 foreach ($list[2] as $item) {
7779 $values[] = $this->normalizeValue($item);
7782 $key = array_search($this->normalizeValue($value), $values);
7784 return false === $key ? static::$null : new Number($key + 1, '');
7787 protected static $libRgb = [
7788 ['color'],
7789 ['color', 'alpha'],
7790 ['channels'],
7791 ['red', 'green', 'blue'],
7792 ['red', 'green', 'blue', 'alpha'] ];
7793 protected function libRgb($args, $kwargs, $funcName = 'rgb')
7795 switch (\count($args)) {
7796 case 1:
7797 if (! $color = $this->coerceColor($args[0], true)) {
7798 $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
7800 break;
7802 case 3:
7803 $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
7805 if (! $color = $this->coerceColor($color)) {
7806 $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
7809 return $color;
7811 case 2:
7812 if ($color = $this->coerceColor($args[0], true)) {
7813 $alpha = $this->compileRGBAValue($args[1], true);
7815 if (is_numeric($alpha)) {
7816 $color[4] = $alpha;
7817 } else {
7818 $color = [Type::T_STRING, '',
7819 [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
7821 } else {
7822 $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']];
7824 break;
7826 case 4:
7827 default:
7828 $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
7830 if (! $color = $this->coerceColor($color)) {
7831 $color = [Type::T_STRING, '',
7832 [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
7834 break;
7837 return $color;
7840 protected static $libRgba = [
7841 ['color'],
7842 ['color', 'alpha'],
7843 ['channels'],
7844 ['red', 'green', 'blue'],
7845 ['red', 'green', 'blue', 'alpha'] ];
7846 protected function libRgba($args, $kwargs)
7848 return $this->libRgb($args, $kwargs, 'rgba');
7852 * Helper function for adjust_color, change_color, and scale_color
7854 * @param array<array|Number> $args
7855 * @param string $operation
7856 * @param callable $fn
7858 * @return array
7860 * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn
7862 protected function alterColor(array $args, $operation, $fn)
7864 $color = $this->assertColor($args[0], 'color');
7866 if ($args[1][2]) {
7867 throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
7870 $kwargs = $this->getArgumentListKeywords($args[1]);
7872 $scale = $operation === 'scale';
7873 $change = $operation === 'change';
7876 * @param string $name
7877 * @param float|int $max
7878 * @param bool $checkPercent
7879 * @param bool $assertPercent
7881 * @return float|int|null
7883 $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) {
7884 if (!isset($kwargs[$name])) {
7885 return null;
7888 $number = $this->assertNumber($kwargs[$name], $name);
7889 unset($kwargs[$name]);
7891 if (!$scale && $checkPercent) {
7892 if (!$number->hasUnit('%')) {
7893 $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated.");
7894 $this->logger->warn($warning->getMessage(), true);
7898 if ($scale || $assertPercent) {
7899 $number->assertUnit('%', $name);
7902 if ($scale) {
7903 $max = 100;
7906 return $number->valueInRange($change ? 0 : -$max, $max, $name);
7909 $alpha = $getParam('alpha', 1);
7910 $red = $getParam('red', 255);
7911 $green = $getParam('green', 255);
7912 $blue = $getParam('blue', 255);
7914 if ($scale || !isset($kwargs['hue'])) {
7915 $hue = null;
7916 } else {
7917 $hueNumber = $this->assertNumber($kwargs['hue'], 'hue');
7918 unset($kwargs['hue']);
7919 $hue = $hueNumber->getDimension();
7921 $saturation = $getParam('saturation', 100, true);
7922 $lightness = $getParam('lightness', 100, true);
7923 $whiteness = $getParam('whiteness', 100, false, true);
7924 $blackness = $getParam('blackness', 100, false, true);
7926 if (!empty($kwargs)) {
7927 $unknownNames = array_keys($kwargs);
7928 $lastName = array_pop($unknownNames);
7929 $message = sprintf(
7930 'No argument%s named $%s%s.',
7931 $unknownNames ? 's' : '',
7932 $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
7933 $lastName
7935 throw new SassScriptException($message);
7938 $hasRgb = $red !== null || $green !== null || $blue !== null;
7939 $hasSL = $saturation !== null || $lightness !== null;
7940 $hasWB = $whiteness !== null || $blackness !== null;
7941 $found = false;
7943 if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
7944 throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL'));
7947 if ($hasWB && $hasSL) {
7948 throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
7951 if ($hasRgb) {
7952 $color[1] = round($fn($color[1], $red, 255));
7953 $color[2] = round($fn($color[2], $green, 255));
7954 $color[3] = round($fn($color[3], $blue, 255));
7955 } elseif ($hasWB) {
7956 $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
7957 if ($hue !== null) {
7958 $hwb[1] = $change ? $hue : $hwb[1] + $hue;
7960 $hwb[2] = $fn($hwb[2], $whiteness, 100);
7961 $hwb[3] = $fn($hwb[3], $blackness, 100);
7963 $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]);
7965 if (isset($color[4])) {
7966 $rgb[4] = $color[4];
7969 $color = $rgb;
7970 } elseif ($hue !== null || $hasSL) {
7971 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7973 if ($hue !== null) {
7974 $hsl[1] = $change ? $hue : $hsl[1] + $hue;
7976 $hsl[2] = $fn($hsl[2], $saturation, 100);
7977 $hsl[3] = $fn($hsl[3], $lightness, 100);
7979 $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
7981 if (isset($color[4])) {
7982 $rgb[4] = $color[4];
7985 $color = $rgb;
7988 if ($alpha !== null) {
7989 $existingAlpha = isset($color[4]) ? $color[4] : 1;
7990 $color[4] = $fn($existingAlpha, $alpha, 1);
7993 return $color;
7996 protected static $libAdjustColor = ['color', 'kwargs...'];
7997 protected function libAdjustColor($args)
7999 return $this->alterColor($args, 'adjust', function ($base, $alter, $max) {
8000 if ($alter === null) {
8001 return $base;
8004 $new = $base + $alter;
8006 if ($new < 0) {
8007 return 0;
8010 if ($new > $max) {
8011 return $max;
8014 return $new;
8018 protected static $libChangeColor = ['color', 'kwargs...'];
8019 protected function libChangeColor($args)
8021 return $this->alterColor($args,'change', function ($base, $alter, $max) {
8022 if ($alter === null) {
8023 return $base;
8026 return $alter;
8030 protected static $libScaleColor = ['color', 'kwargs...'];
8031 protected function libScaleColor($args)
8033 return $this->alterColor($args, 'scale', function ($base, $scale, $max) {
8034 if ($scale === null) {
8035 return $base;
8038 $scale = $scale / 100;
8040 if ($scale < 0) {
8041 return $base * $scale + $base;
8044 return ($max - $base) * $scale + $base;
8048 protected static $libIeHexStr = ['color'];
8049 protected function libIeHexStr($args)
8051 $color = $this->coerceColor($args[0]);
8053 if (\is_null($color)) {
8054 throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color');
8057 $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
8059 return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
8062 protected static $libRed = ['color'];
8063 protected function libRed($args)
8065 $color = $this->coerceColor($args[0]);
8067 if (\is_null($color)) {
8068 throw $this->error('Error: argument `$color` of `red($color)` must be a color');
8071 return new Number((int) $color[1], '');
8074 protected static $libGreen = ['color'];
8075 protected function libGreen($args)
8077 $color = $this->coerceColor($args[0]);
8079 if (\is_null($color)) {
8080 throw $this->error('Error: argument `$color` of `green($color)` must be a color');
8083 return new Number((int) $color[2], '');
8086 protected static $libBlue = ['color'];
8087 protected function libBlue($args)
8089 $color = $this->coerceColor($args[0]);
8091 if (\is_null($color)) {
8092 throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
8095 return new Number((int) $color[3], '');
8098 protected static $libAlpha = ['color'];
8099 protected function libAlpha($args)
8101 if ($color = $this->coerceColor($args[0])) {
8102 return new Number(isset($color[4]) ? $color[4] : 1, '');
8105 // this might be the IE function, so return value unchanged
8106 return null;
8109 protected static $libOpacity = ['color'];
8110 protected function libOpacity($args)
8112 $value = $args[0];
8114 if ($value instanceof Number) {
8115 return null;
8118 return $this->libAlpha($args);
8121 // mix two colors
8122 protected static $libMix = [
8123 ['color1', 'color2', 'weight:50%'],
8124 ['color-1', 'color-2', 'weight:50%']
8126 protected function libMix($args)
8128 list($first, $second, $weight) = $args;
8130 $first = $this->assertColor($first, 'color1');
8131 $second = $this->assertColor($second, 'color2');
8132 $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100;
8134 $firstAlpha = isset($first[4]) ? $first[4] : 1;
8135 $secondAlpha = isset($second[4]) ? $second[4] : 1;
8137 $normalizedWeight = $weightScale * 2 - 1;
8138 $alphaDistance = $firstAlpha - $secondAlpha;
8140 $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance);
8141 $weight1 = ($combinedWeight + 1) / 2.0;
8142 $weight2 = 1.0 - $weight1;
8144 $new = [Type::T_COLOR,
8145 $weight1 * $first[1] + $weight2 * $second[1],
8146 $weight1 * $first[2] + $weight2 * $second[2],
8147 $weight1 * $first[3] + $weight2 * $second[3],
8150 if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
8151 $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale);
8154 return $this->fixColor($new);
8157 protected static $libHsl = [
8158 ['channels'],
8159 ['hue', 'saturation'],
8160 ['hue', 'saturation', 'lightness'],
8161 ['hue', 'saturation', 'lightness', 'alpha'] ];
8162 protected function libHsl($args, $kwargs, $funcName = 'hsl')
8164 $args_to_check = $args;
8166 if (\count($args) == 1) {
8167 if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
8168 return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
8171 $args = $args[0][2];
8172 $args_to_check = $kwargs['channels'][2];
8175 if (\count($args) === 2) {
8176 // if var() is used as an argument, return as a css function
8177 foreach ($args as $arg) {
8178 if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) {
8179 return null;
8183 throw new SassScriptException('Missing argument $lightness.');
8186 foreach ($kwargs as $k => $arg) {
8187 if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8188 return null;
8192 foreach ($args_to_check as $k => $arg) {
8193 if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
8194 if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8195 return null;
8198 $args[$k] = $this->stringifyFncallArgs($arg);
8201 if (
8202 $k >= 2 && count($args) === 4 &&
8203 in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
8204 in_array($arg[1], ['calc','env'])
8206 return null;
8210 $hue = $this->reduce($args[0]);
8211 $saturation = $this->reduce($args[1]);
8212 $lightness = $this->reduce($args[2]);
8213 $alpha = null;
8215 if (\count($args) === 4) {
8216 $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
8218 if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
8219 return [Type::T_STRING, '',
8220 [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
8222 } else {
8223 if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
8224 return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
8228 $hueValue = fmod($hue->getDimension(), 360);
8230 while ($hueValue < 0) {
8231 $hueValue += 360;
8234 $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
8236 if (! \is_null($alpha)) {
8237 $color[4] = $alpha;
8240 return $color;
8243 protected static $libHsla = [
8244 ['channels'],
8245 ['hue', 'saturation'],
8246 ['hue', 'saturation', 'lightness'],
8247 ['hue', 'saturation', 'lightness', 'alpha']];
8248 protected function libHsla($args, $kwargs)
8250 return $this->libHsl($args, $kwargs, 'hsla');
8253 protected static $libHue = ['color'];
8254 protected function libHue($args)
8256 $color = $this->assertColor($args[0], 'color');
8257 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8259 return new Number($hsl[1], 'deg');
8262 protected static $libSaturation = ['color'];
8263 protected function libSaturation($args)
8265 $color = $this->assertColor($args[0], 'color');
8266 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8268 return new Number($hsl[2], '%');
8271 protected static $libLightness = ['color'];
8272 protected function libLightness($args)
8274 $color = $this->assertColor($args[0], 'color');
8275 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8277 return new Number($hsl[3], '%');
8281 * Todo : a integrer dans le futur module color
8282 protected static $libHwb = [
8283 ['channels'],
8284 ['hue', 'whiteness', 'blackness'],
8285 ['hue', 'whiteness', 'blackness', 'alpha'] ];
8286 protected function libHwb($args, $kwargs, $funcName = 'hwb')
8288 $args_to_check = $args;
8290 if (\count($args) == 1) {
8291 if ($args[0][0] !== Type::T_LIST) {
8292 throw $this->error("Missing elements \$whiteness and \$blackness");
8295 if (\trim($args[0][1])) {
8296 throw $this->error("\$channels must be a space-separated list.");
8299 if (! empty($args[0]['enclosing'])) {
8300 throw $this->error("\$channels must be an unbracketed list.");
8303 $args = $args[0][2];
8304 if (\count($args) > 3) {
8305 throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed");
8308 $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]);
8309 if (\count($args_to_check) !== \count($kwargs['channels'][2])) {
8310 $args = $args_to_check;
8314 if (\count($args_to_check) < 2) {
8315 throw $this->error("Missing elements \$whiteness and \$blackness");
8317 if (\count($args_to_check) < 3) {
8318 throw $this->error("Missing element \$blackness");
8320 if (\count($args_to_check) > 4) {
8321 throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed");
8324 foreach ($kwargs as $k => $arg) {
8325 if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8326 return null;
8330 foreach ($args_to_check as $k => $arg) {
8331 if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
8332 if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
8333 return null;
8336 $args[$k] = $this->stringifyFncallArgs($arg);
8339 if (
8340 $k >= 2 && count($args) === 4 &&
8341 in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
8342 in_array($arg[1], ['calc','env'])
8344 return null;
8348 $hue = $this->reduce($args[0]);
8349 $whiteness = $this->reduce($args[1]);
8350 $blackness = $this->reduce($args[2]);
8351 $alpha = null;
8353 if (\count($args) === 4) {
8354 $alpha = $this->compileColorPartValue($args[3], 0, 1, false);
8356 if (! \is_numeric($alpha)) {
8357 $val = $this->compileValue($args[3]);
8358 throw $this->error("\$alpha: $val is not a number");
8362 $this->assertNumber($hue, 'hue');
8363 $this->assertUnit($whiteness, ['%'], 'whiteness');
8364 $this->assertUnit($blackness, ['%'], 'blackness');
8366 $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness");
8367 $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness");
8369 $w = $whiteness->getDimension();
8370 $b = $blackness->getDimension();
8372 $hueValue = $hue->getDimension() % 360;
8374 while ($hueValue < 0) {
8375 $hueValue += 360;
8378 $color = $this->HWBtoRGB($hueValue, $w, $b);
8380 if (! \is_null($alpha)) {
8381 $color[4] = $alpha;
8384 return $color;
8387 protected static $libWhiteness = ['color'];
8388 protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') {
8390 $color = $this->assertColor($args[0]);
8391 $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8393 return new Number($hwb[2], '%');
8396 protected static $libBlackness = ['color'];
8397 protected function libBlackness($args, $kwargs, $funcName = 'blackness') {
8399 $color = $this->assertColor($args[0]);
8400 $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
8402 return new Number($hwb[3], '%');
8407 * @param array $color
8408 * @param int $idx
8409 * @param int|float $amount
8411 * @return array
8413 protected function adjustHsl($color, $idx, $amount)
8415 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8416 $hsl[$idx] += $amount;
8418 if ($idx !== 1) {
8419 // Clamp the saturation and lightness
8420 $hsl[$idx] = min(max(0, $hsl[$idx]), 100);
8423 $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
8425 if (isset($color[4])) {
8426 $out[4] = $color[4];
8429 return $out;
8432 protected static $libAdjustHue = ['color', 'degrees'];
8433 protected function libAdjustHue($args)
8435 $color = $this->assertColor($args[0], 'color');
8436 $degrees = $this->assertNumber($args[1], 'degrees')->getDimension();
8438 return $this->adjustHsl($color, 1, $degrees);
8441 protected static $libLighten = ['color', 'amount'];
8442 protected function libLighten($args)
8444 $color = $this->assertColor($args[0], 'color');
8445 $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8447 return $this->adjustHsl($color, 3, $amount);
8450 protected static $libDarken = ['color', 'amount'];
8451 protected function libDarken($args)
8453 $color = $this->assertColor($args[0], 'color');
8454 $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
8456 return $this->adjustHsl($color, 3, -$amount);
8459 protected static $libSaturate = [['color', 'amount'], ['amount']];
8460 protected function libSaturate($args)
8462 $value = $args[0];
8464 if (count($args) === 1) {
8465 $this->assertNumber($args[0], 'amount');
8467 return null;
8470 $color = $this->assertColor($args[0], 'color');
8471 $amount = $this->assertNumber($args[1], 'amount');
8473 return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount'));
8476 protected static $libDesaturate = ['color', 'amount'];
8477 protected function libDesaturate($args)
8479 $color = $this->assertColor($args[0], 'color');
8480 $amount = $this->assertNumber($args[1], 'amount');
8482 return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount'));
8485 protected static $libGrayscale = ['color'];
8486 protected function libGrayscale($args)
8488 $value = $args[0];
8490 if ($value instanceof Number) {
8491 return null;
8494 return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100);
8497 protected static $libComplement = ['color'];
8498 protected function libComplement($args)
8500 return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180);
8503 protected static $libInvert = ['color', 'weight:100%'];
8504 protected function libInvert($args)
8506 $value = $args[0];
8508 $weight = $this->assertNumber($args[1], 'weight');
8510 if ($value instanceof Number) {
8511 if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) {
8512 throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.');
8515 return null;
8518 $color = $this->assertColor($value, 'color');
8519 $inverted = $color;
8520 $inverted[1] = 255 - $inverted[1];
8521 $inverted[2] = 255 - $inverted[2];
8522 $inverted[3] = 255 - $inverted[3];
8524 return $this->libMix([$inverted, $color, $weight]);
8527 // increases opacity by amount
8528 protected static $libOpacify = ['color', 'amount'];
8529 protected function libOpacify($args)
8531 $color = $this->assertColor($args[0], 'color');
8532 $amount = $this->assertNumber($args[1], 'amount');
8534 $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount');
8535 $color[4] = min(1, max(0, $color[4]));
8537 return $color;
8540 protected static $libFadeIn = ['color', 'amount'];
8541 protected function libFadeIn($args)
8543 return $this->libOpacify($args);
8546 // decreases opacity by amount
8547 protected static $libTransparentize = ['color', 'amount'];
8548 protected function libTransparentize($args)
8550 $color = $this->assertColor($args[0], 'color');
8551 $amount = $this->assertNumber($args[1], 'amount');
8553 $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount');
8554 $color[4] = min(1, max(0, $color[4]));
8556 return $color;
8559 protected static $libFadeOut = ['color', 'amount'];
8560 protected function libFadeOut($args)
8562 return $this->libTransparentize($args);
8565 protected static $libUnquote = ['string'];
8566 protected function libUnquote($args)
8568 try {
8569 $str = $this->assertString($args[0], 'string');
8570 } catch (SassScriptException $e) {
8571 $value = $this->compileValue($args[0]);
8572 $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
8573 $line = $this->sourceLine;
8575 $message = "Passing $value, a non-string value, to unquote()
8576 will be an error in future versions of Sass.\n on line $line of $fname";
8578 $this->logger->warn($message, true);
8580 return $args[0];
8583 $str[1] = '';
8585 return $str;
8588 protected static $libQuote = ['string'];
8589 protected function libQuote($args)
8591 $value = $this->assertString($args[0], 'string');
8593 $value[1] = '"';
8595 return $value;
8598 protected static $libPercentage = ['number'];
8599 protected function libPercentage($args)
8601 $num = $this->assertNumber($args[0], 'number');
8602 $num->assertNoUnits('number');
8604 return new Number($num->getDimension() * 100, '%');
8607 protected static $libRound = ['number'];
8608 protected function libRound($args)
8610 $num = $this->assertNumber($args[0], 'number');
8612 return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8615 protected static $libFloor = ['number'];
8616 protected function libFloor($args)
8618 $num = $this->assertNumber($args[0], 'number');
8620 return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8623 protected static $libCeil = ['number'];
8624 protected function libCeil($args)
8626 $num = $this->assertNumber($args[0], 'number');
8628 return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8631 protected static $libAbs = ['number'];
8632 protected function libAbs($args)
8634 $num = $this->assertNumber($args[0], 'number');
8636 return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
8639 protected static $libMin = ['numbers...'];
8640 protected function libMin($args)
8643 * @var Number|null
8645 $min = null;
8647 foreach ($args[0][2] as $arg) {
8648 $number = $this->assertNumber($arg);
8650 if (\is_null($min) || $min->greaterThan($number)) {
8651 $min = $number;
8655 if (!\is_null($min)) {
8656 return $min;
8659 throw $this->error('At least one argument must be passed.');
8662 protected static $libMax = ['numbers...'];
8663 protected function libMax($args)
8666 * @var Number|null
8668 $max = null;
8670 foreach ($args[0][2] as $arg) {
8671 $number = $this->assertNumber($arg);
8673 if (\is_null($max) || $max->lessThan($number)) {
8674 $max = $number;
8678 if (!\is_null($max)) {
8679 return $max;
8682 throw $this->error('At least one argument must be passed.');
8685 protected static $libLength = ['list'];
8686 protected function libLength($args)
8688 $list = $this->coerceList($args[0], ',', true);
8690 return new Number(\count($list[2]), '');
8693 protected static $libListSeparator = ['list'];
8694 protected function libListSeparator($args)
8696 if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
8697 return [Type::T_KEYWORD, 'space'];
8700 $list = $this->coerceList($args[0]);
8702 if ($list[1] === '' && \count($list[2]) <= 1 && empty($list['enclosing'])) {
8703 return [Type::T_KEYWORD, 'space'];
8706 if ($list[1] === ',') {
8707 return [Type::T_KEYWORD, 'comma'];
8710 if ($list[1] === '/') {
8711 return [Type::T_KEYWORD, 'slash'];
8714 return [Type::T_KEYWORD, 'space'];
8717 protected static $libNth = ['list', 'n'];
8718 protected function libNth($args)
8720 $list = $this->coerceList($args[0], ',', false);
8721 $n = $this->assertNumber($args[1])->getDimension();
8723 if ($n > 0) {
8724 $n--;
8725 } elseif ($n < 0) {
8726 $n += \count($list[2]);
8729 return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
8732 protected static $libSetNth = ['list', 'n', 'value'];
8733 protected function libSetNth($args)
8735 $list = $this->coerceList($args[0]);
8736 $n = $this->assertNumber($args[1])->getDimension();
8738 if ($n > 0) {
8739 $n--;
8740 } elseif ($n < 0) {
8741 $n += \count($list[2]);
8744 if (! isset($list[2][$n])) {
8745 throw $this->error('Invalid argument for "n"');
8748 $list[2][$n] = $args[2];
8750 return $list;
8753 protected static $libMapGet = ['map', 'key', 'keys...'];
8754 protected function libMapGet($args)
8756 $map = $this->assertMap($args[0], 'map');
8757 if (!isset($args[2])) {
8758 // BC layer for usages of the function from PHP code rather than from the Sass function
8759 $args[2] = self::$emptyArgumentList;
8761 $keys = array_merge([$args[1]], $args[2][2]);
8762 $value = static::$null;
8764 foreach ($keys as $key) {
8765 if (!\is_array($map) || $map[0] !== Type::T_MAP) {
8766 return static::$null;
8769 $map = $this->mapGet($map, $key);
8771 if ($map === null) {
8772 return static::$null;
8775 $value = $map;
8778 return $value;
8782 * Gets the value corresponding to that key in the map
8784 * @param array $map
8785 * @param Number|array $key
8787 * @return Number|array|null
8789 private function mapGet(array $map, $key)
8791 $index = $this->mapGetEntryIndex($map, $key);
8793 if ($index !== null) {
8794 return $map[2][$index];
8797 return null;
8801 * Gets the index corresponding to that key in the map entries
8803 * @param array $map
8804 * @param Number|array $key
8806 * @return int|null
8808 private function mapGetEntryIndex(array $map, $key)
8810 $key = $this->compileStringContent($this->coerceString($key));
8812 for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8813 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
8814 return $i;
8818 return null;
8821 protected static $libMapKeys = ['map'];
8822 protected function libMapKeys($args)
8824 $map = $this->assertMap($args[0], 'map');
8825 $keys = $map[1];
8827 return [Type::T_LIST, ',', $keys];
8830 protected static $libMapValues = ['map'];
8831 protected function libMapValues($args)
8833 $map = $this->assertMap($args[0], 'map');
8834 $values = $map[2];
8836 return [Type::T_LIST, ',', $values];
8839 protected static $libMapRemove = [
8840 ['map'],
8841 ['map', 'key', 'keys...'],
8843 protected function libMapRemove($args)
8845 $map = $this->assertMap($args[0], 'map');
8847 if (\count($args) === 1) {
8848 return $map;
8851 $keys = [];
8852 $keys[] = $this->compileStringContent($this->coerceString($args[1]));
8854 foreach ($args[2][2] as $key) {
8855 $keys[] = $this->compileStringContent($this->coerceString($key));
8858 for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8859 if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
8860 array_splice($map[1], $i, 1);
8861 array_splice($map[2], $i, 1);
8865 return $map;
8868 protected static $libMapHasKey = ['map', 'key', 'keys...'];
8869 protected function libMapHasKey($args)
8871 $map = $this->assertMap($args[0], 'map');
8872 if (!isset($args[2])) {
8873 // BC layer for usages of the function from PHP code rather than from the Sass function
8874 $args[2] = self::$emptyArgumentList;
8876 $keys = array_merge([$args[1]], $args[2][2]);
8877 $lastKey = array_pop($keys);
8879 foreach ($keys as $key) {
8880 $value = $this->mapGet($map, $key);
8882 if ($value === null || $value instanceof Number || $value[0] !== Type::T_MAP) {
8883 return self::$false;
8886 $map = $value;
8889 return $this->toBool($this->mapHasKey($map, $lastKey));
8893 * @param array|Number $keyValue
8895 * @return bool
8897 private function mapHasKey(array $map, $keyValue)
8899 $key = $this->compileStringContent($this->coerceString($keyValue));
8901 for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
8902 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
8903 return true;
8907 return false;
8910 protected static $libMapMerge = [
8911 ['map1', 'map2'],
8912 ['map-1', 'map-2'],
8913 ['map1', 'args...']
8915 protected function libMapMerge($args)
8917 $map1 = $this->assertMap($args[0], 'map1');
8918 $map2 = $args[1];
8919 $keys = [];
8920 if ($map2[0] === Type::T_LIST && isset($map2[3]) && \is_array($map2[3])) {
8921 // This is an argument list for the variadic signature
8922 if (\count($map2[2]) === 0) {
8923 throw new SassScriptException('Expected $args to contain a key.');
8925 if (\count($map2[2]) === 1) {
8926 throw new SassScriptException('Expected $args to contain a value.');
8928 $keys = $map2[2];
8929 $map2 = array_pop($keys);
8931 $map2 = $this->assertMap($map2, 'map2');
8933 return $this->modifyMap($map1, $keys, function ($oldValue) use ($map2) {
8934 $nestedMap = $this->tryMap($oldValue);
8936 if ($nestedMap === null) {
8937 return $map2;
8940 return $this->mergeMaps($nestedMap, $map2);
8945 * @param array $map
8946 * @param array $keys
8947 * @param callable $modify
8948 * @param bool $addNesting
8950 * @return Number|array
8952 * @phpstan-param array<Number|array> $keys
8953 * @phpstan-param callable(Number|array): (Number|array) $modify
8955 private function modifyMap(array $map, array $keys, callable $modify, $addNesting = true)
8957 if ($keys === []) {
8958 return $modify($map);
8961 return $this->modifyNestedMap($map, $keys, $modify, $addNesting);
8965 * @param array $map
8966 * @param array $keys
8967 * @param callable $modify
8968 * @param bool $addNesting
8970 * @return array
8972 * @phpstan-param non-empty-array<Number|array> $keys
8973 * @phpstan-param callable(Number|array): (Number|array) $modify
8975 private function modifyNestedMap(array $map, array $keys, callable $modify, $addNesting)
8977 $key = array_shift($keys);
8979 $nestedValueIndex = $this->mapGetEntryIndex($map, $key);
8981 if ($keys === []) {
8982 if ($nestedValueIndex !== null) {
8983 $map[2][$nestedValueIndex] = $modify($map[2][$nestedValueIndex]);
8984 } else {
8985 $map[1][] = $key;
8986 $map[2][] = $modify(self::$null);
8989 return $map;
8992 $nestedMap = $nestedValueIndex !== null ? $this->tryMap($map[2][$nestedValueIndex]) : null;
8994 if ($nestedMap === null && !$addNesting) {
8995 return $map;
8998 if ($nestedMap === null) {
8999 $nestedMap = self::$emptyMap;
9002 $newNestedMap = $this->modifyNestedMap($nestedMap, $keys, $modify, $addNesting);
9004 if ($nestedValueIndex !== null) {
9005 $map[2][$nestedValueIndex] = $newNestedMap;
9006 } else {
9007 $map[1][] = $key;
9008 $map[2][] = $newNestedMap;
9011 return $map;
9015 * Merges 2 Sass maps together
9017 * @param array $map1
9018 * @param array $map2
9020 * @return array
9022 private function mergeMaps(array $map1, array $map2)
9024 foreach ($map2[1] as $i2 => $key2) {
9025 $map1EntryIndex = $this->mapGetEntryIndex($map1, $key2);
9027 if ($map1EntryIndex !== null) {
9028 $map1[2][$map1EntryIndex] = $map2[2][$i2];
9029 continue;
9032 $map1[1][] = $key2;
9033 $map1[2][] = $map2[2][$i2];
9036 return $map1;
9039 protected static $libKeywords = ['args'];
9040 protected function libKeywords($args)
9042 $value = $args[0];
9044 if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
9045 $compiledValue = $this->compileValue($value);
9047 throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args');
9050 $keys = [];
9051 $values = [];
9053 foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
9054 $keys[] = [Type::T_KEYWORD, $name];
9055 $values[] = $arg;
9058 return [Type::T_MAP, $keys, $values];
9061 protected static $libIsBracketed = ['list'];
9062 protected function libIsBracketed($args)
9064 $list = $args[0];
9065 $this->coerceList($list, ' ');
9067 if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
9068 return self::$true;
9071 return self::$false;
9075 * @param array $list1
9076 * @param array|Number|null $sep
9078 * @return string
9079 * @throws CompilerException
9081 * @deprecated
9083 protected function listSeparatorForJoin($list1, $sep)
9085 @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
9087 if (! isset($sep)) {
9088 return $list1[1];
9091 switch ($this->compileValue($sep)) {
9092 case 'comma':
9093 return ',';
9095 case 'space':
9096 return ' ';
9098 default:
9099 return $list1[1];
9103 protected static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto'];
9104 protected function libJoin($args)
9106 list($list1, $list2, $sep, $bracketed) = $args;
9108 $list1 = $this->coerceList($list1, ' ', true);
9109 $list2 = $this->coerceList($list2, ' ', true);
9111 switch ($this->compileStringContent($this->assertString($sep, 'separator'))) {
9112 case 'comma':
9113 $separator = ',';
9114 break;
9116 case 'space':
9117 $separator = ' ';
9118 break;
9120 case 'slash':
9121 $separator = '/';
9122 break;
9124 case 'auto':
9125 if ($list1[1] !== '' || count($list1[2]) > 1 || !empty($list1['enclosing']) && $list1['enclosing'] !== 'parent') {
9126 $separator = $list1[1] ?: ' ';
9127 } elseif ($list2[1] !== '' || count($list2[2]) > 1 || !empty($list2['enclosing']) && $list2['enclosing'] !== 'parent') {
9128 $separator = $list2[1] ?: ' ';
9129 } else {
9130 $separator = ' ';
9132 break;
9134 default:
9135 throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator');
9138 if ($bracketed === static::$true) {
9139 $bracketed = true;
9140 } elseif ($bracketed === static::$false) {
9141 $bracketed = false;
9142 } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
9143 $bracketed = 'auto';
9144 } elseif ($bracketed === static::$null) {
9145 $bracketed = false;
9146 } else {
9147 $bracketed = $this->compileValue($bracketed);
9148 $bracketed = ! ! $bracketed;
9150 if ($bracketed === true) {
9151 $bracketed = true;
9155 if ($bracketed === 'auto') {
9156 $bracketed = false;
9158 if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
9159 $bracketed = true;
9163 $res = [Type::T_LIST, $separator, array_merge($list1[2], $list2[2])];
9165 if ($bracketed) {
9166 $res['enclosing'] = 'bracket';
9169 return $res;
9172 protected static $libAppend = ['list', 'val', 'separator:auto'];
9173 protected function libAppend($args)
9175 list($list1, $value, $sep) = $args;
9177 $list1 = $this->coerceList($list1, ' ', true);
9179 switch ($this->compileStringContent($this->assertString($sep, 'separator'))) {
9180 case 'comma':
9181 $separator = ',';
9182 break;
9184 case 'space':
9185 $separator = ' ';
9186 break;
9188 case 'slash':
9189 $separator = '/';
9190 break;
9192 case 'auto':
9193 $separator = $list1[1] === '' && \count($list1[2]) <= 1 && (empty($list1['enclosing']) || $list1['enclosing'] === 'parent') ? ' ' : $list1[1];
9194 break;
9196 default:
9197 throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator');
9200 $res = [Type::T_LIST, $separator, array_merge($list1[2], [$value])];
9202 if (isset($list1['enclosing'])) {
9203 $res['enclosing'] = $list1['enclosing'];
9206 return $res;
9209 protected static $libZip = ['lists...'];
9210 protected function libZip($args)
9212 $argLists = [];
9213 foreach ($args[0][2] as $arg) {
9214 $argLists[] = $this->coerceList($arg);
9217 $lists = [];
9218 $firstList = array_shift($argLists);
9220 $result = [Type::T_LIST, ',', $lists];
9221 if (! \is_null($firstList)) {
9222 foreach ($firstList[2] as $key => $item) {
9223 $list = [Type::T_LIST, ' ', [$item]];
9225 foreach ($argLists as $arg) {
9226 if (isset($arg[2][$key])) {
9227 $list[2][] = $arg[2][$key];
9228 } else {
9229 break 2;
9233 $lists[] = $list;
9236 $result[2] = $lists;
9237 } else {
9238 $result['enclosing'] = 'parent';
9241 return $result;
9244 protected static $libTypeOf = ['value'];
9245 protected function libTypeOf($args)
9247 $value = $args[0];
9249 return [Type::T_KEYWORD, $this->getTypeOf($value)];
9253 * @param array|Number $value
9255 * @return string
9257 private function getTypeOf($value)
9259 switch ($value[0]) {
9260 case Type::T_KEYWORD:
9261 if ($value === static::$true || $value === static::$false) {
9262 return 'bool';
9265 if ($this->coerceColor($value)) {
9266 return 'color';
9269 // fall-thru
9270 case Type::T_FUNCTION:
9271 return 'string';
9273 case Type::T_FUNCTION_REFERENCE:
9274 return 'function';
9276 case Type::T_LIST:
9277 if (isset($value[3]) && \is_array($value[3])) {
9278 return 'arglist';
9281 // fall-thru
9282 default:
9283 return $value[0];
9287 protected static $libUnit = ['number'];
9288 protected function libUnit($args)
9290 $num = $this->assertNumber($args[0], 'number');
9292 return [Type::T_STRING, '"', [$num->unitStr()]];
9295 protected static $libUnitless = ['number'];
9296 protected function libUnitless($args)
9298 $value = $this->assertNumber($args[0], 'number');
9300 return $this->toBool($value->unitless());
9303 protected static $libComparable = [
9304 ['number1', 'number2'],
9305 ['number-1', 'number-2']
9307 protected function libComparable($args)
9309 list($number1, $number2) = $args;
9311 if (
9312 ! $number1 instanceof Number ||
9313 ! $number2 instanceof Number
9315 throw $this->error('Invalid argument(s) for "comparable"');
9318 return $this->toBool($number1->isComparableTo($number2));
9321 protected static $libStrIndex = ['string', 'substring'];
9322 protected function libStrIndex($args)
9324 $string = $this->assertString($args[0], 'string');
9325 $stringContent = $this->compileStringContent($string);
9327 $substring = $this->assertString($args[1], 'substring');
9328 $substringContent = $this->compileStringContent($substring);
9330 if (! \strlen($substringContent)) {
9331 $result = 0;
9332 } else {
9333 $result = Util::mbStrpos($stringContent, $substringContent);
9336 return $result === false ? static::$null : new Number($result + 1, '');
9339 protected static $libStrInsert = ['string', 'insert', 'index'];
9340 protected function libStrInsert($args)
9342 $string = $this->assertString($args[0], 'string');
9343 $stringContent = $this->compileStringContent($string);
9345 $insert = $this->assertString($args[1], 'insert');
9346 $insertContent = $this->compileStringContent($insert);
9348 $index = $this->assertInteger($args[2], 'index');
9349 if ($index > 0) {
9350 $index = $index - 1;
9352 if ($index < 0) {
9353 $index = Util::mbStrlen($stringContent) + 1 + $index;
9356 $string[2] = [
9357 Util::mbSubstr($stringContent, 0, $index),
9358 $insertContent,
9359 Util::mbSubstr($stringContent, $index)
9362 return $string;
9365 protected static $libStrLength = ['string'];
9366 protected function libStrLength($args)
9368 $string = $this->assertString($args[0], 'string');
9369 $stringContent = $this->compileStringContent($string);
9371 return new Number(Util::mbStrlen($stringContent), '');
9374 protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
9375 protected function libStrSlice($args)
9377 $string = $this->assertString($args[0], 'string');
9378 $stringContent = $this->compileStringContent($string);
9380 $start = $this->assertNumber($args[1], 'start-at');
9381 $start->assertNoUnits('start-at');
9382 $startInt = $this->assertInteger($start, 'start-at');
9383 $end = $this->assertNumber($args[2], 'end-at');
9384 $end->assertNoUnits('end-at');
9385 $endInt = $this->assertInteger($end, 'end-at');
9387 if ($endInt === 0) {
9388 return [Type::T_STRING, $string[1], []];
9391 if ($startInt > 0) {
9392 $startInt--;
9395 if ($endInt < 0) {
9396 $endInt = Util::mbStrlen($stringContent) + $endInt;
9397 } else {
9398 $endInt--;
9401 if ($endInt < $startInt) {
9402 return [Type::T_STRING, $string[1], []];
9405 $length = $endInt - $startInt + 1; // The end of the slice is inclusive
9407 $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)];
9409 return $string;
9412 protected static $libToLowerCase = ['string'];
9413 protected function libToLowerCase($args)
9415 $string = $this->assertString($args[0], 'string');
9416 $stringContent = $this->compileStringContent($string);
9418 $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
9420 return $string;
9423 protected static $libToUpperCase = ['string'];
9424 protected function libToUpperCase($args)
9426 $string = $this->assertString($args[0], 'string');
9427 $stringContent = $this->compileStringContent($string);
9429 $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
9431 return $string;
9435 * Apply a filter on a string content, only on ascii chars
9436 * let extended chars untouched
9438 * @param string $stringContent
9439 * @param callable $filter
9440 * @return string
9442 protected function stringTransformAsciiOnly($stringContent, $filter)
9444 $mblength = Util::mbStrlen($stringContent);
9445 if ($mblength === strlen($stringContent)) {
9446 return $filter($stringContent);
9448 $filteredString = "";
9449 for ($i = 0; $i < $mblength; $i++) {
9450 $char = Util::mbSubstr($stringContent, $i, 1);
9451 if (strlen($char) > 1) {
9452 $filteredString .= $char;
9453 } else {
9454 $filteredString .= $filter($char);
9458 return $filteredString;
9461 protected static $libFeatureExists = ['feature'];
9462 protected function libFeatureExists($args)
9464 $string = $this->assertString($args[0], 'feature');
9465 $name = $this->compileStringContent($string);
9467 return $this->toBool(
9468 \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
9472 protected static $libFunctionExists = ['name'];
9473 protected function libFunctionExists($args)
9475 $string = $this->assertString($args[0], 'name');
9476 $name = $this->compileStringContent($string);
9478 // user defined functions
9479 if ($this->has(static::$namespaces['function'] . $name)) {
9480 return self::$true;
9483 $name = $this->normalizeName($name);
9485 if (isset($this->userFunctions[$name])) {
9486 return self::$true;
9489 // built-in functions
9490 $f = $this->getBuiltinFunction($name);
9492 return $this->toBool(\is_callable($f));
9495 protected static $libGlobalVariableExists = ['name'];
9496 protected function libGlobalVariableExists($args)
9498 $string = $this->assertString($args[0], 'name');
9499 $name = $this->compileStringContent($string);
9501 return $this->toBool($this->has($name, $this->rootEnv));
9504 protected static $libMixinExists = ['name'];
9505 protected function libMixinExists($args)
9507 $string = $this->assertString($args[0], 'name');
9508 $name = $this->compileStringContent($string);
9510 return $this->toBool($this->has(static::$namespaces['mixin'] . $name));
9513 protected static $libVariableExists = ['name'];
9514 protected function libVariableExists($args)
9516 $string = $this->assertString($args[0], 'name');
9517 $name = $this->compileStringContent($string);
9519 return $this->toBool($this->has($name));
9522 protected static $libCounter = ['args...'];
9524 * Workaround IE7's content counter bug.
9526 * @param array $args
9528 * @return array
9530 protected function libCounter($args)
9532 $list = array_map([$this, 'compileValue'], $args[0][2]);
9534 return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
9537 protected static $libRandom = ['limit:null'];
9538 protected function libRandom($args)
9540 if (isset($args[0]) && $args[0] !== static::$null) {
9541 $n = $this->assertInteger($args[0], 'limit');
9543 if ($n < 1) {
9544 throw new SassScriptException("\$limit: Must be greater than 0, was $n.");
9547 return new Number(mt_rand(1, $n), '');
9550 $max = mt_getrandmax();
9551 return new Number(mt_rand(0, $max - 1) / $max, '');
9554 protected static $libUniqueId = [];
9555 protected function libUniqueId()
9557 static $id;
9559 if (! isset($id)) {
9560 $id = PHP_INT_SIZE === 4
9561 ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
9562 : mt_rand(0, pow(36, 8));
9565 $id += mt_rand(0, 10) + 1;
9567 return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
9571 * @param array|Number $value
9572 * @param bool $force_enclosing_display
9574 * @return array
9576 protected function inspectFormatValue($value, $force_enclosing_display = false)
9578 if ($value === static::$null) {
9579 $value = [Type::T_KEYWORD, 'null'];
9582 $stringValue = [$value];
9584 if ($value instanceof Number) {
9585 return [Type::T_STRING, '', $stringValue];
9588 if ($value[0] === Type::T_LIST) {
9589 if (end($value[2]) === static::$null) {
9590 array_pop($value[2]);
9591 $value[2][] = [Type::T_STRING, '', ['']];
9592 $force_enclosing_display = true;
9595 if (
9596 ! empty($value['enclosing']) &&
9597 ($force_enclosing_display ||
9598 ($value['enclosing'] === 'bracket') ||
9599 ! \count($value[2]))
9601 $value['enclosing'] = 'forced_' . $value['enclosing'];
9602 $force_enclosing_display = true;
9603 } elseif (! \count($value[2])) {
9604 $value['enclosing'] = 'forced_parent';
9607 foreach ($value[2] as $k => $listelement) {
9608 $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
9611 $stringValue = [$value];
9614 return [Type::T_STRING, '', $stringValue];
9617 protected static $libInspect = ['value'];
9618 protected function libInspect($args)
9620 $value = $args[0];
9622 return $this->inspectFormatValue($value);
9626 * Preprocess selector args
9628 * @param array $arg
9629 * @param string|null $varname
9630 * @param bool $allowParent
9632 * @return array
9634 protected function getSelectorArg($arg, $varname = null, $allowParent = false)
9636 static $parser = null;
9638 if (\is_null($parser)) {
9639 $parser = $this->parserFactory(__METHOD__);
9642 if (! $this->checkSelectorArgType($arg)) {
9643 $var_value = $this->compileValue($arg);
9644 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);
9648 if ($arg[0] === Type::T_STRING) {
9649 $arg[1] = '';
9651 $arg = $this->compileValue($arg);
9653 $parsedSelector = [];
9655 if ($parser->parseSelector($arg, $parsedSelector, true)) {
9656 $selector = $this->evalSelectors($parsedSelector);
9657 $gluedSelector = $this->glueFunctionSelectors($selector);
9659 if (! $allowParent) {
9660 foreach ($gluedSelector as $selector) {
9661 foreach ($selector as $s) {
9662 if (in_array(static::$selfSelector, $s)) {
9663 throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname);
9669 return $gluedSelector;
9672 throw SassScriptException::forArgument("expected more input, invalid selector.", $varname);
9676 * Check variable type for getSelectorArg() function
9677 * @param array $arg
9678 * @param int $maxDepth
9679 * @return bool
9681 protected function checkSelectorArgType($arg, $maxDepth = 2)
9683 if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
9684 foreach ($arg[2] as $elt) {
9685 if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
9686 return false;
9689 return true;
9691 if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
9692 return false;
9694 return true;
9698 * Postprocess selector to output in right format
9700 * @param array $selectors
9702 * @return array
9704 protected function formatOutputSelector($selectors)
9706 $selectors = $this->collapseSelectorsAsList($selectors);
9708 return $selectors;
9711 protected static $libIsSuperselector = ['super', 'sub'];
9712 protected function libIsSuperselector($args)
9714 list($super, $sub) = $args;
9716 $super = $this->getSelectorArg($super, 'super');
9717 $sub = $this->getSelectorArg($sub, 'sub');
9719 return $this->toBool($this->isSuperSelector($super, $sub));
9723 * Test a $super selector again $sub
9725 * @param array $super
9726 * @param array $sub
9728 * @return bool
9730 protected function isSuperSelector($super, $sub)
9732 // one and only one selector for each arg
9733 if (! $super) {
9734 throw $this->error('Invalid super selector for isSuperSelector()');
9737 if (! $sub) {
9738 throw $this->error('Invalid sub selector for isSuperSelector()');
9741 if (count($sub) > 1) {
9742 foreach ($sub as $s) {
9743 if (! $this->isSuperSelector($super, [$s])) {
9744 return false;
9747 return true;
9750 if (count($super) > 1) {
9751 foreach ($super as $s) {
9752 if ($this->isSuperSelector([$s], $sub)) {
9753 return true;
9756 return false;
9759 $super = reset($super);
9760 $sub = reset($sub);
9762 $i = 0;
9763 $nextMustMatch = false;
9765 foreach ($super as $node) {
9766 $compound = '';
9768 array_walk_recursive(
9769 $node,
9770 function ($value, $key) use (&$compound) {
9771 $compound .= $value;
9775 if ($this->isImmediateRelationshipCombinator($compound)) {
9776 if ($node !== $sub[$i]) {
9777 return false;
9780 $nextMustMatch = true;
9781 $i++;
9782 } else {
9783 while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
9784 if ($nextMustMatch) {
9785 return false;
9788 $i++;
9791 if ($i >= \count($sub)) {
9792 return false;
9795 $nextMustMatch = false;
9796 $i++;
9800 return true;
9804 * Test a part of super selector again a part of sub selector
9806 * @param array $superParts
9807 * @param array $subParts
9809 * @return bool
9811 protected function isSuperPart($superParts, $subParts)
9813 $i = 0;
9815 foreach ($superParts as $superPart) {
9816 while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
9817 $i++;
9820 if ($i >= \count($subParts)) {
9821 return false;
9824 $i++;
9827 return true;
9830 protected static $libSelectorAppend = ['selector...'];
9831 protected function libSelectorAppend($args)
9833 // get the selector... list
9834 $args = reset($args);
9835 $args = $args[2];
9837 if (\count($args) < 1) {
9838 throw $this->error('selector-append() needs at least 1 argument');
9841 $selectors = [];
9842 foreach ($args as $arg) {
9843 $selectors[] = $this->getSelectorArg($arg, 'selector');
9846 return $this->formatOutputSelector($this->selectorAppend($selectors));
9850 * Append parts of the last selector in the list to the previous, recursively
9852 * @param array $selectors
9854 * @return array
9856 * @throws \ScssPhp\ScssPhp\Exception\CompilerException
9858 protected function selectorAppend($selectors)
9860 $lastSelectors = array_pop($selectors);
9862 if (! $lastSelectors) {
9863 throw $this->error('Invalid selector list in selector-append()');
9866 while (\count($selectors)) {
9867 $previousSelectors = array_pop($selectors);
9869 if (! $previousSelectors) {
9870 throw $this->error('Invalid selector list in selector-append()');
9873 // do the trick, happening $lastSelector to $previousSelector
9874 $appended = [];
9876 foreach ($previousSelectors as $previousSelector) {
9877 foreach ($lastSelectors as $lastSelector) {
9878 $previous = $previousSelector;
9879 foreach ($previousSelector as $j => $previousSelectorParts) {
9880 foreach ($lastSelector as $lastSelectorParts) {
9881 foreach ($lastSelectorParts as $lastSelectorPart) {
9882 $previous[$j][] = $lastSelectorPart;
9887 $appended[] = $previous;
9891 $lastSelectors = $appended;
9894 return $lastSelectors;
9897 protected static $libSelectorExtend = [
9898 ['selector', 'extendee', 'extender'],
9899 ['selectors', 'extendee', 'extender']
9901 protected function libSelectorExtend($args)
9903 list($selectors, $extendee, $extender) = $args;
9905 $selectors = $this->getSelectorArg($selectors, 'selector');
9906 $extendee = $this->getSelectorArg($extendee, 'extendee');
9907 $extender = $this->getSelectorArg($extender, 'extender');
9909 if (! $selectors || ! $extendee || ! $extender) {
9910 throw $this->error('selector-extend() invalid arguments');
9913 $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
9915 return $this->formatOutputSelector($extended);
9918 protected static $libSelectorReplace = [
9919 ['selector', 'original', 'replacement'],
9920 ['selectors', 'original', 'replacement']
9922 protected function libSelectorReplace($args)
9924 list($selectors, $original, $replacement) = $args;
9926 $selectors = $this->getSelectorArg($selectors, 'selector');
9927 $original = $this->getSelectorArg($original, 'original');
9928 $replacement = $this->getSelectorArg($replacement, 'replacement');
9930 if (! $selectors || ! $original || ! $replacement) {
9931 throw $this->error('selector-replace() invalid arguments');
9934 $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
9936 return $this->formatOutputSelector($replaced);
9940 * Extend/replace in selectors
9941 * used by selector-extend and selector-replace that use the same logic
9943 * @param array $selectors
9944 * @param array $extendee
9945 * @param array $extender
9946 * @param bool $replace
9948 * @return array
9950 protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
9952 $saveExtends = $this->extends;
9953 $saveExtendsMap = $this->extendsMap;
9955 $this->extends = [];
9956 $this->extendsMap = [];
9958 foreach ($extendee as $es) {
9959 if (\count($es) !== 1) {
9960 throw $this->error('Can\'t extend complex selector.');
9963 // only use the first one
9964 $this->pushExtends(reset($es), $extender, null);
9967 $extended = [];
9969 foreach ($selectors as $selector) {
9970 if (! $replace) {
9971 $extended[] = $selector;
9974 $n = \count($extended);
9976 $this->matchExtends($selector, $extended);
9978 // if didnt match, keep the original selector if we are in a replace operation
9979 if ($replace && \count($extended) === $n) {
9980 $extended[] = $selector;
9984 $this->extends = $saveExtends;
9985 $this->extendsMap = $saveExtendsMap;
9987 return $extended;
9990 protected static $libSelectorNest = ['selector...'];
9991 protected function libSelectorNest($args)
9993 // get the selector... list
9994 $args = reset($args);
9995 $args = $args[2];
9997 if (\count($args) < 1) {
9998 throw $this->error('selector-nest() needs at least 1 argument');
10001 $selectorsMap = [];
10002 foreach ($args as $arg) {
10003 $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
10006 $envs = [];
10008 foreach ($selectorsMap as $selectors) {
10009 $env = new Environment();
10010 $env->selectors = $selectors;
10012 $envs[] = $env;
10015 $envs = array_reverse($envs);
10016 $env = $this->extractEnv($envs);
10017 $outputSelectors = $this->multiplySelectors($env);
10019 return $this->formatOutputSelector($outputSelectors);
10022 protected static $libSelectorParse = [
10023 ['selector'],
10024 ['selectors']
10026 protected function libSelectorParse($args)
10028 $selectors = reset($args);
10029 $selectors = $this->getSelectorArg($selectors, 'selector');
10031 return $this->formatOutputSelector($selectors);
10034 protected static $libSelectorUnify = ['selectors1', 'selectors2'];
10035 protected function libSelectorUnify($args)
10037 list($selectors1, $selectors2) = $args;
10039 $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
10040 $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
10042 if (! $selectors1 || ! $selectors2) {
10043 throw $this->error('selector-unify() invalid arguments');
10046 // only consider the first compound of each
10047 $compound1 = reset($selectors1);
10048 $compound2 = reset($selectors2);
10050 // unify them and that's it
10051 $unified = $this->unifyCompoundSelectors($compound1, $compound2);
10053 return $this->formatOutputSelector($unified);
10057 * The selector-unify magic as its best
10058 * (at least works as expected on test cases)
10060 * @param array $compound1
10061 * @param array $compound2
10063 * @return array
10065 protected function unifyCompoundSelectors($compound1, $compound2)
10067 if (! \count($compound1)) {
10068 return $compound2;
10071 if (! \count($compound2)) {
10072 return $compound1;
10075 // check that last part are compatible
10076 $lastPart1 = array_pop($compound1);
10077 $lastPart2 = array_pop($compound2);
10078 $last = $this->mergeParts($lastPart1, $lastPart2);
10080 if (! $last) {
10081 return [[]];
10084 $unifiedCompound = [$last];
10085 $unifiedSelectors = [$unifiedCompound];
10087 // do the rest
10088 while (\count($compound1) || \count($compound2)) {
10089 $part1 = end($compound1);
10090 $part2 = end($compound2);
10092 if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
10093 list($compound2, $part2, $after2) = $match2;
10095 if ($after2) {
10096 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
10099 $c = $this->mergeParts($part1, $part2);
10100 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
10102 $part1 = $part2 = null;
10104 array_pop($compound1);
10107 if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
10108 list($compound1, $part1, $after1) = $match1;
10110 if ($after1) {
10111 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
10114 $c = $this->mergeParts($part2, $part1);
10115 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
10117 $part1 = $part2 = null;
10119 array_pop($compound2);
10122 $new = [];
10124 if ($part1 && $part2) {
10125 array_pop($compound1);
10126 array_pop($compound2);
10128 $s = $this->prependSelectors($unifiedSelectors, [$part2]);
10129 $new = array_merge($new, $this->prependSelectors($s, [$part1]));
10130 $s = $this->prependSelectors($unifiedSelectors, [$part1]);
10131 $new = array_merge($new, $this->prependSelectors($s, [$part2]));
10132 } elseif ($part1) {
10133 array_pop($compound1);
10135 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
10136 } elseif ($part2) {
10137 array_pop($compound2);
10139 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
10142 if ($new) {
10143 $unifiedSelectors = $new;
10147 return $unifiedSelectors;
10151 * Prepend each selector from $selectors with $parts
10153 * @param array $selectors
10154 * @param array $parts
10156 * @return array
10158 protected function prependSelectors($selectors, $parts)
10160 $new = [];
10162 foreach ($selectors as $compoundSelector) {
10163 array_unshift($compoundSelector, $parts);
10165 $new[] = $compoundSelector;
10168 return $new;
10172 * Try to find a matching part in a compound:
10173 * - with same html tag name
10174 * - with some class or id or something in common
10176 * @param array $part
10177 * @param array $compound
10179 * @return array|false
10181 protected function matchPartInCompound($part, $compound)
10183 $partTag = $this->findTagName($part);
10184 $before = $compound;
10185 $after = [];
10187 // try to find a match by tag name first
10188 while (\count($before)) {
10189 $p = array_pop($before);
10191 if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
10192 return [$before, $p, $after];
10195 $after[] = $p;
10198 // try again matching a non empty intersection and a compatible tagname
10199 $before = $compound;
10200 $after = [];
10202 while (\count($before)) {
10203 $p = array_pop($before);
10205 if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
10206 if (\count(array_intersect($part, $p))) {
10207 return [$before, $p, $after];
10211 $after[] = $p;
10214 return false;
10218 * Merge two part list taking care that
10219 * - the html tag is coming first - if any
10220 * - the :something are coming last
10222 * @param array $parts1
10223 * @param array $parts2
10225 * @return array
10227 protected function mergeParts($parts1, $parts2)
10229 $tag1 = $this->findTagName($parts1);
10230 $tag2 = $this->findTagName($parts2);
10231 $tag = $this->checkCompatibleTags($tag1, $tag2);
10233 // not compatible tags
10234 if ($tag === false) {
10235 return [];
10238 if ($tag) {
10239 if ($tag1) {
10240 $parts1 = array_diff($parts1, [$tag1]);
10243 if ($tag2) {
10244 $parts2 = array_diff($parts2, [$tag2]);
10248 $mergedParts = array_merge($parts1, $parts2);
10249 $mergedOrderedParts = [];
10251 foreach ($mergedParts as $part) {
10252 if (strpos($part, ':') === 0) {
10253 $mergedOrderedParts[] = $part;
10257 $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
10258 $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
10260 if ($tag) {
10261 array_unshift($mergedParts, $tag);
10264 return $mergedParts;
10268 * Check the compatibility between two tag names:
10269 * if both are defined they should be identical or one has to be '*'
10271 * @param string $tag1
10272 * @param string $tag2
10274 * @return array|false
10276 protected function checkCompatibleTags($tag1, $tag2)
10278 $tags = [$tag1, $tag2];
10279 $tags = array_unique($tags);
10280 $tags = array_filter($tags);
10282 if (\count($tags) > 1) {
10283 $tags = array_diff($tags, ['*']);
10286 // not compatible nodes
10287 if (\count($tags) > 1) {
10288 return false;
10291 return $tags;
10295 * Find the html tag name in a selector parts list
10297 * @param string[] $parts
10299 * @return string
10301 protected function findTagName($parts)
10303 foreach ($parts as $part) {
10304 if (! preg_match('/^[\[.:#%_-]/', $part)) {
10305 return $part;
10309 return '';
10312 protected static $libSimpleSelectors = ['selector'];
10313 protected function libSimpleSelectors($args)
10315 $selector = reset($args);
10316 $selector = $this->getSelectorArg($selector, 'selector');
10318 // remove selectors list layer, keeping the first one
10319 $selector = reset($selector);
10321 // remove parts list layer, keeping the first part
10322 $part = reset($selector);
10324 $listParts = [];
10326 foreach ($part as $p) {
10327 $listParts[] = [Type::T_STRING, '', [$p]];
10330 return [Type::T_LIST, ',', $listParts];
10333 protected static $libScssphpGlob = ['pattern'];
10334 protected function libScssphpGlob($args)
10336 @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);
10338 $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true);
10340 $string = $this->assertString($args[0], 'pattern');
10341 $pattern = $this->compileStringContent($string);
10342 $matches = glob($pattern);
10343 $listParts = [];
10345 foreach ($matches as $match) {
10346 if (! is_file($match)) {
10347 continue;
10350 $listParts[] = [Type::T_STRING, '"', [$match]];
10353 return [Type::T_LIST, ',', $listParts];