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