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