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