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