Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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