MDL-70301 lib: Upgrade scssphp to 1.4.1
[moodle.git] / lib / scssphp / Compiler.php
blob0997814eef0d665ea8b011ae68f187408156898d
1 <?php
3 /**
4 * SCSSPHP
6 * @copyright 2012-2020 Leaf Corcoran
8 * @license http://opensource.org/licenses/MIT MIT
10 * @link http://scssphp.github.io/scssphp
13 namespace ScssPhp\ScssPhp;
15 use ScssPhp\ScssPhp\Base\Range;
16 use ScssPhp\ScssPhp\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;
27 /**
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.
41 * In summary:
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
46 * time.
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.
54 /**
55 * SCSS compiler
57 * @author Leaf Corcoran <leafot@gmail.com>
59 class Compiler
61 /**
62 * @deprecated
64 const LINE_COMMENTS = 1;
65 /**
66 * @deprecated
68 const DEBUG_INFO = 2;
70 /**
71 * @deprecated
73 const WITH_RULE = 1;
74 /**
75 * @deprecated
77 const WITH_MEDIA = 2;
78 /**
79 * @deprecated
81 const WITH_SUPPORTS = 4;
82 /**
83 * @deprecated
85 const WITH_ALL = 7;
87 const SOURCE_MAP_NONE = 0;
88 const SOURCE_MAP_INLINE = 1;
89 const SOURCE_MAP_FILE = 2;
91 /**
92 * @var array<string, string>
94 protected static $operatorNames = [
95 '+' => 'add',
96 '-' => 'sub',
97 '*' => 'mul',
98 '/' => 'div',
99 '%' => 'mod',
101 '==' => 'eq',
102 '!=' => 'neq',
103 '<' => 'lt',
104 '>' => 'gt',
106 '<=' => 'lte',
107 '>=' => 'gte',
111 * @var array<string, string>
113 protected static $namespaces = [
114 'special' => '%',
115 'mixin' => '@',
116 'function' => '^',
119 public static $true = [Type::T_KEYWORD, 'true'];
120 public static $false = [Type::T_KEYWORD, 'false'];
121 /** @deprecated */
122 public static $NaN = [Type::T_KEYWORD, 'NaN'];
123 /** @deprecated */
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 = [];
144 * @var string[]
146 protected $importedFiles = [];
147 protected $userFunctions = [];
148 protected $registeredVars = [];
150 * @var array<string, bool>
152 protected $registeredFeatures = [
153 'extend-selector-pseudoclass' => false,
154 'at-error' => true,
155 'units-level-3' => true,
156 'global-variable-shadowing' => false,
160 * @var string|null
162 protected $encoding = null;
164 * @deprecated
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;
181 * @var Environment
183 protected $rootEnv;
185 * @var OutputBlock|null
187 protected $rootBlock;
190 * @var \ScssPhp\ScssPhp\Compiler\Environment
192 protected $env;
194 * @var OutputBlock|null
196 protected $scope;
198 * @var Environment|null
200 protected $storeEnv;
202 * @var bool|null
204 protected $charsetSeen;
206 * @var array<int, string>
208 protected $sourceNames;
211 * @var Cache|null
213 protected $cache;
216 * @var int
218 protected $indentLevel;
220 * @var array[]
222 protected $extends;
224 * @var array<string, int[]>
226 protected $extendsMap;
228 * @var array<string, int>
230 protected $parsedFiles;
232 * @var Parser|null
234 protected $parser;
236 * @var int|null
238 protected $sourceIndex;
240 * @var int|null
242 protected $sourceLine;
244 * @var int|null
246 protected $sourceColumn;
248 * @var resource
250 protected $stderr;
252 * @var bool|null
254 protected $shouldEvaluate;
256 * @var null
257 * @deprecated
259 protected $ignoreErrors;
261 * @var bool
263 protected $ignoreCallStackMessage = false;
266 * @var array[]
268 protected $callStack = [];
271 * The directory of the currently processed file
273 * @var string|null
275 private $currentDirectory;
278 * The directory of the input file
280 * @var string
282 private $rootDirectory;
284 private $legacyCwdImportPath = true;
287 * Constructor
289 * @param array|null $cacheOptions
291 public function __construct($cacheOptions = null)
293 $this->parsedFiles = [];
294 $this->sourceNames = [];
296 if ($cacheOptions) {
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()
310 $options = [
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,
321 return $options;
325 * Set an alternative error output stream, for testing purpose only
327 * @param resource $handle
329 * @return void
331 public function setErrorOuput($handle)
333 $this->stderr = $handle;
337 * Compile scss
339 * @api
341 * @param string $code
342 * @param string $path
344 * @return string
346 public function compile($code, $path = null)
348 if ($this->cache) {
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) {
357 unset($cache);
358 break;
362 if (isset($cache)) {
363 return $cache['out'];
369 $this->indentLevel = -1;
370 $this->extends = [];
371 $this->extendsMap = [];
372 $this->sourceIndex = null;
373 $this->sourceLine = null;
374 $this->sourceColumn = null;
375 $this->env = null;
376 $this->scope = 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;
386 } else {
387 $this->currentDirectory = null;
388 $this->rootDirectory = getcwd();
391 try {
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);
402 $this->popEnv();
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);
417 $prefix = '';
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));
433 break;
435 case self::SOURCE_MAP_FILE:
436 $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
437 break;
440 $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
442 } catch (SassScriptException $e) {
443 throw $this->error($e->getMessage());
446 if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
447 $v = [
448 'dependencies' => $this->getParsedFiles(),
449 'out' => &$out,
452 $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
455 return $out;
459 * Instantiate parser
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!
472 $cssOnly = false;
474 if (substr($path, '-4') === '.css') {
475 $cssOnly = true;
478 $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
480 $this->sourceNames[] = $path;
481 $this->addParsedFile($path);
483 return $parser;
487 * Is self extend?
489 * @param array $target
490 * @param array $origin
492 * @return boolean
494 protected function isSelfExtend($target, $origin)
496 foreach ($origin as $sel) {
497 if (\in_array($target, $sel)) {
498 return true;
502 return false;
506 * Push extends
508 * @param array $target
509 * @param array $origin
510 * @param array|null $block
512 * @return void
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;
522 } else {
523 $this->extendsMap[$part] = [$i];
529 * Make output block
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();
539 $out->type = $type;
540 $out->lines = [];
541 $out->children = [];
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;
550 } else {
551 $out->sourceName = null;
552 $out->sourceLine = null;
553 $out->sourceColumn = null;
556 return $out;
560 * Compile root
562 * @param \ScssPhp\ScssPhp\Block $rootBlock
564 * @return void
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
578 * @return void
580 protected function missingSelectors()
582 foreach ($this->extends as $extend) {
583 if (isset($extend[3])) {
584 continue;
587 list($target, $origin, $block) = $extend;
589 // ignore if !optional
590 if ($block[2]) {
591 continue;
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.");
603 * Flatten selectors
605 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
606 * @param string $parentKey
608 * @return void
610 protected function flattenSelectors(OutputBlock $block, $parentKey = null)
612 if ($block->selectors) {
613 $selectors = [];
615 foreach ($block->selectors as $s) {
616 $selectors[] = $s;
618 if (! \is_array($s)) {
619 continue;
622 // check extends
623 if (! empty($this->extendsMap)) {
624 $this->matchExtends($s, $selectors);
626 // remove duplicates
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;
645 continue;
648 $block->selectors[] = $this->compileSelector($selector);
651 if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
652 unset($block->parent->children[$parentKey]);
654 return;
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
668 * @return array
670 protected function glueFunctionSelectors($parts)
672 $new = [];
674 foreach ($parts as $part) {
675 if (\is_array($part)) {
676 $part = $this->glueFunctionSelectors($part);
677 $new[] = $part;
678 } else {
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
681 if (
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;
689 } else {
690 $new[] = $part;
695 return $new;
699 * Match extends
701 * @param array $selector
702 * @param array $out
703 * @param integer $from
704 * @param boolean $initial
706 * @return void
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)) {
714 return;
717 $outRecurs = [];
719 foreach ($selector as $i => $part) {
720 if ($i < $from) {
721 continue;
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))) {
729 continue 2;
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) {
742 $k = 0;
744 // remove shared parts
745 if (\count($new) > 1) {
746 while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
747 $k++;
751 if (\count($nonBreakableBefore) && $k === \count($new)) {
752 $k--;
755 $replacement = [];
756 $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
758 for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
759 $slice = [];
761 foreach ($tempReplacement[$l] as $chunk) {
762 if (! \in_array($chunk, $slice)) {
763 $slice[] = $chunk;
767 array_unshift($replacement, $slice);
769 if (! $this->isImmediateRelationshipCombinator(end($slice))) {
770 break;
774 $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
776 // Merge shared direct relationships.
777 $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
779 $result = array_merge(
780 $before,
781 $mergedBefore,
782 $replacement,
783 $after
786 if ($result === $selector) {
787 continue;
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);
797 } else {
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(
809 $preSharedParts,
810 $betweenSharedParts,
811 $postSharedParts,
812 $nonBreakabl2,
813 $nonBreakableBefore,
814 $replacement,
815 $after
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
837 * @return boolean
839 protected function isPseudoSelector($part, &$matches)
841 if (
842 strpos($part, ':') === 0 &&
843 preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
845 return true;
848 return false;
852 * Push extended selector except if
853 * - this is a pseudo selector
854 * - same as previous
855 * - in a white list
856 * in this case we merge the pseudo selector content
858 * @param array $out
859 * @param array $extended
861 * @return void
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);
869 if (
870 $this->isPseudoSelector($part, $matchesExtended) &&
871 \in_array($matchesExtended[1], [ 'slotted' ])
873 $prev = end($out);
874 $prev = $this->glueFunctionSelectors($prev);
876 if (\count($prev) === 1 && \count(reset($prev)) === 1) {
877 $single = reset($prev);
878 $part = reset($single);
880 if (
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 ]];
888 array_pop($out);
893 $out[] = $extended;
897 * Match extends single
899 * @param array $rawSingle
900 * @param array $outOrigin
901 * @param boolean $initial
903 * @return boolean
905 protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
907 $counts = [];
908 $single = [];
910 // simple usual cases, no need to do the whole trick
911 if (\in_array($rawSingle, [['>'],['+'],['~']])) {
912 return false;
915 foreach ($rawSingle as $part) {
916 // matches Number
917 if (! \is_string($part)) {
918 return false;
921 if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
922 $single[\count($single) - 1] .= $part;
923 } else {
924 $single[] = $part;
928 $extendingDecoratedTag = false;
930 if (\count($single) > 1) {
931 $matches = null;
932 $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
935 $outOrigin = [];
936 $found = 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;
945 if (
946 $initial &&
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) {
955 $subExtended = [];
956 $this->matchExtends($subSelector, $subExtended, 0, false);
958 if ($subExtended) {
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 ];
970 $found = true;
977 foreach ($counts as $idx => $count) {
978 list($target, $origin, /* $block */) = $this->extends[$idx];
980 $origin = $this->glueFunctionSelectors($origin);
982 // check count
983 if ($count !== \count($target)) {
984 continue;
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) {
994 return false;
997 $replacement = end($new);
999 // Extending a decorated tag with another tag is not possible.
1000 if (
1001 $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
1002 preg_match('/^[a-z0-9]+$/i', $replacement[0])
1004 unset($origin[$j]);
1005 continue;
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);
1017 $found = true;
1020 return $found;
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
1029 * the rest.
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)
1037 $parents = [];
1038 $children = [];
1040 $j = $i = \count($fragment);
1042 for (;;) {
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])) {
1048 break;
1051 $j -= 2;
1054 return [$parents, $children];
1058 * Combine selector single
1060 * @param array $base
1061 * @param array $other
1063 * @return array
1065 protected function combineSelectorSingle($base, $other)
1067 $tag = [];
1068 $out = [];
1069 $wasTag = false;
1070 $pseudo = [];
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)) {
1081 $out[] = $part;
1082 $wasTag = false;
1083 } elseif (preg_match('/^[\.#]/', $part)) {
1084 array_unshift($out, $part);
1085 $wasTag = false;
1086 } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
1087 $tag[] = $part;
1088 $wasTag = true;
1089 } elseif ($wasTag) {
1090 $tag[\count($tag) - 1] .= $part;
1091 } else {
1092 array_unshift($out, $part);
1094 $rang--;
1098 if (\count($tag)) {
1099 array_unshift($out, $tag[0]);
1102 while (\count($pseudo)) {
1103 $out[] = array_shift($pseudo);
1106 return $out;
1110 * Compile media
1112 * @param \ScssPhp\ScssPhp\Block $media
1114 * @return void
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
1134 $needsWrap = false;
1136 foreach ($media->children as $child) {
1137 $type = $child[0];
1139 if (
1140 $type !== Type::T_BLOCK &&
1141 $type !== Type::T_MEDIA &&
1142 $type !== Type::T_DIRECTIVE &&
1143 $type !== Type::T_IMPORT
1145 $needsWrap = true;
1146 break;
1150 if ($needsWrap) {
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;
1169 $this->popEnv();
1173 * Media parent
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) {
1183 break;
1186 $scope = $scope->parent;
1189 return $scope;
1193 * Compile directive
1195 * @param \ScssPhp\ScssPhp\Block|array $block
1196 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1198 * @return void
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]);
1217 } else {
1218 $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
1220 } else {
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]);
1230 } else {
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);
1253 * Compile at-root
1255 * @param \ScssPhp\ScssPhp\Block $block
1257 * @return void
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;
1284 if (
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);
1304 $this->popEnv();
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 = [];
1319 $childStash = [];
1321 if ($scope->type === TYPE::T_ROOT) {
1322 return $scope;
1325 // start from the root
1326 while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1327 array_unshift($childStash, $scope);
1328 $scope = $scope->parent;
1331 for (;;) {
1332 if (! $scope) {
1333 break;
1336 if ($this->isWith($scope, $with, $without)) {
1337 $s = clone $scope;
1338 $s->children = [];
1339 $s->lines = [];
1340 $s->parent = null;
1342 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1343 $s->selectors = [];
1346 $filteredScopes[] = $s;
1349 if (\count($childStash)) {
1350 $scope = array_shift($childStash);
1351 } elseif ($scope->children) {
1352 $scope = end($scope->children);
1353 } else {
1354 $scope = null;
1358 if (! \count($filteredScopes)) {
1359 return $this->rootBlock;
1362 $newScope = array_shift($filteredScopes);
1363 $newScope->parent = $this->rootBlock;
1365 $this->rootBlock->children[] = $newScope;
1367 $p = &$newScope;
1369 while (\count($filteredScopes)) {
1370 $s = array_shift($filteredScopes);
1371 $s->parent = $p;
1372 $p->children[] = $s;
1373 $newScope = &$p->children[0];
1374 $p = &$p->children[0];
1377 return $newScope;
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);
1401 return $scope;
1405 * Find a selector by the depth node in the scope
1407 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1408 * @param integer $depth
1410 * @return array
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)) {
1421 return $s;
1426 return [];
1430 * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1432 * @param array $withCondition
1434 * @return array
1436 protected function compileWith($withCondition)
1438 // just compile what we have in 2 lists
1439 $with = [];
1440 $without = ['rule' => true];
1442 if ($withCondition) {
1443 if ($withCondition[0] === Type::T_INTERPOLATE) {
1444 $w = $this->compileValue($withCondition);
1446 $buffer = "($w)";
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];
1481 * Filter env stack
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)
1493 $filtered = [];
1495 foreach ($envs as $e) {
1496 if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1497 $ec = clone $e;
1498 $ec->block = null;
1499 $ec->selectors = [];
1501 $filtered[] = $ec;
1502 } else {
1503 $filtered[] = $e;
1507 return $this->extractEnv($filtered);
1511 * Filter WITH rules
1513 * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1514 * @param array $with
1515 * @param array $without
1517 * @return boolean
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);
1531 } else {
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)) {
1541 $s = reset($s);
1544 if (\is_object($s) && $s instanceof Number) {
1545 return $this->testWithWithout('keyframes', $with, $without);
1549 return $this->testWithWithout('rule', $with, $without);
1552 return true;
1556 * Test a single type of block against with/without lists
1558 * @param string $what
1559 * @param array $with
1560 * @param array $without
1562 * @return boolean
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
1583 * @return void
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);
1593 }));
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);
1604 $this->popEnv();
1608 * Compile nested properties lines
1610 * @param \ScssPhp\ScssPhp\Block $block
1611 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1613 * @return void
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);
1632 break;
1634 case Type::T_NESTED_PROPERTY:
1635 array_unshift($child[1]->prefix[2], $prefix);
1636 break;
1639 $this->compileChild($child, $nested);
1644 * Compile nested block
1646 * @param \ScssPhp\ScssPhp\Block $block
1647 * @param array $selectors
1649 * @return void
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') {
1661 // need wrapping?
1662 $needWrapping = false;
1664 foreach ($block->children as $child) {
1665 if ($child[0] === Type::T_ASSIGN) {
1666 $needWrapping = true;
1667 break;
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;
1691 $this->popEnv();
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
1712 * @return void
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;
1742 $this->popEnv();
1747 * Compile the value of a comment that can have interpolation
1749 * @param array $value
1750 * @param boolean $pushEnv
1752 * @return string
1754 protected function compileCommentValue($value, $pushEnv = false)
1756 $c = $value[1];
1758 if (isset($value[2])) {
1759 if ($pushEnv) {
1760 $this->pushEnv();
1763 $ignoreCallStackMessage = $this->ignoreCallStackMessage;
1764 $this->ignoreCallStackMessage = true;
1766 try {
1767 $c = $this->compileValue($value[2]);
1768 } catch (\Exception $e) {
1769 // ignore error in comment compilation which are only interpolation
1772 $this->ignoreCallStackMessage = $ignoreCallStackMessage;
1774 if ($pushEnv) {
1775 $this->popEnv();
1779 return $c;
1783 * Compile root level comment
1785 * @param array $block
1787 * @return void
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
1802 * @return array
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__);
1816 try {
1817 $isValid = $parser->parseSelector($buffer, $newSelectors, true);
1818 } catch (ParserException $e) {
1819 throw $this->error($e->getMessage());
1822 if ($isValid) {
1823 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1827 return $selectors;
1831 * Evaluate selector
1833 * @param array $selector
1835 * @return array
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
1847 * @return array
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;
1859 } elseif (
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
1879 * @return string
1881 protected function collapseSelectors($selectors, $selectorFormat = false)
1883 $parts = [];
1885 foreach ($selectors as $selector) {
1886 $output = [];
1887 $glueNext = false;
1889 foreach ($selector as $node) {
1890 $compound = '';
1892 array_walk_recursive(
1893 $node,
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;
1902 } else {
1903 $output[] = $compound;
1906 $glueNext = true;
1907 } elseif ($glueNext) {
1908 $output[\count($output) - 1] .= ' ' . $compound;
1909 $glueNext = false;
1910 } else {
1911 $output[] = $compound;
1915 if ($selectorFormat) {
1916 foreach ($output as &$o) {
1917 $o = [Type::T_STRING, '', [$o]];
1920 $output = [Type::T_LIST, ' ', $output];
1921 } else {
1922 $output = implode(' ', $output);
1925 $parts[] = $output;
1928 if ($selectorFormat) {
1929 $parts = [Type::T_LIST, ',', $parts];
1930 } else {
1931 $parts = implode(', ', $parts);
1934 return $parts;
1938 * Parse down the selector and revert [self] to "&" before a reparsing
1940 * @param array $selectors
1942 * @return array
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);
1953 $part = $replace;
1954 } else {
1955 $part = $this->replaceSelfSelector($part, $replace);
1960 return $selectors;
1964 * Flatten selector single; joins together .classes and #ids
1966 * @param array $single
1968 * @return array
1970 protected function flattenSelectorSingle($single)
1972 $joined = [];
1974 foreach ($single as $part) {
1975 if (
1976 empty($joined) ||
1977 ! \is_string($part) ||
1978 preg_match('/[\[.:#%]/', $part)
1980 $joined[] = $part;
1981 continue;
1984 if (\is_array(end($joined))) {
1985 $joined[] = $part;
1986 } else {
1987 $joined[\count($joined) - 1] .= $part;
1991 return $joined;
1995 * Compile selector to string; self(&) should have been replaced by now
1997 * @param string|array $selector
1999 * @return string
2001 protected function compileSelector($selector)
2003 if (! \is_array($selector)) {
2004 return $selector; // media and the like
2007 return implode(
2008 ' ',
2009 array_map(
2010 [$this, 'compileSelectorPart'],
2011 $selector
2017 * Compile selector part
2019 * @param array $piece
2021 * @return string
2023 protected function compileSelectorPart($piece)
2025 foreach ($piece as &$p) {
2026 if (! \is_array($p)) {
2027 continue;
2030 switch ($p[0]) {
2031 case Type::T_SELF:
2032 $p = '&';
2033 break;
2035 default:
2036 $p = $this->compileValue($p);
2037 break;
2041 return implode($piece);
2045 * Has selector placeholder?
2047 * @param array $selector
2049 * @return boolean
2051 protected function hasSelectorPlaceholder($selector)
2053 if (! \is_array($selector)) {
2054 return false;
2057 foreach ($selector as $parts) {
2058 foreach ($parts as $part) {
2059 if (\strlen($part) && '%' === $part[0]) {
2060 return true;
2065 return false;
2069 * @param string $name
2071 * @return void
2073 protected function pushCallStack($name = '')
2075 $this->callStack[] = [
2076 'n' => $name,
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);
2093 * @return void
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);
2116 if (isset($ret)) {
2117 $this->popCallStack();
2119 return $ret;
2123 $this->popCallStack();
2125 return null;
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
2136 * @return void
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']);
2153 } else {
2154 $ret = $this->compileChild($stm, $out);
2157 if (isset($ret)) {
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
2171 * @return array
2173 protected function evaluateMediaQuery($queryList)
2175 static $parser = null;
2177 $outQueryList = [];
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
2188 if (
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);
2212 $queries = [];
2214 if ($parser->parseMediaQueryList($queryString, $queries)) {
2215 $queries = $this->evaluateMediaQuery($queries[2]);
2217 while (\count($queries)) {
2218 $outQueryList[] = array_shift($queries);
2221 continue;
2226 $outQueryList[] = $queryList[$kql];
2229 return $outQueryList;
2233 * Compile media query
2235 * @param array $queryList
2237 * @return array
2239 protected function compileMediaQuery($queryList)
2241 $start = '@media ';
2242 $default = trim($start);
2243 $out = [];
2244 $current = '';
2246 foreach ($queryList as $query) {
2247 $type = null;
2248 $parts = [];
2250 $mediaTypeOnly = true;
2252 foreach ($query as $q) {
2253 if ($q[0] !== Type::T_MEDIA_TYPE) {
2254 $mediaTypeOnly = false;
2255 break;
2259 foreach ($query as $q) {
2260 switch ($q[0]) {
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) )) {
2267 if ($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);
2279 if ($current) {
2280 $out[] = $start . $current;
2283 $current = '';
2284 $type = null;
2285 $parts = [];
2289 if ($newType === ['all'] && $default) {
2290 $default = $start . 'all';
2293 // all can be safely ignored and mixed with whatever else
2294 if ($newType !== ['all']) {
2295 if ($type) {
2296 $type = $this->mergeMediaTypes($type, $newType);
2298 if (empty($type)) {
2299 // merge failed : ignore this query that is not valid, skip to the next one
2300 $parts = [];
2301 $default = ''; // if everything fail, no @media at all
2302 continue 3;
2304 } else {
2305 $type = $newType;
2308 break;
2310 case Type::T_MEDIA_EXPRESSION:
2311 if (isset($q[2])) {
2312 $parts[] = '('
2313 . $this->compileValue($q[1])
2314 . $this->formatter->assignSeparator
2315 . $this->compileValue($q[2])
2316 . ')';
2317 } else {
2318 $parts[] = '('
2319 . $this->compileValue($q[1])
2320 . ')';
2322 break;
2324 case Type::T_MEDIA_VALUE:
2325 $parts[] = $this->compileValue($q[1]);
2326 break;
2330 if ($type) {
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);
2343 if ($current) {
2344 $out[] = $start . $current;
2347 // no @media type except all, and no conflict?
2348 if (! $out && $default) {
2349 $out[] = $default;
2352 return $out;
2356 * Merge direct relationships between selectors
2358 * @param array $selectors1
2359 * @param array $selectors2
2361 * @return array
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);
2376 $merged = [];
2378 do {
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);
2386 } else {
2387 $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2390 break;
2393 array_unshift($merged, $part1);
2394 } while (! empty($selectors1) && ! empty($selectors2));
2396 return $merged;
2400 * Merge media types
2402 * @param array $type1
2403 * @param array $type2
2405 * @return array|null
2407 protected function mergeMediaTypes($type1, $type2)
2409 if (empty($type1)) {
2410 return $type2;
2413 if (empty($type2)) {
2414 return $type1;
2417 if (\count($type1) > 1) {
2418 $m1 = strtolower($type1[0]);
2419 $t1 = strtolower($type1[1]);
2420 } else {
2421 $m1 = '';
2422 $t1 = strtolower($type1[0]);
2425 if (\count($type2) > 1) {
2426 $m2 = strtolower($type2[0]);
2427 $t2 = strtolower($type2[1]);
2428 } else {
2429 $m2 = '';
2430 $t2 = strtolower($type2[0]);
2433 if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2434 if ($t1 === $t2) {
2435 return null;
2438 return [
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"
2446 if ($t1 !== $t2) {
2447 return null;
2450 return [Type::T_NOT, $t1];
2453 if ($t1 !== $t2) {
2454 return null;
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
2468 * @return boolean
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;
2481 return true;
2484 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2486 return false;
2489 if ($rawPath[0] === Type::T_LIST) {
2490 // handle a list of strings
2491 if (\count($rawPath[2]) === 0) {
2492 return false;
2495 foreach ($rawPath[2] as $path) {
2496 if ($path[0] !== Type::T_STRING) {
2497 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2499 return false;
2503 foreach ($rawPath[2] as $path) {
2504 $this->compileImport($path, $out, $once);
2507 return true;
2510 $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2512 return false;
2516 * @param array $rawPath
2517 * @return string
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);
2528 } else {
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);
2535 return $path;
2539 * @param array $path
2540 * @return array
2541 * @throws CompilerException
2543 protected function escapeImportPathString($path)
2545 switch ($path[0]) {
2546 case Type::T_LIST:
2547 foreach ($path[2] as $k => $v) {
2548 $path[2][$k] = $this->escapeImportPathString($v);
2550 break;
2551 case Type::T_STRING:
2552 if ($path[1]) {
2553 $path = $this->compileValue($path);
2554 $path = str_replace(' ', '\\ ', $path);
2555 $path = [Type::T_KEYWORD, $path];
2557 break;
2560 return $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
2571 * @return void
2573 protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2575 $root = $out;
2577 while ($root->parent) {
2578 $root = $root->parent;
2581 $i = 0;
2583 while ($i < \count($root->children)) {
2584 if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2585 break;
2588 $i++;
2591 // remove incompatible children from the bottom of the list
2592 $saveChildren = [];
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;
2607 // repush children
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
2621 * @return void
2623 protected function appendOutputLine(OutputBlock $out, $type, $line)
2625 $outWrite = &$out;
2627 // check if it's a flat output or not
2628 if (\count($out->children)) {
2629 $lastChild = &$out->children[\count($out->children) - 1];
2631 if (
2632 $lastChild->depth === $out->depth &&
2633 \is_null($lastChild->selectors) &&
2634 ! \count($lastChild->children)
2636 $outWrite = $lastChild;
2637 } else {
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);
2683 break;
2685 case Type::T_IMPORT:
2686 $rawPath = $this->reduce($child[1]);
2688 $this->compileImport($rawPath, $out);
2689 break;
2691 case Type::T_DIRECTIVE:
2692 $this->compileDirective($child[1], $out);
2693 break;
2695 case Type::T_AT_ROOT:
2696 $this->compileAtRoot($child[1]);
2697 break;
2699 case Type::T_MEDIA:
2700 $this->compileMedia($child[1]);
2701 break;
2703 case Type::T_BLOCK:
2704 $this->compileBlock($child[1]);
2705 break;
2707 case Type::T_CHARSET:
2708 if (! $this->charsetSeen) {
2709 $this->charsetSeen = true;
2710 $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2712 break;
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) {
2724 break;
2728 $compiledValue = $this->compileValue($value);
2730 $line = $this->formatter->customProperty(
2731 $compiledName,
2732 $compiledValue
2735 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2736 break;
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);
2746 if ($isGlobal) {
2747 $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2748 break;
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);
2758 break;
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;
2781 break;
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] === '/') {
2791 $revert = true;
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()) {
2801 $revert = false;
2805 if ($revert) {
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) {
2812 $revert = true;
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()) {
2825 $revert = false;
2830 if ($revert) {
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) {
2846 break;
2850 $compiledValue = $this->compileValue($value);
2852 // ignore empty value
2853 if (\strlen($compiledValue)) {
2854 $line = $this->formatter->property(
2855 $compiledName,
2856 $compiledValue
2858 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2860 break;
2862 case Type::T_COMMENT:
2863 if ($out->type === Type::T_ROOT) {
2864 $this->compileComment($child);
2865 break;
2868 $line = $this->compileCommentValue($child, true);
2869 $this->appendOutputLine($out, Type::T_COMMENT, $line);
2870 break;
2872 case Type::T_MIXIN:
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);
2878 break;
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);
2897 break;
2899 case Type::T_IF:
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) {
2907 if (
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);
2914 break;
2916 case Type::T_EACH:
2917 list(, $each) = $child;
2919 $list = $this->coerceList($this->reduce($each->list), ',', true);
2921 $this->pushEnv();
2923 foreach ($list[2] as $item) {
2924 if (\count($each->vars) === 1) {
2925 $this->set($each->vars[0], $item, true);
2926 } else {
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);
2936 if ($ret) {
2937 $store = $this->env->store;
2938 $this->popEnv();
2939 $this->backPropagateEnv($store, $each->vars);
2941 return $ret;
2944 $store = $this->env->store;
2945 $this->popEnv();
2946 $this->backPropagateEnv($store, $each->vars);
2948 break;
2950 case Type::T_WHILE:
2951 list(, $while) = $child;
2953 while ($this->isTruthy($this->reduce($while->cond, true))) {
2954 $ret = $this->compileChildren($while->children, $out);
2956 if ($ret) {
2957 return $ret;
2960 break;
2962 case Type::T_FOR:
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;
2986 $this->pushEnv();
2988 for (;;) {
2989 if (
2990 (! $for->until && $start - $d == $end) ||
2991 ($for->until && $start == $end)
2993 break;
2996 $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
2997 $start += $d;
2999 $ret = $this->compileChildren($for->children, $out);
3001 if ($ret) {
3002 $store = $this->env->store;
3003 $this->popEnv();
3004 $this->backPropagateEnv($store, [$for->var]);
3006 return $ret;
3010 $store = $this->env->store;
3011 $this->popEnv();
3012 $this->backPropagateEnv($store, [$for->var]);
3014 break;
3016 case Type::T_RETURN:
3017 return $this->reduce($child[1], true);
3019 case Type::T_NESTED_PROPERTY:
3020 $this->compileNestedPropertiesBlock($child[1], $out);
3021 break;
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);
3029 if (! $mixin) {
3030 throw $this->error("Undefined mixin $name");
3033 $callingScope = $this->getStoreEnv();
3035 // push scope, apply args
3036 $this->pushEnv();
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
3041 $selfParent = null;
3043 if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
3044 $selfParent = $child['selfParent'];
3045 } else {
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);
3067 } else {
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);
3074 } else {
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;
3086 } else {
3087 throw $this->error("@mixin $name() without parentEnv");
3090 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
3092 $this->popEnv();
3093 break;
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];
3101 if (! $content) {
3102 break;
3105 $storeEnv = $this->storeEnv;
3106 $varsUsing = [];
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;
3125 break;
3127 case Type::T_DEBUG:
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");
3135 break;
3137 case Type::T_WARN:
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");
3145 break;
3147 case Type::T_ERROR:
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");
3156 default:
3157 throw $this->error("unknown child type: $child[0]");
3162 * Reduce expression to string
3164 * @param array $exp
3165 * @param bool $keepParens
3167 * @return array
3169 protected function expToString($exp, $keepParens = false)
3171 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
3173 $content = [];
3175 if ($keepParens && $inParens) {
3176 $content[] = '(';
3179 $content[] = $this->reduce($left);
3181 if ($whiteLeft) {
3182 $content[] = ' ';
3185 $content[] = $op;
3187 if ($whiteRight) {
3188 $content[] = ' ';
3191 $content[] = $this->reduce($right);
3193 if ($keepParens && $inParens) {
3194 $content[] = ')';
3197 return [Type::T_STRING, '', $content];
3201 * Is truthy?
3203 * @param array|Number $value
3205 * @return boolean
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
3217 * @return boolean
3219 protected function isImmediateRelationshipCombinator($value)
3221 return $value === '>' || $value === '+' || $value === '~';
3225 * Should $value cause its operand to eval
3227 * @param array $value
3229 * @return boolean
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]);
3239 // fall-thru
3240 case Type::T_VARIABLE:
3241 case Type::T_FUNCTION_CALL:
3242 return true;
3245 return false;
3249 * Reduce value
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)) {
3259 return null;
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
3276 if (
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);
3286 $ltype = $left[0];
3287 $rtype = $right[0];
3289 $ucOpName = ucfirst($opName);
3290 $ucLType = ucfirst($ltype);
3291 $ucRType = ucfirst($rtype);
3293 // this tries:
3294 // 1. op[op name][left type][right type]
3295 // 2. op[left type][right type] (passing the op as first arg
3296 // 3. op[op name]
3297 $fn = "op${ucOpName}${ucLType}${ucRType}";
3299 if (
3300 \is_callable([$this, $fn]) ||
3301 (($fn = "op${ucLType}${ucRType}") &&
3302 \is_callable([$this, $fn]) &&
3303 $passOp = true) ||
3304 (($fn = "op${ucOpName}") &&
3305 \is_callable([$this, $fn]) &&
3306 $genOp = true)
3308 $shouldEval = $inParens || $inExp;
3310 if (isset($passOp)) {
3311 $out = $this->$fn($op, $left, $right, $shouldEval);
3312 } else {
3313 $out = $this->$fn($left, $right, $shouldEval);
3316 if (isset($out)) {
3317 return $out;
3321 return $this->expToString($value);
3323 case Type::T_UNARY:
3324 list(, $op, $exp, $inParens) = $value;
3326 $inExp = $inExp || $this->shouldEval($exp);
3327 $exp = $this->reduce($exp);
3329 if ($exp instanceof Number) {
3330 switch ($op) {
3331 case '+':
3332 return $exp;
3334 case '-':
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;
3348 $op = $op . ' ';
3351 return [Type::T_STRING, '', [$op, $exp]];
3353 case Type::T_VARIABLE:
3354 return $this->reduce($this->get($value[1]));
3356 case Type::T_LIST:
3357 foreach ($value[2] as &$item) {
3358 $item = $this->reduce($item);
3361 return $value;
3363 case Type::T_MAP:
3364 foreach ($value[1] as &$item) {
3365 $item = $this->reduce($item);
3368 foreach ($value[2] as &$item) {
3369 $item = $this->reduce($item);
3372 return $value;
3374 case Type::T_STRING:
3375 foreach ($value[2] as &$item) {
3376 if (\is_array($item) || $item instanceof \ArrayAccess) {
3377 $item = $this->reduce($item);
3381 return $value;
3383 case Type::T_INTERPOLATE:
3384 $value[1] = $this->reduce($value[1]);
3386 if ($inExp) {
3387 return $value[1];
3390 return $value;
3392 case Type::T_FUNCTION_CALL:
3393 return $this->fncall($value[1], $value[2]);
3395 case Type::T_SELF:
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;
3402 default:
3403 return $value;
3408 * Function caller
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
3430 $listArgs = [];
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]) {
3447 // SCSS @function
3448 case 'scss':
3449 return $this->callScssFunction($functionReference[3], $argValues);
3451 // native PHP functions
3452 case 'user':
3453 case 'native':
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;
3475 default:
3476 return static::$defaultValue;
3480 protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
3482 switch ($arg[0]) {
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)) {
3488 return false;
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) {
3494 return false;
3498 return $arg;
3500 case Type::T_FUNCTION_CALL:
3501 if (! \in_array($arg[1], $allowed_function)) {
3502 return false;
3504 $cssArgs = [];
3505 foreach ($arg[2] as $argValue) {
3506 if ($argValue === static::$null) {
3507 return false;
3509 $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
3510 if (empty($argValue[0]) && $cssArg !== false) {
3511 $cssArgs[] = [$argValue[0], $cssArg];
3512 } else {
3513 return false;
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'])) {
3522 return false;
3524 return $this->stringifyFncallArgs($arg);
3526 case Type::T_NUMBER:
3527 return $this->stringifyFncallArgs($arg);
3529 case Type::T_LIST:
3530 if (!$inFunction) {
3531 return false;
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) {
3537 return false;
3540 $arg[0] = Type::T_STRING;
3541 return $arg;
3543 return false;
3545 case Type::T_EXPRESSION:
3546 if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
3547 return false;
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) {
3552 return false;
3554 return $this->expToString($arg, true);
3556 case Type::T_VARIABLE:
3557 case Type::T_SELF:
3558 default:
3559 return false;
3565 * Reformat fncall arguments to proper css function output
3567 * @param $arg
3569 * @return array|\ArrayAccess|Number|string|null
3571 protected function stringifyFncallArgs($arg)
3574 switch ($arg[0]) {
3575 case Type::T_LIST:
3576 foreach ($arg[2] as $k => $v) {
3577 $arg[2][$k] = $this->stringifyFncallArgs($v);
3579 break;
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);
3588 break;
3590 case Type::T_FUNCTION_CALL:
3591 $name = strtolower($arg[1]);
3593 if (in_array($name, ['max', 'min', 'calc'])) {
3594 $args = $arg[2];
3595 $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
3597 break;
3600 return $arg;
3604 * Find a function reference
3605 * @param string $name
3606 * @param bool $safeCopy
3607 * @return array
3609 protected function getFunctionReference($name, $safeCopy = false)
3611 // SCSS @function
3612 if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3613 if ($safeCopy) {
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);
3624 $libName = null;
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)) {
3634 $libName = $f[1];
3635 $prototype = isset(static::$$libName) ? static::$$libName : null;
3637 return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3640 return static::$null;
3645 * Normalize name
3647 * @param string $name
3649 * @return string
3651 protected function normalizeName($name)
3653 return str_replace('-', '_', $name);
3657 * Normalize value
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]) {
3668 case Type::T_LIST:
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']);
3683 return $value;
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)];
3691 default:
3692 return $value;
3697 * Add numbers
3699 * @param Number $left
3700 * @param Number $right
3702 * @return Number
3704 protected function opAddNumberNumber(Number $left, Number $right)
3706 return $left->plus($right);
3710 * Multiply numbers
3712 * @param Number $left
3713 * @param Number $right
3715 * @return Number
3717 protected function opMulNumberNumber(Number $left, Number $right)
3719 return $left->times($right);
3723 * Subtract numbers
3725 * @param Number $left
3726 * @param Number $right
3728 * @return Number
3730 protected function opSubNumberNumber(Number $left, Number $right)
3732 return $left->minus($right);
3736 * Divide numbers
3738 * @param Number $left
3739 * @param Number $right
3741 * @return Number
3743 protected function opDivNumberNumber(Number $left, Number $right)
3745 return $left->dividedBy($right);
3749 * Mod numbers
3751 * @param Number $left
3752 * @param Number $right
3754 * @return Number
3756 protected function opModNumberNumber(Number $left, Number $right)
3758 return $left->modulo($right);
3762 * Add strings
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) {
3773 $right[1] = '';
3776 $strLeft[2][] = $right;
3778 return $strLeft;
3781 if ($strRight = $this->coerceString($right)) {
3782 if ($left[0] === Type::T_STRING) {
3783 $left[1] = '';
3786 array_unshift($strRight[2], $left);
3788 return $strRight;
3791 return null;
3795 * Boolean and
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) {
3810 if (! $truthy) {
3811 return null;
3815 if ($left !== static::$false && $left !== static::$null) {
3816 return $this->reduce($right, true);
3819 return $left;
3823 * Boolean or
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) {
3838 if (! $truthy) {
3839 return null;
3843 if ($left !== static::$false && $left !== static::$null) {
3844 return $left;
3847 return $this->reduce($right, true);
3851 * Compare colors
3853 * @param string $op
3854 * @param array $left
3855 * @param array $right
3857 * @return array
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;
3876 switch ($op) {
3877 case '+':
3878 $out[] = $lval + $rval;
3879 break;
3881 case '-':
3882 $out[] = $lval - $rval;
3883 break;
3885 case '*':
3886 $out[] = $lval * $rval;
3887 break;
3889 case '%':
3890 if ($rval == 0) {
3891 throw $this->error("color: Can't take modulo by zero");
3894 $out[] = $lval % $rval;
3895 break;
3897 case '/':
3898 if ($rval == 0) {
3899 throw $this->error("color: Can't divide by zero");
3902 $out[] = (int) ($lval / $rval);
3903 break;
3905 case '==':
3906 return $this->opEq($left, $right);
3908 case '!=':
3909 return $this->opNeq($left, $right);
3911 default:
3912 throw $this->error("color: unknown op $op");
3916 if (isset($left[4])) {
3917 $out[4] = $left[4];
3918 } elseif (isset($right[4])) {
3919 $out[4] = $right[4];
3922 return $this->fixColor($out);
3926 * Compare color and number
3928 * @param string $op
3929 * @param array $left
3930 * @param Number $right
3932 * @return array
3934 protected function opColorNumber($op, $left, Number $right)
3936 if ($op === '==') {
3937 return static::$false;
3940 if ($op === '!=') {
3941 return static::$true;
3944 $value = $right->getDimension();
3946 return $this->opColorColor(
3947 $op,
3948 $left,
3949 [Type::T_COLOR, $value, $value, $value]
3954 * Compare number and color
3956 * @param string $op
3957 * @param Number $left
3958 * @param array $right
3960 * @return array
3962 protected function opNumberColor($op, Number $left, $right)
3964 if ($op === '==') {
3965 return static::$false;
3968 if ($op === '!=') {
3969 return static::$true;
3972 $value = $left->getDimension();
3974 return $this->opColorColor(
3975 $op,
3976 [Type::T_COLOR, $value, $value, $value],
3977 $right
3982 * Compare number1 == number2
3984 * @param array|Number $left
3985 * @param array|Number $right
3987 * @return array
3989 protected function opEq($left, $right)
3991 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3992 $lStr[1] = '';
3993 $rStr[1] = '';
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
4008 * @return array
4010 protected function opNeq($left, $right)
4012 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
4013 $lStr[1] = '';
4014 $rStr[1] = '';
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
4029 * @return array
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
4042 * @return array
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
4055 * @return array
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
4068 * @return array
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
4081 * @return array
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
4094 * @return array
4096 protected function opLtNumberNumber(Number $left, Number $right)
4098 return $this->toBool($left->lessThan($right));
4102 * Cast to boolean
4104 * @api
4106 * @param mixed $thing
4108 * @return array
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
4118 * @return 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) . ' '],
4138 [ '\\', ' '],
4139 $string
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) {
4147 $string .= " ";
4149 $string .= $next;
4154 return $string;
4158 * Compiles a primitive value into a CSS property value.
4160 * Values in scssphp are typed by being wrapped in arrays, their format is
4161 * typically:
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.
4168 * @api
4170 * @param array|Number|string $value
4172 * @return string
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);
4183 return $value[1];
4185 case Type::T_COLOR:
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)) {
4203 return $colorName;
4206 if (is_numeric($alpha)) {
4207 $a = new Number($alpha, '');
4208 } else {
4209 $a = $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)) {
4223 return $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];
4233 return $h;
4235 case Type::T_NUMBER:
4236 return $value->output($this);
4238 case Type::T_STRING:
4239 $content = $this->compileStringContent($value);
4241 if ($value[1]) {
4242 $content = str_replace('\\', '\\\\', $content);
4244 $content = $this->escapeNonPrintableChars($content);
4246 // force double quote as string quote for the output in certain cases
4247 if (
4248 $value[1] === "'" &&
4249 (strpos($content, '"') === false or strpos($content, "'") !== false) &&
4250 strpbrk($content, '{}\\\'') !== false
4252 $value[1] = '"';
4253 } elseif (
4254 $value[1] === '"' &&
4255 (strpos($content, '"') !== false and strpos($content, "'") === false)
4257 $value[1] = "'";
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\")";
4275 case Type::T_LIST:
4276 $value = $this->extractInterpolation($value);
4278 if ($value[0] !== Type::T_LIST) {
4279 return $this->compileValue($value);
4282 list(, $delim, $items) = $value;
4283 $pre = $post = '';
4285 if (! empty($value['enclosing'])) {
4286 switch ($value['enclosing']) {
4287 case 'parent':
4288 //$pre = '(';
4289 //$post = ')';
4290 break;
4291 case 'forced_parent':
4292 $pre = '(';
4293 $post = ')';
4294 break;
4295 case 'bracket':
4296 case 'forced_bracket':
4297 $pre = '[';
4298 $post = ']';
4299 break;
4303 $prefix_value = '';
4305 if ($delim !== ' ') {
4306 $prefix_value = ' ';
4309 $filtered = [];
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;
4320 break;
4325 if ($item[0] === Type::T_NULL) {
4326 continue;
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;
4343 case Type::T_MAP:
4344 $keys = $value[1];
4345 $values = $value[2];
4346 $filtered = [];
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;
4363 $delim = $left[1];
4365 if ($delim && $delim !== ' ' && ! $whiteLeft) {
4366 $delim .= ' ';
4369 $left = \count($left[2]) > 0
4370 ? $this->compileValue($left) . $delim . $whiteLeft
4371 : '';
4373 $delim = $right[1];
4375 if ($delim && $delim !== ' ') {
4376 $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]) {
4389 case Type::T_LIST:
4390 $reduced = $this->extractInterpolation($reduced);
4392 if ($reduced[0] !== Type::T_LIST) {
4393 break;
4396 list(, $delim, $items) = $reduced;
4398 if ($delim !== ' ') {
4399 $delim .= ' ';
4402 $filtered = [];
4404 foreach ($items as $item) {
4405 if ($item[0] === Type::T_NULL) {
4406 continue;
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];
4415 } else {
4416 $filtered[] = $this->compileValue($temp);
4420 $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
4421 break;
4423 case Type::T_STRING:
4424 $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
4425 break;
4427 case Type::T_NULL:
4428 $reduced = [Type::T_KEYWORD, ''];
4431 return $this->compileValue($reduced);
4433 case Type::T_NULL:
4434 return 'null';
4436 case Type::T_COMMENT:
4437 return $this->compileCommentValue($value);
4439 default:
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);
4457 default:
4458 return $this->compileValue($value);
4463 * Flatten list
4465 * @param array $list
4467 * @return string
4469 protected function flattenList($list)
4471 return $this->compileValue($list);
4475 * Compile string content
4477 * @param array $string
4479 * @return string
4481 protected function compileStringContent($string)
4483 $parts = [];
4485 foreach ($string[2] as $part) {
4486 if (\is_array($part) || $part instanceof \ArrayAccess) {
4487 $parts[] = $this->compileValue($part);
4488 } else {
4489 $parts[] = $part;
4493 return implode($parts);
4497 * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4499 * @param array $list
4501 * @return array
4503 protected function extractInterpolation($list)
4505 $items = $list[2];
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];
4516 return $list;
4520 * Find the final set of selectors
4522 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4523 * @param \ScssPhp\ScssPhp\Block $selfParent
4525 * @return array
4527 protected function multiplySelectors(Environment $env, $selfParent = null)
4529 $envs = $this->compactEnv($env);
4530 $selectors = [];
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)) {
4541 continue;
4544 $selectors = $env->selectors;
4546 do {
4547 $stillHasSelf = false;
4548 $prevSelectors = $selectors;
4549 $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;
4559 } else {
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;
4577 return $selectors;
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
4588 * @return array
4590 protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4592 $setSelf = false;
4593 $out = [];
4595 foreach ($child as $part) {
4596 $newPart = [];
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) {
4605 $setSelf = true;
4607 if (\is_null($selfParentSelectors)) {
4608 $selfParentSelectors = $parent;
4611 foreach ($selfParentSelectors as $i => $parentPart) {
4612 if ($i > 0) {
4613 $out[] = $newPart;
4614 $newPart = [];
4617 foreach ($parentPart as $pp) {
4618 if (\is_array($pp)) {
4619 $flatten = [];
4621 array_walk_recursive($pp, function ($a) use (&$flatten) {
4622 $flatten[] = $a;
4625 $pp = implode($flatten);
4628 $newPart[] = $pp;
4631 } else {
4632 $newPart[] = $p;
4636 $out[] = $newPart;
4639 return $setSelf ? $out : array_merge($parent, $child);
4643 * Multiply media
4645 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4646 * @param array $childQueries
4648 * @return array
4650 protected function multiplyMedia(Environment $env = null, $childQueries = null)
4652 if (
4653 ! isset($env) ||
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];
4670 $this->env = $env;
4671 $this->storeEnv = null;
4672 $parentQueries = $this->evaluateMediaQuery($parentQueries);
4674 list($this->env, $this->storeEnv) = $store;
4676 if (\is_null($childQueries)) {
4677 $childQueries = $parentQueries;
4678 } else {
4679 $originalQueries = $childQueries;
4680 $childQueries = [];
4682 foreach ($parentQueries as $parentQuery) {
4683 foreach ($originalQueries as $childQuery) {
4684 $childQueries[] = array_merge(
4685 $parentQuery,
4686 [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
4687 $childQuery
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) {
4708 $envs[] = $env;
4711 return $envs;
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);) {
4726 $e->parent = $env;
4727 $env = $e;
4730 return $env;
4734 * Push environment
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;
4745 $env->store = [];
4746 $env->block = $block;
4747 $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
4749 $this->env = $env;
4750 $this->storeEnv = null;
4752 return $env;
4756 * Pop environment
4758 * @return void
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
4772 * @return void
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;
4794 * Set variable
4796 * @param string $name
4797 * @param mixed $value
4798 * @param boolean $shadow
4799 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4800 * @param mixed $valueUnreduced
4802 * @return void
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();
4812 if ($shadow) {
4813 $this->setRaw($name, $value, $env, $valueUnreduced);
4814 } else {
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
4827 * @return void
4829 protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4831 $storeEnv = $env;
4832 $specialContentKey = static::$namespaces['special'] . 'content';
4834 $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
4836 $maxDepth = 10000;
4838 for (;;) {
4839 if ($maxDepth-- <= 0) {
4840 break;
4843 if (\array_key_exists($name, $env->store)) {
4844 break;
4847 if (! $hasNamespace && isset($env->marker)) {
4848 if (! empty($env->store[$specialContentKey])) {
4849 $env = $env->store[$specialContentKey]->scope;
4850 continue;
4853 if (! empty($env->declarationScopeParent)) {
4854 $env = $env->declarationScopeParent;
4855 continue;
4856 } else {
4857 $env = $storeEnv;
4858 break;
4862 if (isset($env->parentStore)) {
4863 $env = $env->parentStore;
4864 } elseif (isset($env->parent)) {
4865 $env = $env->parent;
4866 } else {
4867 $env = $storeEnv;
4868 break;
4872 $env->store[$name] = $value;
4874 if ($valueUnreduced) {
4875 $env->storeUnreduced[$name] = $valueUnreduced;
4880 * Set raw variable
4882 * @param string $name
4883 * @param mixed $value
4884 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4885 * @param mixed $valueUnreduced
4887 * @return void
4889 protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
4891 $env->store[$name] = $value;
4893 if ($valueUnreduced) {
4894 $env->storeUnreduced[$name] = $valueUnreduced;
4899 * Get variable
4901 * @api
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] === '%';
4921 $maxDepth = 10000;
4923 for (;;) {
4924 if ($maxDepth-- <= 0) {
4925 break;
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;
4939 continue;
4942 if (! empty($env->declarationScopeParent)) {
4943 $env = $env->declarationScopeParent;
4944 } else {
4945 $env = $this->rootEnv;
4947 continue;
4950 if (isset($env->parentStore)) {
4951 $env = $env->parentStore;
4952 } elseif (isset($env->parent)) {
4953 $env = $env->parent;
4954 } else {
4955 break;
4959 if ($shouldThrow) {
4960 throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
4963 // found nothing
4964 return null;
4968 * Has variable?
4970 * @param string $name
4971 * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4973 * @return boolean
4975 protected function has($name, Environment $env = null)
4977 return ! \is_null($this->get($name, false, $env));
4981 * Inject variables
4983 * @param array $args
4985 * @return void
4987 protected function injectVariables(array $args)
4989 if (empty($args)) {
4990 return;
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);
5009 * Set variables
5011 * @api
5013 * @param array $variables
5015 * @return void
5017 public function setVariables(array $variables)
5019 $this->registeredVars = array_merge($this->registeredVars, $variables);
5023 * Unset variable
5025 * @api
5027 * @param string $name
5029 * @return void
5031 public function unsetVariable($name)
5033 unset($this->registeredVars[$name]);
5037 * Returns list of variables
5039 * @api
5041 * @return array
5043 public function getVariables()
5045 return $this->registeredVars;
5049 * Adds to list of parsed files
5051 * @api
5053 * @param string $path
5055 * @return void
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
5067 * @api
5069 * @return array
5071 public function getParsedFiles()
5073 return $this->parsedFiles;
5077 * Add import path
5079 * @api
5081 * @param string|callable $path
5083 * @return void
5085 public function addImportPath($path)
5087 if (! \in_array($path, $this->importPaths)) {
5088 $this->importPaths[] = $path;
5093 * Set import paths
5095 * @api
5097 * @param string|array<string|callable> $path
5099 * @return void
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
5120 * @api
5122 * @param integer $numberPrecision
5124 * @return void
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.
5137 * @api
5139 * @param string $style One of the OutputStyle constants
5141 * @return void
5143 * @phpstan-param OutputStyle::* $style
5145 public function setOutputStyle($style)
5147 switch ($style) {
5148 case OutputStyle::EXPANDED:
5149 $this->formatter = Expanded::class;
5150 break;
5152 case OutputStyle::COMPRESSED:
5153 $this->formatter = Compressed::class;
5154 break;
5156 default:
5157 throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
5162 * Set formatter
5164 * @api
5166 * @param string $formatterName
5168 * @return void
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
5185 * @api
5187 * @param string $lineNumberStyle
5189 * @return void
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
5202 * @api
5204 * @param integer $sourceMap
5206 * @return void
5208 * @phpstan-param self::SOURCE_MAP_* $sourceMap
5210 public function setSourceMap($sourceMap)
5212 $this->sourceMap = $sourceMap;
5216 * Set source map options
5218 * @api
5220 * @param array $sourceMapOptions
5222 * @return void
5224 public function setSourceMapOptions($sourceMapOptions)
5226 $this->sourceMapOptions = $sourceMapOptions;
5230 * Register function
5232 * @api
5234 * @param string $name
5235 * @param callable $func
5236 * @param array|null $prototype
5238 * @return void
5240 public function registerFunction($name, $func, $prototype = null)
5242 $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
5246 * Unregister function
5248 * @api
5250 * @param string $name
5252 * @return void
5254 public function unregisterFunction($name)
5256 unset($this->userFunctions[$this->normalizeName($name)]);
5260 * Add feature
5262 * @api
5264 * @param string $name
5266 * @return void
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;
5278 * Import file
5280 * @param string $path
5281 * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
5283 * @return void
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];
5295 } else {
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
5314 * @api
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)) {
5327 continue;
5330 if (\is_callable($dir)) {
5331 // check custom callback for import path
5332 $file = \call_user_func($dir, $url);
5334 if (! \is_null($file)) {
5335 return $file;
5339 return null;
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)) {
5355 return $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)) {
5362 return $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);
5373 return $path;
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)) {
5399 return $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) {
5413 return null;
5416 if (\count($paths) === 1) {
5417 return $paths[0];
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
5432 * @return string[]
5434 private function tryImportPathWithExtensions($path)
5436 $result = $this->tryImportPath($path.'.scss');
5438 if ($result) {
5439 return $result;
5442 return $this->tryImportPath($path.'.css');
5446 * @param string $path
5448 * @return string[]
5450 private function tryImportPath($path)
5452 $partial = dirname($path).'/_'.basename($path);
5454 $candidates = [];
5456 if (is_file($partial)) {
5457 $candidates[] = $partial;
5460 if (is_file($path)) {
5461 $candidates[] = $path;
5464 return $candidates;
5468 * @param string $path
5470 * @return string|null
5472 private function tryImportPathAsDirectory($path)
5474 if (!is_dir($path)) {
5475 return null;
5478 return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index'));
5482 * @param string $path
5484 * @return string
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));
5500 return $path;
5504 * Set encoding
5506 * @api
5508 * @param string $encoding
5510 * @return void
5512 public function setEncoding($encoding)
5514 $this->encoding = $encoding;
5518 * Ignore errors?
5520 * @api
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);
5532 return $this;
5536 * Get source position
5538 * @api
5540 * @return array
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)
5552 * @api
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)
5562 @trigger_error(
5563 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
5564 E_USER_DEPRECATED
5567 throw $this->error(...func_get_args());
5571 * Build an error (exception)
5573 * @api
5575 * @param string $msg Message with optional sprintf()-style vararg parameters
5577 * @return CompilerException
5579 public function error($msg, ...$args)
5581 if ($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.',
5618 $nbExpected,
5619 $functionName,
5620 $nbActual
5622 } else {
5623 $missing = [];
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.',
5631 $functionName,
5632 count($missing) > 1 ? 's' : '',
5633 implode(', ', $missing)
5639 * Beautify call stack for output
5641 * @param boolean $all
5642 * @param null $limit
5644 * @return string
5646 protected function callStackMessage($all = false, $limit = null)
5648 $callStackMsg = [];
5649 $ncall = 0;
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) {
5663 break;
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) {
5683 continue;
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
5700 * @return array
5702 protected function callScssFunction($func, $argValues)
5704 if (! $func) {
5705 return static::$defaultValue;
5707 $name = $func->name;
5709 $this->pushEnv();
5711 // set the args
5712 if (isset($func->args)) {
5713 $this->applyArguments($func->args, $argValues);
5716 // throw away lines and children
5717 $tmp = new OutputBlock();
5718 $tmp->lines = [];
5719 $tmp->children = [];
5721 $this->env->marker = 'function';
5723 if (! empty($func->parentEnv)) {
5724 $this->env->declarationScopeParent = $func->parentEnv;
5725 } else {
5726 throw $this->error("@function $name() without parentEnv");
5729 $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
5731 $this->popEnv();
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)) {
5752 return null;
5754 @list($sorted, $kwargs) = $sorted_kwargs;
5756 if ($name !== 'if') {
5757 $inExp = true;
5759 if ($name === 'join') {
5760 $inExp = false;
5763 foreach ($sorted as &$val) {
5764 $val = $this->reduce($val, $inExp);
5768 $returnValue = \call_user_func($function, $sorted, $kwargs);
5770 if (! isset($returnValue)) {
5771 return null;
5774 return $this->coerceValue($returnValue);
5778 * Get built-in function
5780 * @param string $name Normalized name
5782 * @return array
5784 protected function getBuiltinFunction($name)
5786 $libName = self::normalizeNativeFunctionName($name);
5787 return [$this, $libName];
5791 * Normalize native function name
5792 * @param string $name
5793 * @return string
5795 public static function normalizeNativeFunctionName($name)
5797 $name = str_replace("-", "_", $name);
5798 $libName = 'lib' . preg_replace_callback(
5799 '/_(.)/',
5800 function ($m) {
5801 return ucfirst($m[1]);
5803 ucfirst($name)
5805 return $libName;
5809 * Check if a function is a native built-in scss function, for css parsing
5810 * @param string $name
5811 * @return bool
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)) {
5832 $keyArgs = [];
5833 $posArgs = [];
5835 if (\is_array($args) && \count($args) && \end($args) === static::$null) {
5836 array_pop($args);
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;
5845 } else {
5846 $keyArgs[$key[1]] = $value;
5850 return [$posArgs, $keyArgs];
5853 // specific cases ?
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];
5864 $args[$k] = $arg;
5870 $finalArgs = [];
5872 if (! \is_array(reset($prototypes))) {
5873 $prototypes = [$prototypes];
5876 $keyArgs = [];
5878 // trying each prototypes
5879 $prototypeHasMatch = false;
5880 $exceptionMessage = '';
5882 foreach ($prototypes as $prototype) {
5883 $argDef = [];
5885 foreach ($prototype as $i => $p) {
5886 $default = null;
5887 $p = explode(':', $p, 2);
5888 $name = array_shift($p);
5890 if (\count($p)) {
5891 $p = trim(reset($p));
5893 if ($p === 'null') {
5894 // differentiate this null from the static::$null
5895 $default = [Type::T_KEYWORD, 'null'];
5896 } else {
5897 if (\is_null($parser)) {
5898 $parser = $this->parserFactory(__METHOD__);
5901 $parser->parseValue($p, $default);
5905 $isVariable = false;
5907 if (substr($name, -3) === '...') {
5908 $isVariable = true;
5909 $name = substr($name, 0, -3);
5912 $argDef[] = [$name, $default, $isVariable];
5915 $ignoreCallStackMessage = $this->ignoreCallStackMessage;
5916 $this->ignoreCallStackMessage = true;
5918 try {
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'])) {
5971 return null;
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
5991 * @return array
5993 * @throws \Exception
5995 protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5997 $output = [];
5999 if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) {
6000 array_pop($argValues);
6003 if ($storeInEnv) {
6004 $storeEnv = $this->getStoreEnv();
6006 $env = new Environment();
6007 $env->store = $storeEnv->store;
6010 $hasVariable = false;
6011 $args = [];
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;
6021 $keywordArgs = [];
6022 $deferredKeywordArgs = [];
6023 $deferredNamedKeywordArgs = [];
6024 $remaining = [];
6025 $hasKeywordArgument = false;
6027 // assign the keyword args
6028 foreach ((array) $argValues as $arg) {
6029 if (! empty($arg[0])) {
6030 $hasKeywordArgument = true;
6032 $name = $arg[0][1];
6034 if (! isset($args[$name])) {
6035 foreach (array_keys($args) as $an) {
6036 if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
6037 $name = $an;
6038 break;
6043 if (! isset($args[$name]) || $args[$name][3]) {
6044 if ($hasVariable) {
6045 $deferredNamedKeywordArgs[$name] = $arg[1];
6046 } else {
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]);
6051 } else {
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)) {
6064 $name = $an;
6065 break;
6070 if ($hasVariable) {
6071 $deferredKeywordArgs[$name] = $item;
6072 } else {
6073 $keywordArgs[$name] = $item;
6075 } else {
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)) {
6092 $name = $an;
6093 break;
6098 if ($hasVariable) {
6099 $deferredKeywordArgs[$name] = $item;
6100 } else {
6101 $keywordArgs[$name] = $item;
6103 } else {
6104 if (\is_null($splatSeparator)) {
6105 $splatSeparator = $val[1];
6108 $remaining[] = $item;
6111 } else {
6112 $remaining[] = $val;
6114 } elseif ($hasKeywordArgument) {
6115 throw $this->error('Positional arguments must come before keyword arguments.');
6116 } else {
6117 $remaining[] = $arg[1];
6121 foreach ($args as $arg) {
6122 list($i, $name, $default, $isVariable) = $arg;
6124 if ($isVariable) {
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)) {
6151 continue;
6152 } else {
6153 throw $this->error("Missing argument $name");
6156 if ($storeInEnv) {
6157 $this->set($name, $this->reduce($val, true), true, $env);
6158 } else {
6159 $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
6163 if ($storeInEnv) {
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)) {
6171 continue;
6174 if ($storeInEnv) {
6175 $this->set($name, $this->reduce($default, true), true);
6176 } else {
6177 $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
6181 return $output;
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) {
6194 return $value;
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);
6216 if ($color) {
6217 return $color;
6220 return $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) {
6233 return $item;
6236 if (
6237 $item[0] === static::$emptyList[0] &&
6238 $item[1] === static::$emptyList[1] &&
6239 $item[2] === static::$emptyList[2]
6241 return static::$emptyMap;
6244 return $item;
6248 * Coerce something to list
6250 * @param array $item
6251 * @param string $delim
6252 * @param boolean $removeTrailingNull
6254 * @return array
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]);
6264 return $item;
6267 if (isset($item) && $item[0] === Type::T_MAP) {
6268 $keys = $item[1];
6269 $values = $item[2];
6270 $list = [];
6272 for ($i = 0, $s = \count($keys); $i < $s; $i++) {
6273 $key = $keys[$i];
6274 $value = $values[$i];
6276 switch ($key[0]) {
6277 case Type::T_LIST:
6278 case Type::T_MAP:
6279 case Type::T_STRING:
6280 case Type::T_NULL:
6281 break;
6283 default:
6284 $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
6285 break;
6288 $list[] = [
6289 Type::T_LIST,
6291 [$key, $value]
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)) {
6311 return $color;
6314 return $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]) {
6328 case Type::T_COLOR:
6329 for ($i = 1; $i <= 3; $i++) {
6330 if (! is_numeric($value[$i])) {
6331 $cv = $this->compileRGBAValue($value[$i]);
6333 if (! is_numeric($cv)) {
6334 return null;
6337 $value[$i] = $cv;
6340 if (isset($value[4])) {
6341 if (! is_numeric($value[4])) {
6342 $cv = $this->compileRGBAValue($value[4], true);
6344 if (! is_numeric($cv)) {
6345 return null;
6348 $value[4] = $cv;
6353 return $value;
6355 case Type::T_LIST:
6356 if ($inRGBFunction) {
6357 if (\count($value[2]) == 3 || \count($value[2]) == 4) {
6358 $color = $value[2];
6359 array_unshift($color, Type::T_COLOR);
6361 return $this->coerceColor($color);
6365 return null;
6367 case Type::T_KEYWORD:
6368 if (! \is_string($value[1])) {
6369 return null;
6372 $name = strtolower($value[1]);
6374 // hexa color?
6375 if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
6376 $nofValues = \strlen($m[1]);
6378 if (\in_array($nofValues, [3, 4, 6, 8])) {
6379 $nbChannels = 3;
6380 $color = [];
6381 $num = hexdec($m[1]);
6383 switch ($nofValues) {
6384 case 4:
6385 $nbChannels = 4;
6386 // then continuing with the case 3:
6387 case 3:
6388 for ($i = 0; $i < $nbChannels; $i++) {
6389 $t = $num & 0xf;
6390 array_unshift($color, $t << 4 | $t);
6391 $num >>= 4;
6394 break;
6396 case 8:
6397 $nbChannels = 4;
6398 // then continuing with the case 6:
6399 case 6:
6400 for ($i = 0; $i < $nbChannels; $i++) {
6401 array_unshift($color, $num & 0xff);
6402 $num >>= 8;
6405 break;
6408 if ($nbChannels === 4) {
6409 if ($color[3] === 255) {
6410 $color[3] = 1; // fully opaque
6411 } else {
6412 $color[3] = round($color[3] / 255, Number::PRECISION);
6416 array_unshift($color, Type::T_COLOR);
6418 return $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]];
6428 return null;
6431 return null;
6435 * @param integer|Number $value
6436 * @param boolean $isAlpha
6438 * @return integer|mixed
6440 protected function compileRGBAValue($value, $isAlpha = false)
6442 if ($isAlpha) {
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) {
6464 $value = $reduced;
6468 if ($value instanceof Number) {
6469 if ($value->unitless()) {
6470 $num = $value->getDimension();
6471 } elseif ($value->hasUnit('%')) {
6472 $num = $max * $value->getDimension() / 100;
6473 } else {
6474 throw $this->error('Expected %s to have no units or "%%".', $value);
6477 $value = $num;
6478 } elseif (\is_array($value)) {
6479 $value = $this->compileValue($value);
6483 if (is_numeric($value)) {
6484 if ($isInt) {
6485 $value = round($value);
6488 $value = min($max, max($min, $value));
6490 return $value;
6493 return $value;
6497 * Coerce value to string
6499 * @param array|Number $value
6501 * @return array
6503 protected function coerceString($value)
6505 if ($value[0] === Type::T_STRING) {
6506 return $value;
6509 return [Type::T_STRING, '', [$this->compileValue($value)]];
6513 * Assert value is a string (or keyword)
6515 * @api
6517 * @param array|Number $value
6518 * @param string $varName
6520 * @return array
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);
6539 return $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();
6559 return 0;
6563 * Assert value is a map
6565 * @api
6567 * @param array|Number $value
6569 * @return array
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]);
6581 return $value;
6585 * Assert value is a list
6587 * @api
6589 * @param array|Number $value
6591 * @return array
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]);
6601 return $value;
6605 * Assert value is a color
6607 * @api
6609 * @param array|Number $value
6611 * @return array
6613 * @throws \Exception
6615 public function assertColor($value)
6617 if ($color = $this->coerceColor($value)) {
6618 return $color;
6621 throw $this->error('expecting color, %s received', $value[0]);
6625 * Assert value is a number
6627 * @api
6629 * @param array|Number $value
6630 * @param string $varName
6632 * @return Number
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.");
6644 return $value;
6648 * Assert value is a integer
6650 * @api
6652 * @param array|Number $value
6653 * @param string $varName
6655 * @return integer
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
6675 * @param array $c
6677 * @return array
6679 protected function fixColor($c)
6681 foreach ([1, 2, 3] as $i) {
6682 if ($c[$i] < 0) {
6683 $c[$i] = 0;
6686 if ($c[$i] > 255) {
6687 $c[$i] = 255;
6691 return $c;
6695 * Convert RGB to HSL
6697 * @api
6699 * @param integer $red
6700 * @param integer $green
6701 * @param integer $blue
6703 * @return array
6705 public function toHSL($red, $green, $blue)
6707 $min = min($red, $green, $blue);
6708 $max = max($red, $green, $blue);
6710 $l = $min + $max;
6711 $d = $max - $min;
6713 if ((int) $d === 0) {
6714 $h = $s = 0;
6715 } else {
6716 if ($l < 255) {
6717 $s = $d / $l;
6718 } else {
6719 $s = $d / (510 - $l);
6722 if ($red == $max) {
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];
6735 * Hue to RGB helper
6737 * @param float $m1
6738 * @param float $m2
6739 * @param float $h
6741 * @return float
6743 protected function hueToRGB($m1, $m2, $h)
6745 if ($h < 0) {
6746 $h += 1;
6747 } elseif ($h > 1) {
6748 $h -= 1;
6751 if ($h * 6 < 1) {
6752 return $m1 + ($m2 - $m1) * $h * 6;
6755 if ($h * 2 < 1) {
6756 return $m2;
6759 if ($h * 3 < 2) {
6760 return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
6763 return $m1;
6767 * Convert HSL to RGB
6769 * @api
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
6775 * @return array
6777 public function toRGB($hue, $saturation, $lightness)
6779 if ($hue < 0) {
6780 $hue += 360;
6783 $h = $hue / 360;
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;
6788 $m1 = $l * 2 - $m2;
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];
6796 return $out;
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]);
6822 $callArgs = [];
6824 // $kwargs['args'] is [Type::T_LIST, ',', [..]]
6825 foreach ($kwargs['args'][2] as $varname => $arg) {
6826 if (is_numeric($varname)) {
6827 $varname = null;
6828 } else {
6829 $varname = [ 'var', $varname];
6832 $callArgs[] = [$varname, $arg, false];
6835 return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
6839 protected static $libGetFunction = [
6840 ['name'],
6841 ['name', 'css']
6843 protected function libGetFunction($args)
6845 $name = $this->compileStringContent($this->coerceString(array_shift($args)));
6846 $isCss = false;
6848 if (count($args)) {
6849 $isCss = array_shift($args);
6850 $isCss = (($isCss === static::$true) ? true : false);
6853 if ($isCss) {
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;
6877 if (
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) {
6894 $key = 0;
6895 foreach ($list[2] as $item) {
6896 $key++;
6897 $itemValue = $this->normalizeValue($item);
6899 if ($itemValue instanceof Number && $value->equals($itemValue)) {
6900 return new Number($key, '');
6903 return static::$null;
6906 $values = [];
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 = [
6919 ['color'],
6920 ['color', 'alpha'],
6921 ['channels'],
6922 ['red', 'green', 'blue'],
6923 ['red', 'green', 'blue', 'alpha'] ];
6924 protected function libRgb($args, $kwargs, $funcName = 'rgb')
6926 switch (\count($args)) {
6927 case 1:
6928 if (! $color = $this->coerceColor($args[0], true)) {
6929 $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6931 break;
6933 case 3:
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], ')']];
6940 return $color;
6942 case 2:
6943 if ($color = $this->coerceColor($args[0], true)) {
6944 $alpha = $this->compileRGBAValue($args[1], true);
6946 if (is_numeric($alpha)) {
6947 $color[4] = $alpha;
6948 } else {
6949 $color = [Type::T_STRING, '',
6950 [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
6952 } else {
6953 $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6955 break;
6957 case 4:
6958 default:
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], ')']];
6965 break;
6968 return $color;
6971 protected static $libRgba = [
6972 ['color'],
6973 ['color', 'alpha'],
6974 ['channels'],
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
6988 * @return array
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];
7022 $color = $rgb;
7025 return $color;
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) {
7046 return $alter;
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) {
7057 // 1, 2, 3 - rgb
7058 // 4, 5, 6 - hsl
7059 // 7 - a
7060 switch ($i) {
7061 case 1:
7062 case 2:
7063 case 3:
7064 $max = 255;
7065 break;
7067 case 4:
7068 $max = 360;
7069 break;
7071 case 7:
7072 $max = 1;
7073 break;
7075 default:
7076 $max = 100;
7079 $scale = $scale / 100;
7081 if ($scale < 0) {
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');
7112 return $color[1];
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');
7124 return $color[2];
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');
7136 return $color[3];
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
7147 return null;
7150 protected static $libOpacity = ['color'];
7151 protected function libOpacity($args)
7153 $value = $args[0];
7155 if ($value instanceof Number) {
7156 return null;
7159 return $this->libAlpha($args);
7162 // mix two colors
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)) {
7175 $weight = 0.5;
7176 } else {
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;
7187 $w2 = 1.0 - $w1;
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 = [
7203 ['channels'],
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'])) {
7221 return null;
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)) {
7228 return null;
7231 $args[$k] = $this->stringifyFncallArgs($arg);
7234 if (
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'])
7239 return null;
7243 $hue = $this->reduce($args[0]);
7244 $saturation = $this->reduce($args[1]);
7245 $lightness = $this->reduce($args[2]);
7246 $alpha = null;
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], ')']];
7255 } else {
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) {
7264 $hueValue += 360;
7267 $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
7269 if (! \is_null($alpha)) {
7270 $color[4] = $alpha;
7273 return $color;
7276 protected static $libHsla = [
7277 ['channels'],
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];
7322 return $out;
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)
7355 $value = $args[0];
7357 if ($value instanceof Number) {
7358 return null;
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)
7384 $value = $args[0];
7386 if ($value instanceof Number) {
7387 return null;
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)) {
7405 $weight = 1;
7406 } else {
7407 $weight = $this->coercePercent($weight);
7410 if ($value instanceof Number) {
7411 return null;
7414 $color = $this->assertColor($value);
7415 $inverted = $color;
7416 $inverted[1] = 255 - $inverted[1];
7417 $inverted[2] = 255 - $inverted[2];
7418 $inverted[3] = 255 - $inverted[3];
7420 if ($weight < 1) {
7421 return $this->libMix([$inverted, $color, new Number($weight, '')]);
7424 return $inverted;
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]));
7437 return $color;
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]));
7456 return $color;
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)
7468 $str = $args[0];
7470 if ($str[0] === Type::T_STRING) {
7471 $str[1] = '';
7474 return $str;
7477 protected static $libQuote = ['string'];
7478 protected function libQuote($args)
7480 $value = $args[0];
7482 if ($value[0] === Type::T_STRING && ! empty($value[1])) {
7483 $value[1] = '"';
7484 return $value;
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)
7534 * @var Number|null
7536 $min = null;
7538 foreach ($args as $arg) {
7539 $number = $this->assertNumber($arg);
7541 if (\is_null($min) || $min->greaterThan($number)) {
7542 $min = $number;
7546 if (!\is_null($min)) {
7547 return $min;
7550 throw $this->error('At least one argument must be passed.');
7553 protected function libMax($args)
7556 * @var Number|null
7558 $max = null;
7560 foreach ($args as $arg) {
7561 $number = $this->assertNumber($arg);
7563 if (\is_null($max) || $max->lessThan($number)) {
7564 $max = $number;
7568 if (!\is_null($max)) {
7569 return $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) {
7587 return 'comma';
7590 if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
7591 return 'space';
7594 $list = $this->coerceList($args[0]);
7596 if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
7597 return 'space';
7600 if ($list[1] === ',') {
7601 return 'comma';
7604 return 'space';
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();
7613 if ($n > 0) {
7614 $n--;
7615 } elseif ($n < 0) {
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();
7628 if ($n > 0) {
7629 $n--;
7630 } elseif ($n < 0) {
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];
7640 return $list;
7643 protected static $libMapGet = ['map', 'key'];
7644 protected function libMapGet($args)
7646 $map = $this->assertMap($args[0]);
7647 $key = $args[1];
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]))) {
7654 return $map[2][$i];
7659 return static::$null;
7662 protected static $libMapKeys = ['map'];
7663 protected function libMapKeys($args)
7665 $map = $this->assertMap($args[0]);
7666 $keys = $map[1];
7668 return [Type::T_LIST, ',', $keys];
7671 protected static $libMapValues = ['map'];
7672 protected function libMapValues($args)
7674 $map = $this->assertMap($args[0]);
7675 $values = $map[2];
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]);
7686 $keys = [];
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);
7699 return $map;
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]))) {
7710 return true;
7714 return false;
7717 protected static $libMapMerge = [
7718 ['map1', 'map2'],
7719 ['map-1', 'map-2']
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];
7732 continue 2;
7736 $map1[1][] = $map2[1][$i2];
7737 $map1[2][] = $map2[2][$i2];
7740 return $map1;
7743 protected static $libKeywords = ['args'];
7744 protected function libKeywords($args)
7746 $this->assertList($args[0]);
7748 $keys = [];
7749 $values = [];
7751 foreach ($args[0][2] as $name => $arg) {
7752 $keys[] = [Type::T_KEYWORD, $name];
7753 $values[] = $arg;
7756 return [Type::T_MAP, $keys, $values];
7759 protected static $libIsBracketed = ['list'];
7760 protected function libIsBracketed($args)
7762 $list = $args[0];
7763 $this->coerceList($list, ' ');
7765 if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
7766 return true;
7769 return false;
7773 * @param array $list1
7774 * @param array|Number|null $sep
7776 * @return string
7777 * @throws CompilerException
7779 protected function listSeparatorForJoin($list1, $sep)
7781 if (! isset($sep)) {
7782 return $list1[1];
7785 switch ($this->compileValue($sep)) {
7786 case 'comma':
7787 return ',';
7789 case 'space':
7790 return ' ';
7792 default:
7793 return $list1[1];
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) {
7807 $bracketed = true;
7808 } elseif ($bracketed === static::$false) {
7809 $bracketed = false;
7810 } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
7811 $bracketed = 'auto';
7812 } elseif ($bracketed === static::$null) {
7813 $bracketed = false;
7814 } else {
7815 $bracketed = $this->compileValue($bracketed);
7816 $bracketed = ! ! $bracketed;
7818 if ($bracketed === true) {
7819 $bracketed = true;
7823 if ($bracketed === 'auto') {
7824 $bracketed = false;
7826 if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
7827 $bracketed = true;
7831 $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
7833 if (isset($list1['enclosing'])) {
7834 $res['enlcosing'] = $list1['enclosing'];
7837 if ($bracketed) {
7838 $res['enclosing'] = 'bracket';
7841 return $res;
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'];
7857 return $res;
7860 protected function libZip($args)
7862 foreach ($args as $key => $arg) {
7863 $args[$key] = $this->coerceList($arg);
7866 $lists = [];
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];
7877 } else {
7878 break 2;
7882 $lists[] = $list;
7885 $result[2] = $lists;
7886 } else {
7887 $result['enclosing'] = 'parent';
7890 return $result;
7893 protected static $libTypeOf = ['value'];
7894 protected function libTypeOf($args)
7896 $value = $args[0];
7898 switch ($value[0]) {
7899 case Type::T_KEYWORD:
7900 if ($value === static::$true || $value === static::$false) {
7901 return 'bool';
7904 if ($this->coerceColor($value)) {
7905 return 'color';
7908 // fall-thru
7909 case Type::T_FUNCTION:
7910 return 'string';
7912 case Type::T_FUNCTION_REFERENCE:
7913 return 'function';
7915 case Type::T_LIST:
7916 if (isset($value[3]) && $value[3]) {
7917 return 'arglist';
7920 // fall-thru
7921 default:
7922 return $value[0];
7926 protected static $libUnit = ['number'];
7927 protected function libUnit($args)
7929 $num = $args[0];
7931 if ($num instanceof Number) {
7932 return [Type::T_STRING, '"', [$num->unitStr()]];
7935 return '';
7938 protected static $libUnitless = ['number'];
7939 protected function libUnitless($args)
7941 $value = $args[0];
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;
7954 if (
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)) {
7974 $result = 0;
7975 } else {
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');
7992 if ($index > 0) {
7993 $index = $index - 1;
7995 if ($index < 0) {
7996 $index = Util::mbStrlen($stringContent) + 1 + $index;
7999 $string[2] = [
8000 Util::mbSubstr($stringContent, 0, $index),
8001 $insertContent,
8002 Util::mbSubstr($stringContent, $index)
8005 return $string;
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];
8029 if ($start > 0) {
8030 $start--;
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)];
8040 return $string;
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')];
8051 return $string;
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')];
8062 return $string;
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
8071 * @return string
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;
8084 } else {
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)) {
8111 return true;
8114 $name = $this->normalizeName($name);
8116 if (isset($this->userFunctions[$name])) {
8117 return true;
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
8158 * @return array
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();
8173 if ($n < 1) {
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()
8190 static $id;
8192 if (! isset($id)) {
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;
8218 if (
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)
8241 $value = $args[0];
8243 return $this->inspectFormatValue($value);
8247 * Preprocess selector args
8249 * @param array $arg
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
8297 * @param array $arg
8298 * @param int $maxDepth
8299 * @return bool
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)) {
8306 return false;
8309 return true;
8311 if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
8312 return false;
8314 return true;
8318 * Postprocess selector to output in right format
8320 * @param array $selectors
8322 * @return string
8324 protected function formatOutputSelector($selectors)
8326 $selectors = $this->collapseSelectors($selectors, true);
8328 return $selectors;
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
8346 * @param array $sub
8348 * @return boolean
8350 protected function isSuperSelector($super, $sub)
8352 // one and only one selector for each arg
8353 if (! $super) {
8354 throw $this->error('Invalid super selector for isSuperSelector()');
8357 if (! $sub) {
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])) {
8364 return false;
8367 return true;
8370 if (count($super) > 1) {
8371 foreach ($super as $s) {
8372 if ($this->isSuperSelector([$s], $sub)) {
8373 return true;
8376 return false;
8379 $super = reset($super);
8380 $sub = reset($sub);
8382 $i = 0;
8383 $nextMustMatch = false;
8385 foreach ($super as $node) {
8386 $compound = '';
8388 array_walk_recursive(
8389 $node,
8390 function ($value, $key) use (&$compound) {
8391 $compound .= $value;
8395 if ($this->isImmediateRelationshipCombinator($compound)) {
8396 if ($node !== $sub[$i]) {
8397 return false;
8400 $nextMustMatch = true;
8401 $i++;
8402 } else {
8403 while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
8404 if ($nextMustMatch) {
8405 return false;
8408 $i++;
8411 if ($i >= \count($sub)) {
8412 return false;
8415 $nextMustMatch = false;
8416 $i++;
8420 return true;
8424 * Test a part of super selector again a part of sub selector
8426 * @param array $superParts
8427 * @param array $subParts
8429 * @return boolean
8431 protected function isSuperPart($superParts, $subParts)
8433 $i = 0;
8435 foreach ($superParts as $superPart) {
8436 while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
8437 $i++;
8440 if ($i >= \count($subParts)) {
8441 return false;
8444 $i++;
8447 return true;
8450 protected static $libSelectorAppend = ['selector...'];
8451 protected function libSelectorAppend($args)
8453 // get the selector... list
8454 $args = reset($args);
8455 $args = $args[2];
8457 if (\count($args) < 1) {
8458 throw $this->error('selector-append() needs at least 1 argument');
8461 $selectors = [];
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
8474 * @return array
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
8494 $appended = [];
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) {
8510 $appended[] = $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
8571 * @return array
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);
8586 $extended = [];
8588 foreach ($selectors as $selector) {
8589 if (! $replace) {
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;
8606 return $extended;
8609 protected static $libSelectorNest = ['selector...'];
8610 protected function libSelectorNest($args)
8612 // get the selector... list
8613 $args = reset($args);
8614 $args = $args[2];
8616 if (\count($args) < 1) {
8617 throw $this->error('selector-nest() needs at least 1 argument');
8620 $selectorsMap = [];
8621 foreach ($args as $arg) {
8622 $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
8625 $envs = [];
8627 foreach ($selectorsMap as $selectors) {
8628 $env = new Environment();
8629 $env->selectors = $selectors;
8631 $envs[] = $env;
8634 $envs = array_reverse($envs);
8635 $env = $this->extractEnv($envs);
8636 $outputSelectors = $this->multiplySelectors($env);
8638 return $this->formatOutputSelector($outputSelectors);
8641 protected static $libSelectorParse = [
8642 ['selector'],
8643 ['selectors']
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)) {
8687 return $compound2;
8690 if (! \count($compound2)) {
8691 return $compound1;
8694 // check that last part are compatible
8695 $lastPart1 = array_pop($compound1);
8696 $lastPart2 = array_pop($compound2);
8697 $last = $this->mergeParts($lastPart1, $lastPart2);
8699 if (! $last) {
8700 return [[]];
8703 $unifiedCompound = [$last];
8704 $unifiedSelectors = [$unifiedCompound];
8706 // do the rest
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;
8714 if ($after2) {
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;
8729 if ($after1) {
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);
8741 $new = [];
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]));
8751 } elseif ($part1) {
8752 array_pop($compound1);
8754 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
8755 } elseif ($part2) {
8756 array_pop($compound2);
8758 $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
8761 if ($new) {
8762 $unifiedSelectors = $new;
8766 return $unifiedSelectors;
8770 * Prepend each selector from $selectors with $parts
8772 * @param array $selectors
8773 * @param array $parts
8775 * @return array
8777 protected function prependSelectors($selectors, $parts)
8779 $new = [];
8781 foreach ($selectors as $compoundSelector) {
8782 array_unshift($compoundSelector, $parts);
8784 $new[] = $compoundSelector;
8787 return $new;
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;
8804 $after = [];
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];
8814 $after[] = $p;
8817 // try again matching a non empty intersection and a compatible tagname
8818 $before = $compound;
8819 $after = [];
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];
8830 $after[] = $p;
8833 return false;
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
8844 * @return array
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) {
8854 return [];
8857 if ($tag) {
8858 if ($tag1) {
8859 $parts1 = array_diff($parts1, [$tag1]);
8862 if ($tag2) {
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);
8879 if ($tag) {
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) {
8907 return false;
8910 return $tags;
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)) {
8924 return $part;
8928 return '';
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);
8943 $listParts = [];
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);
8958 $listParts = [];
8960 foreach ($matches as $match) {
8961 if (! is_file($match)) {
8962 continue;
8965 $listParts[] = [Type::T_STRING, '"', [$match]];
8968 return [Type::T_LIST, ',', $listParts];