Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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