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