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\Compiler\Environment
;
17 use ScssPhp\ScssPhp\Exception\CompilerException
;
18 use ScssPhp\ScssPhp\Exception\ParserException
;
19 use ScssPhp\ScssPhp\Exception\SassScriptException
;
20 use ScssPhp\ScssPhp\Formatter\Compressed
;
21 use ScssPhp\ScssPhp\Formatter\Expanded
;
22 use ScssPhp\ScssPhp\Formatter\OutputBlock
;
23 use ScssPhp\ScssPhp\Node\Number
;
24 use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
;
25 use ScssPhp\ScssPhp\Util\Path
;
28 * The scss compiler and parser.
30 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
31 * by `Parser` into a syntax tree, then it is compiled into another tree
32 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
33 * formatter, like `Formatter` which then outputs CSS as a string.
35 * During the first compile, all values are *reduced*, which means that their
36 * types are brought to the lowest form before being dump as strings. This
37 * handles math equations, variable dereferences, and the like.
39 * The `compile` function of `Compiler` is the entry point.
43 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
44 * then transforms the resulting tree to a CSS tree. This class also holds the
45 * evaluation context, such as all available mixins and variables at any given
48 * The `Parser` class is only concerned with parsing its input.
50 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
51 * handling things like indentation.
57 * @author Leaf Corcoran <leafot@gmail.com>
64 const LINE_COMMENTS
= 1;
81 const WITH_SUPPORTS
= 4;
87 const SOURCE_MAP_NONE
= 0;
88 const SOURCE_MAP_INLINE
= 1;
89 const SOURCE_MAP_FILE
= 2;
92 * @var array<string, string>
94 protected static $operatorNames = [
111 * @var array<string, string>
113 protected static $namespaces = [
119 public static $true = [Type
::T_KEYWORD
, 'true'];
120 public static $false = [Type
::T_KEYWORD
, 'false'];
122 public static $NaN = [Type
::T_KEYWORD
, 'NaN'];
124 public static $Infinity = [Type
::T_KEYWORD
, 'Infinity'];
125 public static $null = [Type
::T_NULL
];
126 public static $nullString = [Type
::T_STRING
, '', []];
127 public static $defaultValue = [Type
::T_KEYWORD
, ''];
128 public static $selfSelector = [Type
::T_SELF
];
129 public static $emptyList = [Type
::T_LIST
, '', []];
130 public static $emptyMap = [Type
::T_MAP
, [], []];
131 public static $emptyString = [Type
::T_STRING
, '"', []];
132 public static $with = [Type
::T_KEYWORD
, 'with'];
133 public static $without = [Type
::T_KEYWORD
, 'without'];
136 * @var array<int, string|callable>
138 protected $importPaths = [];
140 * @var array<string, Block>
142 protected $importCache = [];
146 protected $importedFiles = [];
147 protected $userFunctions = [];
148 protected $registeredVars = [];
150 * @var array<string, bool>
152 protected $registeredFeatures = [
153 'extend-selector-pseudoclass' => false,
155 'units-level-3' => true,
156 'global-variable-shadowing' => false,
162 protected $encoding = null;
166 protected $lineNumberStyle = null;
169 * @var int|SourceMapGenerator
170 * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
172 protected $sourceMap = self
::SOURCE_MAP_NONE
;
173 protected $sourceMapOptions = [];
176 * @var string|\ScssPhp\ScssPhp\Formatter
178 protected $formatter = Expanded
::class;
185 * @var OutputBlock|null
187 protected $rootBlock;
190 * @var \ScssPhp\ScssPhp\Compiler\Environment
194 * @var OutputBlock|null
198 * @var Environment|null
204 protected $charsetSeen;
206 * @var array<int, string>
208 protected $sourceNames;
218 protected $indentLevel;
224 * @var array<string, int[]>
226 protected $extendsMap;
228 * @var array<string, int>
230 protected $parsedFiles;
238 protected $sourceIndex;
242 protected $sourceLine;
246 protected $sourceColumn;
254 protected $shouldEvaluate;
259 protected $ignoreErrors;
263 protected $ignoreCallStackMessage = false;
268 protected $callStack = [];
271 * The directory of the currently processed file
275 private $currentDirectory;
278 * The directory of the input file
282 private $rootDirectory;
284 private $legacyCwdImportPath = true;
289 * @param array|null $cacheOptions
291 public function __construct($cacheOptions = null)
293 $this->parsedFiles
= [];
294 $this->sourceNames
= [];
297 $this->cache
= new Cache($cacheOptions);
300 $this->stderr
= fopen('php://stderr', 'w');
304 * Get compiler options
306 * @return array<string, mixed>
308 public function getCompileOptions()
311 'importPaths' => $this->importPaths
,
312 'registeredVars' => $this->registeredVars
,
313 'registeredFeatures' => $this->registeredFeatures
,
314 'encoding' => $this->encoding
,
315 'sourceMap' => serialize($this->sourceMap
),
316 'sourceMapOptions' => $this->sourceMapOptions
,
317 'formatter' => $this->formatter
,
318 'legacyImportPath' => $this->legacyCwdImportPath
,
325 * Set an alternative error output stream, for testing purpose only
327 * @param resource $handle
331 public function setErrorOuput($handle)
333 $this->stderr
= $handle;
341 * @param string $code
342 * @param string $path
346 public function compile($code, $path = null)
349 $cacheKey = ($path ?
$path : '(stdin)') . ':' . md5($code);
350 $compileOptions = $this->getCompileOptions();
351 $cache = $this->cache
->getCache('compile', $cacheKey, $compileOptions);
353 if (\
is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
354 // check if any dependency file changed before accepting the cache
355 foreach ($cache['dependencies'] as $file => $mtime) {
356 if (! is_file($file) ||
filemtime($file) !== $mtime) {
363 return $cache['out'];
369 $this->indentLevel
= -1;
371 $this->extendsMap
= [];
372 $this->sourceIndex
= null;
373 $this->sourceLine
= null;
374 $this->sourceColumn
= null;
377 $this->storeEnv
= null;
378 $this->charsetSeen
= null;
379 $this->shouldEvaluate
= null;
380 $this->ignoreCallStackMessage
= false;
382 if (!\
is_null($path) && is_file($path)) {
383 $path = realpath($path) ?
: $path;
384 $this->currentDirectory
= dirname($path);
385 $this->rootDirectory
= $this->currentDirectory
;
387 $this->currentDirectory
= null;
388 $this->rootDirectory
= getcwd();
392 $this->parser
= $this->parserFactory($path);
393 $tree = $this->parser
->parse($code);
394 $this->parser
= null;
396 $this->formatter
= new $this->formatter();
397 $this->rootBlock
= null;
398 $this->rootEnv
= $this->pushEnv($tree);
400 $this->injectVariables($this->registeredVars
);
401 $this->compileRoot($tree);
404 $sourceMapGenerator = null;
406 if ($this->sourceMap
) {
407 if (\
is_object($this->sourceMap
) && $this->sourceMap
instanceof SourceMapGenerator
) {
408 $sourceMapGenerator = $this->sourceMap
;
409 $this->sourceMap
= self
::SOURCE_MAP_FILE
;
410 } elseif ($this->sourceMap
!== self
::SOURCE_MAP_NONE
) {
411 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions
);
415 $out = $this->formatter
->format($this->scope
, $sourceMapGenerator);
419 if (!$this->charsetSeen
) {
420 if (strlen($out) !== Util
::mbStrlen($out)) {
421 $prefix = '@charset "UTF-8";' . "\n";
422 $out = $prefix . $out;
426 if (! empty($out) && $this->sourceMap
&& $this->sourceMap
!== self
::SOURCE_MAP_NONE
) {
427 $sourceMap = $sourceMapGenerator->generateJson($prefix);
428 $sourceMapUrl = null;
430 switch ($this->sourceMap
) {
431 case self
::SOURCE_MAP_INLINE
:
432 $sourceMapUrl = sprintf('data:application/json,%s', Util
::encodeURIComponent($sourceMap));
435 case self
::SOURCE_MAP_FILE
:
436 $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
440 $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
442 } catch (SassScriptException
$e) {
443 throw $this->error($e->getMessage());
446 if ($this->cache
&& isset($cacheKey) && isset($compileOptions)) {
448 'dependencies' => $this->getParsedFiles(),
452 $this->cache
->setCache('compile', $cacheKey, $v, $compileOptions);
461 * @param string $path
463 * @return \ScssPhp\ScssPhp\Parser
465 protected function parserFactory($path)
467 // https://sass-lang.com/documentation/at-rules/import
468 // CSS files imported by Sass don’t allow any special Sass features.
469 // In order to make sure authors don’t accidentally write Sass in their CSS,
470 // all Sass features that aren’t also valid CSS will produce errors.
471 // Otherwise, the CSS will be rendered as-is. It can even be extended!
474 if (substr($path, '-4') === '.css') {
478 $parser = new Parser($path, \
count($this->sourceNames
), $this->encoding
, $this->cache
, $cssOnly);
480 $this->sourceNames
[] = $path;
481 $this->addParsedFile($path);
489 * @param array $target
490 * @param array $origin
494 protected function isSelfExtend($target, $origin)
496 foreach ($origin as $sel) {
497 if (\
in_array($target, $sel)) {
508 * @param array $target
509 * @param array $origin
510 * @param array|null $block
514 protected function pushExtends($target, $origin, $block)
516 $i = \
count($this->extends);
517 $this->extends[] = [$target, $origin, $block];
519 foreach ($target as $part) {
520 if (isset($this->extendsMap
[$part])) {
521 $this->extendsMap
[$part][] = $i;
523 $this->extendsMap
[$part] = [$i];
531 * @param string $type
532 * @param array $selectors
534 * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
536 protected function makeOutputBlock($type, $selectors = null)
538 $out = new OutputBlock();
542 $out->parent
= $this->scope
;
543 $out->selectors
= $selectors;
544 $out->depth
= $this->env
->depth
;
546 if ($this->env
->block
instanceof Block
) {
547 $out->sourceName
= $this->env
->block
->sourceName
;
548 $out->sourceLine
= $this->env
->block
->sourceLine
;
549 $out->sourceColumn
= $this->env
->block
->sourceColumn
;
551 $out->sourceName
= null;
552 $out->sourceLine
= null;
553 $out->sourceColumn
= null;
562 * @param \ScssPhp\ScssPhp\Block $rootBlock
566 protected function compileRoot(Block
$rootBlock)
568 $this->rootBlock
= $this->scope
= $this->makeOutputBlock(Type
::T_ROOT
);
570 $this->compileChildrenNoReturn($rootBlock->children
, $this->scope
);
571 $this->flattenSelectors($this->scope
);
572 $this->missingSelectors();
576 * Report missing selectors
580 protected function missingSelectors()
582 foreach ($this->extends as $extend) {
583 if (isset($extend[3])) {
587 list($target, $origin, $block) = $extend;
589 // ignore if !optional
594 $target = implode(' ', $target);
595 $origin = $this->collapseSelectors($origin);
597 $this->sourceLine
= $block[Parser
::SOURCE_LINE
];
598 throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
605 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
606 * @param string $parentKey
610 protected function flattenSelectors(OutputBlock
$block, $parentKey = null)
612 if ($block->selectors
) {
615 foreach ($block->selectors
as $s) {
618 if (! \
is_array($s)) {
623 if (! empty($this->extendsMap
)) {
624 $this->matchExtends($s, $selectors);
627 array_walk($selectors, function (&$value) {
628 $value = serialize($value);
631 $selectors = array_unique($selectors);
633 array_walk($selectors, function (&$value) {
634 $value = unserialize($value);
639 $block->selectors
= [];
640 $placeholderSelector = false;
642 foreach ($selectors as $selector) {
643 if ($this->hasSelectorPlaceholder($selector)) {
644 $placeholderSelector = true;
648 $block->selectors
[] = $this->compileSelector($selector);
651 if ($placeholderSelector && 0 === \
count($block->selectors
) && null !== $parentKey) {
652 unset($block->parent
->children
[$parentKey]);
658 foreach ($block->children
as $key => $child) {
659 $this->flattenSelectors($child, $key);
664 * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
666 * @param array $parts
670 protected function glueFunctionSelectors($parts)
674 foreach ($parts as $part) {
675 if (\
is_array($part)) {
676 $part = $this->glueFunctionSelectors($part);
679 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
680 // and need to be joined to this
682 \
count($new) && \
is_string($new[\
count($new) - 1]) &&
683 \
strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
685 while (\
count($new) > 1 && substr($new[\
count($new) - 1], -1) !== '(') {
686 $part = array_pop($new) . $part;
688 $new[\
count($new) - 1] .= $part;
701 * @param array $selector
703 * @param integer $from
704 * @param boolean $initial
708 protected function matchExtends($selector, &$out, $from = 0, $initial = true)
710 static $partsPile = [];
711 $selector = $this->glueFunctionSelectors($selector);
713 if (\
count($selector) == 1 && \
in_array(reset($selector), $partsPile)) {
719 foreach ($selector as $i => $part) {
724 // check that we are not building an infinite loop of extensions
725 // if the new part is just including a previous part don't try to extend anymore
726 if (\
count($part) > 1) {
727 foreach ($partsPile as $previousPart) {
728 if (! \
count(array_diff($previousPart, $part))) {
734 $partsPile[] = $part;
736 if ($this->matchExtendsSingle($part, $origin, $initial)) {
737 $after = \array_slice
($selector, $i +
1);
738 $before = \array_slice
($selector, 0, $i);
739 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
741 foreach ($origin as $new) {
744 // remove shared parts
745 if (\
count($new) > 1) {
746 while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
751 if (\
count($nonBreakableBefore) && $k === \
count($new)) {
756 $tempReplacement = $k > 0 ? \array_slice
($new, $k) : $new;
758 for ($l = \
count($tempReplacement) - 1; $l >= 0; $l--) {
761 foreach ($tempReplacement[$l] as $chunk) {
762 if (! \
in_array($chunk, $slice)) {
767 array_unshift($replacement, $slice);
769 if (! $this->isImmediateRelationshipCombinator(end($slice))) {
774 $afterBefore = $l != 0 ? \array_slice
($tempReplacement, 0, $l) : [];
776 // Merge shared direct relationships.
777 $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
779 $result = array_merge(
786 if ($result === $selector) {
790 $this->pushOrMergeExtentedSelector($out, $result);
792 // recursively check for more matches
793 $startRecurseFrom = \
count($before) +
min(\
count($nonBreakableBefore), \
count($mergedBefore));
795 if (\
count($origin) > 1) {
796 $this->matchExtends($result, $out, $startRecurseFrom, false);
798 $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
801 // selector sequence merging
802 if (! empty($before) && \
count($new) > 1) {
803 $preSharedParts = $k > 0 ? \array_slice
($before, 0, $k) : [];
804 $postSharedParts = $k > 0 ? \array_slice
($before, $k) : $before;
806 list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
808 $result2 = array_merge(
818 $this->pushOrMergeExtentedSelector($out, $result2);
822 array_pop($partsPile);
825 while (\
count($outRecurs)) {
826 $result = array_shift($outRecurs);
827 $this->pushOrMergeExtentedSelector($out, $result);
832 * Test a part for being a pseudo selector
834 * @param string $part
835 * @param array $matches
839 protected function isPseudoSelector($part, &$matches)
842 strpos($part, ':') === 0 &&
843 preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
852 * Push extended selector except if
853 * - this is a pseudo selector
856 * in this case we merge the pseudo selector content
859 * @param array $extended
863 protected function pushOrMergeExtentedSelector(&$out, $extended)
865 if (\
count($out) && \
count($extended) === 1 && \
count(reset($extended)) === 1) {
866 $single = reset($extended);
867 $part = reset($single);
870 $this->isPseudoSelector($part, $matchesExtended) &&
871 \
in_array($matchesExtended[1], [ 'slotted' ])
874 $prev = $this->glueFunctionSelectors($prev);
876 if (\
count($prev) === 1 && \
count(reset($prev)) === 1) {
877 $single = reset($prev);
878 $part = reset($single);
881 $this->isPseudoSelector($part, $matchesPrev) &&
882 $matchesPrev[1] === $matchesExtended[1]
884 $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
885 $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
886 $extended = implode($matchesExtended[1] . '(', $extended);
887 $extended = [ [ $extended ]];
897 * Match extends single
899 * @param array $rawSingle
900 * @param array $outOrigin
901 * @param boolean $initial
905 protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
910 // simple usual cases, no need to do the whole trick
911 if (\
in_array($rawSingle, [['>'],['+'],['~']])) {
915 foreach ($rawSingle as $part) {
917 if (! \
is_string($part)) {
921 if (! preg_match('/^[\[.:#%]/', $part) && \
count($single)) {
922 $single[\
count($single) - 1] .= $part;
928 $extendingDecoratedTag = false;
930 if (\
count($single) > 1) {
932 $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ?
$matches[0] : false;
938 foreach ($single as $k => $part) {
939 if (isset($this->extendsMap
[$part])) {
940 foreach ($this->extendsMap
[$part] as $idx) {
941 $counts[$idx] = isset($counts[$idx]) ?
$counts[$idx] +
1 : 1;
947 $this->isPseudoSelector($part, $matches) &&
948 ! \
in_array($matches[1], [ 'not' ])
950 $buffer = $matches[2];
951 $parser = $this->parserFactory(__METHOD__
);
953 if ($parser->parseSelector($buffer, $subSelectors, false)) {
954 foreach ($subSelectors as $ksub => $subSelector) {
956 $this->matchExtends($subSelector, $subExtended, 0, false);
959 $subSelectorsExtended = $subSelectors;
960 $subSelectorsExtended[$ksub] = $subExtended;
962 foreach ($subSelectorsExtended as $ksse => $sse) {
963 $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
966 $subSelectorsExtended = implode(', ', $subSelectorsExtended);
967 $singleExtended = $single;
968 $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
969 $outOrigin[] = [ $singleExtended ];
977 foreach ($counts as $idx => $count) {
978 list($target, $origin, /* $block */) = $this->extends[$idx];
980 $origin = $this->glueFunctionSelectors($origin);
983 if ($count !== \
count($target)) {
987 $this->extends[$idx][3] = true;
989 $rem = array_diff($single, $target);
991 foreach ($origin as $j => $new) {
992 // prevent infinite loop when target extends itself
993 if ($this->isSelfExtend($single, $origin) && ! $initial) {
997 $replacement = end($new);
999 // Extending a decorated tag with another tag is not possible.
1001 $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
1002 preg_match('/^[a-z0-9]+$/i', $replacement[0])
1008 $combined = $this->combineSelectorSingle($replacement, $rem);
1010 if (\
count(array_diff($combined, $origin[$j][\
count($origin[$j]) - 1]))) {
1011 $origin[$j][\
count($origin[$j]) - 1] = $combined;
1015 $outOrigin = array_merge($outOrigin, $origin);
1024 * Extract a relationship from the fragment.
1026 * When extracting the last portion of a selector we will be left with a
1027 * fragment which may end with a direction relationship combinator. This
1028 * method will extract the relationship fragment and return it along side
1031 * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
1033 * @return array The selector without the relationship fragment if any, the relationship fragment.
1035 protected function extractRelationshipFromFragment(array $fragment)
1040 $j = $i = \
count($fragment);
1043 $children = $j != $i ? \array_slice
($fragment, $j, $i - $j) : [];
1044 $parents = \array_slice
($fragment, 0, $j);
1045 $slice = end($parents);
1047 if (empty($slice) ||
! $this->isImmediateRelationshipCombinator($slice[0])) {
1054 return [$parents, $children];
1058 * Combine selector single
1060 * @param array $base
1061 * @param array $other
1065 protected function combineSelectorSingle($base, $other)
1072 while (\
count($other) && strpos(end($other), ':') === 0) {
1073 array_unshift($pseudo, array_pop($other));
1076 foreach ([array_reverse($base), array_reverse($other)] as $single) {
1077 $rang = count($single);
1079 foreach ($single as $part) {
1080 if (preg_match('/^[\[:]/', $part)) {
1083 } elseif (preg_match('/^[\.#]/', $part)) {
1084 array_unshift($out, $part);
1086 } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
1089 } elseif ($wasTag) {
1090 $tag[\
count($tag) - 1] .= $part;
1092 array_unshift($out, $part);
1099 array_unshift($out, $tag[0]);
1102 while (\
count($pseudo)) {
1103 $out[] = array_shift($pseudo);
1112 * @param \ScssPhp\ScssPhp\Block $media
1116 protected function compileMedia(Block
$media)
1118 $this->pushEnv($media);
1120 $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env
));
1122 if (! empty($mediaQueries) && $mediaQueries) {
1123 $previousScope = $this->scope
;
1124 $parentScope = $this->mediaParent($this->scope
);
1126 foreach ($mediaQueries as $mediaQuery) {
1127 $this->scope
= $this->makeOutputBlock(Type
::T_MEDIA
, [$mediaQuery]);
1129 $parentScope->children
[] = $this->scope
;
1130 $parentScope = $this->scope
;
1133 // top level properties in a media cause it to be wrapped
1136 foreach ($media->children
as $child) {
1140 $type !== Type
::T_BLOCK
&&
1141 $type !== Type
::T_MEDIA
&&
1142 $type !== Type
::T_DIRECTIVE
&&
1143 $type !== Type
::T_IMPORT
1151 $wrapped = new Block();
1152 $wrapped->sourceName
= $media->sourceName
;
1153 $wrapped->sourceIndex
= $media->sourceIndex
;
1154 $wrapped->sourceLine
= $media->sourceLine
;
1155 $wrapped->sourceColumn
= $media->sourceColumn
;
1156 $wrapped->selectors
= [];
1157 $wrapped->comments
= [];
1158 $wrapped->parent
= $media;
1159 $wrapped->children
= $media->children
;
1161 $media->children
= [[Type
::T_BLOCK
, $wrapped]];
1164 $this->compileChildrenNoReturn($media->children
, $this->scope
);
1166 $this->scope
= $previousScope;
1175 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1177 * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1179 protected function mediaParent(OutputBlock
$scope)
1181 while (! empty($scope->parent
)) {
1182 if (! empty($scope->type
) && $scope->type
!== Type
::T_MEDIA
) {
1186 $scope = $scope->parent
;
1195 * @param \ScssPhp\ScssPhp\Block|array $block
1196 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1200 protected function compileDirective($directive, OutputBlock
$out)
1202 if (\
is_array($directive)) {
1203 $directiveName = $this->compileDirectiveName($directive[0]);
1204 $s = '@' . $directiveName;
1206 if (! empty($directive[1])) {
1207 $s .= ' ' . $this->compileValue($directive[1]);
1209 // sass-spec compliance on newline after directives, a bit tricky :/
1210 $appendNewLine = (! empty($directive[2]) ||
strpos($s, "\n")) ?
"\n" : "";
1211 if (\
is_array($directive[0]) && empty($directive[1])) {
1212 $appendNewLine = "\n";
1215 if (empty($directive[3])) {
1216 $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type
::T_COMMENT
, Type
::T_DIRECTIVE
]);
1218 $this->appendOutputLine($out, Type
::T_DIRECTIVE
, $s . ';');
1221 $directive->name
= $this->compileDirectiveName($directive->name
);
1222 $s = '@' . $directive->name
;
1224 if (! empty($directive->value
)) {
1225 $s .= ' ' . $this->compileValue($directive->value
);
1228 if ($directive->name
=== 'keyframes' ||
substr($directive->name
, -10) === '-keyframes') {
1229 $this->compileKeyframeBlock($directive, [$s]);
1231 $this->compileNestedBlock($directive, [$s]);
1237 * directive names can include some interpolation
1239 * @param string|array $directiveName
1240 * @return array|string
1241 * @throws CompilerException
1243 protected function compileDirectiveName($directiveName)
1245 if (is_string($directiveName)) {
1246 return $directiveName;
1249 return $this->compileValue($directiveName);
1255 * @param \ScssPhp\ScssPhp\Block $block
1259 protected function compileAtRoot(Block
$block)
1261 $env = $this->pushEnv($block);
1262 $envs = $this->compactEnv($env);
1263 list($with, $without) = $this->compileWith(isset($block->with
) ?
$block->with
: null);
1265 // wrap inline selector
1266 if ($block->selector
) {
1267 $wrapped = new Block();
1268 $wrapped->sourceName
= $block->sourceName
;
1269 $wrapped->sourceIndex
= $block->sourceIndex
;
1270 $wrapped->sourceLine
= $block->sourceLine
;
1271 $wrapped->sourceColumn
= $block->sourceColumn
;
1272 $wrapped->selectors
= $block->selector
;
1273 $wrapped->comments
= [];
1274 $wrapped->parent
= $block;
1275 $wrapped->children
= $block->children
;
1276 $wrapped->selfParent
= $block->selfParent
;
1278 $block->children
= [[Type
::T_BLOCK
, $wrapped]];
1279 $block->selector
= null;
1282 $selfParent = $block->selfParent
;
1285 ! $block->selfParent
->selectors
&&
1286 isset($block->parent
) && $block->parent
&&
1287 isset($block->parent
->selectors
) && $block->parent
->selectors
1289 $selfParent = $block->parent
;
1292 $this->env
= $this->filterWithWithout($envs, $with, $without);
1294 $saveScope = $this->scope
;
1295 $this->scope
= $this->filterScopeWithWithout($saveScope, $with, $without);
1297 // propagate selfParent to the children where they still can be useful
1298 $this->compileChildrenNoReturn($block->children
, $this->scope
, $selfParent);
1300 $this->scope
= $this->completeScope($this->scope
, $saveScope);
1301 $this->scope
= $saveScope;
1302 $this->env
= $this->extractEnv($envs);
1308 * Filter at-root scope depending of with/without option
1310 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1311 * @param array $with
1312 * @param array $without
1314 * @return OutputBlock
1316 protected function filterScopeWithWithout($scope, $with, $without)
1318 $filteredScopes = [];
1321 if ($scope->type
=== TYPE
::T_ROOT
) {
1325 // start from the root
1326 while ($scope->parent
&& $scope->parent
->type
!== TYPE
::T_ROOT
) {
1327 array_unshift($childStash, $scope);
1328 $scope = $scope->parent
;
1336 if ($this->isWith($scope, $with, $without)) {
1342 if ($s->type
!== Type
::T_MEDIA
&& $s->type
!== Type
::T_DIRECTIVE
) {
1346 $filteredScopes[] = $s;
1349 if (\
count($childStash)) {
1350 $scope = array_shift($childStash);
1351 } elseif ($scope->children
) {
1352 $scope = end($scope->children
);
1358 if (! \
count($filteredScopes)) {
1359 return $this->rootBlock
;
1362 $newScope = array_shift($filteredScopes);
1363 $newScope->parent
= $this->rootBlock
;
1365 $this->rootBlock
->children
[] = $newScope;
1369 while (\
count($filteredScopes)) {
1370 $s = array_shift($filteredScopes);
1372 $p->children
[] = $s;
1373 $newScope = &$p->children
[0];
1374 $p = &$p->children
[0];
1381 * found missing selector from a at-root compilation in the previous scope
1382 * (if at-root is just enclosing a property, the selector is in the parent tree)
1384 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1385 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1387 * @return OutputBlock
1389 protected function completeScope($scope, $previousScope)
1391 if (! $scope->type
&& (! $scope->selectors ||
! \
count($scope->selectors
)) && \
count($scope->lines
)) {
1392 $scope->selectors
= $this->findScopeSelectors($previousScope, $scope->depth
);
1395 if ($scope->children
) {
1396 foreach ($scope->children
as $k => $c) {
1397 $scope->children
[$k] = $this->completeScope($c, $previousScope);
1405 * Find a selector by the depth node in the scope
1407 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1408 * @param integer $depth
1412 protected function findScopeSelectors($scope, $depth)
1414 if ($scope->depth
=== $depth && $scope->selectors
) {
1415 return $scope->selectors
;
1418 if ($scope->children
) {
1419 foreach (array_reverse($scope->children
) as $c) {
1420 if ($s = $this->findScopeSelectors($c, $depth)) {
1430 * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1432 * @param array $withCondition
1436 protected function compileWith($withCondition)
1438 // just compile what we have in 2 lists
1440 $without = ['rule' => true];
1442 if ($withCondition) {
1443 if ($withCondition[0] === Type
::T_INTERPOLATE
) {
1444 $w = $this->compileValue($withCondition);
1447 $parser = $this->parserFactory(__METHOD__
);
1449 if ($parser->parseValue($buffer, $reParsedWith)) {
1450 $withCondition = $reParsedWith;
1454 if ($this->libMapHasKey([$withCondition, static::$with])) {
1455 $without = []; // cancel the default
1456 $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1458 foreach ($list[2] as $item) {
1459 $keyword = $this->compileStringContent($this->coerceString($item));
1461 $with[$keyword] = true;
1465 if ($this->libMapHasKey([$withCondition, static::$without])) {
1466 $without = []; // cancel the default
1467 $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1469 foreach ($list[2] as $item) {
1470 $keyword = $this->compileStringContent($this->coerceString($item));
1472 $without[$keyword] = true;
1477 return [$with, $without];
1483 * @param Environment[] $envs
1484 * @param array $with
1485 * @param array $without
1487 * @return Environment
1489 * @phpstan-param non-empty-array<Environment> $envs
1491 protected function filterWithWithout($envs, $with, $without)
1495 foreach ($envs as $e) {
1496 if ($e->block
&& ! $this->isWith($e->block
, $with, $without)) {
1499 $ec->selectors
= [];
1507 return $this->extractEnv($filtered);
1513 * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1514 * @param array $with
1515 * @param array $without
1519 protected function isWith($block, $with, $without)
1521 if (isset($block->type
)) {
1522 if ($block->type
=== Type
::T_MEDIA
) {
1523 return $this->testWithWithout('media', $with, $without);
1526 if ($block->type
=== Type
::T_DIRECTIVE
) {
1527 if (isset($block->name
)) {
1528 return $this->testWithWithout($this->compileDirectiveName($block->name
), $with, $without);
1529 } elseif (isset($block->selectors
) && preg_match(',@(\w+),ims', json_encode($block->selectors
), $m)) {
1530 return $this->testWithWithout($m[1], $with, $without);
1532 return $this->testWithWithout('???', $with, $without);
1535 } elseif (isset($block->selectors
)) {
1536 // a selector starting with number is a keyframe rule
1537 if (\
count($block->selectors
)) {
1538 $s = reset($block->selectors
);
1540 while (\
is_array($s)) {
1544 if (\
is_object($s) && $s instanceof Number
) {
1545 return $this->testWithWithout('keyframes', $with, $without);
1549 return $this->testWithWithout('rule', $with, $without);
1556 * Test a single type of block against with/without lists
1558 * @param string $what
1559 * @param array $with
1560 * @param array $without
1563 * true if the block should be kept, false to reject
1565 protected function testWithWithout($what, $with, $without)
1567 // if without, reject only if in the list (or 'all' is in the list)
1568 if (\
count($without)) {
1569 return (isset($without[$what]) ||
isset($without['all'])) ?
false : true;
1572 // otherwise reject all what is not in the with list
1573 return (isset($with[$what]) ||
isset($with['all'])) ?
true : false;
1578 * Compile keyframe block
1580 * @param \ScssPhp\ScssPhp\Block $block
1581 * @param array $selectors
1585 protected function compileKeyframeBlock(Block
$block, $selectors)
1587 $env = $this->pushEnv($block);
1589 $envs = $this->compactEnv($env);
1591 $this->env
= $this->extractEnv(array_filter($envs, function (Environment
$e) {
1592 return ! isset($e->block
->selectors
);
1595 $this->scope
= $this->makeOutputBlock($block->type
, $selectors);
1596 $this->scope
->depth
= 1;
1597 $this->scope
->parent
->children
[] = $this->scope
;
1599 $this->compileChildrenNoReturn($block->children
, $this->scope
);
1601 $this->scope
= $this->scope
->parent
;
1602 $this->env
= $this->extractEnv($envs);
1608 * Compile nested properties lines
1610 * @param \ScssPhp\ScssPhp\Block $block
1611 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1615 protected function compileNestedPropertiesBlock(Block
$block, OutputBlock
$out)
1617 $prefix = $this->compileValue($block->prefix
) . '-';
1619 $nested = $this->makeOutputBlock($block->type
);
1620 $nested->parent
= $out;
1622 if ($block->hasValue
) {
1623 $nested->depth
= $out->depth +
1;
1626 $out->children
[] = $nested;
1628 foreach ($block->children
as $child) {
1629 switch ($child[0]) {
1630 case Type
::T_ASSIGN
:
1631 array_unshift($child[1][2], $prefix);
1634 case Type
::T_NESTED_PROPERTY
:
1635 array_unshift($child[1]->prefix
[2], $prefix);
1639 $this->compileChild($child, $nested);
1644 * Compile nested block
1646 * @param \ScssPhp\ScssPhp\Block $block
1647 * @param array $selectors
1651 protected function compileNestedBlock(Block
$block, $selectors)
1653 $this->pushEnv($block);
1655 $this->scope
= $this->makeOutputBlock($block->type
, $selectors);
1656 $this->scope
->parent
->children
[] = $this->scope
;
1658 // wrap assign children in a block
1659 // except for @font-face
1660 if ($block->type
!== Type
::T_DIRECTIVE ||
$this->compileDirectiveName($block->name
) !== 'font-face') {
1662 $needWrapping = false;
1664 foreach ($block->children
as $child) {
1665 if ($child[0] === Type
::T_ASSIGN
) {
1666 $needWrapping = true;
1671 if ($needWrapping) {
1672 $wrapped = new Block();
1673 $wrapped->sourceName
= $block->sourceName
;
1674 $wrapped->sourceIndex
= $block->sourceIndex
;
1675 $wrapped->sourceLine
= $block->sourceLine
;
1676 $wrapped->sourceColumn
= $block->sourceColumn
;
1677 $wrapped->selectors
= [];
1678 $wrapped->comments
= [];
1679 $wrapped->parent
= $block;
1680 $wrapped->children
= $block->children
;
1681 $wrapped->selfParent
= $block->selfParent
;
1683 $block->children
= [[Type
::T_BLOCK
, $wrapped]];
1687 $this->compileChildrenNoReturn($block->children
, $this->scope
);
1689 $this->scope
= $this->scope
->parent
;
1695 * Recursively compiles a block.
1697 * A block is analogous to a CSS block in most cases. A single SCSS document
1698 * is encapsulated in a block when parsed, but it does not have parent tags
1699 * so all of its children appear on the root level when compiled.
1701 * Blocks are made up of selectors and children.
1703 * The children of a block are just all the blocks that are defined within.
1705 * Compiling the block involves pushing a fresh environment on the stack,
1706 * and iterating through the props, compiling each one.
1708 * @see Compiler::compileChild()
1710 * @param \ScssPhp\ScssPhp\Block $block
1714 protected function compileBlock(Block
$block)
1716 $env = $this->pushEnv($block);
1717 $env->selectors
= $this->evalSelectors($block->selectors
);
1719 $out = $this->makeOutputBlock(null);
1721 $this->scope
->children
[] = $out;
1723 if (\
count($block->children
)) {
1724 $out->selectors
= $this->multiplySelectors($env, $block->selfParent
);
1726 // propagate selfParent to the children where they still can be useful
1727 $selfParentSelectors = null;
1729 if (isset($block->selfParent
->selectors
)) {
1730 $selfParentSelectors = $block->selfParent
->selectors
;
1731 $block->selfParent
->selectors
= $out->selectors
;
1734 $this->compileChildrenNoReturn($block->children
, $out, $block->selfParent
);
1736 // and revert for the following children of the same block
1737 if ($selfParentSelectors) {
1738 $block->selfParent
->selectors
= $selfParentSelectors;
1747 * Compile the value of a comment that can have interpolation
1749 * @param array $value
1750 * @param boolean $pushEnv
1754 protected function compileCommentValue($value, $pushEnv = false)
1758 if (isset($value[2])) {
1763 $ignoreCallStackMessage = $this->ignoreCallStackMessage
;
1764 $this->ignoreCallStackMessage
= true;
1767 $c = $this->compileValue($value[2]);
1768 } catch (\Exception
$e) {
1769 // ignore error in comment compilation which are only interpolation
1772 $this->ignoreCallStackMessage
= $ignoreCallStackMessage;
1783 * Compile root level comment
1785 * @param array $block
1789 protected function compileComment($block)
1791 $out = $this->makeOutputBlock(Type
::T_COMMENT
);
1792 $out->lines
[] = $this->compileCommentValue($block, true);
1794 $this->scope
->children
[] = $out;
1798 * Evaluate selectors
1800 * @param array $selectors
1804 protected function evalSelectors($selectors)
1806 $this->shouldEvaluate
= false;
1808 $selectors = array_map([$this, 'evalSelector'], $selectors);
1810 // after evaluating interpolates, we might need a second pass
1811 if ($this->shouldEvaluate
) {
1812 $selectors = $this->replaceSelfSelector($selectors, '&');
1813 $buffer = $this->collapseSelectors($selectors);
1814 $parser = $this->parserFactory(__METHOD__
);
1817 $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1818 } catch (ParserException
$e) {
1819 throw $this->error($e->getMessage());
1823 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1833 * @param array $selector
1837 protected function evalSelector($selector)
1839 return array_map([$this, 'evalSelectorPart'], $selector);
1843 * Evaluate selector part; replaces all the interpolates, stripping quotes
1845 * @param array $part
1849 protected function evalSelectorPart($part)
1851 foreach ($part as &$p) {
1852 if (\
is_array($p) && ($p[0] === Type
::T_INTERPOLATE ||
$p[0] === Type
::T_STRING
)) {
1853 $p = $this->compileValue($p);
1855 // force re-evaluation if self char or non standard char
1856 if (preg_match(',[^\w-],', $p)) {
1857 $this->shouldEvaluate
= true;
1860 \
is_string($p) && \
strlen($p) >= 2 &&
1861 ($first = $p[0]) && ($first === '"' ||
$first === "'") &&
1862 substr($p, -1) === $first
1864 $p = substr($p, 1, -1);
1868 return $this->flattenSelectorSingle($part);
1872 * Collapse selectors
1874 * @param array $selectors
1875 * @param boolean $selectorFormat
1876 * if false return a collapsed string
1877 * if true return an array description of a structured selector
1881 protected function collapseSelectors($selectors, $selectorFormat = false)
1885 foreach ($selectors as $selector) {
1889 foreach ($selector as $node) {
1892 array_walk_recursive(
1894 function ($value, $key) use (&$compound) {
1895 $compound .= $value;
1899 if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1900 if (\
count($output)) {
1901 $output[\
count($output) - 1] .= ' ' . $compound;
1903 $output[] = $compound;
1907 } elseif ($glueNext) {
1908 $output[\
count($output) - 1] .= ' ' . $compound;
1911 $output[] = $compound;
1915 if ($selectorFormat) {
1916 foreach ($output as &$o) {
1917 $o = [Type
::T_STRING
, '', [$o]];
1920 $output = [Type
::T_LIST
, ' ', $output];
1922 $output = implode(' ', $output);
1928 if ($selectorFormat) {
1929 $parts = [Type
::T_LIST
, ',', $parts];
1931 $parts = implode(', ', $parts);
1938 * Parse down the selector and revert [self] to "&" before a reparsing
1940 * @param array $selectors
1944 protected function replaceSelfSelector($selectors, $replace = null)
1946 foreach ($selectors as &$part) {
1947 if (\
is_array($part)) {
1948 if ($part === [Type
::T_SELF
]) {
1949 if (\
is_null($replace)) {
1950 $replace = $this->reduce([Type
::T_SELF
]);
1951 $replace = $this->compileValue($replace);
1955 $part = $this->replaceSelfSelector($part, $replace);
1964 * Flatten selector single; joins together .classes and #ids
1966 * @param array $single
1970 protected function flattenSelectorSingle($single)
1974 foreach ($single as $part) {
1977 ! \
is_string($part) ||
1978 preg_match('/[\[.:#%]/', $part)
1984 if (\
is_array(end($joined))) {
1987 $joined[\
count($joined) - 1] .= $part;
1995 * Compile selector to string; self(&) should have been replaced by now
1997 * @param string|array $selector
2001 protected function compileSelector($selector)
2003 if (! \
is_array($selector)) {
2004 return $selector; // media and the like
2010 [$this, 'compileSelectorPart'],
2017 * Compile selector part
2019 * @param array $piece
2023 protected function compileSelectorPart($piece)
2025 foreach ($piece as &$p) {
2026 if (! \
is_array($p)) {
2036 $p = $this->compileValue($p);
2041 return implode($piece);
2045 * Has selector placeholder?
2047 * @param array $selector
2051 protected function hasSelectorPlaceholder($selector)
2053 if (! \
is_array($selector)) {
2057 foreach ($selector as $parts) {
2058 foreach ($parts as $part) {
2059 if (\
strlen($part) && '%' === $part[0]) {
2069 * @param string $name
2073 protected function pushCallStack($name = '')
2075 $this->callStack
[] = [
2077 Parser
::SOURCE_INDEX
=> $this->sourceIndex
,
2078 Parser
::SOURCE_LINE
=> $this->sourceLine
,
2079 Parser
::SOURCE_COLUMN
=> $this->sourceColumn
2082 // infinite calling loop
2083 if (\
count($this->callStack
) > 25000) {
2084 // not displayed but you can var_dump it to deep debug
2085 $msg = $this->callStackMessage(true, 100);
2086 $msg = 'Infinite calling loop';
2088 throw $this->error($msg);
2095 protected function popCallStack()
2097 array_pop($this->callStack
);
2101 * Compile children and return result
2103 * @param array $stms
2104 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2105 * @param string $traceName
2107 * @return array|null
2109 protected function compileChildren($stms, OutputBlock
$out, $traceName = '')
2111 $this->pushCallStack($traceName);
2113 foreach ($stms as $stm) {
2114 $ret = $this->compileChild($stm, $out);
2117 $this->popCallStack();
2123 $this->popCallStack();
2129 * Compile children and throw exception if unexpected `@return`
2131 * @param array $stms
2132 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2133 * @param \ScssPhp\ScssPhp\Block $selfParent
2134 * @param string $traceName
2138 * @throws \Exception
2140 protected function compileChildrenNoReturn($stms, OutputBlock
$out, $selfParent = null, $traceName = '')
2142 $this->pushCallStack($traceName);
2144 foreach ($stms as $stm) {
2145 if ($selfParent && isset($stm[1]) && \
is_object($stm[1]) && $stm[1] instanceof Block
) {
2146 $stm[1]->selfParent
= $selfParent;
2147 $ret = $this->compileChild($stm, $out);
2148 $stm[1]->selfParent
= null;
2149 } elseif ($selfParent && \
in_array($stm[0], [TYPE
::T_INCLUDE
, TYPE
::T_EXTEND
])) {
2150 $stm['selfParent'] = $selfParent;
2151 $ret = $this->compileChild($stm, $out);
2152 unset($stm['selfParent']);
2154 $ret = $this->compileChild($stm, $out);
2158 throw $this->error('@return may only be used within a function');
2162 $this->popCallStack();
2167 * evaluate media query : compile internal value keeping the structure unchanged
2169 * @param array $queryList
2173 protected function evaluateMediaQuery($queryList)
2175 static $parser = null;
2179 foreach ($queryList as $kql => $query) {
2180 $shouldReparse = false;
2182 foreach ($query as $kq => $q) {
2183 for ($i = 1; $i < \
count($q); $i++
) {
2184 $value = $this->compileValue($q[$i]);
2186 // the parser had no mean to know if media type or expression if it was an interpolation
2187 // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
2189 $q[0] == Type
::T_MEDIA_TYPE
&&
2190 (strpos($value, '(') !== false ||
2191 strpos($value, ')') !== false ||
2192 strpos($value, ':') !== false ||
2193 strpos($value, ',') !== false)
2195 $shouldReparse = true;
2198 $queryList[$kql][$kq][$i] = [Type
::T_KEYWORD
, $value];
2202 if ($shouldReparse) {
2203 if (\
is_null($parser)) {
2204 $parser = $this->parserFactory(__METHOD__
);
2207 $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2208 $queryString = reset($queryString);
2210 if (strpos($queryString, '@media ') === 0) {
2211 $queryString = substr($queryString, 7);
2214 if ($parser->parseMediaQueryList($queryString, $queries)) {
2215 $queries = $this->evaluateMediaQuery($queries[2]);
2217 while (\
count($queries)) {
2218 $outQueryList[] = array_shift($queries);
2226 $outQueryList[] = $queryList[$kql];
2229 return $outQueryList;
2233 * Compile media query
2235 * @param array $queryList
2239 protected function compileMediaQuery($queryList)
2242 $default = trim($start);
2246 foreach ($queryList as $query) {
2250 $mediaTypeOnly = true;
2252 foreach ($query as $q) {
2253 if ($q[0] !== Type
::T_MEDIA_TYPE
) {
2254 $mediaTypeOnly = false;
2259 foreach ($query as $q) {
2261 case Type
::T_MEDIA_TYPE
:
2262 $newType = array_map([$this, 'compileValue'], \array_slice
($q, 1));
2264 // combining not and anything else than media type is too risky and should be avoided
2265 if (! $mediaTypeOnly) {
2266 if (\
in_array(Type
::T_NOT
, $newType) ||
($type && \
in_array(Type
::T_NOT
, $type) )) {
2268 array_unshift($parts, implode(' ', array_filter($type)));
2271 if (! empty($parts)) {
2272 if (\
strlen($current)) {
2273 $current .= $this->formatter
->tagSeparator
;
2276 $current .= implode(' and ', $parts);
2280 $out[] = $start . $current;
2289 if ($newType === ['all'] && $default) {
2290 $default = $start . 'all';
2293 // all can be safely ignored and mixed with whatever else
2294 if ($newType !== ['all']) {
2296 $type = $this->mergeMediaTypes($type, $newType);
2299 // merge failed : ignore this query that is not valid, skip to the next one
2301 $default = ''; // if everything fail, no @media at all
2310 case Type
::T_MEDIA_EXPRESSION
:
2313 . $this->compileValue($q[1])
2314 . $this->formatter
->assignSeparator
2315 . $this->compileValue($q[2])
2319 . $this->compileValue($q[1])
2324 case Type
::T_MEDIA_VALUE
:
2325 $parts[] = $this->compileValue($q[1]);
2331 array_unshift($parts, implode(' ', array_filter($type)));
2334 if (! empty($parts)) {
2335 if (\
strlen($current)) {
2336 $current .= $this->formatter
->tagSeparator
;
2339 $current .= implode(' and ', $parts);
2344 $out[] = $start . $current;
2347 // no @media type except all, and no conflict?
2348 if (! $out && $default) {
2356 * Merge direct relationships between selectors
2358 * @param array $selectors1
2359 * @param array $selectors2
2363 protected function mergeDirectRelationships($selectors1, $selectors2)
2365 if (empty($selectors1) ||
empty($selectors2)) {
2366 return array_merge($selectors1, $selectors2);
2369 $part1 = end($selectors1);
2370 $part2 = end($selectors2);
2372 if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2373 return array_merge($selectors1, $selectors2);
2379 $part1 = array_pop($selectors1);
2380 $part2 = array_pop($selectors2);
2382 if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2383 if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2384 array_unshift($merged, [$part1[0] . $part2[0]]);
2385 $merged = array_merge($selectors1, $selectors2, $merged);
2387 $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2393 array_unshift($merged, $part1);
2394 } while (! empty($selectors1) && ! empty($selectors2));
2402 * @param array $type1
2403 * @param array $type2
2405 * @return array|null
2407 protected function mergeMediaTypes($type1, $type2)
2409 if (empty($type1)) {
2413 if (empty($type2)) {
2417 if (\
count($type1) > 1) {
2418 $m1 = strtolower($type1[0]);
2419 $t1 = strtolower($type1[1]);
2422 $t1 = strtolower($type1[0]);
2425 if (\
count($type2) > 1) {
2426 $m2 = strtolower($type2[0]);
2427 $t2 = strtolower($type2[1]);
2430 $t2 = strtolower($type2[0]);
2433 if (($m1 === Type
::T_NOT
) ^
($m2 === Type
::T_NOT
)) {
2439 $m1 === Type
::T_NOT ?
$m2 : $m1,
2440 $m1 === Type
::T_NOT ?
$t2 : $t1,
2444 if ($m1 === Type
::T_NOT
&& $m2 === Type
::T_NOT
) {
2445 // CSS has no way of representing "neither screen nor print"
2450 return [Type
::T_NOT
, $t1];
2457 // t1 == t2, neither m1 nor m2 are "not"
2458 return [empty($m1) ?
$m2 : $m1, $t1];
2462 * Compile import; returns true if the value was something that could be imported
2464 * @param array $rawPath
2465 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2466 * @param boolean $once
2470 protected function compileImport($rawPath, OutputBlock
$out, $once = false)
2472 if ($rawPath[0] === Type
::T_STRING
) {
2473 $path = $this->compileStringContent($rawPath);
2475 if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
2476 if (! $once ||
! \
in_array($path, $this->importedFiles
)) {
2477 $this->importFile($path, $out);
2478 $this->importedFiles
[] = $path;
2484 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2489 if ($rawPath[0] === Type
::T_LIST
) {
2490 // handle a list of strings
2491 if (\
count($rawPath[2]) === 0) {
2495 foreach ($rawPath[2] as $path) {
2496 if ($path[0] !== Type
::T_STRING
) {
2497 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2503 foreach ($rawPath[2] as $path) {
2504 $this->compileImport($path, $out, $once);
2510 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2516 * @param array $rawPath
2518 * @throws CompilerException
2520 protected function compileImportPath($rawPath)
2522 $path = $this->compileValue($rawPath);
2524 // case url() without quotes : suppress \r \n remaining in the path
2525 // if this is a real string there can not be CR or LF char
2526 if (strpos($path, 'url(') === 0) {
2527 $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2529 // if this is a file name in a string, spaces should be escaped
2530 $path = $this->reduce($rawPath);
2531 $path = $this->escapeImportPathString($path);
2532 $path = $this->compileValue($path);
2539 * @param array $path
2541 * @throws CompilerException
2543 protected function escapeImportPathString($path)
2547 foreach ($path[2] as $k => $v) {
2548 $path[2][$k] = $this->escapeImportPathString($v);
2551 case Type
::T_STRING
:
2553 $path = $this->compileValue($path);
2554 $path = str_replace(' ', '\\ ', $path);
2555 $path = [Type
::T_KEYWORD
, $path];
2564 * Append a root directive like @import or @charset as near as the possible from the source code
2565 * (keeping before comments, @import and @charset coming before in the source code)
2567 * @param string $line
2568 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2569 * @param array $allowed
2573 protected function appendRootDirective($line, $out, $allowed = [Type
::T_COMMENT
])
2577 while ($root->parent
) {
2578 $root = $root->parent
;
2583 while ($i < \
count($root->children
)) {
2584 if (! isset($root->children
[$i]->type
) ||
! \
in_array($root->children
[$i]->type
, $allowed)) {
2591 // remove incompatible children from the bottom of the list
2594 while ($i < \
count($root->children
)) {
2595 $saveChildren[] = array_pop($root->children
);
2598 // insert the directive as a comment
2599 $child = $this->makeOutputBlock(Type
::T_COMMENT
);
2600 $child->lines
[] = $line;
2601 $child->sourceName
= $this->sourceNames
[$this->sourceIndex
];
2602 $child->sourceLine
= $this->sourceLine
;
2603 $child->sourceColumn
= $this->sourceColumn
;
2605 $root->children
[] = $child;
2608 while (\
count($saveChildren)) {
2609 $root->children
[] = array_pop($saveChildren);
2614 * Append lines to the current output block:
2615 * directly to the block or through a child if necessary
2617 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2618 * @param string $type
2619 * @param string|mixed $line
2623 protected function appendOutputLine(OutputBlock
$out, $type, $line)
2627 // check if it's a flat output or not
2628 if (\
count($out->children
)) {
2629 $lastChild = &$out->children
[\
count($out->children
) - 1];
2632 $lastChild->depth
=== $out->depth
&&
2633 \
is_null($lastChild->selectors
) &&
2634 ! \
count($lastChild->children
)
2636 $outWrite = $lastChild;
2638 $nextLines = $this->makeOutputBlock($type);
2639 $nextLines->parent
= $out;
2640 $nextLines->depth
= $out->depth
;
2642 $out->children
[] = $nextLines;
2643 $outWrite = &$nextLines;
2647 $outWrite->lines
[] = $line;
2651 * Compile child; returns a value to halt execution
2653 * @param array $child
2654 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2656 * @return array|Number|null
2658 protected function compileChild($child, OutputBlock
$out)
2660 if (isset($child[Parser
::SOURCE_LINE
])) {
2661 $this->sourceIndex
= isset($child[Parser
::SOURCE_INDEX
]) ?
$child[Parser
::SOURCE_INDEX
] : null;
2662 $this->sourceLine
= isset($child[Parser
::SOURCE_LINE
]) ?
$child[Parser
::SOURCE_LINE
] : -1;
2663 $this->sourceColumn
= isset($child[Parser
::SOURCE_COLUMN
]) ?
$child[Parser
::SOURCE_COLUMN
] : -1;
2664 } elseif (\
is_array($child) && isset($child[1]->sourceLine
)) {
2665 $this->sourceIndex
= $child[1]->sourceIndex
;
2666 $this->sourceLine
= $child[1]->sourceLine
;
2667 $this->sourceColumn
= $child[1]->sourceColumn
;
2668 } elseif (! empty($out->sourceLine
) && ! empty($out->sourceName
)) {
2669 $this->sourceLine
= $out->sourceLine
;
2670 $this->sourceIndex
= array_search($out->sourceName
, $this->sourceNames
);
2671 $this->sourceColumn
= $out->sourceColumn
;
2673 if ($this->sourceIndex
=== false) {
2674 $this->sourceIndex
= null;
2678 switch ($child[0]) {
2679 case Type
::T_SCSSPHP_IMPORT_ONCE
:
2680 $rawPath = $this->reduce($child[1]);
2682 $this->compileImport($rawPath, $out, true);
2685 case Type
::T_IMPORT
:
2686 $rawPath = $this->reduce($child[1]);
2688 $this->compileImport($rawPath, $out);
2691 case Type
::T_DIRECTIVE
:
2692 $this->compileDirective($child[1], $out);
2695 case Type
::T_AT_ROOT
:
2696 $this->compileAtRoot($child[1]);
2700 $this->compileMedia($child[1]);
2704 $this->compileBlock($child[1]);
2707 case Type
::T_CHARSET
:
2708 if (! $this->charsetSeen
) {
2709 $this->charsetSeen
= true;
2710 $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2714 case Type
::T_CUSTOM_PROPERTY
:
2715 list(, $name, $value) = $child;
2716 $compiledName = $this->compileValue($name);
2718 // if the value reduces to null from something else then
2719 // the property should be discarded
2720 if ($value[0] !== Type
::T_NULL
) {
2721 $value = $this->reduce($value);
2723 if ($value[0] === Type
::T_NULL ||
$value === static::$nullString) {
2728 $compiledValue = $this->compileValue($value);
2730 $line = $this->formatter
->customProperty(
2735 $this->appendOutputLine($out, Type
::T_ASSIGN
, $line);
2738 case Type
::T_ASSIGN
:
2739 list(, $name, $value) = $child;
2741 if ($name[0] === Type
::T_VARIABLE
) {
2742 $flags = isset($child[3]) ?
$child[3] : [];
2743 $isDefault = \
in_array('!default', $flags);
2744 $isGlobal = \
in_array('!global', $flags);
2747 $this->set($name[1], $this->reduce($value), false, $this->rootEnv
, $value);
2751 $shouldSet = $isDefault &&
2752 (\
is_null($result = $this->get($name[1], false)) ||
2753 $result === static::$null);
2755 if (! $isDefault ||
$shouldSet) {
2756 $this->set($name[1], $this->reduce($value), true, null, $value);
2761 $compiledName = $this->compileValue($name);
2763 // handle shorthand syntaxes : size / line-height...
2764 if (\
in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2765 if ($value[0] === Type
::T_VARIABLE
) {
2766 // if the font value comes from variable, the content is already reduced
2767 // (i.e., formulas were already calculated), so we need the original unreduced value
2768 $value = $this->get($value[1], true, null, true);
2771 $shorthandValue=&$value;
2773 $shorthandDividerNeedsUnit = false;
2774 $maxListElements = null;
2775 $maxShorthandDividers = 1;
2777 switch ($compiledName) {
2778 case 'border-radius':
2779 $maxListElements = 4;
2780 $shorthandDividerNeedsUnit = true;
2784 if ($compiledName === 'font' && $value[0] === Type
::T_LIST
&& $value[1] === ',') {
2785 // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2786 // we need to handle the first list element
2787 $shorthandValue=&$value[2][0];
2790 if ($shorthandValue[0] === Type
::T_EXPRESSION
&& $shorthandValue[1] === '/') {
2793 if ($shorthandDividerNeedsUnit) {
2794 $divider = $shorthandValue[3];
2796 if (\
is_array($divider)) {
2797 $divider = $this->reduce($divider, true);
2800 if ($divider instanceof Number
&& \
intval($divider->getDimension()) && $divider->unitless()) {
2806 $shorthandValue = $this->expToString($shorthandValue);
2808 } elseif ($shorthandValue[0] === Type
::T_LIST
) {
2809 foreach ($shorthandValue[2] as &$item) {
2810 if ($item[0] === Type
::T_EXPRESSION
&& $item[1] === '/') {
2811 if ($maxShorthandDividers > 0) {
2814 // if the list of values is too long, this has to be a shorthand,
2815 // otherwise it could be a real division
2816 if (\
is_null($maxListElements) || \
count($shorthandValue[2]) <= $maxListElements) {
2817 if ($shorthandDividerNeedsUnit) {
2818 $divider = $item[3];
2820 if (\
is_array($divider)) {
2821 $divider = $this->reduce($divider, true);
2824 if ($divider instanceof Number
&& \
intval($divider->getDimension()) && $divider->unitless()) {
2831 $item = $this->expToString($item);
2832 $maxShorthandDividers--;
2840 // if the value reduces to null from something else then
2841 // the property should be discarded
2842 if ($value[0] !== Type
::T_NULL
) {
2843 $value = $this->reduce($value);
2845 if ($value[0] === Type
::T_NULL ||
$value === static::$nullString) {
2850 $compiledValue = $this->compileValue($value);
2852 // ignore empty value
2853 if (\
strlen($compiledValue)) {
2854 $line = $this->formatter
->property(
2858 $this->appendOutputLine($out, Type
::T_ASSIGN
, $line);
2862 case Type
::T_COMMENT
:
2863 if ($out->type
=== Type
::T_ROOT
) {
2864 $this->compileComment($child);
2868 $line = $this->compileCommentValue($child, true);
2869 $this->appendOutputLine($out, Type
::T_COMMENT
, $line);
2873 case Type
::T_FUNCTION
:
2874 list(, $block) = $child;
2875 // the block need to be able to go up to it's parent env to resolve vars
2876 $block->parentEnv
= $this->getStoreEnv();
2877 $this->set(static::$namespaces[$block->type
] . $block->name
, $block, true);
2880 case Type
::T_EXTEND
:
2881 foreach ($child[1] as $sel) {
2882 $sel = $this->replaceSelfSelector($sel);
2883 $results = $this->evalSelectors([$sel]);
2885 foreach ($results as $result) {
2886 // only use the first one
2887 $result = current($result);
2888 $selectors = $out->selectors
;
2890 if (! $selectors && isset($child['selfParent'])) {
2891 $selectors = $this->multiplySelectors($this->env
, $child['selfParent']);
2894 $this->pushExtends($result, $selectors, $child);
2900 list(, $if) = $child;
2902 if ($this->isTruthy($this->reduce($if->cond
, true))) {
2903 return $this->compileChildren($if->children
, $out);
2906 foreach ($if->cases
as $case) {
2908 $case->type
=== Type
::T_ELSE ||
2909 $case->type
=== Type
::T_ELSEIF
&& $this->isTruthy($this->reduce($case->cond
))
2911 return $this->compileChildren($case->children
, $out);
2917 list(, $each) = $child;
2919 $list = $this->coerceList($this->reduce($each->list), ',', true);
2923 foreach ($list[2] as $item) {
2924 if (\
count($each->vars
) === 1) {
2925 $this->set($each->vars
[0], $item, true);
2927 list(,, $values) = $this->coerceList($item);
2929 foreach ($each->vars
as $i => $var) {
2930 $this->set($var, isset($values[$i]) ?
$values[$i] : static::$null, true);
2934 $ret = $this->compileChildren($each->children
, $out);
2937 $store = $this->env
->store
;
2939 $this->backPropagateEnv($store, $each->vars
);
2944 $store = $this->env
->store
;
2946 $this->backPropagateEnv($store, $each->vars
);
2951 list(, $while) = $child;
2953 while ($this->isTruthy($this->reduce($while->cond
, true))) {
2954 $ret = $this->compileChildren($while->children
, $out);
2963 list(, $for) = $child;
2965 $start = $this->reduce($for->start
, true);
2966 $end = $this->reduce($for->end
, true);
2968 if (! $start instanceof Number
) {
2969 throw $this->error('%s is not a number', $start[0]);
2972 if (! $end instanceof Number
) {
2973 throw $this->error('%s is not a number', $end[0]);
2976 $start->assertSameUnitOrUnitless($end);
2978 $numeratorUnits = $start->getNumeratorUnits();
2979 $denominatorUnits = $start->getDenominatorUnits();
2981 $start = $start->getDimension();
2982 $end = $end->getDimension();
2984 $d = $start < $end ?
1 : -1;
2990 (! $for->until
&& $start - $d == $end) ||
2991 ($for->until
&& $start == $end)
2996 $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
2999 $ret = $this->compileChildren($for->children
, $out);
3002 $store = $this->env
->store
;
3004 $this->backPropagateEnv($store, [$for->var]);
3010 $store = $this->env
->store
;
3012 $this->backPropagateEnv($store, [$for->var]);
3016 case Type
::T_RETURN
:
3017 return $this->reduce($child[1], true);
3019 case Type
::T_NESTED_PROPERTY
:
3020 $this->compileNestedPropertiesBlock($child[1], $out);
3023 case Type
::T_INCLUDE
:
3024 // including a mixin
3025 list(, $name, $argValues, $content, $argUsing) = $child;
3027 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
3030 throw $this->error("Undefined mixin $name");
3033 $callingScope = $this->getStoreEnv();
3035 // push scope, apply args
3037 $this->env
->depth
--;
3039 // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
3040 // and assign this fake parent to childs
3043 if (isset($child['selfParent']) && isset($child['selfParent']->selectors
)) {
3044 $selfParent = $child['selfParent'];
3046 $parentSelectors = $this->multiplySelectors($this->env
);
3048 if ($parentSelectors) {
3049 $parent = new Block();
3050 $parent->selectors
= $parentSelectors;
3052 foreach ($mixin->children
as $k => $child) {
3053 if (isset($child[1]) && \
is_object($child[1]) && $child[1] instanceof Block
) {
3054 $mixin->children
[$k][1]->parent
= $parent;
3060 // clone the stored content to not have its scope spoiled by a further call to the same mixin
3061 // i.e., recursive @include of the same mixin
3062 if (isset($content)) {
3063 $copyContent = clone $content;
3064 $copyContent->scope
= clone $callingScope;
3066 $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env
);
3068 $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env
);
3071 // save the "using" argument list for applying it to when "@content" is invoked
3072 if (isset($argUsing)) {
3073 $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env
);
3075 $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env
);
3078 if (isset($mixin->args
)) {
3079 $this->applyArguments($mixin->args
, $argValues);
3082 $this->env
->marker
= 'mixin';
3084 if (! empty($mixin->parentEnv
)) {
3085 $this->env
->declarationScopeParent
= $mixin->parentEnv
;
3087 throw $this->error("@mixin $name() without parentEnv");
3090 $this->compileChildrenNoReturn($mixin->children
, $out, $selfParent, $this->env
->marker
. ' ' . $name);
3095 case Type
::T_MIXIN_CONTENT
:
3096 $env = isset($this->storeEnv
) ?
$this->storeEnv
: $this->env
;
3097 $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
3098 $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env);
3099 $argContent = $child[1];
3105 $storeEnv = $this->storeEnv
;
3108 if (isset($argUsing) && isset($argContent)) {
3109 // Get the arguments provided for the content with the names provided in the "using" argument list
3110 $this->storeEnv
= null;
3111 $varsUsing = $this->applyArguments($argUsing, $argContent, false);
3114 // restore the scope from the @content
3115 $this->storeEnv
= $content->scope
;
3117 // append the vars from using if any
3118 foreach ($varsUsing as $name => $val) {
3119 $this->set($name, $val, true, $this->storeEnv
);
3122 $this->compileChildrenNoReturn($content->children
, $out);
3124 $this->storeEnv
= $storeEnv;
3128 list(, $value) = $child;
3130 $fname = $this->getPrettyPath($this->sourceNames
[$this->sourceIndex
]);
3131 $line = $this->sourceLine
;
3132 $value = $this->compileDebugValue($value);
3134 fwrite($this->stderr
, "$fname:$line DEBUG: $value\n");
3138 list(, $value) = $child;
3140 $fname = $this->getPrettyPath($this->sourceNames
[$this->sourceIndex
]);
3141 $line = $this->sourceLine
;
3142 $value = $this->compileDebugValue($value);
3144 fwrite($this->stderr
, "WARNING: $value\n on line $line of $fname\n\n");
3148 list(, $value) = $child;
3150 $fname = $this->getPrettyPath($this->sourceNames
[$this->sourceIndex
]);
3151 $line = $this->sourceLine
;
3152 $value = $this->compileValue($this->reduce($value, true));
3154 throw $this->error("File $fname on line $line ERROR: $value\n");
3157 throw $this->error("unknown child type: $child[0]");
3162 * Reduce expression to string
3165 * @param bool $keepParens
3169 protected function expToString($exp, $keepParens = false)
3171 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3175 if ($keepParens && $inParens) {
3179 $content[] = $this->reduce($left);
3191 $content[] = $this->reduce($right);
3193 if ($keepParens && $inParens) {
3197 return [Type
::T_STRING
, '', $content];
3203 * @param array|Number $value
3207 protected function isTruthy($value)
3209 return $value !== static::$false && $value !== static::$null;
3213 * Is the value a direct relationship combinator?
3215 * @param string $value
3219 protected function isImmediateRelationshipCombinator($value)
3221 return $value === '>' ||
$value === '+' ||
$value === '~';
3225 * Should $value cause its operand to eval
3227 * @param array $value
3231 protected function shouldEval($value)
3233 switch ($value[0]) {
3234 case Type
::T_EXPRESSION
:
3235 if ($value[1] === '/') {
3236 return $this->shouldEval($value[2]) ||
$this->shouldEval($value[3]);
3240 case Type
::T_VARIABLE
:
3241 case Type
::T_FUNCTION_CALL
:
3251 * @param array|Number $value
3252 * @param boolean $inExp
3254 * @return null|string|array|Number
3256 protected function reduce($value, $inExp = false)
3258 if (\
is_null($value)) {
3262 switch ($value[0]) {
3263 case Type
::T_EXPRESSION
:
3264 list(, $op, $left, $right, $inParens) = $value;
3266 $opName = isset(static::$operatorNames[$op]) ?
static::$operatorNames[$op] : $op;
3267 $inExp = $inExp ||
$this->shouldEval($left) ||
$this->shouldEval($right);
3269 $left = $this->reduce($left, true);
3271 if ($op !== 'and' && $op !== 'or') {
3272 $right = $this->reduce($right, true);
3275 // special case: looks like css shorthand
3277 $opName == 'div' && ! $inParens && ! $inExp &&
3278 (($right[0] !== Type
::T_NUMBER
&& isset($right[2]) && $right[2] != '') ||
3279 ($right[0] === Type
::T_NUMBER
&& ! $right->unitless()))
3281 return $this->expToString($value);
3284 $left = $this->coerceForExpression($left);
3285 $right = $this->coerceForExpression($right);
3289 $ucOpName = ucfirst($opName);
3290 $ucLType = ucfirst($ltype);
3291 $ucRType = ucfirst($rtype);
3294 // 1. op[op name][left type][right type]
3295 // 2. op[left type][right type] (passing the op as first arg
3297 $fn = "op${ucOpName}${ucLType}${ucRType}";
3300 \
is_callable([$this, $fn]) ||
3301 (($fn = "op${ucLType}${ucRType}") &&
3302 \
is_callable([$this, $fn]) &&
3304 (($fn = "op${ucOpName}") &&
3305 \
is_callable([$this, $fn]) &&
3308 $shouldEval = $inParens ||
$inExp;
3310 if (isset($passOp)) {
3311 $out = $this->$fn($op, $left, $right, $shouldEval);
3313 $out = $this->$fn($left, $right, $shouldEval);
3321 return $this->expToString($value);
3324 list(, $op, $exp, $inParens) = $value;
3326 $inExp = $inExp ||
$this->shouldEval($exp);
3327 $exp = $this->reduce($exp);
3329 if ($exp instanceof Number
) {
3335 return $exp->unaryMinus();
3339 if ($op === 'not') {
3340 if ($inExp ||
$inParens) {
3341 if ($exp === static::$false ||
$exp === static::$null) {
3342 return static::$true;
3345 return static::$false;
3351 return [Type
::T_STRING
, '', [$op, $exp]];
3353 case Type
::T_VARIABLE
:
3354 return $this->reduce($this->get($value[1]));
3357 foreach ($value[2] as &$item) {
3358 $item = $this->reduce($item);
3364 foreach ($value[1] as &$item) {
3365 $item = $this->reduce($item);
3368 foreach ($value[2] as &$item) {
3369 $item = $this->reduce($item);
3374 case Type
::T_STRING
:
3375 foreach ($value[2] as &$item) {
3376 if (\
is_array($item) ||
$item instanceof \ArrayAccess
) {
3377 $item = $this->reduce($item);
3383 case Type
::T_INTERPOLATE
:
3384 $value[1] = $this->reduce($value[1]);
3392 case Type
::T_FUNCTION_CALL
:
3393 return $this->fncall($value[1], $value[2]);
3396 $selfParent = ! empty($this->env
->block
->selfParent
) ?
$this->env
->block
->selfParent
: null;
3397 $selfSelector = $this->multiplySelectors($this->env
, $selfParent);
3398 $selfSelector = $this->collapseSelectors($selfSelector, true);
3400 return $selfSelector;
3410 * @param string $name
3411 * @param array $argValues
3413 * @return array|Number
3415 protected function fncall($functionReference, $argValues)
3417 // a string means this is a static hard reference coming from the parsing
3418 if (is_string($functionReference)) {
3419 $name = $functionReference;
3421 $functionReference = $this->getFunctionReference($name);
3422 if ($functionReference === static::$null ||
$functionReference[0] !== Type
::T_FUNCTION_REFERENCE
) {
3423 $functionReference = [Type
::T_FUNCTION
, $name, [Type
::T_LIST
, ',', []]];
3427 // a function type means we just want a plain css function call
3428 if ($functionReference[0] === Type
::T_FUNCTION
) {
3429 // for CSS functions, simply flatten the arguments into a list
3432 foreach ((array) $argValues as $arg) {
3433 if (empty($arg[0]) ||
count($argValues) === 1) {
3434 $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
3438 return [Type
::T_FUNCTION
, $functionReference[1], [Type
::T_LIST
, ',', $listArgs]];
3441 if ($functionReference === static::$null ||
$functionReference[0] !== Type
::T_FUNCTION_REFERENCE
) {
3442 return static::$defaultValue;
3446 switch ($functionReference[1]) {
3449 return $this->callScssFunction($functionReference[3], $argValues);
3451 // native PHP functions
3454 list(,,$name, $fn, $prototype) = $functionReference;
3456 // special cases of css valid functions min/max
3457 $name = strtolower($name);
3458 if (\
in_array($name, ['min', 'max'])) {
3459 $cssFunction = $this->cssValidArg(
3460 [Type
::T_FUNCTION_CALL
, $name, $argValues],
3461 ['min', 'max', 'calc', 'env', 'var']
3463 if ($cssFunction !== false) {
3464 return $cssFunction;
3467 $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
3469 if (! isset($returnValue)) {
3470 return $this->fncall([Type
::T_FUNCTION
, $name, [Type
::T_LIST
, ',', []]], $argValues);
3473 return $returnValue;
3476 return static::$defaultValue;
3480 protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3483 case Type
::T_INTERPOLATE
:
3484 return [Type
::T_KEYWORD
, $this->CompileValue($arg)];
3486 case Type
::T_FUNCTION
:
3487 if (! \
in_array($arg[1], $allowed_function)) {
3490 if ($arg[2][0] === Type
::T_LIST
) {
3491 foreach ($arg[2][2] as $k => $subarg) {
3492 $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
3493 if ($arg[2][2][$k] === false) {
3500 case Type
::T_FUNCTION_CALL
:
3501 if (! \
in_array($arg[1], $allowed_function)) {
3505 foreach ($arg[2] as $argValue) {
3506 if ($argValue === static::$null) {
3509 $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3510 if (empty($argValue[0]) && $cssArg !== false) {
3511 $cssArgs[] = [$argValue[0], $cssArg];
3517 return $this->fncall([Type
::T_FUNCTION
, $arg[1], [Type
::T_LIST
, ',', []]], $cssArgs);
3519 case Type
::T_STRING
:
3520 case Type
::T_KEYWORD
:
3521 if (!$inFunction or !\
in_array($inFunction, ['calc', 'env', 'var'])) {
3524 return $this->stringifyFncallArgs($arg);
3526 case Type
::T_NUMBER
:
3527 return $this->stringifyFncallArgs($arg);
3533 if (empty($arg['enclosing']) and $arg[1] === '') {
3534 foreach ($arg[2] as $k => $subarg) {
3535 $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
3536 if ($arg[2][$k] === false) {
3540 $arg[0] = Type
::T_STRING
;
3545 case Type
::T_EXPRESSION
:
3546 if (! \
in_array($arg[1], ['+', '-', '/', '*'])) {
3549 $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
3550 $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
3551 if ($arg[2] === false ||
$arg[3] === false) {
3554 return $this->expToString($arg, true);
3556 case Type
::T_VARIABLE
:
3565 * Reformat fncall arguments to proper css function output
3569 * @return array|\ArrayAccess|Number|string|null
3571 protected function stringifyFncallArgs($arg)
3576 foreach ($arg[2] as $k => $v) {
3577 $arg[2][$k] = $this->stringifyFncallArgs($v);
3581 case Type
::T_EXPRESSION
:
3582 if ($arg[1] === '/') {
3583 $arg[2] = $this->stringifyFncallArgs($arg[2]);
3584 $arg[3] = $this->stringifyFncallArgs($arg[3]);
3585 $arg[5] = $arg[6] = false; // no space around /
3586 $arg = $this->expToString($arg);
3590 case Type
::T_FUNCTION_CALL
:
3591 $name = strtolower($arg[1]);
3593 if (in_array($name, ['max', 'min', 'calc'])) {
3595 $arg = $this->fncall([Type
::T_FUNCTION
, $name, [Type
::T_LIST
, ',', []]], $args);
3604 * Find a function reference
3605 * @param string $name
3606 * @param bool $safeCopy
3609 protected function getFunctionReference($name, $safeCopy = false)
3612 if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3614 $func = clone $func;
3617 return [Type
::T_FUNCTION_REFERENCE
, 'scss', $name, $func];
3620 // native PHP functions
3622 // try to find a native lib function
3623 $normalizedName = $this->normalizeName($name);
3626 if (isset($this->userFunctions
[$normalizedName])) {
3627 // see if we can find a user function
3628 list($f, $prototype) = $this->userFunctions
[$normalizedName];
3630 return [Type
::T_FUNCTION_REFERENCE
, 'user', $name, $f, $prototype];
3633 if (($f = $this->getBuiltinFunction($normalizedName)) && \
is_callable($f)) {
3635 $prototype = isset(static::$
$libName) ?
static::$
$libName : null;
3637 return [Type
::T_FUNCTION_REFERENCE
, 'native', $name, $f, $prototype];
3640 return static::$null;
3647 * @param string $name
3651 protected function normalizeName($name)
3653 return str_replace('-', '_', $name);
3659 * @param array|Number $value
3661 * @return array|Number
3663 public function normalizeValue($value)
3665 $value = $this->coerceForExpression($this->reduce($value));
3667 switch ($value[0]) {
3669 $value = $this->extractInterpolation($value);
3671 if ($value[0] !== Type
::T_LIST
) {
3672 return [Type
::T_KEYWORD
, $this->compileValue($value)];
3675 foreach ($value[2] as $key => $item) {
3676 $value[2][$key] = $this->normalizeValue($item);
3679 if (! empty($value['enclosing'])) {
3680 unset($value['enclosing']);
3685 case Type
::T_STRING
:
3686 return [$value[0], '"', [$this->compileStringContent($value)]];
3688 case Type
::T_INTERPOLATE
:
3689 return [Type
::T_KEYWORD
, $this->compileValue($value)];
3699 * @param Number $left
3700 * @param Number $right
3704 protected function opAddNumberNumber(Number
$left, Number
$right)
3706 return $left->plus($right);
3712 * @param Number $left
3713 * @param Number $right
3717 protected function opMulNumberNumber(Number
$left, Number
$right)
3719 return $left->times($right);
3725 * @param Number $left
3726 * @param Number $right
3730 protected function opSubNumberNumber(Number
$left, Number
$right)
3732 return $left->minus($right);
3738 * @param Number $left
3739 * @param Number $right
3743 protected function opDivNumberNumber(Number
$left, Number
$right)
3745 return $left->dividedBy($right);
3751 * @param Number $left
3752 * @param Number $right
3756 protected function opModNumberNumber(Number
$left, Number
$right)
3758 return $left->modulo($right);
3764 * @param array $left
3765 * @param array $right
3767 * @return array|null
3769 protected function opAdd($left, $right)
3771 if ($strLeft = $this->coerceString($left)) {
3772 if ($right[0] === Type
::T_STRING
) {
3776 $strLeft[2][] = $right;
3781 if ($strRight = $this->coerceString($right)) {
3782 if ($left[0] === Type
::T_STRING
) {
3786 array_unshift($strRight[2], $left);
3797 * @param array|Number $left
3798 * @param array|Number $right
3799 * @param boolean $shouldEval
3801 * @return array|Number|null
3803 protected function opAnd($left, $right, $shouldEval)
3805 $truthy = ($left === static::$null ||
$right === static::$null) ||
3806 ($left === static::$false ||
$left === static::$true) &&
3807 ($right === static::$false ||
$right === static::$true);
3809 if (! $shouldEval) {
3815 if ($left !== static::$false && $left !== static::$null) {
3816 return $this->reduce($right, true);
3825 * @param array|Number $left
3826 * @param array|Number $right
3827 * @param boolean $shouldEval
3829 * @return array|Number|null
3831 protected function opOr($left, $right, $shouldEval)
3833 $truthy = ($left === static::$null ||
$right === static::$null) ||
3834 ($left === static::$false ||
$left === static::$true) &&
3835 ($right === static::$false ||
$right === static::$true);
3837 if (! $shouldEval) {
3843 if ($left !== static::$false && $left !== static::$null) {
3847 return $this->reduce($right, true);
3854 * @param array $left
3855 * @param array $right
3859 protected function opColorColor($op, $left, $right)
3861 if ($op !== '==' && $op !== '!=') {
3862 $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
3863 . "Consider using Sass's color functions instead.";
3864 $fname = $this->getPrettyPath($this->sourceNames
[$this->sourceIndex
]);
3865 $line = $this->sourceLine
;
3867 fwrite($this->stderr
, "DEPRECATION WARNING: $warning\n on line $line of $fname\n\n");
3870 $out = [Type
::T_COLOR
];
3872 foreach ([1, 2, 3] as $i) {
3873 $lval = isset($left[$i]) ?
$left[$i] : 0;
3874 $rval = isset($right[$i]) ?
$right[$i] : 0;
3878 $out[] = $lval +
$rval;
3882 $out[] = $lval - $rval;
3886 $out[] = $lval * $rval;
3891 throw $this->error("color: Can't take modulo by zero");
3894 $out[] = $lval %
$rval;
3899 throw $this->error("color: Can't divide by zero");
3902 $out[] = (int) ($lval / $rval);
3906 return $this->opEq($left, $right);
3909 return $this->opNeq($left, $right);
3912 throw $this->error("color: unknown op $op");
3916 if (isset($left[4])) {
3918 } elseif (isset($right[4])) {
3919 $out[4] = $right[4];
3922 return $this->fixColor($out);
3926 * Compare color and number
3929 * @param array $left
3930 * @param Number $right
3934 protected function opColorNumber($op, $left, Number
$right)
3937 return static::$false;
3941 return static::$true;
3944 $value = $right->getDimension();
3946 return $this->opColorColor(
3949 [Type
::T_COLOR
, $value, $value, $value]
3954 * Compare number and color
3957 * @param Number $left
3958 * @param array $right
3962 protected function opNumberColor($op, Number
$left, $right)
3965 return static::$false;
3969 return static::$true;
3972 $value = $left->getDimension();
3974 return $this->opColorColor(
3976 [Type
::T_COLOR
, $value, $value, $value],
3982 * Compare number1 == number2
3984 * @param array|Number $left
3985 * @param array|Number $right
3989 protected function opEq($left, $right)
3991 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3995 $left = $this->compileValue($lStr);
3996 $right = $this->compileValue($rStr);
3999 return $this->toBool($left === $right);
4003 * Compare number1 != number2
4005 * @param array|Number $left
4006 * @param array|Number $right
4010 protected function opNeq($left, $right)
4012 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4016 $left = $this->compileValue($lStr);
4017 $right = $this->compileValue($rStr);
4020 return $this->toBool($left !== $right);
4024 * Compare number1 == number2
4026 * @param Number $left
4027 * @param Number $right
4031 protected function opEqNumberNumber(Number
$left, Number
$right)
4033 return $this->toBool($left->equals($right));
4037 * Compare number1 != number2
4039 * @param Number $left
4040 * @param Number $right
4044 protected function opNeqNumberNumber(Number
$left, Number
$right)
4046 return $this->toBool(!$left->equals($right));
4050 * Compare number1 >= number2
4052 * @param Number $left
4053 * @param Number $right
4057 protected function opGteNumberNumber(Number
$left, Number
$right)
4059 return $this->toBool($left->greaterThanOrEqual($right));
4063 * Compare number1 > number2
4065 * @param Number $left
4066 * @param Number $right
4070 protected function opGtNumberNumber(Number
$left, Number
$right)
4072 return $this->toBool($left->greaterThan($right));
4076 * Compare number1 <= number2
4078 * @param Number $left
4079 * @param Number $right
4083 protected function opLteNumberNumber(Number
$left, Number
$right)
4085 return $this->toBool($left->lessThanOrEqual($right));
4089 * Compare number1 < number2
4091 * @param Number $left
4092 * @param Number $right
4096 protected function opLtNumberNumber(Number
$left, Number
$right)
4098 return $this->toBool($left->lessThan($right));
4106 * @param mixed $thing
4110 public function toBool($thing)
4112 return $thing ?
static::$true : static::$false;
4116 * Escape non printable chars in strings output as in dart-sass
4117 * @param string $string
4120 public function escapeNonPrintableChars($string, $inKeyword = false)
4122 static $replacement = [];
4123 if (empty($replacement[$inKeyword])) {
4124 for ($i = 0; $i < 32; $i++
) {
4125 if ($i !== 9 ||
$inKeyword) {
4126 $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ?
' ' : chr(0));
4130 $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
4131 // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
4132 if (strpos($string, chr(0)) !== false) {
4133 if (substr($string, -1) === chr(0)) {
4134 $string = substr($string, 0, -1);
4136 $string = str_replace(
4137 [chr(0) . '\\',chr(0) . ' '],
4141 if (strpos($string, chr(0)) !== false) {
4142 $parts = explode(chr(0), $string);
4143 $string = array_shift($parts);
4144 while (count($parts)) {
4145 $next = array_shift($parts);
4146 if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
4158 * Compiles a primitive value into a CSS property value.
4160 * Values in scssphp are typed by being wrapped in arrays, their format is
4163 * array(type, contents [, additional_contents]*)
4165 * The input is expected to be reduced. This function will not work on
4166 * things like expressions and variables.
4170 * @param array|Number|string $value
4174 public function compileValue($value)
4176 $value = $this->reduce($value);
4178 switch ($value[0]) {
4179 case Type
::T_KEYWORD
:
4180 if (is_string($value[1])) {
4181 $value[1] = $this->escapeNonPrintableChars($value[1], true);
4186 // [1] - red component (either number for a %)
4187 // [2] - green component
4188 // [3] - blue component
4189 // [4] - optional alpha component
4190 list(, $r, $g, $b) = $value;
4192 $r = $this->compileRGBAValue($r);
4193 $g = $this->compileRGBAValue($g);
4194 $b = $this->compileRGBAValue($b);
4196 if (\
count($value) === 5) {
4197 $alpha = $this->compileRGBAValue($value[4], true);
4199 if (! is_numeric($alpha) ||
$alpha < 1) {
4200 $colorName = Colors
::RGBaToColorName($r, $g, $b, $alpha);
4202 if (! \
is_null($colorName)) {
4206 if (is_numeric($alpha)) {
4207 $a = new Number($alpha, '');
4212 return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
4216 if (! is_numeric($r) ||
! is_numeric($g) ||
! is_numeric($b)) {
4217 return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
4220 $colorName = Colors
::RGBaToColorName($r, $g, $b);
4222 if (! \
is_null($colorName)) {
4226 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
4228 // Converting hex color to short notation (e.g. #003399 to #039)
4229 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
4230 $h = '#' . $h[1] . $h[3] . $h[5];
4235 case Type
::T_NUMBER
:
4236 return $value->output($this);
4238 case Type
::T_STRING
:
4239 $content = $this->compileStringContent($value);
4242 $content = str_replace('\\', '\\\\', $content);
4244 $content = $this->escapeNonPrintableChars($content);
4246 // force double quote as string quote for the output in certain cases
4248 $value[1] === "'" &&
4249 (strpos($content, '"') === false or strpos($content, "'") !== false) &&
4250 strpbrk($content, '{}\\\'') !== false
4254 $value[1] === '"' &&
4255 (strpos($content, '"') !== false and strpos($content, "'") === false)
4260 $content = str_replace($value[1], '\\' . $value[1], $content);
4263 return $value[1] . $content . $value[1];
4265 case Type
::T_FUNCTION
:
4266 $args = ! empty($value[2]) ?
$this->compileValue($value[2]) : '';
4268 return "$value[1]($args)";
4270 case Type
::T_FUNCTION_REFERENCE
:
4271 $name = ! empty($value[2]) ?
$value[2] : '';
4273 return "get-function(\"$name\")";
4276 $value = $this->extractInterpolation($value);
4278 if ($value[0] !== Type
::T_LIST
) {
4279 return $this->compileValue($value);
4282 list(, $delim, $items) = $value;
4285 if (! empty($value['enclosing'])) {
4286 switch ($value['enclosing']) {
4291 case 'forced_parent':
4296 case 'forced_bracket':
4305 if ($delim !== ' ') {
4306 $prefix_value = ' ';
4311 $same_string_quote = null;
4312 foreach ($items as $item) {
4313 if (\
is_null($same_string_quote)) {
4314 $same_string_quote = false;
4315 if ($item[0] === Type
::T_STRING
) {
4316 $same_string_quote = $item[1];
4317 foreach ($items as $ii) {
4318 if ($ii[0] !== Type
::T_STRING
) {
4319 $same_string_quote = false;
4325 if ($item[0] === Type
::T_NULL
) {
4328 if ($same_string_quote === '"' && $item[0] === Type
::T_STRING
&& $item[1]) {
4329 $item[1] = $same_string_quote;
4332 $compiled = $this->compileValue($item);
4334 if ($prefix_value && \
strlen($compiled)) {
4335 $compiled = $prefix_value . $compiled;
4338 $filtered[] = $compiled;
4341 return $pre . substr(implode("$delim", $filtered), \
strlen($prefix_value)) . $post;
4345 $values = $value[2];
4348 for ($i = 0, $s = \
count($keys); $i < $s; $i++
) {
4349 $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
4352 array_walk($filtered, function (&$value, $key) {
4353 $value = $key . ': ' . $value;
4356 return '(' . implode(', ', $filtered) . ')';
4358 case Type
::T_INTERPOLATED
:
4359 // node created by extractInterpolation
4360 list(, $interpolate, $left, $right) = $value;
4361 list(,, $whiteLeft, $whiteRight) = $interpolate;
4365 if ($delim && $delim !== ' ' && ! $whiteLeft) {
4369 $left = \
count($left[2]) > 0
4370 ?
$this->compileValue($left) . $delim . $whiteLeft
4375 if ($delim && $delim !== ' ') {
4379 $right = \
count($right[2]) > 0 ?
4380 $whiteRight . $delim . $this->compileValue($right) : '';
4382 return $left . $this->compileValue($interpolate) . $right;
4384 case Type
::T_INTERPOLATE
:
4385 // strip quotes if it's a string
4386 $reduced = $this->reduce($value[1]);
4388 switch ($reduced[0]) {
4390 $reduced = $this->extractInterpolation($reduced);
4392 if ($reduced[0] !== Type
::T_LIST
) {
4396 list(, $delim, $items) = $reduced;
4398 if ($delim !== ' ') {
4404 foreach ($items as $item) {
4405 if ($item[0] === Type
::T_NULL
) {
4409 $temp = $this->compileValue([Type
::T_KEYWORD
, $item]);
4411 if ($temp[0] === Type
::T_STRING
) {
4412 $filtered[] = $this->compileStringContent($temp);
4413 } elseif ($temp[0] === Type
::T_KEYWORD
) {
4414 $filtered[] = $temp[1];
4416 $filtered[] = $this->compileValue($temp);
4420 $reduced = [Type
::T_KEYWORD
, implode("$delim", $filtered)];
4423 case Type
::T_STRING
:
4424 $reduced = [Type
::T_STRING
, '', [$this->compileStringContent($reduced)]];
4428 $reduced = [Type
::T_KEYWORD
, ''];
4431 return $this->compileValue($reduced);
4436 case Type
::T_COMMENT
:
4437 return $this->compileCommentValue($value);
4440 throw $this->error('unknown value type: ' . json_encode($value));
4445 * @param array $value
4447 * @return array|string
4449 protected function compileDebugValue($value)
4451 $value = $this->reduce($value, true);
4453 switch ($value[0]) {
4454 case Type
::T_STRING
:
4455 return $this->compileStringContent($value);
4458 return $this->compileValue($value);
4465 * @param array $list
4469 protected function flattenList($list)
4471 return $this->compileValue($list);
4475 * Compile string content
4477 * @param array $string
4481 protected function compileStringContent($string)
4485 foreach ($string[2] as $part) {
4486 if (\
is_array($part) ||
$part instanceof \ArrayAccess
) {
4487 $parts[] = $this->compileValue($part);
4493 return implode($parts);
4497 * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4499 * @param array $list
4503 protected function extractInterpolation($list)
4507 foreach ($items as $i => $item) {
4508 if ($item[0] === Type
::T_INTERPOLATE
) {
4509 $before = [Type
::T_LIST
, $list[1], \array_slice
($items, 0, $i)];
4510 $after = [Type
::T_LIST
, $list[1], \array_slice
($items, $i +
1)];
4512 return [Type
::T_INTERPOLATED
, $item, $before, $after];
4520 * Find the final set of selectors
4522 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4523 * @param \ScssPhp\ScssPhp\Block $selfParent
4527 protected function multiplySelectors(Environment
$env, $selfParent = null)
4529 $envs = $this->compactEnv($env);
4531 $parentSelectors = [[]];
4533 $selfParentSelectors = null;
4535 if (! \
is_null($selfParent) && $selfParent->selectors
) {
4536 $selfParentSelectors = $this->evalSelectors($selfParent->selectors
);
4539 while ($env = array_pop($envs)) {
4540 if (empty($env->selectors
)) {
4544 $selectors = $env->selectors
;
4547 $stillHasSelf = false;
4548 $prevSelectors = $selectors;
4551 foreach ($parentSelectors as $parent) {
4552 foreach ($prevSelectors as $selector) {
4553 if ($selfParentSelectors) {
4554 foreach ($selfParentSelectors as $selfParent) {
4555 // if no '&' in the selector, each call will give same result, only add once
4556 $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
4557 $selectors[serialize($s)] = $s;
4560 $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
4561 $selectors[serialize($s)] = $s;
4565 } while ($stillHasSelf);
4567 $parentSelectors = $selectors;
4570 $selectors = array_values($selectors);
4572 // case we are just starting a at-root : nothing to multiply but parentSelectors
4573 if (! $selectors && $selfParentSelectors) {
4574 $selectors = $selfParentSelectors;
4581 * Join selectors; looks for & to replace, or append parent before child
4583 * @param array $parent
4584 * @param array $child
4585 * @param boolean $stillHasSelf
4586 * @param array $selfParentSelectors
4590 protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4595 foreach ($child as $part) {
4598 foreach ($part as $p) {
4599 // only replace & once and should be recalled to be able to make combinations
4600 if ($p === static::$selfSelector && $setSelf) {
4601 $stillHasSelf = true;
4604 if ($p === static::$selfSelector && ! $setSelf) {
4607 if (\
is_null($selfParentSelectors)) {
4608 $selfParentSelectors = $parent;
4611 foreach ($selfParentSelectors as $i => $parentPart) {
4617 foreach ($parentPart as $pp) {
4618 if (\
is_array($pp)) {
4621 array_walk_recursive($pp, function ($a) use (&$flatten) {
4625 $pp = implode($flatten);
4639 return $setSelf ?
$out : array_merge($parent, $child);
4645 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4646 * @param array $childQueries
4650 protected function multiplyMedia(Environment
$env = null, $childQueries = null)
4654 ! empty($env->block
->type
) && $env->block
->type
!== Type
::T_MEDIA
4656 return $childQueries;
4659 // plain old block, skip
4660 if (empty($env->block
->type
)) {
4661 return $this->multiplyMedia($env->parent
, $childQueries);
4664 $parentQueries = isset($env->block
->queryList
)
4665 ?
$env->block
->queryList
4666 : [[[Type
::T_MEDIA_VALUE
, $env->block
->value
]]];
4668 $store = [$this->env
, $this->storeEnv
];
4671 $this->storeEnv
= null;
4672 $parentQueries = $this->evaluateMediaQuery($parentQueries);
4674 list($this->env
, $this->storeEnv
) = $store;
4676 if (\
is_null($childQueries)) {
4677 $childQueries = $parentQueries;
4679 $originalQueries = $childQueries;
4682 foreach ($parentQueries as $parentQuery) {
4683 foreach ($originalQueries as $childQuery) {
4684 $childQueries[] = array_merge(
4686 [[Type
::T_MEDIA_TYPE
, [Type
::T_KEYWORD
, 'all']]],
4693 return $this->multiplyMedia($env->parent
, $childQueries);
4697 * Convert env linked list to stack
4699 * @param Environment $env
4701 * @return Environment[]
4703 * @phpstan-return non-empty-array<Environment>
4705 protected function compactEnv(Environment
$env)
4707 for ($envs = []; $env; $env = $env->parent
) {
4715 * Convert env stack to singly linked list
4717 * @param Environment[] $envs
4719 * @return Environment
4721 * @phpstan-param non-empty-array<Environment> $envs
4723 protected function extractEnv($envs)
4725 for ($env = null; $e = array_pop($envs);) {
4736 * @param \ScssPhp\ScssPhp\Block $block
4738 * @return \ScssPhp\ScssPhp\Compiler\Environment
4740 protected function pushEnv(Block
$block = null)
4742 $env = new Environment();
4743 $env->parent
= $this->env
;
4744 $env->parentStore
= $this->storeEnv
;
4746 $env->block
= $block;
4747 $env->depth
= isset($this->env
->depth
) ?
$this->env
->depth +
1 : 0;
4750 $this->storeEnv
= null;
4760 protected function popEnv()
4762 $this->storeEnv
= $this->env
->parentStore
;
4763 $this->env
= $this->env
->parent
;
4767 * Propagate vars from a just poped Env (used in @each and @for)
4769 * @param array $store
4770 * @param null|string[] $excludedVars
4774 protected function backPropagateEnv($store, $excludedVars = null)
4776 foreach ($store as $key => $value) {
4777 if (empty($excludedVars) ||
! \
in_array($key, $excludedVars)) {
4778 $this->set($key, $value, true);
4784 * Get store environment
4786 * @return \ScssPhp\ScssPhp\Compiler\Environment
4788 protected function getStoreEnv()
4790 return isset($this->storeEnv
) ?
$this->storeEnv
: $this->env
;
4796 * @param string $name
4797 * @param mixed $value
4798 * @param boolean $shadow
4799 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4800 * @param mixed $valueUnreduced
4804 protected function set($name, $value, $shadow = false, Environment
$env = null, $valueUnreduced = null)
4806 $name = $this->normalizeName($name);
4808 if (! isset($env)) {
4809 $env = $this->getStoreEnv();
4813 $this->setRaw($name, $value, $env, $valueUnreduced);
4815 $this->setExisting($name, $value, $env, $valueUnreduced);
4820 * Set existing variable
4822 * @param string $name
4823 * @param mixed $value
4824 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4825 * @param mixed $valueUnreduced
4829 protected function setExisting($name, $value, Environment
$env, $valueUnreduced = null)
4832 $specialContentKey = static::$namespaces['special'] . 'content';
4834 $hasNamespace = $name[0] === '^' ||
$name[0] === '@' ||
$name[0] === '%';
4839 if ($maxDepth-- <= 0) {
4843 if (\array_key_exists
($name, $env->store
)) {
4847 if (! $hasNamespace && isset($env->marker
)) {
4848 if (! empty($env->store
[$specialContentKey])) {
4849 $env = $env->store
[$specialContentKey]->scope
;
4853 if (! empty($env->declarationScopeParent
)) {
4854 $env = $env->declarationScopeParent
;
4862 if (isset($env->parentStore
)) {
4863 $env = $env->parentStore
;
4864 } elseif (isset($env->parent
)) {
4865 $env = $env->parent
;
4872 $env->store
[$name] = $value;
4874 if ($valueUnreduced) {
4875 $env->storeUnreduced
[$name] = $valueUnreduced;
4882 * @param string $name
4883 * @param mixed $value
4884 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4885 * @param mixed $valueUnreduced
4889 protected function setRaw($name, $value, Environment
$env, $valueUnreduced = null)
4891 $env->store
[$name] = $value;
4893 if ($valueUnreduced) {
4894 $env->storeUnreduced
[$name] = $valueUnreduced;
4903 * @param string $name
4904 * @param boolean $shouldThrow
4905 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4906 * @param boolean $unreduced
4908 * @return mixed|null
4910 public function get($name, $shouldThrow = true, Environment
$env = null, $unreduced = false)
4912 $normalizedName = $this->normalizeName($name);
4913 $specialContentKey = static::$namespaces['special'] . 'content';
4915 if (! isset($env)) {
4916 $env = $this->getStoreEnv();
4919 $hasNamespace = $normalizedName[0] === '^' ||
$normalizedName[0] === '@' ||
$normalizedName[0] === '%';
4924 if ($maxDepth-- <= 0) {
4928 if (\array_key_exists
($normalizedName, $env->store
)) {
4929 if ($unreduced && isset($env->storeUnreduced
[$normalizedName])) {
4930 return $env->storeUnreduced
[$normalizedName];
4933 return $env->store
[$normalizedName];
4936 if (! $hasNamespace && isset($env->marker
)) {
4937 if (! empty($env->store
[$specialContentKey])) {
4938 $env = $env->store
[$specialContentKey]->scope
;
4942 if (! empty($env->declarationScopeParent
)) {
4943 $env = $env->declarationScopeParent
;
4945 $env = $this->rootEnv
;
4950 if (isset($env->parentStore
)) {
4951 $env = $env->parentStore
;
4952 } elseif (isset($env->parent
)) {
4953 $env = $env->parent
;
4960 throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ?
' (infinite recursion)' : ''));
4970 * @param string $name
4971 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4975 protected function has($name, Environment
$env = null)
4977 return ! \
is_null($this->get($name, false, $env));
4983 * @param array $args
4987 protected function injectVariables(array $args)
4993 $parser = $this->parserFactory(__METHOD__
);
4995 foreach ($args as $name => $strValue) {
4996 if ($name[0] === '$') {
4997 $name = substr($name, 1);
5000 if (! $parser->parseValue($strValue, $value)) {
5001 $value = $this->coerceValue($strValue);
5004 $this->set($name, $value);
5013 * @param array $variables
5017 public function setVariables(array $variables)
5019 $this->registeredVars
= array_merge($this->registeredVars
, $variables);
5027 * @param string $name
5031 public function unsetVariable($name)
5033 unset($this->registeredVars
[$name]);
5037 * Returns list of variables
5043 public function getVariables()
5045 return $this->registeredVars
;
5049 * Adds to list of parsed files
5053 * @param string $path
5057 public function addParsedFile($path)
5059 if (isset($path) && is_file($path)) {
5060 $this->parsedFiles
[realpath($path)] = filemtime($path);
5065 * Returns list of parsed files
5071 public function getParsedFiles()
5073 return $this->parsedFiles
;
5081 * @param string|callable $path
5085 public function addImportPath($path)
5087 if (! \
in_array($path, $this->importPaths
)) {
5088 $this->importPaths
[] = $path;
5097 * @param string|array<string|callable> $path
5101 public function setImportPaths($path)
5103 $paths = (array) $path;
5104 $actualImportPaths = array_filter($paths, function ($path) {
5105 return $path !== '';
5108 $this->legacyCwdImportPath
= \
count($actualImportPaths) !== \
count($paths);
5110 if ($this->legacyCwdImportPath
) {
5111 @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 "compile()" instead.', E_USER_DEPRECATED
);
5114 $this->importPaths
= $actualImportPaths;
5118 * Set number precision
5122 * @param integer $numberPrecision
5126 * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
5128 public function setNumberPrecision($numberPrecision)
5130 @trigger_error
('The number precision is not configurable anymore. '
5131 . 'The default is enough for all browsers.', E_USER_DEPRECATED
);
5135 * Sets the output style.
5139 * @param string $style One of the OutputStyle constants
5143 * @phpstan-param OutputStyle::* $style
5145 public function setOutputStyle($style)
5148 case OutputStyle
::EXPANDED
:
5149 $this->formatter
= Expanded
::class;
5152 case OutputStyle
::COMPRESSED
:
5153 $this->formatter
= Compressed
::class;
5157 throw new \
InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5166 * @param string $formatterName
5170 * @deprecated Use {@see setOutputStyle} instead.
5172 public function setFormatter($formatterName)
5174 if (!\
in_array($formatterName, [Expanded
::class, Compressed
::class], true)) {
5175 @trigger_error
('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED
);
5177 @trigger_error
('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED
);
5179 $this->formatter
= $formatterName;
5183 * Set line number style
5187 * @param string $lineNumberStyle
5191 * @deprecated The line number output is not supported anymore. Use source maps instead.
5193 public function setLineNumberStyle($lineNumberStyle)
5195 @trigger_error
('The line number output is not supported anymore. '
5196 . 'Use source maps instead.', E_USER_DEPRECATED
);
5200 * Enable/disable source maps
5204 * @param integer $sourceMap
5208 * @phpstan-param self::SOURCE_MAP_* $sourceMap
5210 public function setSourceMap($sourceMap)
5212 $this->sourceMap
= $sourceMap;
5216 * Set source map options
5220 * @param array $sourceMapOptions
5224 public function setSourceMapOptions($sourceMapOptions)
5226 $this->sourceMapOptions
= $sourceMapOptions;
5234 * @param string $name
5235 * @param callable $func
5236 * @param array|null $prototype
5240 public function registerFunction($name, $func, $prototype = null)
5242 $this->userFunctions
[$this->normalizeName($name)] = [$func, $prototype];
5246 * Unregister function
5250 * @param string $name
5254 public function unregisterFunction($name)
5256 unset($this->userFunctions
[$this->normalizeName($name)]);
5264 * @param string $name
5268 * @deprecated Registering additional features is deprecated.
5270 public function addFeature($name)
5272 @trigger_error
('Registering additional features is deprecated.', E_USER_DEPRECATED
);
5274 $this->registeredFeatures
[$name] = true;
5280 * @param string $path
5281 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
5285 protected function importFile($path, OutputBlock
$out)
5287 $this->pushCallStack('import ' . $this->getPrettyPath($path));
5288 // see if tree is cached
5289 $realPath = realpath($path);
5291 if (isset($this->importCache
[$realPath])) {
5292 $this->handleImportLoop($realPath);
5294 $tree = $this->importCache
[$realPath];
5296 $code = file_get_contents($path);
5297 $parser = $this->parserFactory($path);
5298 $tree = $parser->parse($code);
5300 $this->importCache
[$realPath] = $tree;
5303 $currentDirectory = $this->currentDirectory
;
5304 $this->currentDirectory
= dirname($path);
5306 $this->compileChildrenNoReturn($tree->children
, $out);
5307 $this->currentDirectory
= $currentDirectory;
5308 $this->popCallStack();
5312 * Return the file path for an import url if it exists
5316 * @param string $url
5318 * @return string|null
5320 public function findImport($url)
5322 // for "normal" scss imports (ignore vanilla css and external requests)
5323 // Callback importers are still called for BC.
5324 if (preg_match('~\.css$|^https?://|^//~', $url)) {
5325 foreach ($this->importPaths
as $dir) {
5326 if (\
is_string($dir)) {
5330 if (\
is_callable($dir)) {
5331 // check custom callback for import path
5332 $file = \
call_user_func($dir, $url);
5334 if (! \
is_null($file)) {
5342 if (!\
is_null($this->currentDirectory
)) {
5343 $relativePath = $this->resolveImportPath($url, $this->currentDirectory
);
5345 if (!\
is_null($relativePath)) {
5346 return $relativePath;
5350 foreach ($this->importPaths
as $dir) {
5351 if (\
is_string($dir)) {
5352 $path = $this->resolveImportPath($url, $dir);
5354 if (!\
is_null($path)) {
5357 } elseif (\
is_callable($dir)) {
5358 // check custom callback for import path
5359 $file = \
call_user_func($dir, $url);
5361 if (! \
is_null($file)) {
5367 if ($this->legacyCwdImportPath
) {
5368 $path = $this->resolveImportPath($url, getcwd());
5370 if (!\
is_null($path)) {
5371 @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 "compile()" instead.', E_USER_DEPRECATED
);
5377 throw $this->error("`$url` file not found for @import");
5381 * @param string $url
5382 * @param string $baseDir
5384 * @return string|null
5386 private function resolveImportPath($url, $baseDir)
5388 $path = Path
::join($baseDir, $url);
5390 $hasExtension = preg_match('/.scss$/', $url);
5392 if ($hasExtension) {
5393 return $this->checkImportPathConflicts($this->tryImportPath($path));
5396 $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path));
5398 if (!\
is_null($result)) {
5402 return $this->tryImportPathAsDirectory($path);
5406 * @param string[] $paths
5408 * @return string|null
5410 private function checkImportPathConflicts(array $paths)
5412 if (\
count($paths) === 0) {
5416 if (\
count($paths) === 1) {
5420 $formattedPrettyPaths = [];
5422 foreach ($paths as $path) {
5423 $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path);
5426 throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
5430 * @param string $path
5434 private function tryImportPathWithExtensions($path)
5436 $result = $this->tryImportPath($path.'.scss');
5442 return $this->tryImportPath($path.'.css');
5446 * @param string $path
5450 private function tryImportPath($path)
5452 $partial = dirname($path).'/_'.basename($path);
5456 if (is_file($partial)) {
5457 $candidates[] = $partial;
5460 if (is_file($path)) {
5461 $candidates[] = $path;
5468 * @param string $path
5470 * @return string|null
5472 private function tryImportPathAsDirectory($path)
5474 if (!is_dir($path)) {
5478 return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5482 * @param string $path
5486 private function getPrettyPath($path)
5488 $normalizedPath = $path;
5489 $normalizedRootDirectory = $this->rootDirectory
.'/';
5491 if (\DIRECTORY_SEPARATOR
=== '\\') {
5492 $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
5493 $normalizedPath = str_replace('\\', '/', $path);
5496 if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
5497 return substr($normalizedPath, \
strlen($normalizedRootDirectory));
5508 * @param string $encoding
5512 public function setEncoding($encoding)
5514 $this->encoding
= $encoding;
5522 * @param boolean $ignoreErrors
5524 * @return \ScssPhp\ScssPhp\Compiler
5526 * @deprecated Ignoring Sass errors is not longer supported.
5528 public function setIgnoreErrors($ignoreErrors)
5530 @trigger_error
('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED
);
5536 * Get source position
5542 public function getSourcePosition()
5544 $sourceFile = isset($this->sourceNames
[$this->sourceIndex
]) ?
$this->sourceNames
[$this->sourceIndex
] : '';
5546 return [$sourceFile, $this->sourceLine
, $this->sourceColumn
];
5550 * Throw error (exception)
5554 * @param string $msg Message with optional sprintf()-style vararg parameters
5556 * @throws \ScssPhp\ScssPhp\Exception\CompilerException
5558 * @deprecated use "error" and throw the exception in the caller instead.
5560 public function throwError($msg)
5563 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
5567 throw $this->error(...func_get_args());
5571 * Build an error (exception)
5575 * @param string $msg Message with optional sprintf()-style vararg parameters
5577 * @return CompilerException
5579 public function error($msg, ...$args)
5582 $msg = sprintf($msg, ...$args);
5585 if (! $this->ignoreCallStackMessage
) {
5586 $line = $this->sourceLine
;
5587 $column = $this->sourceColumn
;
5589 $loc = isset($this->sourceNames
[$this->sourceIndex
])
5590 ?
$this->getPrettyPath($this->sourceNames
[$this->sourceIndex
]) . " on line $line, at column $column"
5591 : "line: $line, column: $column";
5593 $msg = "$msg: $loc";
5595 $callStackMsg = $this->callStackMessage();
5597 if ($callStackMsg) {
5598 $msg .= "\nCall Stack:\n" . $callStackMsg;
5602 return new CompilerException($msg);
5606 * @param string $functionName
5607 * @param array $ExpectedArgs
5608 * @param int $nbActual
5609 * @return CompilerException
5611 public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
5613 $nbExpected = \
count($ExpectedArgs);
5615 if ($nbActual > $nbExpected) {
5616 return $this->error(
5617 'Error: Only %d arguments allowed in %s(), but %d were passed.',
5625 while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
5626 array_unshift($missing, array_pop($ExpectedArgs));
5629 return $this->error(
5630 'Error: %s() argument%s %s missing.',
5632 count($missing) > 1 ?
's' : '',
5633 implode(', ', $missing)
5639 * Beautify call stack for output
5641 * @param boolean $all
5642 * @param null $limit
5646 protected function callStackMessage($all = false, $limit = null)
5651 if ($this->callStack
) {
5652 foreach (array_reverse($this->callStack
) as $call) {
5653 if ($all ||
(isset($call['n']) && $call['n'])) {
5654 $msg = '#' . $ncall++
. ' ' . $call['n'] . ' ';
5655 $msg .= (isset($this->sourceNames
[$call[Parser
::SOURCE_INDEX
]])
5656 ?
$this->getPrettyPath($this->sourceNames
[$call[Parser
::SOURCE_INDEX
]])
5657 : '(unknown file)');
5658 $msg .= ' on line ' . $call[Parser
::SOURCE_LINE
];
5660 $callStackMsg[] = $msg;
5662 if (! \
is_null($limit) && $ncall > $limit) {
5669 return implode("\n", $callStackMsg);
5673 * Handle import loop
5675 * @param string $name
5677 * @throws \Exception
5679 protected function handleImportLoop($name)
5681 for ($env = $this->env
; $env; $env = $env->parent
) {
5682 if (! $env->block
) {
5686 $file = $this->sourceNames
[$env->block
->sourceIndex
];
5688 if (realpath($file) === $name) {
5689 throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
5695 * Call SCSS @function
5697 * @param Object $func
5698 * @param array $argValues
5702 protected function callScssFunction($func, $argValues)
5705 return static::$defaultValue;
5707 $name = $func->name
;
5712 if (isset($func->args
)) {
5713 $this->applyArguments($func->args
, $argValues);
5716 // throw away lines and children
5717 $tmp = new OutputBlock();
5719 $tmp->children
= [];
5721 $this->env
->marker
= 'function';
5723 if (! empty($func->parentEnv
)) {
5724 $this->env
->declarationScopeParent
= $func->parentEnv
;
5726 throw $this->error("@function $name() without parentEnv");
5729 $ret = $this->compileChildren($func->children
, $tmp, $this->env
->marker
. ' ' . $name);
5733 return ! isset($ret) ?
static::$defaultValue : $ret;
5737 * Call built-in and registered (PHP) functions
5739 * @param string $name
5740 * @param string|array $function
5741 * @param array $prototype
5742 * @param array $args
5744 * @return array|Number|null
5746 protected function callNativeFunction($name, $function, $prototype, $args)
5748 $libName = (is_array($function) ?
end($function) : null);
5749 $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
5751 if (\
is_null($sorted_kwargs)) {
5754 @list
($sorted, $kwargs) = $sorted_kwargs;
5756 if ($name !== 'if') {
5759 if ($name === 'join') {
5763 foreach ($sorted as &$val) {
5764 $val = $this->reduce($val, $inExp);
5768 $returnValue = \
call_user_func($function, $sorted, $kwargs);
5770 if (! isset($returnValue)) {
5774 return $this->coerceValue($returnValue);
5778 * Get built-in function
5780 * @param string $name Normalized name
5784 protected function getBuiltinFunction($name)
5786 $libName = self
::normalizeNativeFunctionName($name);
5787 return [$this, $libName];
5791 * Normalize native function name
5792 * @param string $name
5795 public static function normalizeNativeFunctionName($name)
5797 $name = str_replace("-", "_", $name);
5798 $libName = 'lib' . preg_replace_callback(
5801 return ucfirst($m[1]);
5809 * Check if a function is a native built-in scss function, for css parsing
5810 * @param string $name
5813 public static function isNativeFunction($name)
5815 return method_exists(Compiler
::class, self
::normalizeNativeFunctionName($name));
5819 * Sorts keyword arguments
5821 * @param string $functionName
5822 * @param array $prototypes
5823 * @param array $args
5825 * @return array|null
5827 protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
5829 static $parser = null;
5831 if (! isset($prototypes)) {
5835 if (\
is_array($args) && \
count($args) && \
end($args) === static::$null) {
5839 // separate positional and keyword arguments
5840 foreach ($args as $arg) {
5841 list($key, $value) = $arg;
5843 if (empty($key) or empty($key[1])) {
5844 $posArgs[] = empty($arg[2]) ?
$value : $arg;
5846 $keyArgs[$key[1]] = $value;
5850 return [$posArgs, $keyArgs];
5854 if (\
in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5855 // notation 100 127 255 / 0 is in fact a simple list of 4 values
5856 foreach ($args as $k => $arg) {
5857 if ($arg[1][0] === Type
::T_LIST
&& \
count($arg[1][2]) === 3) {
5858 $last = end($arg[1][2]);
5860 if ($last[0] === Type
::T_EXPRESSION
&& $last[1] === '/') {
5861 array_pop($arg[1][2]);
5862 $arg[1][2][] = $last[2];
5863 $arg[1][2][] = $last[3];
5872 if (! \
is_array(reset($prototypes))) {
5873 $prototypes = [$prototypes];
5878 // trying each prototypes
5879 $prototypeHasMatch = false;
5880 $exceptionMessage = '';
5882 foreach ($prototypes as $prototype) {
5885 foreach ($prototype as $i => $p) {
5887 $p = explode(':', $p, 2);
5888 $name = array_shift($p);
5891 $p = trim(reset($p));
5893 if ($p === 'null') {
5894 // differentiate this null from the static::$null
5895 $default = [Type
::T_KEYWORD
, 'null'];
5897 if (\
is_null($parser)) {
5898 $parser = $this->parserFactory(__METHOD__
);
5901 $parser->parseValue($p, $default);
5905 $isVariable = false;
5907 if (substr($name, -3) === '...') {
5909 $name = substr($name, 0, -3);
5912 $argDef[] = [$name, $default, $isVariable];
5915 $ignoreCallStackMessage = $this->ignoreCallStackMessage
;
5916 $this->ignoreCallStackMessage
= true;
5919 if (\
count($args) > \
count($argDef)) {
5920 $lastDef = end($argDef);
5922 // check that last arg is not a ...
5923 if (empty($lastDef[2])) {
5924 throw $this->errorArgsNumber($functionName, $argDef, \
count($args));
5927 $vars = $this->applyArguments($argDef, $args, false, false);
5929 // ensure all args are populated
5930 foreach ($prototype as $i => $p) {
5931 $name = explode(':', $p)[0];
5933 if (! isset($finalArgs[$i])) {
5934 $finalArgs[$i] = null;
5938 // apply positional args
5939 foreach (array_values($vars) as $i => $val) {
5940 $finalArgs[$i] = $val;
5943 $keyArgs = array_merge($keyArgs, $vars);
5944 $prototypeHasMatch = true;
5946 // overwrite positional args with keyword args
5947 foreach ($prototype as $i => $p) {
5948 $name = explode(':', $p)[0];
5950 if (isset($keyArgs[$name])) {
5951 $finalArgs[$i] = $keyArgs[$name];
5954 // special null value as default: translate to real null here
5955 if ($finalArgs[$i] === [Type
::T_KEYWORD
, 'null']) {
5956 $finalArgs[$i] = null;
5959 // should we break if this prototype seems fulfilled?
5960 } catch (CompilerException
$e) {
5961 $exceptionMessage = $e->getMessage();
5963 $this->ignoreCallStackMessage
= $ignoreCallStackMessage;
5966 if ($exceptionMessage && ! $prototypeHasMatch) {
5967 if (\
in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5968 // if var() or calc() is used as an argument, return as a css function
5969 foreach ($args as $arg) {
5970 if ($arg[1][0] === Type
::T_FUNCTION_CALL
&& in_array($arg[1][1], ['var'])) {
5976 throw $this->error($exceptionMessage);
5979 return [$finalArgs, $keyArgs];
5983 * Apply argument values per definition
5985 * @param array $argDef
5986 * @param array $argValues
5987 * @param boolean $storeInEnv
5988 * @param boolean $reduce
5989 * only used if $storeInEnv = false
5993 * @throws \Exception
5995 protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5999 if (\
is_array($argValues) && \
count($argValues) && end($argValues) === static::$null) {
6000 array_pop($argValues);
6004 $storeEnv = $this->getStoreEnv();
6006 $env = new Environment();
6007 $env->store
= $storeEnv->store
;
6010 $hasVariable = false;
6013 foreach ($argDef as $i => $arg) {
6014 list($name, $default, $isVariable) = $argDef[$i];
6016 $args[$name] = [$i, $name, $default, $isVariable];
6017 $hasVariable |
= $isVariable;
6020 $splatSeparator = null;
6022 $deferredKeywordArgs = [];
6023 $deferredNamedKeywordArgs = [];
6025 $hasKeywordArgument = false;
6027 // assign the keyword args
6028 foreach ((array) $argValues as $arg) {
6029 if (! empty($arg[0])) {
6030 $hasKeywordArgument = true;
6034 if (! isset($args[$name])) {
6035 foreach (array_keys($args) as $an) {
6036 if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
6043 if (! isset($args[$name]) ||
$args[$name][3]) {
6045 $deferredNamedKeywordArgs[$name] = $arg[1];
6047 throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
6049 } elseif ($args[$name][0] < \
count($remaining)) {
6050 throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
6052 $keywordArgs[$name] = $arg[1];
6054 } elseif (! empty($arg[2])) {
6055 // $arg[2] means a var followed by ... in the arg ($list... )
6056 $val = $this->reduce($arg[1], true);
6058 if ($val[0] === Type
::T_LIST
) {
6059 foreach ($val[2] as $name => $item) {
6060 if (! is_numeric($name)) {
6061 if (! isset($args[$name])) {
6062 foreach (array_keys($args) as $an) {
6063 if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
6071 $deferredKeywordArgs[$name] = $item;
6073 $keywordArgs[$name] = $item;
6076 if (\
is_null($splatSeparator)) {
6077 $splatSeparator = $val[1];
6080 $remaining[] = $item;
6083 } elseif ($val[0] === Type
::T_MAP
) {
6084 foreach ($val[1] as $i => $name) {
6085 $name = $this->compileStringContent($this->coerceString($name));
6086 $item = $val[2][$i];
6088 if (! is_numeric($name)) {
6089 if (! isset($args[$name])) {
6090 foreach (array_keys($args) as $an) {
6091 if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
6099 $deferredKeywordArgs[$name] = $item;
6101 $keywordArgs[$name] = $item;
6104 if (\
is_null($splatSeparator)) {
6105 $splatSeparator = $val[1];
6108 $remaining[] = $item;
6112 $remaining[] = $val;
6114 } elseif ($hasKeywordArgument) {
6115 throw $this->error('Positional arguments must come before keyword arguments.');
6117 $remaining[] = $arg[1];
6121 foreach ($args as $arg) {
6122 list($i, $name, $default, $isVariable) = $arg;
6125 // only if more than one arg : can not be passed as position and value
6126 // see https://github.com/sass/libsass/issues/2927
6127 if (count($args) > 1) {
6128 if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
6129 throw $this->error("The argument $%s was passed both by position and by name.", $name);
6133 $val = [Type
::T_LIST
, \
is_null($splatSeparator) ?
',' : $splatSeparator , [], $isVariable];
6135 for ($count = \
count($remaining); $i < $count; $i++
) {
6136 $val[2][] = $remaining[$i];
6139 foreach ($deferredKeywordArgs as $itemName => $item) {
6140 $val[2][$itemName] = $item;
6143 foreach ($deferredNamedKeywordArgs as $itemName => $item) {
6144 $val[2][$itemName] = $item;
6146 } elseif (isset($remaining[$i])) {
6147 $val = $remaining[$i];
6148 } elseif (isset($keywordArgs[$name])) {
6149 $val = $keywordArgs[$name];
6150 } elseif (! empty($default)) {
6153 throw $this->error("Missing argument $name");
6157 $this->set($name, $this->reduce($val, true), true, $env);
6159 $output[$name] = ($reduce ?
$this->reduce($val, true) : $val);
6164 $storeEnv->store
= $env->store
;
6167 foreach ($args as $arg) {
6168 list($i, $name, $default, $isVariable) = $arg;
6170 if ($isVariable ||
isset($remaining[$i]) ||
isset($keywordArgs[$name]) ||
empty($default)) {
6175 $this->set($name, $this->reduce($default, true), true);
6177 $output[$name] = ($reduce ?
$this->reduce($default, true) : $default);
6185 * Coerce a php value into a scss one
6187 * @param mixed $value
6189 * @return array|Number
6191 protected function coerceValue($value)
6193 if (\
is_array($value) ||
$value instanceof \ArrayAccess
) {
6197 if (\
is_bool($value)) {
6198 return $this->toBool($value);
6201 if (\
is_null($value)) {
6202 return static::$null;
6205 if (is_numeric($value)) {
6206 return new Number($value, '');
6209 if ($value === '') {
6210 return static::$emptyString;
6213 $value = [Type
::T_KEYWORD
, $value];
6214 $color = $this->coerceColor($value);
6224 * Coerce something to map
6226 * @param array|Number $item
6228 * @return array|Number
6230 protected function coerceMap($item)
6232 if ($item[0] === Type
::T_MAP
) {
6237 $item[0] === static::$emptyList[0] &&
6238 $item[1] === static::$emptyList[1] &&
6239 $item[2] === static::$emptyList[2]
6241 return static::$emptyMap;
6248 * Coerce something to list
6250 * @param array $item
6251 * @param string $delim
6252 * @param boolean $removeTrailingNull
6256 protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
6258 if (isset($item) && $item[0] === Type
::T_LIST
) {
6259 // remove trailing null from the list
6260 if ($removeTrailingNull && end($item[2]) === static::$null) {
6261 array_pop($item[2]);
6267 if (isset($item) && $item[0] === Type
::T_MAP
) {
6272 for ($i = 0, $s = \
count($keys); $i < $s; $i++
) {
6274 $value = $values[$i];
6279 case Type
::T_STRING
:
6284 $key = [Type
::T_KEYWORD
, $this->compileStringContent($this->coerceString($key))];
6295 return [Type
::T_LIST
, ',', $list];
6298 return [Type
::T_LIST
, $delim, ! isset($item) ?
[] : [$item]];
6302 * Coerce color for expression
6304 * @param array|Number $value
6306 * @return array|Number
6308 protected function coerceForExpression($value)
6310 if ($color = $this->coerceColor($value)) {
6318 * Coerce value to color
6320 * @param array|Number $value
6321 * @param bool $inRGBFunction
6323 * @return array|null
6325 protected function coerceColor($value, $inRGBFunction = false)
6327 switch ($value[0]) {
6329 for ($i = 1; $i <= 3; $i++
) {
6330 if (! is_numeric($value[$i])) {
6331 $cv = $this->compileRGBAValue($value[$i]);
6333 if (! is_numeric($cv)) {
6340 if (isset($value[4])) {
6341 if (! is_numeric($value[4])) {
6342 $cv = $this->compileRGBAValue($value[4], true);
6344 if (! is_numeric($cv)) {
6356 if ($inRGBFunction) {
6357 if (\
count($value[2]) == 3 || \
count($value[2]) == 4) {
6359 array_unshift($color, Type
::T_COLOR
);
6361 return $this->coerceColor($color);
6367 case Type
::T_KEYWORD
:
6368 if (! \
is_string($value[1])) {
6372 $name = strtolower($value[1]);
6375 if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
6376 $nofValues = \
strlen($m[1]);
6378 if (\
in_array($nofValues, [3, 4, 6, 8])) {
6381 $num = hexdec($m[1]);
6383 switch ($nofValues) {
6386 // then continuing with the case 3:
6388 for ($i = 0; $i < $nbChannels; $i++
) {
6390 array_unshift($color, $t << 4 |
$t);
6398 // then continuing with the case 6:
6400 for ($i = 0; $i < $nbChannels; $i++
) {
6401 array_unshift($color, $num & 0xff);
6408 if ($nbChannels === 4) {
6409 if ($color[3] === 255) {
6410 $color[3] = 1; // fully opaque
6412 $color[3] = round($color[3] / 255, Number
::PRECISION
);
6416 array_unshift($color, Type
::T_COLOR
);
6422 if ($rgba = Colors
::colorNameToRGBa($name)) {
6423 return isset($rgba[3])
6424 ?
[Type
::T_COLOR
, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
6425 : [Type
::T_COLOR
, $rgba[0], $rgba[1], $rgba[2]];
6435 * @param integer|Number $value
6436 * @param boolean $isAlpha
6438 * @return integer|mixed
6440 protected function compileRGBAValue($value, $isAlpha = false)
6443 return $this->compileColorPartValue($value, 0, 1, false);
6446 return $this->compileColorPartValue($value, 0, 255, true);
6450 * @param mixed $value
6451 * @param integer|float $min
6452 * @param integer|float $max
6453 * @param boolean $isInt
6455 * @return integer|mixed
6457 protected function compileColorPartValue($value, $min, $max, $isInt = true)
6459 if (! is_numeric($value)) {
6460 if (\
is_array($value)) {
6461 $reduced = $this->reduce($value);
6463 if ($reduced instanceof Number
) {
6468 if ($value instanceof Number
) {
6469 if ($value->unitless()) {
6470 $num = $value->getDimension();
6471 } elseif ($value->hasUnit('%')) {
6472 $num = $max * $value->getDimension() / 100;
6474 throw $this->error('Expected %s to have no units or "%%".', $value);
6478 } elseif (\
is_array($value)) {
6479 $value = $this->compileValue($value);
6483 if (is_numeric($value)) {
6485 $value = round($value);
6488 $value = min($max, max($min, $value));
6497 * Coerce value to string
6499 * @param array|Number $value
6503 protected function coerceString($value)
6505 if ($value[0] === Type
::T_STRING
) {
6509 return [Type
::T_STRING
, '', [$this->compileValue($value)]];
6513 * Assert value is a string (or keyword)
6517 * @param array|Number $value
6518 * @param string $varName
6522 * @throws \Exception
6524 public function assertString($value, $varName = null)
6526 // case of url(...) parsed a a function
6527 if ($value[0] === Type
::T_FUNCTION
) {
6528 $value = $this->coerceString($value);
6531 if (! \
in_array($value[0], [Type
::T_STRING
, Type
::T_KEYWORD
])) {
6532 $value = $this->compileValue($value);
6533 $var_display = ($varName ?
" \${$varName}:" : '');
6534 throw $this->error("Error:{$var_display} $value is not a string.");
6537 $value = $this->coerceString($value);
6543 * Coerce value to a percentage
6545 * @param array|Number $value
6547 * @return integer|float
6549 protected function coercePercent($value)
6551 if ($value instanceof Number
) {
6552 if ($value->hasUnit('%')) {
6553 return $value->getDimension() / 100;
6556 return $value->getDimension();
6563 * Assert value is a map
6567 * @param array|Number $value
6571 * @throws \Exception
6573 public function assertMap($value)
6575 $value = $this->coerceMap($value);
6577 if ($value[0] !== Type
::T_MAP
) {
6578 throw $this->error('expecting map, %s received', $value[0]);
6585 * Assert value is a list
6589 * @param array|Number $value
6593 * @throws \Exception
6595 public function assertList($value)
6597 if ($value[0] !== Type
::T_LIST
) {
6598 throw $this->error('expecting list, %s received', $value[0]);
6605 * Assert value is a color
6609 * @param array|Number $value
6613 * @throws \Exception
6615 public function assertColor($value)
6617 if ($color = $this->coerceColor($value)) {
6621 throw $this->error('expecting color, %s received', $value[0]);
6625 * Assert value is a number
6629 * @param array|Number $value
6630 * @param string $varName
6634 * @throws \Exception
6636 public function assertNumber($value, $varName = null)
6638 if (!$value instanceof Number
) {
6639 $value = $this->compileValue($value);
6640 $var_display = ($varName ?
" \${$varName}:" : '');
6641 throw $this->error("Error:{$var_display} $value is not a number.");
6648 * Assert value is a integer
6652 * @param array|Number $value
6653 * @param string $varName
6657 * @throws \Exception
6659 public function assertInteger($value, $varName = null)
6662 $value = $this->assertNumber($value, $varName)->getDimension();
6663 if (round($value - \
intval($value), Number
::PRECISION
) > 0) {
6664 $var_display = ($varName ?
" \${$varName}:" : '');
6665 throw $this->error("Error:{$var_display} $value is not an integer.");
6668 return intval($value);
6673 * Make sure a color's components don't go out of bounds
6679 protected function fixColor($c)
6681 foreach ([1, 2, 3] as $i) {
6695 * Convert RGB to HSL
6699 * @param integer $red
6700 * @param integer $green
6701 * @param integer $blue
6705 public function toHSL($red, $green, $blue)
6707 $min = min($red, $green, $blue);
6708 $max = max($red, $green, $blue);
6713 if ((int) $d === 0) {
6719 $s = $d / (510 - $l);
6723 $h = 60 * ($green - $blue) / $d;
6724 } elseif ($green == $max) {
6725 $h = 60 * ($blue - $red) / $d +
120;
6726 } elseif ($blue == $max) {
6727 $h = 60 * ($red - $green) / $d +
240;
6731 return [Type
::T_HSL
, fmod($h, 360), $s * 100, $l / 5.1];
6743 protected function hueToRGB($m1, $m2, $h)
6752 return $m1 +
($m2 - $m1) * $h * 6;
6760 return $m1 +
($m2 - $m1) * (2 / 3 - $h) * 6;
6767 * Convert HSL to RGB
6771 * @param integer $hue H from 0 to 360
6772 * @param integer $saturation S from 0 to 100
6773 * @param integer $lightness L from 0 to 100
6777 public function toRGB($hue, $saturation, $lightness)
6784 $s = min(100, max(0, $saturation)) / 100;
6785 $l = min(100, max(0, $lightness)) / 100;
6787 $m2 = $l <= 0.5 ?
$l * ($s +
1) : $l +
$s - $l * $s;
6790 $r = $this->hueToRGB($m1, $m2, $h +
1 / 3) * 255;
6791 $g = $this->hueToRGB($m1, $m2, $h) * 255;
6792 $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
6794 $out = [Type
::T_COLOR
, $r, $g, $b];
6799 // Built in functions
6801 protected static $libCall = ['function', 'args...'];
6802 protected function libCall($args, $kwargs)
6804 $functionReference = array_shift($args);
6806 if (in_array($functionReference[0], [Type
::T_STRING
, Type
::T_KEYWORD
])) {
6807 $name = $this->compileStringContent($this->coerceString($functionReference));
6808 $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
6809 . "in Sass 4.0. Use call(function-reference($name)) instead.";
6810 fwrite($this->stderr
, "$warning\n\n");
6811 $functionReference = $this->libGetFunction([$functionReference]);
6814 if ($functionReference === static::$null) {
6815 return static::$null;
6818 if (! in_array($functionReference[0], [Type
::T_FUNCTION_REFERENCE
, Type
::T_FUNCTION
])) {
6819 throw $this->error('Function reference expected, got ' . $functionReference[0]);
6824 // $kwargs['args'] is [Type::T_LIST, ',', [..]]
6825 foreach ($kwargs['args'][2] as $varname => $arg) {
6826 if (is_numeric($varname)) {
6829 $varname = [ 'var', $varname];
6832 $callArgs[] = [$varname, $arg, false];
6835 return $this->reduce([Type
::T_FUNCTION_CALL
, $functionReference, $callArgs]);
6839 protected static $libGetFunction = [
6843 protected function libGetFunction($args)
6845 $name = $this->compileStringContent($this->coerceString(array_shift($args)));
6849 $isCss = array_shift($args);
6850 $isCss = (($isCss === static::$true) ?
true : false);
6854 return [Type
::T_FUNCTION
, $name, [Type
::T_LIST
, ',', []]];
6857 return $this->getFunctionReference($name, true);
6860 protected static $libIf = ['condition', 'if-true', 'if-false:'];
6861 protected function libIf($args)
6863 list($cond, $t, $f) = $args;
6865 if (! $this->isTruthy($this->reduce($cond, true))) {
6866 return $this->reduce($f, true);
6869 return $this->reduce($t, true);
6872 protected static $libIndex = ['list', 'value'];
6873 protected function libIndex($args)
6875 list($list, $value) = $args;
6878 $list[0] === Type
::T_MAP ||
6879 $list[0] === Type
::T_STRING ||
6880 $list[0] === Type
::T_KEYWORD ||
6881 $list[0] === Type
::T_INTERPOLATE
6883 $list = $this->coerceList($list, ' ');
6886 if ($list[0] !== Type
::T_LIST
) {
6887 return static::$null;
6890 // Numbers are represented with value objects, for which the PHP equality operator does not
6891 // match the Sass rules (and we cannot overload it). As they are the only type of values
6892 // represented with a value object for now, they require a special case.
6893 if ($value instanceof Number
) {
6895 foreach ($list[2] as $item) {
6897 $itemValue = $this->normalizeValue($item);
6899 if ($itemValue instanceof Number
&& $value->equals($itemValue)) {
6900 return new Number($key, '');
6903 return static::$null;
6909 foreach ($list[2] as $item) {
6910 $values[] = $this->normalizeValue($item);
6913 $key = array_search($this->normalizeValue($value), $values);
6915 return false === $key ?
static::$null : $key +
1;
6918 protected static $libRgb = [
6922 ['red', 'green', 'blue'],
6923 ['red', 'green', 'blue', 'alpha'] ];
6924 protected function libRgb($args, $kwargs, $funcName = 'rgb')
6926 switch (\
count($args)) {
6928 if (! $color = $this->coerceColor($args[0], true)) {
6929 $color = [Type
::T_STRING
, '', [$funcName . '(', $args[0], ')']];
6934 $color = [Type
::T_COLOR
, $args[0], $args[1], $args[2]];
6936 if (! $color = $this->coerceColor($color)) {
6937 $color = [Type
::T_STRING
, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6943 if ($color = $this->coerceColor($args[0], true)) {
6944 $alpha = $this->compileRGBAValue($args[1], true);
6946 if (is_numeric($alpha)) {
6949 $color = [Type
::T_STRING
, '',
6950 [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
6953 $color = [Type
::T_STRING
, '', [$funcName . '(', $args[0], ')']];
6959 $color = [Type
::T_COLOR
, $args[0], $args[1], $args[2], $args[3]];
6961 if (! $color = $this->coerceColor($color)) {
6962 $color = [Type
::T_STRING
, '',
6963 [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6971 protected static $libRgba = [
6975 ['red', 'green', 'blue'],
6976 ['red', 'green', 'blue', 'alpha'] ];
6977 protected function libRgba($args, $kwargs)
6979 return $this->libRgb($args, $kwargs, 'rgba');
6983 * Helper function for adjust_color, change_color, and scale_color
6985 * @param array<array|Number> $args
6986 * @param callable $fn
6990 protected function alterColor($args, $fn)
6992 $color = $this->assertColor($args[0]);
6994 foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
6995 if (isset($args[$iarg])) {
6996 $val = $this->assertNumber($args[$iarg])->getDimension();
6998 if (! isset($color[$irgba])) {
6999 $color[$irgba] = (($irgba < 4) ?
0 : 1);
7002 $color[$irgba] = \
call_user_func($fn, $color[$irgba], $val, $iarg);
7006 if (! empty($args[4]) ||
! empty($args[5]) ||
! empty($args[6])) {
7007 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7009 foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
7010 if (! empty($args[$iarg])) {
7011 $val = $this->assertNumber($args[$iarg])->getDimension();
7012 $hsl[$ihsl] = \
call_user_func($fn, $hsl[$ihsl], $val, $iarg);
7016 $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
7018 if (isset($color[4])) {
7019 $rgb[4] = $color[4];
7028 protected static $libAdjustColor = [
7029 'color', 'red:null', 'green:null', 'blue:null',
7030 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
7032 protected function libAdjustColor($args)
7034 return $this->alterColor($args, function ($base, $alter, $i) {
7035 return $base +
$alter;
7039 protected static $libChangeColor = [
7040 'color', 'red:null', 'green:null', 'blue:null',
7041 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
7043 protected function libChangeColor($args)
7045 return $this->alterColor($args, function ($base, $alter, $i) {
7050 protected static $libScaleColor = [
7051 'color', 'red:null', 'green:null', 'blue:null',
7052 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
7054 protected function libScaleColor($args)
7056 return $this->alterColor($args, function ($base, $scale, $i) {
7079 $scale = $scale / 100;
7082 return $base * $scale +
$base;
7085 return ($max - $base) * $scale +
$base;
7089 protected static $libIeHexStr = ['color'];
7090 protected function libIeHexStr($args)
7092 $color = $this->coerceColor($args[0]);
7094 if (\
is_null($color)) {
7095 throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color');
7098 $color[4] = isset($color[4]) ?
round(255 * $color[4]) : 255;
7100 return [Type
::T_STRING
, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
7103 protected static $libRed = ['color'];
7104 protected function libRed($args)
7106 $color = $this->coerceColor($args[0]);
7108 if (\
is_null($color)) {
7109 throw $this->error('Error: argument `$color` of `red($color)` must be a color');
7115 protected static $libGreen = ['color'];
7116 protected function libGreen($args)
7118 $color = $this->coerceColor($args[0]);
7120 if (\
is_null($color)) {
7121 throw $this->error('Error: argument `$color` of `green($color)` must be a color');
7127 protected static $libBlue = ['color'];
7128 protected function libBlue($args)
7130 $color = $this->coerceColor($args[0]);
7132 if (\
is_null($color)) {
7133 throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
7139 protected static $libAlpha = ['color'];
7140 protected function libAlpha($args)
7142 if ($color = $this->coerceColor($args[0])) {
7143 return isset($color[4]) ?
$color[4] : 1;
7146 // this might be the IE function, so return value unchanged
7150 protected static $libOpacity = ['color'];
7151 protected function libOpacity($args)
7155 if ($value instanceof Number
) {
7159 return $this->libAlpha($args);
7163 protected static $libMix = [
7164 ['color1', 'color2', 'weight:0.5'],
7165 ['color-1', 'color-2', 'weight:0.5']
7167 protected function libMix($args)
7169 list($first, $second, $weight) = $args;
7171 $first = $this->assertColor($first);
7172 $second = $this->assertColor($second);
7174 if (! isset($weight)) {
7177 $weight = $this->coercePercent($weight);
7180 $firstAlpha = isset($first[4]) ?
$first[4] : 1;
7181 $secondAlpha = isset($second[4]) ?
$second[4] : 1;
7183 $w = $weight * 2 - 1;
7184 $a = $firstAlpha - $secondAlpha;
7186 $w1 = (($w * $a === -1 ?
$w : ($w +
$a) / (1 +
$w * $a)) +
1) / 2.0;
7189 $new = [Type
::T_COLOR
,
7190 $w1 * $first[1] +
$w2 * $second[1],
7191 $w1 * $first[2] +
$w2 * $second[2],
7192 $w1 * $first[3] +
$w2 * $second[3],
7195 if ($firstAlpha != 1.0 ||
$secondAlpha != 1.0) {
7196 $new[] = $firstAlpha * $weight +
$secondAlpha * (1 - $weight);
7199 return $this->fixColor($new);
7202 protected static $libHsl = [
7204 ['hue', 'saturation', 'lightness'],
7205 ['hue', 'saturation', 'lightness', 'alpha'] ];
7206 protected function libHsl($args, $kwargs, $funcName = 'hsl')
7208 $args_to_check = $args;
7210 if (\
count($args) == 1) {
7211 if ($args[0][0] !== Type
::T_LIST || \
count($args[0][2]) < 3 || \
count($args[0][2]) > 4) {
7212 return [Type
::T_STRING
, '', [$funcName . '(', $args[0], ')']];
7215 $args = $args[0][2];
7216 $args_to_check = $kwargs['channels'][2];
7219 foreach ($kwargs as $k => $arg) {
7220 if (in_array($arg[0], [Type
::T_FUNCTION_CALL
]) && in_array($arg[1], ['min', 'max'])) {
7225 foreach ($args_to_check as $k => $arg) {
7226 if (in_array($arg[0], [Type
::T_FUNCTION_CALL
]) && in_array($arg[1], ['min', 'max'])) {
7227 if (count($kwargs) > 1 ||
($k >= 2 && count($args) === 4)) {
7231 $args[$k] = $this->stringifyFncallArgs($arg);
7235 $k >= 2 && count($args) === 4 &&
7236 in_array($arg[0], [Type
::T_FUNCTION_CALL
, Type
::T_FUNCTION
]) &&
7237 in_array($arg[1], ['calc','env'])
7243 $hue = $this->reduce($args[0]);
7244 $saturation = $this->reduce($args[1]);
7245 $lightness = $this->reduce($args[2]);
7248 if (\
count($args) === 4) {
7249 $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
7251 if (!$hue instanceof Number ||
!$saturation instanceof Number ||
! $lightness instanceof Number ||
! is_numeric($alpha)) {
7252 return [Type
::T_STRING
, '',
7253 [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
7256 if (!$hue instanceof Number ||
!$saturation instanceof Number ||
! $lightness instanceof Number
) {
7257 return [Type
::T_STRING
, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
7261 $hueValue = $hue->getDimension() %
360;
7263 while ($hueValue < 0) {
7267 $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
7269 if (! \
is_null($alpha)) {
7276 protected static $libHsla = [
7278 ['hue', 'saturation', 'lightness'],
7279 ['hue', 'saturation', 'lightness', 'alpha']];
7280 protected function libHsla($args, $kwargs)
7282 return $this->libHsl($args, $kwargs, 'hsla');
7285 protected static $libHue = ['color'];
7286 protected function libHue($args)
7288 $color = $this->assertColor($args[0]);
7289 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7291 return new Number($hsl[1], 'deg');
7294 protected static $libSaturation = ['color'];
7295 protected function libSaturation($args)
7297 $color = $this->assertColor($args[0]);
7298 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7300 return new Number($hsl[2], '%');
7303 protected static $libLightness = ['color'];
7304 protected function libLightness($args)
7306 $color = $this->assertColor($args[0]);
7307 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7309 return new Number($hsl[3], '%');
7312 protected function adjustHsl($color, $idx, $amount)
7314 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
7315 $hsl[$idx] +
= $amount;
7316 $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
7318 if (isset($color[4])) {
7319 $out[4] = $color[4];
7325 protected static $libAdjustHue = ['color', 'degrees'];
7326 protected function libAdjustHue($args)
7328 $color = $this->assertColor($args[0]);
7329 $degrees = $this->assertNumber($args[1])->getDimension();
7331 return $this->adjustHsl($color, 1, $degrees);
7334 protected static $libLighten = ['color', 'amount'];
7335 protected function libLighten($args)
7337 $color = $this->assertColor($args[0]);
7338 $amount = Util
::checkRange('amount', new Range(0, 100), $args[1], '%');
7340 return $this->adjustHsl($color, 3, $amount);
7343 protected static $libDarken = ['color', 'amount'];
7344 protected function libDarken($args)
7346 $color = $this->assertColor($args[0]);
7347 $amount = Util
::checkRange('amount', new Range(0, 100), $args[1], '%');
7349 return $this->adjustHsl($color, 3, -$amount);
7352 protected static $libSaturate = [['color', 'amount'], ['amount']];
7353 protected function libSaturate($args)
7357 if ($value instanceof Number
) {
7361 if (count($args) === 1) {
7362 $val = $this->compileValue($value);
7363 throw $this->error("\$amount: $val is not a number");
7366 $color = $this->assertColor($value);
7367 $amount = 100 * $this->coercePercent($args[1]);
7369 return $this->adjustHsl($color, 2, $amount);
7372 protected static $libDesaturate = ['color', 'amount'];
7373 protected function libDesaturate($args)
7375 $color = $this->assertColor($args[0]);
7376 $amount = 100 * $this->coercePercent($args[1]);
7378 return $this->adjustHsl($color, 2, -$amount);
7381 protected static $libGrayscale = ['color'];
7382 protected function libGrayscale($args)
7386 if ($value instanceof Number
) {
7390 return $this->adjustHsl($this->assertColor($value), 2, -100);
7393 protected static $libComplement = ['color'];
7394 protected function libComplement($args)
7396 return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
7399 protected static $libInvert = ['color', 'weight:1'];
7400 protected function libInvert($args)
7402 list($value, $weight) = $args;
7404 if (! isset($weight)) {
7407 $weight = $this->coercePercent($weight);
7410 if ($value instanceof Number
) {
7414 $color = $this->assertColor($value);
7416 $inverted[1] = 255 - $inverted[1];
7417 $inverted[2] = 255 - $inverted[2];
7418 $inverted[3] = 255 - $inverted[3];
7421 return $this->libMix([$inverted, $color, new Number($weight, '')]);
7427 // increases opacity by amount
7428 protected static $libOpacify = ['color', 'amount'];
7429 protected function libOpacify($args)
7431 $color = $this->assertColor($args[0]);
7432 $amount = $this->coercePercent($args[1]);
7434 $color[4] = (isset($color[4]) ?
$color[4] : 1) +
$amount;
7435 $color[4] = min(1, max(0, $color[4]));
7440 protected static $libFadeIn = ['color', 'amount'];
7441 protected function libFadeIn($args)
7443 return $this->libOpacify($args);
7446 // decreases opacity by amount
7447 protected static $libTransparentize = ['color', 'amount'];
7448 protected function libTransparentize($args)
7450 $color = $this->assertColor($args[0]);
7451 $amount = $this->coercePercent($args[1]);
7453 $color[4] = (isset($color[4]) ?
$color[4] : 1) - $amount;
7454 $color[4] = min(1, max(0, $color[4]));
7459 protected static $libFadeOut = ['color', 'amount'];
7460 protected function libFadeOut($args)
7462 return $this->libTransparentize($args);
7465 protected static $libUnquote = ['string'];
7466 protected function libUnquote($args)
7470 if ($str[0] === Type
::T_STRING
) {
7477 protected static $libQuote = ['string'];
7478 protected function libQuote($args)
7482 if ($value[0] === Type
::T_STRING
&& ! empty($value[1])) {
7487 return [Type
::T_STRING
, '"', [$value]];
7490 protected static $libPercentage = ['number'];
7491 protected function libPercentage($args)
7493 $num = $this->assertNumber($args[0], 'number');
7494 $num->assertNoUnits('number');
7496 return new Number($num->getDimension() * 100, '%');
7499 protected static $libRound = ['number'];
7500 protected function libRound($args)
7502 $num = $this->assertNumber($args[0], 'number');
7504 return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
7507 protected static $libFloor = ['number'];
7508 protected function libFloor($args)
7510 $num = $this->assertNumber($args[0], 'number');
7512 return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
7515 protected static $libCeil = ['number'];
7516 protected function libCeil($args)
7518 $num = $this->assertNumber($args[0], 'number');
7520 return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
7523 protected static $libAbs = ['number'];
7524 protected function libAbs($args)
7526 $num = $this->assertNumber($args[0], 'number');
7528 return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
7531 protected function libMin($args)
7538 foreach ($args as $arg) {
7539 $number = $this->assertNumber($arg);
7541 if (\
is_null($min) ||
$min->greaterThan($number)) {
7546 if (!\
is_null($min)) {
7550 throw $this->error('At least one argument must be passed.');
7553 protected function libMax($args)
7560 foreach ($args as $arg) {
7561 $number = $this->assertNumber($arg);
7563 if (\
is_null($max) ||
$max->lessThan($number)) {
7568 if (!\
is_null($max)) {
7572 throw $this->error('At least one argument must be passed.');
7575 protected static $libLength = ['list'];
7576 protected function libLength($args)
7578 $list = $this->coerceList($args[0], ',', true);
7580 return \
count($list[2]);
7583 //protected static $libListSeparator = ['list...'];
7584 protected function libListSeparator($args)
7586 if (\
count($args) > 1) {
7590 if (! \
in_array($args[0][0], [Type
::T_LIST
, Type
::T_MAP
])) {
7594 $list = $this->coerceList($args[0]);
7596 if (\
count($list[2]) <= 1 && empty($list['enclosing'])) {
7600 if ($list[1] === ',') {
7607 protected static $libNth = ['list', 'n'];
7608 protected function libNth($args)
7610 $list = $this->coerceList($args[0], ',', false);
7611 $n = $this->assertNumber($args[1])->getDimension();
7616 $n +
= \
count($list[2]);
7619 return isset($list[2][$n]) ?
$list[2][$n] : static::$defaultValue;
7622 protected static $libSetNth = ['list', 'n', 'value'];
7623 protected function libSetNth($args)
7625 $list = $this->coerceList($args[0]);
7626 $n = $this->assertNumber($args[1])->getDimension();
7631 $n +
= \
count($list[2]);
7634 if (! isset($list[2][$n])) {
7635 throw $this->error('Invalid argument for "n"');
7638 $list[2][$n] = $args[2];
7643 protected static $libMapGet = ['map', 'key'];
7644 protected function libMapGet($args)
7646 $map = $this->assertMap($args[0]);
7649 if (! \
is_null($key)) {
7650 $key = $this->compileStringContent($this->coerceString($key));
7652 for ($i = \
count($map[1]) - 1; $i >= 0; $i--) {
7653 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
7659 return static::$null;
7662 protected static $libMapKeys = ['map'];
7663 protected function libMapKeys($args)
7665 $map = $this->assertMap($args[0]);
7668 return [Type
::T_LIST
, ',', $keys];
7671 protected static $libMapValues = ['map'];
7672 protected function libMapValues($args)
7674 $map = $this->assertMap($args[0]);
7677 return [Type
::T_LIST
, ',', $values];
7680 protected static $libMapRemove = ['map', 'key...'];
7681 protected function libMapRemove($args)
7683 $map = $this->assertMap($args[0]);
7684 $keyList = $this->assertList($args[1]);
7688 foreach ($keyList[2] as $key) {
7689 $keys[] = $this->compileStringContent($this->coerceString($key));
7692 for ($i = \
count($map[1]) - 1; $i >= 0; $i--) {
7693 if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
7694 array_splice($map[1], $i, 1);
7695 array_splice($map[2], $i, 1);
7702 protected static $libMapHasKey = ['map', 'key'];
7703 protected function libMapHasKey($args)
7705 $map = $this->assertMap($args[0]);
7706 $key = $this->compileStringContent($this->coerceString($args[1]));
7708 for ($i = \
count($map[1]) - 1; $i >= 0; $i--) {
7709 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
7717 protected static $libMapMerge = [
7721 protected function libMapMerge($args)
7723 $map1 = $this->assertMap($args[0]);
7724 $map2 = $this->assertMap($args[1]);
7726 foreach ($map2[1] as $i2 => $key2) {
7727 $key = $this->compileStringContent($this->coerceString($key2));
7729 foreach ($map1[1] as $i1 => $key1) {
7730 if ($key === $this->compileStringContent($this->coerceString($key1))) {
7731 $map1[2][$i1] = $map2[2][$i2];
7736 $map1[1][] = $map2[1][$i2];
7737 $map1[2][] = $map2[2][$i2];
7743 protected static $libKeywords = ['args'];
7744 protected function libKeywords($args)
7746 $this->assertList($args[0]);
7751 foreach ($args[0][2] as $name => $arg) {
7752 $keys[] = [Type
::T_KEYWORD
, $name];
7756 return [Type
::T_MAP
, $keys, $values];
7759 protected static $libIsBracketed = ['list'];
7760 protected function libIsBracketed($args)
7763 $this->coerceList($list, ' ');
7765 if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
7773 * @param array $list1
7774 * @param array|Number|null $sep
7777 * @throws CompilerException
7779 protected function listSeparatorForJoin($list1, $sep)
7781 if (! isset($sep)) {
7785 switch ($this->compileValue($sep)) {
7797 protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
7798 protected function libJoin($args)
7800 list($list1, $list2, $sep, $bracketed) = $args;
7802 $list1 = $this->coerceList($list1, ' ', true);
7803 $list2 = $this->coerceList($list2, ' ', true);
7804 $sep = $this->listSeparatorForJoin($list1, $sep);
7806 if ($bracketed === static::$true) {
7808 } elseif ($bracketed === static::$false) {
7810 } elseif ($bracketed === [Type
::T_KEYWORD
, 'auto']) {
7811 $bracketed = 'auto';
7812 } elseif ($bracketed === static::$null) {
7815 $bracketed = $this->compileValue($bracketed);
7816 $bracketed = ! ! $bracketed;
7818 if ($bracketed === true) {
7823 if ($bracketed === 'auto') {
7826 if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
7831 $res = [Type
::T_LIST
, $sep, array_merge($list1[2], $list2[2])];
7833 if (isset($list1['enclosing'])) {
7834 $res['enlcosing'] = $list1['enclosing'];
7838 $res['enclosing'] = 'bracket';
7844 protected static $libAppend = ['list', 'val', 'separator:null'];
7845 protected function libAppend($args)
7847 list($list1, $value, $sep) = $args;
7849 $list1 = $this->coerceList($list1, ' ', true);
7850 $sep = $this->listSeparatorForJoin($list1, $sep);
7851 $res = [Type
::T_LIST
, $sep, array_merge($list1[2], [$value])];
7853 if (isset($list1['enclosing'])) {
7854 $res['enclosing'] = $list1['enclosing'];
7860 protected function libZip($args)
7862 foreach ($args as $key => $arg) {
7863 $args[$key] = $this->coerceList($arg);
7867 $firstList = array_shift($args);
7869 $result = [Type
::T_LIST
, ',', $lists];
7870 if (! \
is_null($firstList)) {
7871 foreach ($firstList[2] as $key => $item) {
7872 $list = [Type
::T_LIST
, '', [$item]];
7874 foreach ($args as $arg) {
7875 if (isset($arg[2][$key])) {
7876 $list[2][] = $arg[2][$key];
7885 $result[2] = $lists;
7887 $result['enclosing'] = 'parent';
7893 protected static $libTypeOf = ['value'];
7894 protected function libTypeOf($args)
7898 switch ($value[0]) {
7899 case Type
::T_KEYWORD
:
7900 if ($value === static::$true ||
$value === static::$false) {
7904 if ($this->coerceColor($value)) {
7909 case Type
::T_FUNCTION
:
7912 case Type
::T_FUNCTION_REFERENCE
:
7916 if (isset($value[3]) && $value[3]) {
7926 protected static $libUnit = ['number'];
7927 protected function libUnit($args)
7931 if ($num instanceof Number
) {
7932 return [Type
::T_STRING
, '"', [$num->unitStr()]];
7938 protected static $libUnitless = ['number'];
7939 protected function libUnitless($args)
7943 return $value instanceof Number
&& $value->unitless();
7946 protected static $libComparable = [
7947 ['number1', 'number2'],
7948 ['number-1', 'number-2']
7950 protected function libComparable($args)
7952 list($number1, $number2) = $args;
7955 ! $number1 instanceof Number ||
7956 ! $number2 instanceof Number
7958 throw $this->error('Invalid argument(s) for "comparable"');
7961 return $number1->isComparableTo($number2);
7964 protected static $libStrIndex = ['string', 'substring'];
7965 protected function libStrIndex($args)
7967 $string = $this->assertString($args[0], 'string');
7968 $stringContent = $this->compileStringContent($string);
7970 $substring = $this->assertString($args[1], 'substring');
7971 $substringContent = $this->compileStringContent($substring);
7973 if (! \
strlen($substringContent)) {
7976 $result = Util
::mbStrpos($stringContent, $substringContent);
7979 return $result === false ?
static::$null : new Number($result +
1, '');
7982 protected static $libStrInsert = ['string', 'insert', 'index'];
7983 protected function libStrInsert($args)
7985 $string = $this->assertString($args[0], 'string');
7986 $stringContent = $this->compileStringContent($string);
7988 $insert = $this->assertString($args[1], 'insert');
7989 $insertContent = $this->compileStringContent($insert);
7991 $index = $this->assertInteger($args[2], 'index');
7993 $index = $index - 1;
7996 $index = Util
::mbStrlen($stringContent) +
1 +
$index;
8000 Util
::mbSubstr($stringContent, 0, $index),
8002 Util
::mbSubstr($stringContent, $index)
8008 protected static $libStrLength = ['string'];
8009 protected function libStrLength($args)
8011 $string = $this->assertString($args[0], 'string');
8012 $stringContent = $this->compileStringContent($string);
8014 return new Number(Util
::mbStrlen($stringContent), '');
8017 protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
8018 protected function libStrSlice($args)
8020 if (isset($args[2]) && ! $args[2][1]) {
8021 return static::$nullString;
8024 $string = $this->coerceString($args[0]);
8025 $stringContent = $this->compileStringContent($string);
8027 $start = (int) $args[1][1];
8033 $end = isset($args[2]) ?
(int) $args[2][1] : -1;
8034 $length = $end < 0 ?
$end +
1 : ($end > 0 ?
$end - $start : $end);
8036 $string[2] = $length
8037 ?
[substr($stringContent, $start, $length)]
8038 : [substr($stringContent, $start)];
8043 protected static $libToLowerCase = ['string'];
8044 protected function libToLowerCase($args)
8046 $string = $this->coerceString($args[0]);
8047 $stringContent = $this->compileStringContent($string);
8049 $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
8054 protected static $libToUpperCase = ['string'];
8055 protected function libToUpperCase($args)
8057 $string = $this->coerceString($args[0]);
8058 $stringContent = $this->compileStringContent($string);
8060 $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
8066 * Apply a filter on a string content, only on ascii chars
8067 * let extended chars untouched
8069 * @param string $stringContent
8070 * @param callable $filter
8073 protected function stringTransformAsciiOnly($stringContent, $filter)
8075 $mblength = Util
::mbStrlen($stringContent);
8076 if ($mblength === strlen($stringContent)) {
8077 return $filter($stringContent);
8079 $filteredString = "";
8080 for ($i = 0; $i < $mblength; $i++
) {
8081 $char = Util
::mbSubstr($stringContent, $i, 1);
8082 if (strlen($char) > 1) {
8083 $filteredString .= $char;
8085 $filteredString .= $filter($char);
8089 return $filteredString;
8092 protected static $libFeatureExists = ['feature'];
8093 protected function libFeatureExists($args)
8095 $string = $this->coerceString($args[0]);
8096 $name = $this->compileStringContent($string);
8098 return $this->toBool(
8099 \array_key_exists
($name, $this->registeredFeatures
) ?
$this->registeredFeatures
[$name] : false
8103 protected static $libFunctionExists = ['name'];
8104 protected function libFunctionExists($args)
8106 $string = $this->coerceString($args[0]);
8107 $name = $this->compileStringContent($string);
8109 // user defined functions
8110 if ($this->has(static::$namespaces['function'] . $name)) {
8114 $name = $this->normalizeName($name);
8116 if (isset($this->userFunctions
[$name])) {
8120 // built-in functions
8121 $f = $this->getBuiltinFunction($name);
8123 return $this->toBool(\
is_callable($f));
8126 protected static $libGlobalVariableExists = ['name'];
8127 protected function libGlobalVariableExists($args)
8129 $string = $this->coerceString($args[0]);
8130 $name = $this->compileStringContent($string);
8132 return $this->has($name, $this->rootEnv
);
8135 protected static $libMixinExists = ['name'];
8136 protected function libMixinExists($args)
8138 $string = $this->coerceString($args[0]);
8139 $name = $this->compileStringContent($string);
8141 return $this->has(static::$namespaces['mixin'] . $name);
8144 protected static $libVariableExists = ['name'];
8145 protected function libVariableExists($args)
8147 $string = $this->coerceString($args[0]);
8148 $name = $this->compileStringContent($string);
8150 return $this->has($name);
8154 * Workaround IE7's content counter bug.
8156 * @param array $args
8160 protected function libCounter($args)
8162 $list = array_map([$this, 'compileValue'], $args);
8164 return [Type
::T_STRING
, '', ['counter(' . implode(',', $list) . ')']];
8167 protected static $libRandom = ['limit:null'];
8168 protected function libRandom($args)
8170 if (isset($args[0]) & $args[0] !== static::$null) {
8171 $n = $this->assertNumber($args[0])->getDimension();
8174 throw $this->error("\$limit must be greater than or equal to 1");
8177 if (round($n - \
intval($n), Number
::PRECISION
) > 0) {
8178 throw $this->error("Expected \$limit to be an integer but got $n for `random`");
8181 return new Number(mt_rand(1, \
intval($n)), '');
8184 $max = mt_getrandmax();
8185 return new Number(mt_rand(0, $max - 1) / $max, '');
8188 protected function libUniqueId()
8193 $id = PHP_INT_SIZE
=== 4
8194 ?
mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) %
10000000, 7, '0', STR_PAD_LEFT
)
8195 : mt_rand(0, pow(36, 8));
8198 $id +
= mt_rand(0, 10) +
1;
8200 return [Type
::T_STRING
, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT
)]];
8203 protected function inspectFormatValue($value, $force_enclosing_display = false)
8205 if ($value === static::$null) {
8206 $value = [Type
::T_KEYWORD
, 'null'];
8209 $stringValue = [$value];
8211 if ($value[0] === Type
::T_LIST
) {
8212 if (end($value[2]) === static::$null) {
8213 array_pop($value[2]);
8214 $value[2][] = [Type
::T_STRING
, '', ['']];
8215 $force_enclosing_display = true;
8219 ! empty($value['enclosing']) &&
8220 ($force_enclosing_display ||
8221 ($value['enclosing'] === 'bracket') ||
8222 ! \
count($value[2]))
8224 $value['enclosing'] = 'forced_' . $value['enclosing'];
8225 $force_enclosing_display = true;
8228 foreach ($value[2] as $k => $listelement) {
8229 $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
8232 $stringValue = [$value];
8235 return [Type
::T_STRING
, '', $stringValue];
8238 protected static $libInspect = ['value'];
8239 protected function libInspect($args)
8243 return $this->inspectFormatValue($value);
8247 * Preprocess selector args
8251 * @return array|boolean
8253 protected function getSelectorArg($arg, $varname = null, $allowParent = false)
8255 static $parser = null;
8257 if (\
is_null($parser)) {
8258 $parser = $this->parserFactory(__METHOD__
);
8261 if (! $this->checkSelectorArgType($arg)) {
8262 $var_display = ($varname ?
' $' . $varname . ':' : '');
8263 $var_value = $this->compileValue($arg);
8264 throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string,"
8265 . " a list of strings, or a list of lists of strings");
8268 $arg = $this->libUnquote([$arg]);
8269 $arg = $this->compileValue($arg);
8271 $parsedSelector = [];
8273 if ($parser->parseSelector($arg, $parsedSelector, true)) {
8274 $selector = $this->evalSelectors($parsedSelector);
8275 $gluedSelector = $this->glueFunctionSelectors($selector);
8277 if (! $allowParent) {
8278 foreach ($gluedSelector as $selector) {
8279 foreach ($selector as $s) {
8280 if (in_array(static::$selfSelector, $s)) {
8281 $var_display = ($varname ?
' $' . $varname . ':' : '');
8282 throw $this->error("Error:{$var_display} Parent selectors aren't allowed here.");
8288 return $gluedSelector;
8291 $var_display = ($varname ?
' $' . $varname . ':' : '');
8292 throw $this->error("Error:{$var_display} expected more input, invalid selector.");
8296 * Check variable type for getSelectorArg() function
8298 * @param int $maxDepth
8301 protected function checkSelectorArgType($arg, $maxDepth = 2)
8303 if ($arg[0] === Type
::T_LIST
&& $maxDepth > 0) {
8304 foreach ($arg[2] as $elt) {
8305 if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
8311 if (!in_array($arg[0], [Type
::T_STRING
, Type
::T_KEYWORD
])) {
8318 * Postprocess selector to output in right format
8320 * @param array $selectors
8324 protected function formatOutputSelector($selectors)
8326 $selectors = $this->collapseSelectors($selectors, true);
8331 protected static $libIsSuperselector = ['super', 'sub'];
8332 protected function libIsSuperselector($args)
8334 list($super, $sub) = $args;
8336 $super = $this->getSelectorArg($super, 'super');
8337 $sub = $this->getSelectorArg($sub, 'sub');
8339 return $this->isSuperSelector($super, $sub);
8343 * Test a $super selector again $sub
8345 * @param array $super
8350 protected function isSuperSelector($super, $sub)
8352 // one and only one selector for each arg
8354 throw $this->error('Invalid super selector for isSuperSelector()');
8358 throw $this->error('Invalid sub selector for isSuperSelector()');
8361 if (count($sub) > 1) {
8362 foreach ($sub as $s) {
8363 if (! $this->isSuperSelector($super, [$s])) {
8370 if (count($super) > 1) {
8371 foreach ($super as $s) {
8372 if ($this->isSuperSelector([$s], $sub)) {
8379 $super = reset($super);
8383 $nextMustMatch = false;
8385 foreach ($super as $node) {
8388 array_walk_recursive(
8390 function ($value, $key) use (&$compound) {
8391 $compound .= $value;
8395 if ($this->isImmediateRelationshipCombinator($compound)) {
8396 if ($node !== $sub[$i]) {
8400 $nextMustMatch = true;
8403 while ($i < \
count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
8404 if ($nextMustMatch) {
8411 if ($i >= \
count($sub)) {
8415 $nextMustMatch = false;
8424 * Test a part of super selector again a part of sub selector
8426 * @param array $superParts
8427 * @param array $subParts
8431 protected function isSuperPart($superParts, $subParts)
8435 foreach ($superParts as $superPart) {
8436 while ($i < \
count($subParts) && $subParts[$i] !== $superPart) {
8440 if ($i >= \
count($subParts)) {
8450 protected static $libSelectorAppend = ['selector...'];
8451 protected function libSelectorAppend($args)
8453 // get the selector... list
8454 $args = reset($args);
8457 if (\
count($args) < 1) {
8458 throw $this->error('selector-append() needs at least 1 argument');
8462 foreach ($args as $arg) {
8463 $selectors[] = $this->getSelectorArg($arg, 'selector');
8466 return $this->formatOutputSelector($this->selectorAppend($selectors));
8470 * Append parts of the last selector in the list to the previous, recursively
8472 * @param array $selectors
8476 * @throws \ScssPhp\ScssPhp\Exception\CompilerException
8478 protected function selectorAppend($selectors)
8480 $lastSelectors = array_pop($selectors);
8482 if (! $lastSelectors) {
8483 throw $this->error('Invalid selector list in selector-append()');
8486 while (\
count($selectors)) {
8487 $previousSelectors = array_pop($selectors);
8489 if (! $previousSelectors) {
8490 throw $this->error('Invalid selector list in selector-append()');
8493 // do the trick, happening $lastSelector to $previousSelector
8496 foreach ($lastSelectors as $lastSelector) {
8497 $previous = $previousSelectors;
8499 foreach ($lastSelector as $lastSelectorParts) {
8500 foreach ($lastSelectorParts as $lastSelectorPart) {
8501 foreach ($previous as $i => $previousSelector) {
8502 foreach ($previousSelector as $j => $previousSelectorParts) {
8503 $previous[$i][$j][] = $lastSelectorPart;
8509 foreach ($previous as $ps) {
8514 $lastSelectors = $appended;
8517 return $lastSelectors;
8520 protected static $libSelectorExtend = [
8521 ['selector', 'extendee', 'extender'],
8522 ['selectors', 'extendee', 'extender']
8524 protected function libSelectorExtend($args)
8526 list($selectors, $extendee, $extender) = $args;
8528 $selectors = $this->getSelectorArg($selectors, 'selector');
8529 $extendee = $this->getSelectorArg($extendee, 'extendee');
8530 $extender = $this->getSelectorArg($extender, 'extender');
8532 if (! $selectors ||
! $extendee ||
! $extender) {
8533 throw $this->error('selector-extend() invalid arguments');
8536 $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
8538 return $this->formatOutputSelector($extended);
8541 protected static $libSelectorReplace = [
8542 ['selector', 'original', 'replacement'],
8543 ['selectors', 'original', 'replacement']
8545 protected function libSelectorReplace($args)
8547 list($selectors, $original, $replacement) = $args;
8549 $selectors = $this->getSelectorArg($selectors, 'selector');
8550 $original = $this->getSelectorArg($original, 'original');
8551 $replacement = $this->getSelectorArg($replacement, 'replacement');
8553 if (! $selectors ||
! $original ||
! $replacement) {
8554 throw $this->error('selector-replace() invalid arguments');
8557 $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
8559 return $this->formatOutputSelector($replaced);
8563 * Extend/replace in selectors
8564 * used by selector-extend and selector-replace that use the same logic
8566 * @param array $selectors
8567 * @param array $extendee
8568 * @param array $extender
8569 * @param boolean $replace
8573 protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
8575 $saveExtends = $this->extends;
8576 $saveExtendsMap = $this->extendsMap
;
8578 $this->extends = [];
8579 $this->extendsMap
= [];
8581 foreach ($extendee as $es) {
8582 // only use the first one
8583 $this->pushExtends(reset($es), $extender, null);
8588 foreach ($selectors as $selector) {
8590 $extended[] = $selector;
8593 $n = \
count($extended);
8595 $this->matchExtends($selector, $extended);
8597 // if didnt match, keep the original selector if we are in a replace operation
8598 if ($replace && \
count($extended) === $n) {
8599 $extended[] = $selector;
8603 $this->extends = $saveExtends;
8604 $this->extendsMap
= $saveExtendsMap;
8609 protected static $libSelectorNest = ['selector...'];
8610 protected function libSelectorNest($args)
8612 // get the selector... list
8613 $args = reset($args);
8616 if (\
count($args) < 1) {
8617 throw $this->error('selector-nest() needs at least 1 argument');
8621 foreach ($args as $arg) {
8622 $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
8627 foreach ($selectorsMap as $selectors) {
8628 $env = new Environment();
8629 $env->selectors
= $selectors;
8634 $envs = array_reverse($envs);
8635 $env = $this->extractEnv($envs);
8636 $outputSelectors = $this->multiplySelectors($env);
8638 return $this->formatOutputSelector($outputSelectors);
8641 protected static $libSelectorParse = [
8645 protected function libSelectorParse($args)
8647 $selectors = reset($args);
8648 $selectors = $this->getSelectorArg($selectors, 'selector');
8650 return $this->formatOutputSelector($selectors);
8653 protected static $libSelectorUnify = ['selectors1', 'selectors2'];
8654 protected function libSelectorUnify($args)
8656 list($selectors1, $selectors2) = $args;
8658 $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
8659 $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
8661 if (! $selectors1 ||
! $selectors2) {
8662 throw $this->error('selector-unify() invalid arguments');
8665 // only consider the first compound of each
8666 $compound1 = reset($selectors1);
8667 $compound2 = reset($selectors2);
8669 // unify them and that's it
8670 $unified = $this->unifyCompoundSelectors($compound1, $compound2);
8672 return $this->formatOutputSelector($unified);
8676 * The selector-unify magic as its best
8677 * (at least works as expected on test cases)
8679 * @param array $compound1
8680 * @param array $compound2
8682 * @return array|mixed
8684 protected function unifyCompoundSelectors($compound1, $compound2)
8686 if (! \
count($compound1)) {
8690 if (! \
count($compound2)) {
8694 // check that last part are compatible
8695 $lastPart1 = array_pop($compound1);
8696 $lastPart2 = array_pop($compound2);
8697 $last = $this->mergeParts($lastPart1, $lastPart2);
8703 $unifiedCompound = [$last];
8704 $unifiedSelectors = [$unifiedCompound];
8707 while (\
count($compound1) || \
count($compound2)) {
8708 $part1 = end($compound1);
8709 $part2 = end($compound2);
8711 if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
8712 list($compound2, $part2, $after2) = $match2;
8715 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
8718 $c = $this->mergeParts($part1, $part2);
8719 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8721 $part1 = $part2 = null;
8723 array_pop($compound1);
8726 if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
8727 list($compound1, $part1, $after1) = $match1;
8730 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
8733 $c = $this->mergeParts($part2, $part1);
8734 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8736 $part1 = $part2 = null;
8738 array_pop($compound2);
8743 if ($part1 && $part2) {
8744 array_pop($compound1);
8745 array_pop($compound2);
8747 $s = $this->prependSelectors($unifiedSelectors, [$part2]);
8748 $new = array_merge($new, $this->prependSelectors($s, [$part1]));
8749 $s = $this->prependSelectors($unifiedSelectors, [$part1]);
8750 $new = array_merge($new, $this->prependSelectors($s, [$part2]));
8752 array_pop($compound1);
8754 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
8756 array_pop($compound2);
8758 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
8762 $unifiedSelectors = $new;
8766 return $unifiedSelectors;
8770 * Prepend each selector from $selectors with $parts
8772 * @param array $selectors
8773 * @param array $parts
8777 protected function prependSelectors($selectors, $parts)
8781 foreach ($selectors as $compoundSelector) {
8782 array_unshift($compoundSelector, $parts);
8784 $new[] = $compoundSelector;
8791 * Try to find a matching part in a compound:
8792 * - with same html tag name
8793 * - with some class or id or something in common
8795 * @param array $part
8796 * @param array $compound
8798 * @return array|false
8800 protected function matchPartInCompound($part, $compound)
8802 $partTag = $this->findTagName($part);
8803 $before = $compound;
8806 // try to find a match by tag name first
8807 while (\
count($before)) {
8808 $p = array_pop($before);
8810 if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
8811 return [$before, $p, $after];
8817 // try again matching a non empty intersection and a compatible tagname
8818 $before = $compound;
8821 while (\
count($before)) {
8822 $p = array_pop($before);
8824 if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
8825 if (\
count(array_intersect($part, $p))) {
8826 return [$before, $p, $after];
8837 * Merge two part list taking care that
8838 * - the html tag is coming first - if any
8839 * - the :something are coming last
8841 * @param array $parts1
8842 * @param array $parts2
8846 protected function mergeParts($parts1, $parts2)
8848 $tag1 = $this->findTagName($parts1);
8849 $tag2 = $this->findTagName($parts2);
8850 $tag = $this->checkCompatibleTags($tag1, $tag2);
8852 // not compatible tags
8853 if ($tag === false) {
8859 $parts1 = array_diff($parts1, [$tag1]);
8863 $parts2 = array_diff($parts2, [$tag2]);
8867 $mergedParts = array_merge($parts1, $parts2);
8868 $mergedOrderedParts = [];
8870 foreach ($mergedParts as $part) {
8871 if (strpos($part, ':') === 0) {
8872 $mergedOrderedParts[] = $part;
8876 $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
8877 $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
8880 array_unshift($mergedParts, $tag);
8883 return $mergedParts;
8887 * Check the compatibility between two tag names:
8888 * if both are defined they should be identical or one has to be '*'
8890 * @param string $tag1
8891 * @param string $tag2
8893 * @return array|false
8895 protected function checkCompatibleTags($tag1, $tag2)
8897 $tags = [$tag1, $tag2];
8898 $tags = array_unique($tags);
8899 $tags = array_filter($tags);
8901 if (\
count($tags) > 1) {
8902 $tags = array_diff($tags, ['*']);
8905 // not compatible nodes
8906 if (\
count($tags) > 1) {
8914 * Find the html tag name in a selector parts list
8916 * @param array $parts
8918 * @return mixed|string
8920 protected function findTagName($parts)
8922 foreach ($parts as $part) {
8923 if (! preg_match('/^[\[.:#%_-]/', $part)) {
8931 protected static $libSimpleSelectors = ['selector'];
8932 protected function libSimpleSelectors($args)
8934 $selector = reset($args);
8935 $selector = $this->getSelectorArg($selector, 'selector');
8937 // remove selectors list layer, keeping the first one
8938 $selector = reset($selector);
8940 // remove parts list layer, keeping the first part
8941 $part = reset($selector);
8945 foreach ($part as $p) {
8946 $listParts[] = [Type
::T_STRING
, '', [$p]];
8949 return [Type
::T_LIST
, ',', $listParts];
8952 protected static $libScssphpGlob = ['pattern'];
8953 protected function libScssphpGlob($args)
8955 $string = $this->coerceString($args[0]);
8956 $pattern = $this->compileStringContent($string);
8957 $matches = glob($pattern);
8960 foreach ($matches as $match) {
8961 if (! is_file($match)) {
8965 $listParts[] = [Type
::T_STRING
, '"', [$match]];
8968 return [Type
::T_LIST
, ',', $listParts];