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