Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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