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
;
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.
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
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.
72 * @author Leaf Corcoran <leafot@gmail.com>
74 * @final Extending the Compiler is deprecated
81 const LINE_COMMENTS
= 1;
98 const WITH_SUPPORTS
= 4;
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 = [
128 * @var array<string, string>
130 protected static $namespaces = [
136 public static $true = [Type
::T_KEYWORD
, 'true'];
137 public static $false = [Type
::T_KEYWORD
, 'false'];
139 public static $NaN = [Type
::T_KEYWORD
, 'NaN'];
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 = [];
165 protected $importedFiles = [];
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,
182 'units-level-3' => true,
183 'global-variable-shadowing' => false,
189 protected $encoding = null;
194 protected $lineNumberStyle = null;
197 * @var int|SourceMapGenerator
198 * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
200 protected $sourceMap = self
::SOURCE_MAP_NONE
;
204 * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
206 protected $sourceMapOptions = [];
211 private $charset = true;
214 * @var string|\ScssPhp\ScssPhp\Formatter
216 protected $formatter = Expanded
::class;
223 * @var OutputBlock|null
225 protected $rootBlock;
228 * @var \ScssPhp\ScssPhp\Compiler\Environment
232 * @var OutputBlock|null
236 * @var Environment|null
244 protected $charsetSeen;
246 * @var array<int, string|null>
248 protected $sourceNames;
258 protected $cacheCheckImportResolutions = false;
263 protected $indentLevel;
269 * @var array<string, int[]>
271 protected $extendsMap;
274 * @var array<string, int>
276 protected $parsedFiles = [];
285 protected $sourceIndex;
289 protected $sourceLine;
293 protected $sourceColumn;
297 protected $shouldEvaluate;
302 protected $ignoreErrors;
306 protected $ignoreCallStackMessage = false;
311 protected $callStack = [];
315 * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
317 private $resolvedImports = [];
320 * The directory of the currently processed file
324 private $currentDirectory;
327 * The directory of the input file
331 private $rootDirectory;
336 private $legacyCwdImportPath = true;
339 * @var LoggerInterface
344 * @var array<string, bool>
346 private $warnedChildFunctions = [];
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
= [];
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>
375 public function getCompileOptions()
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
,
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
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
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);
425 * @param string $code
426 * @param string|null $path
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();
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)
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;
478 $this->extendsMap
= [];
479 $this->sourceIndex
= null;
480 $this->sourceLine
= null;
481 $this->sourceColumn
= 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
;
496 $this->currentDirectory
= null;
497 $this->rootDirectory
= getcwd();
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);
515 $this->injectVariables($this->registeredVars
);
516 $this->compileRoot($tree);
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);
537 if ($this->charset
&& strlen($out) !== Util
::mbStrlen($out)) {
538 $prefix = '@charset "UTF-8";' . "\n";
539 $out = $prefix . $out;
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));
553 case self
::SOURCE_MAP_FILE
:
554 if (isset($this->sourceMapOptions
['sourceMapURL'])) {
555 $sourceMapUrl = $this->sourceMapOptions
['sourceMapURL'];
560 if ($sourceMapUrl !== null) {
561 $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
564 } catch (SassScriptException
$e) {
565 throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
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
= [];
589 * @param CachedResult $result
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) {
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']) {
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!
640 if ($path !== null && substr($path, -4) === '.css') {
644 $parser = new Parser($path, \
count($this->sourceNames
), $this->encoding
, $this->cache
, $cssOnly, $this->logger
);
646 $this->sourceNames
[] = $path;
647 $this->addParsedFile($path);
655 * @param array $target
656 * @param array $origin
660 protected function isSelfExtend($target, $origin)
662 foreach ($origin as $sel) {
663 if (\
in_array($target, $sel)) {
674 * @param array $target
675 * @param array $origin
676 * @param array|null $block
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;
689 $this->extendsMap
[$part] = [$i];
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();
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
;
717 $out->sourceName
= null;
718 $out->sourceLine
= null;
719 $out->sourceColumn
= null;
728 * @param \ScssPhp\ScssPhp\Block $rootBlock
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
746 protected function missingSelectors()
748 foreach ($this->extends as $extend) {
749 if (isset($extend[3])) {
753 list($target, $origin, $block) = $extend;
755 // ignore if !optional
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.");
771 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
772 * @param string $parentKey
776 protected function flattenSelectors(OutputBlock
$block, $parentKey = null)
778 if ($block->selectors
) {
781 foreach ($block->selectors
as $s) {
784 if (! \
is_array($s)) {
789 if (! empty($this->extendsMap
)) {
790 $this->matchExtends($s, $selectors);
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;
814 $block->selectors
[] = $this->compileSelector($selector);
817 if ($placeholderSelector && 0 === \
count($block->selectors
) && null !== $parentKey) {
818 unset($block->parent
->children
[$parentKey]);
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
836 protected function glueFunctionSelectors($parts)
840 foreach ($parts as $part) {
841 if (\
is_array($part)) {
842 $part = $this->glueFunctionSelectors($part);
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
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;
867 * @param array $selector
870 * @param bool $initial
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)) {
885 foreach ($selector as $i => $part) {
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))) {
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) {
910 // remove shared parts
911 if (\
count($new) > 1) {
912 while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
917 if (\
count($nonBreakableBefore) && $k === \
count($new)) {
922 $tempReplacement = $k > 0 ? \array_slice
($new, $k) : $new;
924 for ($l = \
count($tempReplacement) - 1; $l >= 0; $l--) {
927 foreach ($tempReplacement[$l] as $chunk) {
928 if (! \
in_array($chunk, $slice)) {
933 array_unshift($replacement, $slice);
935 if (! $this->isImmediateRelationshipCombinator(end($slice))) {
940 $afterBefore = $l != 0 ? \array_slice
($tempReplacement, 0, $l) : [];
942 // Merge shared direct relationships.
943 $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
945 $result = array_merge(
952 if ($result === $selector) {
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);
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(
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
1005 protected function isPseudoSelector($part, &$matches)
1008 strpos($part, ':') === 0 &&
1009 preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
1018 * Push extended selector except if
1019 * - this is a pseudo selector
1020 * - same as previous
1022 * in this case we merge the pseudo selector content
1025 * @param array $extended
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);
1036 $this->isPseudoSelector($part, $matchesExtended) &&
1037 \
in_array($matchesExtended[1], [ 'slotted' ])
1040 $prev = $this->glueFunctionSelectors($prev);
1042 if (\
count($prev) === 1 && \
count(reset($prev)) === 1) {
1043 $single = reset($prev);
1044 $part = reset($single);
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 ]];
1063 * Match extends single
1065 * @param array $rawSingle
1066 * @param array $outOrigin
1067 * @param bool $initial
1071 protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
1076 // simple usual cases, no need to do the whole trick
1077 if (\
in_array($rawSingle, [['>'],['+'],['~']])) {
1081 foreach ($rawSingle as $part) {
1083 if (! \
is_string($part)) {
1087 if (! preg_match('/^[\[.:#%]/', $part) && \
count($single)) {
1088 $single[\
count($single) - 1] .= $part;
1094 $extendingDecoratedTag = false;
1096 if (\
count($single) > 1) {
1098 $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ?
$matches[0] : 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;
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) {
1122 $this->matchExtends($subSelector, $subExtended, 0, false);
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 ];
1143 foreach ($counts as $idx => $count) {
1144 list($target, $origin, /* $block */) = $this->extends[$idx];
1146 $origin = $this->glueFunctionSelectors($origin);
1149 if ($count !== \
count($target)) {
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) {
1163 $replacement = end($new);
1165 // Extending a decorated tag with another tag is not possible.
1167 $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
1168 preg_match('/^[a-z0-9]+$/i', $replacement[0])
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);
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
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)
1206 $j = $i = \
count($fragment);
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])) {
1220 return [$parents, $children];
1224 * Combine selector single
1226 * @param array $base
1227 * @param array $other
1231 protected function combineSelectorSingle($base, $other)
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)) {
1249 } elseif (preg_match('/^[\.#]/', $part)) {
1250 array_unshift($out, $part);
1252 } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
1255 } elseif ($wasTag) {
1256 $tag[\
count($tag) - 1] .= $part;
1258 array_unshift($out, $part);
1265 array_unshift($out, $tag[0]);
1268 while (\
count($pseudo)) {
1269 $out[] = array_shift($pseudo);
1278 * @param \ScssPhp\ScssPhp\Block $media
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
1303 foreach ($media->children
as $child) {
1307 $type !== Type
::T_BLOCK
&&
1308 $type !== Type
::T_MEDIA
&&
1309 $type !== Type
::T_DIRECTIVE
&&
1310 $type !== Type
::T_IMPORT
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;
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
) {
1353 $scope = $scope->parent
;
1362 * @param DirectiveBlock|array $directive
1363 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
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
]);
1385 $this->appendOutputLine($out, Type
::T_DIRECTIVE
, $s . ';');
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]);
1398 $this->compileNestedBlock($directive, [$s]);
1404 * directive names can include some interpolation
1406 * @param string|array $directiveName
1408 * @throws CompilerException
1410 protected function compileDirectiveName($directiveName)
1412 if (is_string($directiveName)) {
1413 return $directiveName;
1416 return $this->compileValue($directiveName);
1422 * @param \ScssPhp\ScssPhp\Block $block
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.');
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);
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 = [];
1490 if ($scope->type
=== Type
::T_ROOT
) {
1494 // start from the root
1495 while ($scope->parent
&& $scope->parent
->type
!== Type
::T_ROOT
) {
1496 array_unshift($childStash, $scope);
1497 $scope = $scope->parent
;
1505 if ($this->isWith($scope, $with, $without)) {
1511 if ($s->type
!== Type
::T_MEDIA
&& $s->type
!== Type
::T_DIRECTIVE
) {
1515 $filteredScopes[] = $s;
1518 if (\
count($childStash)) {
1519 $scope = array_shift($childStash);
1520 } elseif ($scope->children
) {
1521 $scope = end($scope->children
);
1527 if (! \
count($filteredScopes)) {
1528 return $this->rootBlock
;
1531 $newScope = array_shift($filteredScopes);
1532 $newScope->parent
= $this->rootBlock
;
1534 $this->rootBlock
->children
[] = $newScope;
1538 while (\
count($filteredScopes)) {
1539 $s = array_shift($filteredScopes);
1541 $p->children
[] = $s;
1542 $newScope = &$p->children
[0];
1543 $p = &$p->children
[0];
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);
1574 * Find a selector by the depth node in the scope
1576 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
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)) {
1599 * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1601 * @param array|null $withCondition
1605 * @phpstan-return array{array<string, bool>, array<string, bool>}
1607 protected function compileWith($withCondition)
1609 // just compile what we have in 2 lists
1611 $without = ['rule' => true];
1613 if ($withCondition) {
1614 if ($withCondition[0] === Type
::T_INTERPOLATE
) {
1615 $w = $this->compileValue($withCondition);
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];
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)
1668 foreach ($envs as $e) {
1669 if ($e->block
&& ! $this->isWith($e->block
, $with, $without)) {
1672 $ec->selectors
= [];
1680 return $this->extractEnv($filtered);
1686 * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1687 * @param array $with
1688 * @param array $without
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);
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)) {
1718 if (\
is_object($s) && $s instanceof Number
) {
1719 return $this->testWithWithout('keyframes', $with, $without);
1723 return $this->testWithWithout('rule', $with, $without);
1730 * Test a single type of block against with/without lists
1732 * @param string $what
1733 * @param array $with
1734 * @param array $without
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
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
);
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);
1782 * Compile nested properties lines
1784 * @param \ScssPhp\ScssPhp\Block $block
1785 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
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);
1809 case Type
::T_NESTED_PROPERTY
:
1810 assert($child[1] instanceof NestedPropertyBlock
);
1811 array_unshift($child[1]->prefix
[2], $prefix);
1815 $this->compileChild($child, $nested);
1820 * Compile nested block
1822 * @param \ScssPhp\ScssPhp\Block $block
1823 * @param string[] $selectors
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') {
1838 $needWrapping = false;
1840 foreach ($block->children
as $child) {
1841 if ($child[0] === Type
::T_ASSIGN
) {
1842 $needWrapping = true;
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
;
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
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;
1923 * Compile the value of a comment that can have interpolation
1925 * @param array $value
1926 * @param bool $pushEnv
1930 protected function compileCommentValue($value, $pushEnv = false)
1934 if (isset($value[2])) {
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
1958 * Compile root level comment
1960 * @param array $block
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
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__
);
1992 $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1993 } catch (ParserException
$e) {
1994 throw $this->error($e->getMessage());
1998 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
2008 * @param array $selector
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
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;
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
2053 protected function collapseSelectors($selectors)
2057 foreach ($selectors as $selector) {
2060 foreach ($selector as $node) {
2063 array_walk_recursive(
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
2086 private function collapseSelectorsAsList($selectors)
2090 foreach ($selectors as $selector) {
2094 foreach ($selector as $node) {
2097 array_walk_recursive(
2099 function ($value, $key) use (&$compound) {
2100 $compound .= $value;
2104 if ($this->isImmediateRelationshipCombinator($compound)) {
2105 if (\
count($output)) {
2106 $output[\
count($output) - 1] .= ' ' . $compound;
2108 $output[] = $compound;
2112 } elseif ($glueNext) {
2113 $output[\
count($output) - 1] .= ' ' . $compound;
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
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);
2149 $part = $this->replaceSelfSelector($part, $replace);
2158 * Flatten selector single; joins together .classes and #ids
2160 * @param array $single
2164 protected function flattenSelectorSingle($single)
2168 foreach ($single as $part) {
2171 ! \
is_string($part) ||
2172 preg_match('/[\[.:#%]/', $part)
2178 if (\
is_array(end($joined))) {
2181 $joined[\
count($joined) - 1] .= $part;
2189 * Compile selector to string; self(&) should have been replaced by now
2191 * @param string|array $selector
2195 protected function compileSelector($selector)
2197 if (! \
is_array($selector)) {
2198 return $selector; // media and the like
2204 [$this, 'compileSelectorPart'],
2211 * Compile selector part
2213 * @param array $piece
2217 protected function compileSelectorPart($piece)
2219 foreach ($piece as &$p) {
2220 if (! \
is_array($p)) {
2230 $p = $this->compileValue($p);
2235 return implode($piece);
2239 * Has selector placeholder?
2241 * @param array $selector
2245 protected function hasSelectorPlaceholder($selector)
2247 if (! \
is_array($selector)) {
2251 foreach ($selector as $parts) {
2252 foreach ($parts as $part) {
2253 if (\
strlen($part) && '%' === $part[0]) {
2263 * @param string $name
2267 protected function pushCallStack($name = '')
2269 $this->callStack
[] = [
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);
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);
2311 $this->popCallStack();
2317 $this->popCallStack();
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
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']);
2348 $ret = $this->compileChild($stm, $out);
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
2367 protected function evaluateMediaQuery($queryList)
2369 static $parser = null;
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
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);
2408 if ($parser->parseMediaQueryList($queryString, $queries)) {
2409 $queries = $this->evaluateMediaQuery($queries[2]);
2411 while (\
count($queries)) {
2412 $outQueryList[] = array_shift($queries);
2420 $outQueryList[] = $queryList[$kql];
2423 return $outQueryList;
2427 * Compile media query
2429 * @param array $queryList
2433 protected function compileMediaQuery($queryList)
2436 $default = trim($start);
2440 foreach ($queryList as $query) {
2444 $mediaTypeOnly = true;
2446 foreach ($query as $q) {
2447 if ($q[0] !== Type
::T_MEDIA_TYPE
) {
2448 $mediaTypeOnly = false;
2453 foreach ($query as $q) {
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) )) {
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);
2474 $out[] = $start . $current;
2483 if ($newType === ['all'] && $default) {
2484 $default = $start . 'all';
2487 // all can be safely ignored and mixed with whatever else
2488 if ($newType !== ['all']) {
2490 $type = $this->mergeMediaTypes($type, $newType);
2493 // merge failed : ignore this query that is not valid, skip to the next one
2495 $default = ''; // if everything fail, no @media at all
2504 case Type
::T_MEDIA_EXPRESSION
:
2507 . $this->compileValue($q[1])
2508 . $this->formatter
->assignSeparator
2509 . $this->compileValue($q[2])
2513 . $this->compileValue($q[1])
2518 case Type
::T_MEDIA_VALUE
:
2519 $parts[] = $this->compileValue($q[1]);
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);
2538 $out[] = $start . $current;
2541 // no @media type except all, and no conflict?
2542 if (! $out && $default) {
2550 * Merge direct relationships between selectors
2552 * @param array $selectors1
2553 * @param array $selectors2
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);
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);
2581 $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2587 array_unshift($merged, $part1);
2588 } while (! empty($selectors1) && ! empty($selectors2));
2596 * @param array $type1
2597 * @param array $type2
2599 * @return array|null
2601 protected function mergeMediaTypes($type1, $type2)
2603 if (empty($type1)) {
2607 if (empty($type2)) {
2611 if (\
count($type1) > 1) {
2612 $m1 = strtolower($type1[0]);
2613 $t1 = strtolower($type1[1]);
2616 $t1 = strtolower($type1[0]);
2619 if (\
count($type2) > 1) {
2620 $m2 = strtolower($type2[0]);
2621 $t2 = strtolower($type2[1]);
2624 $t2 = strtolower($type2[0]);
2627 if (($m1 === Type
::T_NOT
) ^
($m2 === Type
::T_NOT
)) {
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"
2644 return [Type
::T_NOT
, $t1];
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
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;
2680 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2685 if ($rawPath[0] === Type
::T_LIST
) {
2686 // handle a list of strings
2687 if (\
count($rawPath[2]) === 0) {
2691 foreach ($rawPath[2] as $path) {
2692 if ($path[0] !== Type
::T_STRING
) {
2693 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2699 foreach ($rawPath[2] as $path) {
2700 $this->compileImport($path, $out, $once);
2706 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2712 * @param array $rawPath
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);
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);
2735 * @param array $path
2737 * @throws CompilerException
2739 protected function escapeImportPathString($path)
2743 foreach ($path[2] as $k => $v) {
2744 $path[2][$k] = $this->escapeImportPathString($v);
2747 case Type
::T_STRING
:
2749 $path = $this->compileValue($path);
2750 $path = str_replace(' ', '\\ ', $path);
2751 $path = [Type
::T_KEYWORD
, $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
2769 protected function appendRootDirective($line, $out, $allowed = [Type
::T_COMMENT
])
2773 while ($root->parent
) {
2774 $root = $root->parent
;
2779 while ($i < \
count($root->children
)) {
2780 if (! isset($root->children
[$i]->type
) ||
! \
in_array($root->children
[$i]->type
, $allowed)) {
2787 // remove incompatible children from the bottom of the list
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;
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
2819 protected function appendOutputLine(OutputBlock
$out, $type, $line)
2823 // check if it's a flat output or not
2824 if (\
count($out->children
)) {
2825 $lastChild = &$out->children
[\
count($out->children
) - 1];
2828 $lastChild->depth
=== $out->depth
&&
2829 \
is_null($lastChild->selectors
) &&
2830 ! \
count($lastChild->children
)
2832 $outWrite = $lastChild;
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);
2882 case Type
::T_IMPORT
:
2883 $rawPath = $this->reduce($child[1]);
2885 $this->compileImport($rawPath, $out);
2888 case Type
::T_DIRECTIVE
:
2889 $this->compileDirective($child[1], $out);
2892 case Type
::T_AT_ROOT
:
2893 $this->compileAtRoot($child[1]);
2897 $this->compileMedia($child[1]);
2901 $this->compileBlock($child[1]);
2904 case Type
::T_CHARSET
:
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) {
2921 $compiledValue = $this->compileValue($value);
2923 $line = $this->formatter
->customProperty(
2928 $this->appendOutputLine($out, Type
::T_ASSIGN
, $line);
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);
2940 $this->set($name[1], $this->reduce($value), false, $this->rootEnv
, $value);
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);
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;
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] === '/') {
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()) {
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) {
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()) {
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) {
3043 $compiledValue = $this->compileValue($value);
3045 // ignore empty value
3046 if (\
strlen($compiledValue)) {
3047 $line = $this->formatter
->property(
3051 $this->appendOutputLine($out, Type
::T_ASSIGN
, $line);
3055 case Type
::T_COMMENT
:
3056 if ($out->type
=== Type
::T_ROOT
) {
3057 $this->compileComment($child);
3061 $line = $this->compileCommentValue($child, true);
3062 $this->appendOutputLine($out, Type
::T_COMMENT
, $line);
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);
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
;
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.
3109 $this->logger
->warn($message);
3112 $this->pushExtends($result, $selectors, $child);
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) {
3127 $case instanceof ElseBlock ||
3128 $case instanceof ElseifBlock
&& $this->isTruthy($this->reduce($case->cond
))
3130 return $this->compileChildren($case->children
, $out);
3136 list(, $each) = $child;
3137 assert($each instanceof EachBlock
);
3139 $list = $this->coerceList($this->reduce($each->list), ',', true);
3143 foreach ($list[2] as $item) {
3144 if (\
count($each->vars
) === 1) {
3145 $this->set($each->vars
[0], $item, true);
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);
3157 $store = $this->env
->store
;
3159 $this->backPropagateEnv($store, $each->vars
);
3164 $store = $this->env
->store
;
3166 $this->backPropagateEnv($store, $each->vars
);
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);
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;
3203 (! $for->until
&& $start - $d == $end) ||
3204 ($for->until
&& $start == $end)
3209 $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
3212 $ret = $this->compileChildren($for->children
, $out);
3215 $store = $this->env
->store
;
3217 $this->backPropagateEnv($store, [$for->var]);
3223 $store = $this->env
->store
;
3225 $this->backPropagateEnv($store, [$for->var]);
3229 case Type
::T_RETURN
:
3230 return $this->reduce($child[1], true);
3232 case Type
::T_NESTED_PROPERTY
:
3233 $this->compileNestedPropertiesBlock($child[1], $out);
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);
3243 throw $this->error("Undefined mixin $name");
3246 assert($mixin instanceof CallableBlock
);
3248 $callingScope = $this->getStoreEnv();
3250 // push scope, apply args
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
3258 if (isset($child['selfParent']) && isset($child['selfParent']->selectors
)) {
3259 $selfParent = $child['selfParent'];
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
);
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
);
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
;
3302 throw $this->error("@mixin $name() without parentEnv");
3305 $this->compileChildrenNoReturn($mixin->children
, $out, $selfParent, $this->env
->marker
. ' ' . $name);
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];
3320 $storeEnv = $this->storeEnv
;
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;
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");
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");
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");
3372 throw $this->error("unknown child type: $child[0]");
3377 * Reduce expression to string
3380 * @param bool $keepParens
3384 protected function expToString($exp, $keepParens = false)
3386 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3390 if ($keepParens && $inParens) {
3394 $content[] = $this->reduce($left);
3406 $content[] = $this->reduce($right);
3408 if ($keepParens && $inParens) {
3412 return [Type
::T_STRING
, '', $content];
3418 * @param array|Number $value
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
3434 protected function isImmediateRelationshipCombinator($value)
3436 return $value === '>' ||
$value === '+' ||
$value === '~';
3440 * Should $value cause its operand to eval
3442 * @param array $value
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]);
3455 case Type
::T_VARIABLE
:
3456 case Type
::T_FUNCTION_CALL
:
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
) {
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
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);
3504 $ucOpName = ucfirst($opName);
3505 $ucLType = ucfirst($ltype);
3506 $ucRType = ucfirst($rtype);
3509 // 1. op[op name][left type][right type]
3510 // 2. op[left type][right type] (passing the op as first arg
3512 $fn = "op${ucOpName}${ucLType}${ucRType}";
3515 \
is_callable([$this, $fn]) ||
3516 (($fn = "op${ucLType}${ucRType}") &&
3517 \
is_callable([$this, $fn]) &&
3519 (($fn = "op${ucOpName}") &&
3520 \
is_callable([$this, $fn]) &&
3523 $shouldEval = $inParens ||
$inExp;
3525 if (isset($passOp)) {
3526 $out = $this->$fn($op, $left, $right, $shouldEval);
3528 $out = $this->$fn($left, $right, $shouldEval);
3536 return $this->expToString($value);
3539 list(, $op, $exp, $inParens) = $value;
3541 $inExp = $inExp ||
$this->shouldEval($exp);
3542 $exp = $this->reduce($exp);
3544 if ($exp instanceof Number
) {
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;
3566 return [Type
::T_STRING
, '', [$op, $exp]];
3568 case Type
::T_VARIABLE
:
3569 return $this->reduce($this->get($value[1]));
3572 foreach ($value[2] as &$item) {
3573 $item = $this->reduce($item);
3577 if (isset($value[3]) && \
is_array($value[3])) {
3578 foreach ($value[3] as &$item) {
3579 $item = $this->reduce($item);
3587 foreach ($value[1] as &$item) {
3588 $item = $this->reduce($item);
3591 foreach ($value[2] as &$item) {
3592 $item = $this->reduce($item);
3597 case Type
::T_STRING
:
3598 foreach ($value[2] as &$item) {
3599 if (\
is_array($item) ||
$item instanceof Number
) {
3600 $item = $this->reduce($item);
3606 case Type
::T_INTERPOLATE
:
3607 $value[1] = $this->reduce($value[1]);
3610 return [Type
::T_KEYWORD
, $this->compileValue($value, false)];
3615 case Type
::T_FUNCTION_CALL
:
3616 return $this->fncall($value[1], $value[2]);
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;
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
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]) {
3672 return $this->callScssFunction($functionReference[3], $argValues);
3674 // native PHP functions
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;
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);
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)) {
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) {
3734 case Type
::T_FUNCTION_CALL
:
3735 if (! \
in_array($arg[1], $allowed_function)) {
3739 foreach ($arg[2] as $argValue) {
3740 if ($argValue === static::$null) {
3743 $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3744 if (empty($argValue[0]) && $cssArg !== false) {
3745 $cssArgs[] = [$argValue[0], $cssArg];
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'])) {
3758 return $this->stringifyFncallArgs($arg);
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) {
3771 $arg[0] = Type
::T_STRING
;
3776 case Type
::T_EXPRESSION
:
3777 if (! \
in_array($arg[1], ['+', '-', '/', '*'])) {
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) {
3785 return $this->expToString($arg, true);
3787 case Type
::T_VARIABLE
:
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
) {
3810 foreach ($arg[2] as $k => $v) {
3811 $arg[2][$k] = $this->stringifyFncallArgs($v);
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);
3824 case Type
::T_FUNCTION_CALL
:
3825 $name = strtolower($arg[1]);
3827 if (in_array($name, ['max', 'min', 'calc'])) {
3829 $arg = $this->fncall([Type
::T_FUNCTION
, $name, [Type
::T_LIST
, ',', []]], $args);
3838 * Find a function reference
3839 * @param string $name
3840 * @param bool $safeCopy
3843 protected function getFunctionReference($name, $safeCopy = false)
3846 if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
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)) {
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
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
);
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;
3930 * @param string $name
3934 protected function normalizeName($name)
3936 return str_replace('-', '_', $name);
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
) {
3956 switch ($value[0]) {
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) {
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)];
3992 * @param Number $left
3993 * @param Number $right
3997 protected function opAddNumberNumber(Number
$left, Number
$right)
3999 return $left->plus($right);
4005 * @param Number $left
4006 * @param Number $right
4010 protected function opMulNumberNumber(Number
$left, Number
$right)
4012 return $left->times($right);
4018 * @param Number $left
4019 * @param Number $right
4023 protected function opSubNumberNumber(Number
$left, Number
$right)
4025 return $left->minus($right);
4031 * @param Number $left
4032 * @param Number $right
4036 protected function opDivNumberNumber(Number
$left, Number
$right)
4038 return $left->dividedBy($right);
4044 * @param Number $left
4045 * @param Number $right
4049 protected function opModNumberNumber(Number
$left, Number
$right)
4051 return $left->modulo($right);
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
) {
4069 $strLeft[2][] = $right;
4074 if ($strRight = $this->coerceString($right)) {
4075 if ($left[0] === Type
::T_STRING
) {
4079 array_unshift($strRight[2], $left);
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) {
4108 if ($left !== static::$false && $left !== static::$null) {
4109 return $this->reduce($right, true);
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) {
4136 if ($left !== static::$false && $left !== static::$null) {
4140 return $this->reduce($right, true);
4147 * @param array $left
4148 * @param array $right
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;
4171 $out[] = $lval +
$rval;
4175 $out[] = $lval - $rval;
4179 $out[] = $lval * $rval;
4184 throw $this->error("color: Can't take modulo by zero");
4187 $out[] = $lval %
$rval;
4192 throw $this->error("color: Can't divide by zero");
4195 $out[] = (int) ($lval / $rval);
4199 return $this->opEq($left, $right);
4202 return $this->opNeq($left, $right);
4205 throw $this->error("color: unknown op $op");
4209 if (isset($left[4])) {
4211 } elseif (isset($right[4])) {
4212 $out[4] = $right[4];
4215 return $this->fixColor($out);
4219 * Compare color and number
4222 * @param array $left
4223 * @param Number $right
4227 protected function opColorNumber($op, $left, Number
$right)
4230 return static::$false;
4234 return static::$true;
4237 $value = $right->getDimension();
4239 return $this->opColorColor(
4242 [Type
::T_COLOR
, $value, $value, $value]
4247 * Compare number and color
4250 * @param Number $left
4251 * @param array $right
4255 protected function opNumberColor($op, Number
$left, $right)
4258 return static::$false;
4262 return static::$true;
4265 $value = $left->getDimension();
4267 return $this->opColorColor(
4269 [Type
::T_COLOR
, $value, $value, $value],
4275 * Compare number1 == number2
4277 * @param array|Number $left
4278 * @param array|Number $right
4282 protected function opEq($left, $right)
4284 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
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
4303 protected function opNeq($left, $right)
4305 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
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
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
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
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
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
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
4389 protected function opLtNumberNumber(Number
$left, Number
$right)
4391 return $this->toBool($left->lessThan($right));
4399 * @param bool $thing
4403 public function toBool($thing)
4405 return $thing ?
static::$true : static::$false;
4409 * Escape non printable chars in strings output as in dart-sass
4413 * @param string $string
4414 * @param bool $inKeyword
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) . ' '],
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) {
4456 * Compiles a primitive value into a CSS property value.
4458 * Values in scssphp are typed by being wrapped in arrays, their format is
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.
4468 * @param array|Number $value
4469 * @param bool $quote
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);
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)) {
4506 if (is_numeric($alpha)) {
4507 $a = new Number($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)) {
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];
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
4545 $value[1] === "'" &&
4546 (strpos($content, '"') === false or strpos($content, "'") !== false)
4550 $value[1] === '"' &&
4551 (strpos($content, '"') !== false and strpos($content, "'") === false)
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\")";
4572 $value = $this->extractInterpolation($value);
4574 if ($value[0] !== Type
::T_LIST
) {
4575 return $this->compileValue($value, $quote);
4578 list(, $delim, $items) = $value;
4581 if (! empty($value['enclosing'])) {
4582 switch ($value['enclosing']) {
4587 case 'forced_parent':
4592 case 'forced_bracket':
4599 $separator = $delim === '/' ?
' /' : $delim;
4603 if ($delim !== ' ') {
4604 $prefix_value = ' ';
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;
4623 if ($item[0] === Type
::T_NULL
) {
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;
4643 $values = $value[2];
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;
4663 if ($delim && $delim !== ' ' && ! $whiteLeft) {
4667 $left = \
count($left[2]) > 0
4668 ?
$this->compileValue($left, $quote) . $delim . $whiteLeft
4673 if ($delim && $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]) {
4692 $reduced = $this->extractInterpolation($reduced);
4694 if ($reduced[0] !== Type
::T_LIST
) {
4698 list(, $delim, $items) = $reduced;
4700 if ($delim !== ' ') {
4706 foreach ($items as $item) {
4707 if ($item[0] === Type
::T_NULL
) {
4711 if ($item[0] === Type
::T_STRING
) {
4712 $filtered[] = $this->compileStringContent($item, $quote);
4713 } elseif ($item[0] === Type
::T_KEYWORD
) {
4714 $filtered[] = $item[1];
4716 $filtered[] = $this->compileValue($item, $quote);
4720 $reduced = [Type
::T_KEYWORD
, implode("$delim", $filtered)];
4723 case Type
::T_STRING
:
4724 $reduced = [Type
::T_STRING
, '', [$this->compileStringContent($reduced)]];
4728 $reduced = [Type
::T_KEYWORD
, ''];
4731 return $this->compileValue($reduced, $quote);
4736 case Type
::T_COMMENT
:
4737 return $this->compileCommentValue($value);
4740 throw $this->error('unknown value type: ' . json_encode($value));
4745 * @param array|Number $value
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);
4762 return $this->compileValue($value);
4769 * @param array $list
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
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
4809 protected function compileStringContent($string, $quote = true)
4813 foreach ($string[2] as $part) {
4814 if (\
is_array($part) ||
$part instanceof Number
) {
4815 $parts[] = $this->compileValue($part, $quote);
4821 return implode($parts);
4825 * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4827 * @param array $list
4831 protected function extractInterpolation($list)
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];
4848 * Find the final set of selectors
4850 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4851 * @param \ScssPhp\ScssPhp\Block $selfParent
4855 protected function multiplySelectors(Environment
$env, $selfParent = null)
4857 $envs = $this->compactEnv($env);
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
)) {
4872 $selectors = $env->selectors
;
4875 $stillHasSelf = false;
4876 $prevSelectors = $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;
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;
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
4918 protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4923 foreach ($child as $part) {
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) {
4935 if (\
is_null($selfParentSelectors)) {
4936 $selfParentSelectors = $parent;
4939 foreach ($selfParentSelectors as $i => $parentPart) {
4945 foreach ($parentPart as $pp) {
4946 if (\
is_array($pp)) {
4949 array_walk_recursive($pp, function ($a) use (&$flatten) {
4953 $pp = implode($flatten);
4967 return $setSelf ?
$out : array_merge($parent, $child);
4973 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4974 * @param array $childQueries
4978 protected function multiplyMedia(Environment
$env = null, $childQueries = null)
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
];
5001 $this->storeEnv
= null;
5002 $parentQueries = $this->evaluateMediaQuery($parentQueries);
5004 list($this->env
, $this->storeEnv
) = $store;
5006 if (\
is_null($childQueries)) {
5007 $childQueries = $parentQueries;
5009 $originalQueries = $childQueries;
5012 foreach ($parentQueries as $parentQuery) {
5013 foreach ($originalQueries as $childQuery) {
5014 $childQueries[] = array_merge(
5016 [[Type
::T_MEDIA_TYPE
, [Type
::T_KEYWORD
, 'all']]],
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
) {
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);) {
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
;
5076 $env->block
= $block;
5077 $env->depth
= isset($this->env
->depth
) ?
$this->env
->depth +
1 : 0;
5080 $this->storeEnv
= null;
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
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
;
5126 * @param string $name
5127 * @param mixed $value
5128 * @param bool $shadow
5129 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5130 * @param mixed $valueUnreduced
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();
5143 $this->setRaw($name, $value, $env, $valueUnreduced);
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
5159 protected function setExisting($name, $value, Environment
$env, $valueUnreduced = null)
5162 $specialContentKey = static::$namespaces['special'] . 'content';
5164 $hasNamespace = $name[0] === '^' ||
$name[0] === '@' ||
$name[0] === '%';
5169 if ($maxDepth-- <= 0) {
5173 if (\array_key_exists
($name, $env->store
)) {
5177 if (! $hasNamespace && isset($env->marker
)) {
5178 if (! empty($env->store
[$specialContentKey])) {
5179 $env = $env->store
[$specialContentKey]->scope
;
5183 if (! empty($env->declarationScopeParent
)) {
5184 $env = $env->declarationScopeParent
;
5192 if (isset($env->parentStore
)) {
5193 $env = $env->parentStore
;
5194 } elseif (isset($env->parent
)) {
5195 $env = $env->parent
;
5202 $env->store
[$name] = $value;
5204 if ($valueUnreduced) {
5205 $env->storeUnreduced
[$name] = $valueUnreduced;
5212 * @param string $name
5213 * @param mixed $value
5214 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5215 * @param mixed $valueUnreduced
5219 protected function setRaw($name, $value, Environment
$env, $valueUnreduced = null)
5221 $env->store
[$name] = $value;
5223 if ($valueUnreduced) {
5224 $env->storeUnreduced
[$name] = $valueUnreduced;
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] === '%';
5254 if ($maxDepth-- <= 0) {
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
;
5272 if (! empty($env->declarationScopeParent
)) {
5273 $env = $env->declarationScopeParent
;
5275 $env = $this->rootEnv
;
5280 if (isset($env->parentStore
)) {
5281 $env = $env->parentStore
;
5282 } elseif (isset($env->parent
)) {
5283 $env = $env->parent
;
5290 throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ?
' (infinite recursion)' : ''));
5300 * @param string $name
5301 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
5305 protected function has($name, Environment
$env = null)
5307 return ! \
is_null($this->get($name, false, $env));
5313 * @param array $args
5317 protected function injectVariables(array $args)
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
5345 public function replaceVariables(array $variables)
5347 $this->registeredVars
= [];
5348 $this->addVariables($variables);
5352 * Replaces variables.
5354 * @param array<string, mixed> $variables
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
);
5380 * @param array $variables
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);
5398 * @param string $name
5402 public function unsetVariable($name)
5404 unset($this->registeredVars
[$name]);
5408 * Returns list of variables
5414 public function getVariables()
5416 return $this->registeredVars
;
5420 * Adds to list of parsed files
5424 * @param string|null $path
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
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
;
5452 * @param string|callable $path
5456 public function addImportPath($path)
5458 if (! \
in_array($path, $this->importPaths
)) {
5459 $this->importPaths
[] = $path;
5468 * @param string|array<string|callable> $path
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
5493 * @param int $numberPrecision
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.
5510 * @param string $style One of the OutputStyle constants
5514 * @phpstan-param OutputStyle::* $style
5516 public function setOutputStyle($style)
5519 case OutputStyle
::EXPANDED
:
5520 $this->formatter
= Expanded
::class;
5523 case OutputStyle
::COMPRESSED
:
5524 $this->formatter
= Compressed
::class;
5528 throw new \
InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5537 * @param string $formatterName
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
5558 * @param string $lineNumberStyle
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
5578 * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
5580 * @param bool $charset
5584 public function setCharset($charset)
5586 $this->charset
= $charset;
5590 * Enable/disable source maps
5594 * @param int $sourceMap
5598 * @phpstan-param self::SOURCE_MAP_* $sourceMap
5600 public function setSourceMap($sourceMap)
5602 $this->sourceMap
= $sourceMap;
5606 * Set source map options
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
5616 public function setSourceMapOptions($sourceMapOptions)
5618 $this->sourceMapOptions
= $sourceMapOptions;
5626 * @param string $name
5627 * @param callable $callback
5628 * @param string[]|null $argumentDeclaration
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
5650 * @param string $name
5654 public function unregisterFunction($name)
5656 unset($this->userFunctions
[$this->normalizeName($name)]);
5664 * @param string $name
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;
5680 * @param string $path
5681 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
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];
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
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
5742 * @param string $url
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
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)) {
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
;
5785 $callableDescription = $r->name
;
5787 } elseif (\
is_object($dir)) {
5788 $callableDescription = \
get_class($dir) . '::__invoke';
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
);
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)) {
5816 } elseif (\
is_callable($dir)) {
5817 // check custom callback for import path
5818 $file = \
call_user_func($dir, $url);
5820 if (! \
is_null($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
);
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)) {
5861 return $this->tryImportPathAsDirectory($path);
5865 * @param string[] $paths
5867 * @return string|null
5869 private function checkImportPathConflicts(array $paths)
5871 if (\
count($paths) === 0) {
5875 if (\
count($paths) === 1) {
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
5893 private function tryImportPathWithExtensions($path)
5895 $result = array_merge(
5896 $this->tryImportPath($path.'.sass'),
5897 $this->tryImportPath($path.'.scss')
5904 return $this->tryImportPath($path.'.css');
5908 * @param string $path
5912 private function tryImportPath($path)
5914 $partial = dirname($path).'/_'.basename($path);
5918 if (is_file($partial)) {
5919 $candidates[] = $partial;
5922 if (is_file($path)) {
5923 $candidates[] = $path;
5930 * @param string $path
5932 * @return string|null
5934 private function tryImportPathAsDirectory($path)
5936 if (!is_dir($path)) {
5940 return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5944 * @param string|null $path
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));
5974 * @param string|null $encoding
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
);
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;
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
);
6010 * Get source position
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)
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)
6041 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
6045 throw $this->error(...func_get_args());
6049 * Build an error (exception)
6053 * @param string $msg Message with optional sprintf()-style vararg parameters
6055 * @return CompilerException
6057 public function error($msg, ...$args)
6060 $msg = sprintf($msg, ...$args);
6063 if (! $this->ignoreCallStackMessage
) {
6064 $msg = $this->addLocationToMessage($msg);
6067 return new CompilerException($msg);
6071 * @param string $msg
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;
6096 * @param string $functionName
6097 * @param array $ExpectedArgs
6098 * @param int $nbActual
6099 * @return CompilerException
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.',
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.',
6126 count($missing) > 1 ?
's' : '',
6127 implode(', ', $missing)
6133 * Beautify call stack for output
6136 * @param int|null $limit
6140 protected function callStackMessage($all = false, $limit = null)
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) {
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
) {
6180 $file = $this->sourceNames
[$env->block
->sourceIndex
];
6182 if ($file === null) {
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)
6203 return static::$defaultValue;
6205 $name = $func->name
;
6210 if (isset($func->args
)) {
6211 $this->applyArguments($func->args
, $argValues);
6214 // throw away lines and children
6215 $tmp = new OutputBlock();
6217 $tmp->children
= [];
6219 $this->env
->marker
= 'function';
6221 if (! empty($func->parentEnv
)) {
6222 $this->env
->declarationScopeParent
= $func->parentEnv
;
6224 throw $this->error("@function $name() without parentEnv");
6227 $ret = $this->compileChildren($func->children
, $tmp, $this->env
->marker
. ' ' . $name);
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)) {
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)) {
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
6284 protected function getBuiltinFunction($name)
6286 $libName = self
::normalizeNativeFunctionName($name);
6287 return [$this, $libName];
6291 * Normalize native function name
6295 * @param string $name
6299 public static function normalizeNativeFunctionName($name)
6301 $name = str_replace("-", "_", $name);
6302 $libName = 'lib' . preg_replace_callback(
6305 return ucfirst($m[1]);
6313 * Check if a function is a native built-in scss function, for css parsing
6317 * @param string $name
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)) {
6343 if (\
is_array($args) && \
count($args) && \
end($args) === static::$null) {
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;
6354 $keyArgs[$key[1]] = $value;
6358 return [$posArgs, $keyArgs];
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);
6388 foreach ($matchedPrototype['arguments'] as $argument) {
6389 list($normalizedName, $originalName, $default) = $argument;
6391 if (isset($vars[$normalizedName])) {
6392 $value = $vars[$normalizedName];
6397 // special null value as default: translate to real null here
6398 if ($value === [Type
::T_KEYWORD
, '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
6427 * @param string[] $prototype
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;
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.');
6445 $p = explode(':', $p, 2);
6446 $name = str_replace('_', '-', $p[0]);
6449 $defaultSource = trim($p[1]);
6451 if ($defaultSource === 'null') {
6452 // differentiate this null from the static::$null
6453 $default = [Type
::T_KEYWORD
, 'null'];
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);
6466 $arguments[] = [$name, $p[0], $default];
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
6483 * @param array[] $prototypes
6484 * @param int $positional
6485 * @param array<string, string> $names A set of names, as both keys and values
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)
6495 $minMismatchDistance = null;
6497 foreach ($prototypes as $prototype) {
6498 // Ideally, find an exact match.
6499 if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
6503 $mismatchDistance = \
count($prototype['arguments']) - $positional;
6505 if ($minMismatchDistance !== null) {
6506 if (abs($mismatchDistance) > abs($minMismatchDistance)) {
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) {
6517 $minMismatchDistance = $mismatchDistance;
6518 $fuzzyMatch = $prototype;
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
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)
6543 foreach ($prototype['arguments'] as $i => $argument) {
6544 list ($name, $originalName, $default) = $argument;
6546 if ($i < $positional) {
6547 if (isset($names[$name])) {
6550 } elseif (isset($names[$name])) {
6552 } elseif ($default === null) {
6557 if ($prototype['rest_argument'] !== null) {
6561 if ($positional > \
count($prototype['arguments'])) {
6565 if ($nameUsed < \
count($names)) {
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
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)
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])) {
6599 } elseif ($default === null) {
6600 throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
6604 if ($prototype['rest_argument'] !== null) {
6608 if ($positional > \
count($prototype['arguments'])) {
6610 'Only %d %sargument%s allowed, but %d %s passed.',
6611 \
count($prototype['arguments']),
6612 empty($names) ?
'' : 'positional ',
6613 \
count($prototype['arguments']) === 1 ?
'' : 's',
6615 $positional === 1 ?
'was' : 'were'
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);
6630 'No argument%s named $%s%s.',
6631 $unknownNames ?
's' : '',
6632 $unknownNames ?
implode(', $', $unknownNames) . ' or $' : '',
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
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) {
6665 $splatSeparator = null;
6668 $positionalArgs = [];
6669 $hasKeywordArgument = 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);
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;
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.');
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)
6759 return $this->reduce($value, true);
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)
6783 if (\
is_null($argValues)) {
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);
6802 $originalRestArgumentName = $name;
6803 $prototype['rest_argument'] = $normalizedName;
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])) {
6822 $val = $vars[$normalizedName];
6825 $this->set($name, $this->reduce($val, true), true, $env);
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']];
6837 $this->set($name, $this->reduce($val, true), true, $env);
6839 $output[$name] = ($reduce ?
$this->reduce($val, true) : $val);
6844 $storeEnv->store
= $env->store
;
6847 foreach ($prototype['arguments'] as $argument) {
6848 list($normalizedName, $name, $default) = $argument;
6850 if (isset($vars[$normalizedName])) {
6853 assert($default !== null);
6856 $this->set($name, $this->reduce($default, true), true);
6858 $output[$name] = ($reduce ?
$this->reduce($default, true) : $default);
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
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)
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]);
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;
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
) {
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);
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
) {
6978 if ($item[0] === Type
::T_MAP
) {
6983 $item[0] === Type
::T_LIST
&&
6986 return static::$emptyMap;
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) {
7011 * Coerce something to list
7013 * @param array|Number $item
7014 * @param string $delim
7015 * @param bool $removeTrailingNull
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]);
7034 if ($item[0] === Type
::T_MAP
) {
7039 for ($i = 0, $s = \
count($keys); $i < $s; $i++
) {
7041 $value = $values[$i];
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)) {
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
) {
7086 switch ($value[0]) {
7088 for ($i = 1; $i <= 3; $i++
) {
7089 if (! is_numeric($value[$i])) {
7090 $cv = $this->compileRGBAValue($value[$i]);
7092 if (! is_numeric($cv)) {
7099 if (isset($value[4])) {
7100 if (! is_numeric($value[4])) {
7101 $cv = $this->compileRGBAValue($value[4], true);
7103 if (! is_numeric($cv)) {
7115 if ($inRGBFunction) {
7116 if (\
count($value[2]) == 3 || \
count($value[2]) == 4) {
7118 array_unshift($color, Type
::T_COLOR
);
7120 return $this->coerceColor($color);
7126 case Type
::T_KEYWORD
:
7127 if (! \
is_string($value[1])) {
7131 $name = strtolower($value[1]);
7134 if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
7135 $nofValues = \
strlen($m[1]);
7137 if (\
in_array($nofValues, [3, 4, 6, 8])) {
7140 $num = hexdec($m[1]);
7142 switch ($nofValues) {
7145 // then continuing with the case 3:
7147 for ($i = 0; $i < $nbChannels; $i++
) {
7149 array_unshift($color, $t << 4 |
$t);
7157 // then continuing with the case 6:
7159 for ($i = 0; $i < $nbChannels; $i++
) {
7160 array_unshift($color, $num & 0xff);
7167 if ($nbChannels === 4) {
7168 if ($color[3] === 255) {
7169 $color[3] = 1; // fully opaque
7171 $color[3] = round($color[3] / 255, Number
::PRECISION
);
7175 array_unshift($color, Type
::T_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]];
7194 * @param int|Number $value
7195 * @param bool $isAlpha
7199 protected function compileRGBAValue($value, $isAlpha = false)
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
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
) {
7227 if ($value instanceof Number
) {
7228 if ($value->unitless()) {
7229 $num = $value->getDimension();
7230 } elseif ($value->hasUnit('%')) {
7231 $num = $max * $value->getDimension() / 100;
7233 throw $this->error('Expected %s to have no units or "%%".', $value);
7237 } elseif (\
is_array($value)) {
7238 $value = $this->compileValue($value);
7242 if (is_numeric($value)) {
7244 $value = round($value);
7247 $value = min($max, max($min, $value));
7256 * Coerce value to string
7258 * @param array|Number $value
7262 protected function coerceString($value)
7264 if ($value[0] === Type
::T_STRING
) {
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
7277 * The returned value is always using the T_STRING type.
7281 * @param array|Number $value
7282 * @param string|null $varName
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
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();
7328 * Assert value is a map
7332 * @param array|Number $value
7333 * @param string|null $varName
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);
7353 * Assert value is a list
7357 * @param array|Number $value
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]);
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.');
7394 * Assert value is a color
7398 * @param array|Number $value
7399 * @param string|null $varName
7403 * @throws SassScriptException
7405 public function assertColor($value, $varName = null)
7407 if ($color = $this->coerceColor($value)) {
7411 $value = $this->compileValue($value);
7413 throw SassScriptException
::forArgument("$value is not a color.", $varName);
7417 * Assert value is a number
7421 * @param array|Number $value
7422 * @param string|null $varName
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);
7439 * Assert value is a integer
7443 * @param array|Number $value
7444 * @param string|null $varName
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
7467 private function extractSlashAlphaInColorFunction($args)
7470 if (\
count($args) === 3 && $last[0] === Type
::T_EXPRESSION
&& $last[1] === '/') {
7480 * Make sure a color's components don't go out of bounds
7486 protected function fixColor($c)
7488 foreach ([1, 2, 3] as $i) {
7497 if (!\
is_int($c[$i])) {
7498 $c[$i] = round($c[$i]);
7506 * Convert RGB to HSL
7516 public function toHSL($red, $green, $blue)
7518 $min = min($red, $green, $blue);
7519 $max = max($red, $green, $blue);
7524 if ((int) $d === 0) {
7530 $s = $d / (510 - $l);
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];
7554 protected function hueToRGB($m1, $m2, $h)
7563 return $m1 +
($m2 - $m1) * $h * 6;
7571 return $m1 +
($m2 - $m1) * (2 / 3 - $h) * 6;
7578 * Convert HSL to RGB
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
7588 public function toRGB($hue, $saturation, $lightness)
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;
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];
7611 * Convert HWB to RGB
7612 * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
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
7622 private function HWBtoRGB($hue, $whiteness, $blackness)
7624 $w = min(100, max(0, $whiteness)) / 100;
7625 $b = min(100, max(0, $blackness)) / 100;
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);
7644 * Convert RGB to HWB
7654 private function RGBtoHWB($red, $green, $blue)
7656 $min = min($red, $green, $blue);
7657 $max = max($red, $green, $blue);
7661 if ((int) $d === 0) {
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]);
7702 [null, $args[1], true]
7705 return $this->reduce([Type
::T_FUNCTION_CALL
, $functionReference, $callArgs]);
7709 protected static $libGetFunction = [
7713 protected function libGetFunction($args)
7715 $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
7719 $isCss = array_shift($args);
7720 $isCss = (($isCss === static::$true) ?
true : false);
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;
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
) {
7765 foreach ($list[2] as $item) {
7767 $itemValue = $this->normalizeValue($item);
7769 if ($itemValue instanceof Number
&& $value->equals($itemValue)) {
7770 return new Number($key, '');
7773 return static::$null;
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 = [
7791 ['red', 'green', 'blue'],
7792 ['red', 'green', 'blue', 'alpha'] ];
7793 protected function libRgb($args, $kwargs, $funcName = 'rgb')
7795 switch (\
count($args)) {
7797 if (! $color = $this->coerceColor($args[0], true)) {
7798 $color = [Type
::T_STRING
, '', [$funcName . '(', $args[0], ')']];
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], ')']];
7812 if ($color = $this->coerceColor($args[0], true)) {
7813 $alpha = $this->compileRGBAValue($args[1], true);
7815 if (is_numeric($alpha)) {
7818 $color = [Type
::T_STRING
, '',
7819 [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
7822 $color = [Type
::T_STRING
, '', [$funcName . '(', $args[0], ', ', $args[1], ')']];
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], ')']];
7840 protected static $libRgba = [
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
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');
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])) {
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);
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'])) {
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);
7930 'No argument%s named $%s%s.',
7931 $unknownNames ?
's' : '',
7932 $unknownNames ?
implode(', $', $unknownNames) . ' or $' : '',
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;
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.');
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));
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];
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];
7988 if ($alpha !== null) {
7989 $existingAlpha = isset($color[4]) ?
$color[4] : 1;
7990 $color[4] = $fn($existingAlpha, $alpha, 1);
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) {
8004 $new = $base +
$alter;
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) {
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) {
8038 $scale = $scale / 100;
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
8109 protected static $libOpacity = ['color'];
8110 protected function libOpacity($args)
8114 if ($value instanceof Number
) {
8118 return $this->libAlpha($args);
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 = [
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'])) {
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'])) {
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)) {
8198 $args[$k] = $this->stringifyFncallArgs($arg);
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'])
8210 $hue = $this->reduce($args[0]);
8211 $saturation = $this->reduce($args[1]);
8212 $lightness = $this->reduce($args[2]);
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], ')']];
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) {
8234 $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
8236 if (! \
is_null($alpha)) {
8243 protected static $libHsla = [
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 = [
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'])) {
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)) {
8336 $args[$k] = $this->stringifyFncallArgs($arg);
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'])
8348 $hue = $this->reduce($args[0]);
8349 $whiteness = $this->reduce($args[1]);
8350 $blackness = $this->reduce($args[2]);
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) {
8378 $color = $this->HWBtoRGB($hueValue, $w, $b);
8380 if (! \is_null($alpha)) {
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
8409 * @param int|float $amount
8413 protected function adjustHsl($color, $idx, $amount)
8415 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
8416 $hsl[$idx] +
= $amount;
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];
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)
8464 if (count($args) === 1) {
8465 $this->assertNumber($args[0], 'amount');
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)
8490 if ($value instanceof Number
) {
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)
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.');
8518 $color = $this->assertColor($value, '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]));
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]));
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)
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);
8588 protected static $libQuote = ['string'];
8589 protected function libQuote($args)
8591 $value = $this->assertString($args[0], 'string');
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)
8647 foreach ($args[0][2] as $arg) {
8648 $number = $this->assertNumber($arg);
8650 if (\
is_null($min) ||
$min->greaterThan($number)) {
8655 if (!\
is_null($min)) {
8659 throw $this->error('At least one argument must be passed.');
8662 protected static $libMax = ['numbers...'];
8663 protected function libMax($args)
8670 foreach ($args[0][2] as $arg) {
8671 $number = $this->assertNumber($arg);
8673 if (\
is_null($max) ||
$max->lessThan($number)) {
8678 if (!\
is_null($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();
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();
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];
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;
8782 * Gets the value corresponding to that key in the 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];
8801 * Gets the index corresponding to that key in the map entries
8804 * @param Number|array $key
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]))) {
8821 protected static $libMapKeys = ['map'];
8822 protected function libMapKeys($args)
8824 $map = $this->assertMap($args[0], 'map');
8827 return [Type
::T_LIST
, ',', $keys];
8830 protected static $libMapValues = ['map'];
8831 protected function libMapValues($args)
8833 $map = $this->assertMap($args[0], 'map');
8836 return [Type
::T_LIST
, ',', $values];
8839 protected static $libMapRemove = [
8841 ['map', 'key', 'keys...'],
8843 protected function libMapRemove($args)
8845 $map = $this->assertMap($args[0], 'map');
8847 if (\
count($args) === 1) {
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);
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;
8889 return $this->toBool($this->mapHasKey($map, $lastKey));
8893 * @param array|Number $keyValue
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]))) {
8910 protected static $libMapMerge = [
8915 protected function libMapMerge($args)
8917 $map1 = $this->assertMap($args[0], 'map1');
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.');
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) {
8940 return $this->mergeMaps($nestedMap, $map2);
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)
8958 return $modify($map);
8961 return $this->modifyNestedMap($map, $keys, $modify, $addNesting);
8966 * @param array $keys
8967 * @param callable $modify
8968 * @param bool $addNesting
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);
8982 if ($nestedValueIndex !== null) {
8983 $map[2][$nestedValueIndex] = $modify($map[2][$nestedValueIndex]);
8986 $map[2][] = $modify(self
::$null);
8992 $nestedMap = $nestedValueIndex !== null ?
$this->tryMap($map[2][$nestedValueIndex]) : null;
8994 if ($nestedMap === null && !$addNesting) {
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;
9008 $map[2][] = $newNestedMap;
9015 * Merges 2 Sass maps together
9017 * @param array $map1
9018 * @param array $map2
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];
9033 $map1[2][] = $map2[2][$i2];
9039 protected static $libKeywords = ['args'];
9040 protected function libKeywords($args)
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');
9053 foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
9054 $keys[] = [Type
::T_KEYWORD
, $name];
9058 return [Type
::T_MAP
, $keys, $values];
9061 protected static $libIsBracketed = ['list'];
9062 protected function libIsBracketed($args)
9065 $this->coerceList($list, ' ');
9067 if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
9071 return self
::$false;
9075 * @param array $list1
9076 * @param array|Number|null $sep
9079 * @throws CompilerException
9083 protected function listSeparatorForJoin($list1, $sep)
9085 @trigger_error
(sprintf('The "%s" method is deprecated.', __METHOD__
), E_USER_DEPRECATED
);
9087 if (! isset($sep)) {
9091 switch ($this->compileValue($sep)) {
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'))) {
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] ?
: ' ';
9135 throw SassScriptException
::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator');
9138 if ($bracketed === static::$true) {
9140 } elseif ($bracketed === static::$false) {
9142 } elseif ($bracketed === [Type
::T_KEYWORD
, 'auto']) {
9143 $bracketed = 'auto';
9144 } elseif ($bracketed === static::$null) {
9147 $bracketed = $this->compileValue($bracketed);
9148 $bracketed = ! ! $bracketed;
9150 if ($bracketed === true) {
9155 if ($bracketed === 'auto') {
9158 if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
9163 $res = [Type
::T_LIST
, $separator, array_merge($list1[2], $list2[2])];
9166 $res['enclosing'] = 'bracket';
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'))) {
9193 $separator = $list1[1] === '' && \
count($list1[2]) <= 1 && (empty($list1['enclosing']) ||
$list1['enclosing'] === 'parent') ?
' ' : $list1[1];
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'];
9209 protected static $libZip = ['lists...'];
9210 protected function libZip($args)
9213 foreach ($args[0][2] as $arg) {
9214 $argLists[] = $this->coerceList($arg);
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];
9236 $result[2] = $lists;
9238 $result['enclosing'] = 'parent';
9244 protected static $libTypeOf = ['value'];
9245 protected function libTypeOf($args)
9249 return [Type
::T_KEYWORD
, $this->getTypeOf($value)];
9253 * @param array|Number $value
9257 private function getTypeOf($value)
9259 switch ($value[0]) {
9260 case Type
::T_KEYWORD
:
9261 if ($value === static::$true ||
$value === static::$false) {
9265 if ($this->coerceColor($value)) {
9270 case Type
::T_FUNCTION
:
9273 case Type
::T_FUNCTION_REFERENCE
:
9277 if (isset($value[3]) && \
is_array($value[3])) {
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;
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)) {
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');
9350 $index = $index - 1;
9353 $index = Util
::mbStrlen($stringContent) +
1 +
$index;
9357 Util
::mbSubstr($stringContent, 0, $index),
9359 Util
::mbSubstr($stringContent, $index)
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) {
9396 $endInt = Util
::mbStrlen($stringContent) +
$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)];
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')];
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')];
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
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;
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)) {
9483 $name = $this->normalizeName($name);
9485 if (isset($this->userFunctions
[$name])) {
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
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');
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()
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
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;
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)
9622 return $this->inspectFormatValue($value);
9626 * Preprocess selector args
9629 * @param string|null $varname
9630 * @param bool $allowParent
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
) {
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
9678 * @param int $maxDepth
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)) {
9691 if (!in_array($arg[0], [Type
::T_STRING
, Type
::T_KEYWORD
])) {
9698 * Postprocess selector to output in right format
9700 * @param array $selectors
9704 protected function formatOutputSelector($selectors)
9706 $selectors = $this->collapseSelectorsAsList($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
9730 protected function isSuperSelector($super, $sub)
9732 // one and only one selector for each arg
9734 throw $this->error('Invalid super selector for isSuperSelector()');
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])) {
9750 if (count($super) > 1) {
9751 foreach ($super as $s) {
9752 if ($this->isSuperSelector([$s], $sub)) {
9759 $super = reset($super);
9763 $nextMustMatch = false;
9765 foreach ($super as $node) {
9768 array_walk_recursive(
9770 function ($value, $key) use (&$compound) {
9771 $compound .= $value;
9775 if ($this->isImmediateRelationshipCombinator($compound)) {
9776 if ($node !== $sub[$i]) {
9780 $nextMustMatch = true;
9783 while ($i < \
count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
9784 if ($nextMustMatch) {
9791 if ($i >= \
count($sub)) {
9795 $nextMustMatch = false;
9804 * Test a part of super selector again a part of sub selector
9806 * @param array $superParts
9807 * @param array $subParts
9811 protected function isSuperPart($superParts, $subParts)
9815 foreach ($superParts as $superPart) {
9816 while ($i < \
count($subParts) && $subParts[$i] !== $superPart) {
9820 if ($i >= \
count($subParts)) {
9830 protected static $libSelectorAppend = ['selector...'];
9831 protected function libSelectorAppend($args)
9833 // get the selector... list
9834 $args = reset($args);
9837 if (\
count($args) < 1) {
9838 throw $this->error('selector-append() needs at least 1 argument');
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
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
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
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);
9969 foreach ($selectors as $selector) {
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;
9990 protected static $libSelectorNest = ['selector...'];
9991 protected function libSelectorNest($args)
9993 // get the selector... list
9994 $args = reset($args);
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);
10008 foreach ($selectorsMap as $selectors) {
10009 $env = new Environment();
10010 $env->selectors
= $selectors;
10015 $envs = array_reverse($envs);
10016 $env = $this->extractEnv($envs);
10017 $outputSelectors = $this->multiplySelectors($env);
10019 return $this->formatOutputSelector($outputSelectors);
10022 protected static $libSelectorParse = [
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
10065 protected function unifyCompoundSelectors($compound1, $compound2)
10067 if (! \
count($compound1)) {
10071 if (! \
count($compound2)) {
10075 // check that last part are compatible
10076 $lastPart1 = array_pop($compound1);
10077 $lastPart2 = array_pop($compound2);
10078 $last = $this->mergeParts($lastPart1, $lastPart2);
10084 $unifiedCompound = [$last];
10085 $unifiedSelectors = [$unifiedCompound];
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;
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;
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);
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]));
10143 $unifiedSelectors = $new;
10147 return $unifiedSelectors;
10151 * Prepend each selector from $selectors with $parts
10153 * @param array $selectors
10154 * @param array $parts
10158 protected function prependSelectors($selectors, $parts)
10162 foreach ($selectors as $compoundSelector) {
10163 array_unshift($compoundSelector, $parts);
10165 $new[] = $compoundSelector;
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;
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];
10198 // try again matching a non empty intersection and a compatible tagname
10199 $before = $compound;
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];
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
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) {
10240 $parts1 = array_diff($parts1, [$tag1]);
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);
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) {
10295 * Find the html tag name in a selector parts list
10297 * @param string[] $parts
10301 protected function findTagName($parts)
10303 foreach ($parts as $part) {
10304 if (! preg_match('/^[\[.:#%_-]/', $part)) {
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);
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);
10345 foreach ($matches as $match) {
10346 if (! is_file($match)) {
10350 $listParts[] = [Type
::T_STRING
, '"', [$match]];
10353 return [Type
::T_LIST
, ',', $listParts];