Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 9 May 2022 (12 months).
  • Bug fixes for security issues in 3.11.x will end 14 November 2022 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 35 and 311] [Versions 36 and 311] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       1  <?php
       2  
       3  /**
       4   * SCSSPHP
       5   *
       6   * @copyright 2012-2020 Leaf Corcoran
       7   *
       8   * @license http://opensource.org/licenses/MIT MIT
       9   *
      10   * @link http://scssphp.github.io/scssphp
      11   */
      12  
      13  namespace ScssPhp\ScssPhp;
      14  
      15  use ScssPhp\ScssPhp\Base\Range;
      16  use ScssPhp\ScssPhp\Compiler\Environment;
      17  use ScssPhp\ScssPhp\Exception\CompilerException;
      18  use ScssPhp\ScssPhp\Exception\ParserException;
      19  use ScssPhp\ScssPhp\Exception\SassScriptException;
      20  use ScssPhp\ScssPhp\Formatter\Compressed;
      21  use ScssPhp\ScssPhp\Formatter\Expanded;
      22  use ScssPhp\ScssPhp\Formatter\OutputBlock;
      23  use ScssPhp\ScssPhp\Node\Number;
      24  use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
      25  use ScssPhp\ScssPhp\Util\Path;
      26  
      27  /**
      28   * The scss compiler and parser.
      29   *
      30   * Converting SCSS to CSS is a three stage process. The incoming file is parsed
      31   * by `Parser` into a syntax tree, then it is compiled into another tree
      32   * representing the CSS structure by `Compiler`. The CSS tree is fed into a
      33   * formatter, like `Formatter` which then outputs CSS as a string.
      34   *
      35   * During the first compile, all values are *reduced*, which means that their
      36   * types are brought to the lowest form before being dump as strings. This
      37   * handles math equations, variable dereferences, and the like.
      38   *
      39   * The `compile` function of `Compiler` is the entry point.
      40   *
      41   * In summary:
      42   *
      43   * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
      44   * then transforms the resulting tree to a CSS tree. This class also holds the
      45   * evaluation context, such as all available mixins and variables at any given
      46   * time.
      47   *
      48   * The `Parser` class is only concerned with parsing its input.
      49   *
      50   * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
      51   * handling things like indentation.
      52   */
      53  
      54  /**
      55   * SCSS compiler
      56   *
      57   * @author Leaf Corcoran <leafot@gmail.com>
      58   */
      59  class Compiler
      60  {
      61      /**
      62       * @deprecated
      63       */
      64      const LINE_COMMENTS = 1;
      65      /**
      66       * @deprecated
      67       */
      68      const DEBUG_INFO    = 2;
      69  
      70      /**
      71       * @deprecated
      72       */
      73      const WITH_RULE     = 1;
      74      /**
      75       * @deprecated
      76       */
      77      const WITH_MEDIA    = 2;
      78      /**
      79       * @deprecated
      80       */
      81      const WITH_SUPPORTS = 4;
      82      /**
      83       * @deprecated
      84       */
      85      const WITH_ALL      = 7;
      86  
      87      const SOURCE_MAP_NONE   = 0;
      88      const SOURCE_MAP_INLINE = 1;
      89      const SOURCE_MAP_FILE   = 2;
      90  
      91      /**
      92       * @var array<string, string>
      93       */
      94      protected static $operatorNames = [
      95          '+'   => 'add',
      96          '-'   => 'sub',
      97          '*'   => 'mul',
      98          '/'   => 'div',
      99          '%'   => 'mod',
     100  
     101          '=='  => 'eq',
     102          '!='  => 'neq',
     103          '<'   => 'lt',
     104          '>'   => 'gt',
     105  
     106          '<='  => 'lte',
     107          '>='  => 'gte',
     108      ];
     109  
     110      /**
     111       * @var array<string, string>
     112       */
     113      protected static $namespaces = [
     114          'special'  => '%',
     115          'mixin'    => '@',
     116          'function' => '^',
     117      ];
     118  
     119      public static $true         = [Type::T_KEYWORD, 'true'];
     120      public static $false        = [Type::T_KEYWORD, 'false'];
     121      /** @deprecated */
     122      public static $NaN          = [Type::T_KEYWORD, 'NaN'];
     123      /** @deprecated */
     124      public static $Infinity     = [Type::T_KEYWORD, 'Infinity'];
     125      public static $null         = [Type::T_NULL];
     126      public static $nullString   = [Type::T_STRING, '', []];
     127      public static $defaultValue = [Type::T_KEYWORD, ''];
     128      public static $selfSelector = [Type::T_SELF];
     129      public static $emptyList    = [Type::T_LIST, '', []];
     130      public static $emptyMap     = [Type::T_MAP, [], []];
     131      public static $emptyString  = [Type::T_STRING, '"', []];
     132      public static $with         = [Type::T_KEYWORD, 'with'];
     133      public static $without      = [Type::T_KEYWORD, 'without'];
     134  
     135      /**
     136       * @var array<int, string|callable>
     137       */
     138      protected $importPaths = [];
     139      /**
     140       * @var array<string, Block>
     141       */
     142      protected $importCache = [];
     143      /**
     144       * @var string[]
     145       */
     146      protected $importedFiles = [];
     147      protected $userFunctions = [];
     148      protected $registeredVars = [];
     149      /**
     150       * @var array<string, bool>
     151       */
     152      protected $registeredFeatures = [
     153          'extend-selector-pseudoclass' => false,
     154          'at-error'                    => true,
     155          'units-level-3'               => true,
     156          'global-variable-shadowing'   => false,
     157      ];
     158  
     159      /**
     160       * @var string|null
     161       */
     162      protected $encoding = null;
     163      /**
     164       * @deprecated
     165       */
     166      protected $lineNumberStyle = null;
     167  
     168      /**
     169       * @var int|SourceMapGenerator
     170       * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
     171       */
     172      protected $sourceMap = self::SOURCE_MAP_NONE;
     173      protected $sourceMapOptions = [];
     174  
     175      /**
     176       * @var string|\ScssPhp\ScssPhp\Formatter
     177       */
     178      protected $formatter = Expanded::class;
     179  
     180      /**
     181       * @var Environment
     182       */
     183      protected $rootEnv;
     184      /**
     185       * @var OutputBlock|null
     186       */
     187      protected $rootBlock;
     188  
     189      /**
     190       * @var \ScssPhp\ScssPhp\Compiler\Environment
     191       */
     192      protected $env;
     193      /**
     194       * @var OutputBlock|null
     195       */
     196      protected $scope;
     197      /**
     198       * @var Environment|null
     199       */
     200      protected $storeEnv;
     201      /**
     202       * @var bool|null
     203       */
     204      protected $charsetSeen;
     205      /**
     206       * @var array<int, string>
     207       */
     208      protected $sourceNames;
     209  
     210      /**
     211       * @var Cache|null
     212       */
     213      protected $cache;
     214  
     215      /**
     216       * @var int
     217       */
     218      protected $indentLevel;
     219      /**
     220       * @var array[]
     221       */
     222      protected $extends;
     223      /**
     224       * @var array<string, int[]>
     225       */
     226      protected $extendsMap;
     227      /**
     228       * @var array<string, int>
     229       */
     230      protected $parsedFiles;
     231      /**
     232       * @var Parser|null
     233       */
     234      protected $parser;
     235      /**
     236       * @var int|null
     237       */
     238      protected $sourceIndex;
     239      /**
     240       * @var int|null
     241       */
     242      protected $sourceLine;
     243      /**
     244       * @var int|null
     245       */
     246      protected $sourceColumn;
     247      /**
     248       * @var resource
     249       */
     250      protected $stderr;
     251      /**
     252       * @var bool|null
     253       */
     254      protected $shouldEvaluate;
     255      /**
     256       * @var null
     257       * @deprecated
     258       */
     259      protected $ignoreErrors;
     260      /**
     261       * @var bool
     262       */
     263      protected $ignoreCallStackMessage = false;
     264  
     265      /**
     266       * @var array[]
     267       */
     268      protected $callStack = [];
     269  
     270      /**
     271       * The directory of the currently processed file
     272       *
     273       * @var string|null
     274       */
     275      private $currentDirectory;
     276  
     277      /**
     278       * The directory of the input file
     279       *
     280       * @var string
     281       */
     282      private $rootDirectory;
     283  
     284      private $legacyCwdImportPath = true;
     285  
     286      /**
     287       * Constructor
     288       *
     289       * @param array|null $cacheOptions
     290       */
     291      public function __construct($cacheOptions = null)
     292      {
     293          $this->parsedFiles = [];
     294          $this->sourceNames = [];
     295  
     296          if ($cacheOptions) {
     297              $this->cache = new Cache($cacheOptions);
     298          }
     299  
     300          $this->stderr = fopen('php://stderr', 'w');
     301      }
     302  
     303      /**
     304       * Get compiler options
     305       *
     306       * @return array<string, mixed>
     307       */
     308      public function getCompileOptions()
     309      {
     310          $options = [
     311              'importPaths'        => $this->importPaths,
     312              'registeredVars'     => $this->registeredVars,
     313              'registeredFeatures' => $this->registeredFeatures,
     314              'encoding'           => $this->encoding,
     315              'sourceMap'          => serialize($this->sourceMap),
     316              'sourceMapOptions'   => $this->sourceMapOptions,
     317              'formatter'          => $this->formatter,
     318              'legacyImportPath'   => $this->legacyCwdImportPath,
     319          ];
     320  
     321          return $options;
     322      }
     323  
     324      /**
     325       * Set an alternative error output stream, for testing purpose only
     326       *
     327       * @param resource $handle
     328       *
     329       * @return void
     330       */
     331      public function setErrorOuput($handle)
     332      {
     333          $this->stderr = $handle;
     334      }
     335  
     336      /**
     337       * Compile scss
     338       *
     339       * @api
     340       *
     341       * @param string $code
     342       * @param string $path
     343       *
     344       * @return string
     345       */
     346      public function compile($code, $path = null)
     347      {
     348          if ($this->cache) {
     349              $cacheKey       = ($path ? $path : '(stdin)') . ':' . md5($code);
     350              $compileOptions = $this->getCompileOptions();
     351              $cache          = $this->cache->getCache('compile', $cacheKey, $compileOptions);
     352  
     353              if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
     354                  // check if any dependency file changed before accepting the cache
     355                  foreach ($cache['dependencies'] as $file => $mtime) {
     356                      if (! is_file($file) || filemtime($file) !== $mtime) {
     357                          unset($cache);
     358                          break;
     359                      }
     360                  }
     361  
     362                  if (isset($cache)) {
     363                      return $cache['out'];
     364                  }
     365              }
     366          }
     367  
     368  
     369          $this->indentLevel    = -1;
     370          $this->extends        = [];
     371          $this->extendsMap     = [];
     372          $this->sourceIndex    = null;
     373          $this->sourceLine     = null;
     374          $this->sourceColumn   = null;
     375          $this->env            = null;
     376          $this->scope          = null;
     377          $this->storeEnv       = null;
     378          $this->charsetSeen    = null;
     379          $this->shouldEvaluate = null;
     380          $this->ignoreCallStackMessage = false;
     381  
     382          if (!\is_null($path) && is_file($path)) {
     383              $path = realpath($path) ?: $path;
     384              $this->currentDirectory = dirname($path);
     385              $this->rootDirectory = $this->currentDirectory;
     386          } else {
     387              $this->currentDirectory = null;
     388              $this->rootDirectory = getcwd();
     389          }
     390  
     391          try {
     392              $this->parser = $this->parserFactory($path);
     393              $tree         = $this->parser->parse($code);
     394              $this->parser = null;
     395  
     396              $this->formatter = new $this->formatter();
     397              $this->rootBlock = null;
     398              $this->rootEnv   = $this->pushEnv($tree);
     399  
     400              $this->injectVariables($this->registeredVars);
     401              $this->compileRoot($tree);
     402              $this->popEnv();
     403  
     404              $sourceMapGenerator = null;
     405  
     406              if ($this->sourceMap) {
     407                  if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
     408                      $sourceMapGenerator = $this->sourceMap;
     409                      $this->sourceMap = self::SOURCE_MAP_FILE;
     410                  } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
     411                      $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
     412                  }
     413              }
     414  
     415              $out = $this->formatter->format($this->scope, $sourceMapGenerator);
     416  
     417              $prefix = '';
     418  
     419              if (!$this->charsetSeen) {
     420                  if (strlen($out) !== Util::mbStrlen($out)) {
     421                      $prefix = '@charset "UTF-8";' . "\n";
     422                      $out = $prefix . $out;
     423                  }
     424              }
     425  
     426              if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
     427                  $sourceMap    = $sourceMapGenerator->generateJson($prefix);
     428                  $sourceMapUrl = null;
     429  
     430                  switch ($this->sourceMap) {
     431                      case self::SOURCE_MAP_INLINE:
     432                          $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
     433                          break;
     434  
     435                      case self::SOURCE_MAP_FILE:
     436                          $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
     437                          break;
     438                  }
     439  
     440                  $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
     441              }
     442          } catch (SassScriptException $e) {
     443              throw $this->error($e->getMessage());
     444          }
     445  
     446          if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
     447              $v = [
     448                  'dependencies' => $this->getParsedFiles(),
     449                  'out' => &$out,
     450              ];
     451  
     452              $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
     453          }
     454  
     455          return $out;
     456      }
     457  
     458      /**
     459       * Instantiate parser
     460       *
     461       * @param string $path
     462       *
     463       * @return \ScssPhp\ScssPhp\Parser
     464       */
     465      protected function parserFactory($path)
     466      {
     467          // https://sass-lang.com/documentation/at-rules/import
     468          // CSS files imported by Sass don’t allow any special Sass features.
     469          // In order to make sure authors don’t accidentally write Sass in their CSS,
     470          // all Sass features that aren’t also valid CSS will produce errors.
     471          // Otherwise, the CSS will be rendered as-is. It can even be extended!
     472          $cssOnly = false;
     473  
     474          if (substr($path, '-4') === '.css') {
     475              $cssOnly = true;
     476          }
     477  
     478          $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
     479  
     480          $this->sourceNames[] = $path;
     481          $this->addParsedFile($path);
     482  
     483          return $parser;
     484      }
     485  
     486      /**
     487       * Is self extend?
     488       *
     489       * @param array $target
     490       * @param array $origin
     491       *
     492       * @return boolean
     493       */
     494      protected function isSelfExtend($target, $origin)
     495      {
     496          foreach ($origin as $sel) {
     497              if (\in_array($target, $sel)) {
     498                  return true;
     499              }
     500          }
     501  
     502          return false;
     503      }
     504  
     505      /**
     506       * Push extends
     507       *
     508       * @param array      $target
     509       * @param array      $origin
     510       * @param array|null $block
     511       *
     512       * @return void
     513       */
     514      protected function pushExtends($target, $origin, $block)
     515      {
     516          $i = \count($this->extends);
     517          $this->extends[] = [$target, $origin, $block];
     518  
     519          foreach ($target as $part) {
     520              if (isset($this->extendsMap[$part])) {
     521                  $this->extendsMap[$part][] = $i;
     522              } else {
     523                  $this->extendsMap[$part] = [$i];
     524              }
     525          }
     526      }
     527  
     528      /**
     529       * Make output block
     530       *
     531       * @param string $type
     532       * @param array  $selectors
     533       *
     534       * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
     535       */
     536      protected function makeOutputBlock($type, $selectors = null)
     537      {
     538          $out = new OutputBlock();
     539          $out->type      = $type;
     540          $out->lines     = [];
     541          $out->children  = [];
     542          $out->parent    = $this->scope;
     543          $out->selectors = $selectors;
     544          $out->depth     = $this->env->depth;
     545  
     546          if ($this->env->block instanceof Block) {
     547              $out->sourceName   = $this->env->block->sourceName;
     548              $out->sourceLine   = $this->env->block->sourceLine;
     549              $out->sourceColumn = $this->env->block->sourceColumn;
     550          } else {
     551              $out->sourceName   = null;
     552              $out->sourceLine   = null;
     553              $out->sourceColumn = null;
     554          }
     555  
     556          return $out;
     557      }
     558  
     559      /**
     560       * Compile root
     561       *
     562       * @param \ScssPhp\ScssPhp\Block $rootBlock
     563       *
     564       * @return void
     565       */
     566      protected function compileRoot(Block $rootBlock)
     567      {
     568          $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
     569  
     570          $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
     571          $this->flattenSelectors($this->scope);
     572          $this->missingSelectors();
     573      }
     574  
     575      /**
     576       * Report missing selectors
     577       *
     578       * @return void
     579       */
     580      protected function missingSelectors()
     581      {
     582          foreach ($this->extends as $extend) {
     583              if (isset($extend[3])) {
     584                  continue;
     585              }
     586  
     587              list($target, $origin, $block) = $extend;
     588  
     589              // ignore if !optional
     590              if ($block[2]) {
     591                  continue;
     592              }
     593  
     594              $target = implode(' ', $target);
     595              $origin = $this->collapseSelectors($origin);
     596  
     597              $this->sourceLine = $block[Parser::SOURCE_LINE];
     598              throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
     599          }
     600      }
     601  
     602      /**
     603       * Flatten selectors
     604       *
     605       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
     606       * @param string                                 $parentKey
     607       *
     608       * @return void
     609       */
     610      protected function flattenSelectors(OutputBlock $block, $parentKey = null)
     611      {
     612          if ($block->selectors) {
     613              $selectors = [];
     614  
     615              foreach ($block->selectors as $s) {
     616                  $selectors[] = $s;
     617  
     618                  if (! \is_array($s)) {
     619                      continue;
     620                  }
     621  
     622                  // check extends
     623                  if (! empty($this->extendsMap)) {
     624                      $this->matchExtends($s, $selectors);
     625  
     626                      // remove duplicates
     627                      array_walk($selectors, function (&$value) {
     628                          $value = serialize($value);
     629                      });
     630  
     631                      $selectors = array_unique($selectors);
     632  
     633                      array_walk($selectors, function (&$value) {
     634                          $value = unserialize($value);
     635                      });
     636                  }
     637              }
     638  
     639              $block->selectors = [];
     640              $placeholderSelector = false;
     641  
     642              foreach ($selectors as $selector) {
     643                  if ($this->hasSelectorPlaceholder($selector)) {
     644                      $placeholderSelector = true;
     645                      continue;
     646                  }
     647  
     648                  $block->selectors[] = $this->compileSelector($selector);
     649              }
     650  
     651              if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
     652                  unset($block->parent->children[$parentKey]);
     653  
     654                  return;
     655              }
     656          }
     657  
     658          foreach ($block->children as $key => $child) {
     659              $this->flattenSelectors($child, $key);
     660          }
     661      }
     662  
     663      /**
     664       * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
     665       *
     666       * @param array $parts
     667       *
     668       * @return array
     669       */
     670      protected function glueFunctionSelectors($parts)
     671      {
     672          $new = [];
     673  
     674          foreach ($parts as $part) {
     675              if (\is_array($part)) {
     676                  $part = $this->glueFunctionSelectors($part);
     677                  $new[] = $part;
     678              } else {
     679                  // a selector part finishing with a ) is the last part of a :not( or :nth-child(
     680                  // and need to be joined to this
     681                  if (
     682                      \count($new) && \is_string($new[\count($new) - 1]) &&
     683                      \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
     684                  ) {
     685                      while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
     686                          $part = array_pop($new) . $part;
     687                      }
     688                      $new[\count($new) - 1] .= $part;
     689                  } else {
     690                      $new[] = $part;
     691                  }
     692              }
     693          }
     694  
     695          return $new;
     696      }
     697  
     698      /**
     699       * Match extends
     700       *
     701       * @param array   $selector
     702       * @param array   $out
     703       * @param integer $from
     704       * @param boolean $initial
     705       *
     706       * @return void
     707       */
     708      protected function matchExtends($selector, &$out, $from = 0, $initial = true)
     709      {
     710          static $partsPile = [];
     711          $selector = $this->glueFunctionSelectors($selector);
     712  
     713          if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
     714              return;
     715          }
     716  
     717          $outRecurs = [];
     718  
     719          foreach ($selector as $i => $part) {
     720              if ($i < $from) {
     721                  continue;
     722              }
     723  
     724              // check that we are not building an infinite loop of extensions
     725              // if the new part is just including a previous part don't try to extend anymore
     726              if (\count($part) > 1) {
     727                  foreach ($partsPile as $previousPart) {
     728                      if (! \count(array_diff($previousPart, $part))) {
     729                          continue 2;
     730                      }
     731                  }
     732              }
     733  
     734              $partsPile[] = $part;
     735  
     736              if ($this->matchExtendsSingle($part, $origin, $initial)) {
     737                  $after       = \array_slice($selector, $i + 1);
     738                  $before      = \array_slice($selector, 0, $i);
     739                  list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
     740  
     741                  foreach ($origin as $new) {
     742                      $k = 0;
     743  
     744                      // remove shared parts
     745                      if (\count($new) > 1) {
     746                          while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
     747                              $k++;
     748                          }
     749                      }
     750  
     751                      if (\count($nonBreakableBefore) && $k === \count($new)) {
     752                          $k--;
     753                      }
     754  
     755                      $replacement = [];
     756                      $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
     757  
     758                      for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
     759                          $slice = [];
     760  
     761                          foreach ($tempReplacement[$l] as $chunk) {
     762                              if (! \in_array($chunk, $slice)) {
     763                                  $slice[] = $chunk;
     764                              }
     765                          }
     766  
     767                          array_unshift($replacement, $slice);
     768  
     769                          if (! $this->isImmediateRelationshipCombinator(end($slice))) {
     770                              break;
     771                          }
     772                      }
     773  
     774                      $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
     775  
     776                      // Merge shared direct relationships.
     777                      $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
     778  
     779                      $result = array_merge(
     780                          $before,
     781                          $mergedBefore,
     782                          $replacement,
     783                          $after
     784                      );
     785  
     786                      if ($result === $selector) {
     787                          continue;
     788                      }
     789  
     790                      $this->pushOrMergeExtentedSelector($out, $result);
     791  
     792                      // recursively check for more matches
     793                      $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
     794  
     795                      if (\count($origin) > 1) {
     796                          $this->matchExtends($result, $out, $startRecurseFrom, false);
     797                      } else {
     798                          $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
     799                      }
     800  
     801                      // selector sequence merging
     802                      if (! empty($before) && \count($new) > 1) {
     803                          $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
     804                          $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
     805  
     806                          list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
     807  
     808                          $result2 = array_merge(
     809                              $preSharedParts,
     810                              $betweenSharedParts,
     811                              $postSharedParts,
     812                              $nonBreakabl2,
     813                              $nonBreakableBefore,
     814                              $replacement,
     815                              $after
     816                          );
     817  
     818                          $this->pushOrMergeExtentedSelector($out, $result2);
     819                      }
     820                  }
     821              }
     822              array_pop($partsPile);
     823          }
     824  
     825          while (\count($outRecurs)) {
     826              $result = array_shift($outRecurs);
     827              $this->pushOrMergeExtentedSelector($out, $result);
     828          }
     829      }
     830  
     831      /**
     832       * Test a part for being a pseudo selector
     833       *
     834       * @param string $part
     835       * @param array  $matches
     836       *
     837       * @return boolean
     838       */
     839      protected function isPseudoSelector($part, &$matches)
     840      {
     841          if (
     842              strpos($part, ':') === 0 &&
     843              preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
     844          ) {
     845              return true;
     846          }
     847  
     848          return false;
     849      }
     850  
     851      /**
     852       * Push extended selector except if
     853       *  - this is a pseudo selector
     854       *  - same as previous
     855       *  - in a white list
     856       * in this case we merge the pseudo selector content
     857       *
     858       * @param array $out
     859       * @param array $extended
     860       *
     861       * @return void
     862       */
     863      protected function pushOrMergeExtentedSelector(&$out, $extended)
     864      {
     865          if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
     866              $single = reset($extended);
     867              $part = reset($single);
     868  
     869              if (
     870                  $this->isPseudoSelector($part, $matchesExtended) &&
     871                  \in_array($matchesExtended[1], [ 'slotted' ])
     872              ) {
     873                  $prev = end($out);
     874                  $prev = $this->glueFunctionSelectors($prev);
     875  
     876                  if (\count($prev) === 1 && \count(reset($prev)) === 1) {
     877                      $single = reset($prev);
     878                      $part = reset($single);
     879  
     880                      if (
     881                          $this->isPseudoSelector($part, $matchesPrev) &&
     882                          $matchesPrev[1] === $matchesExtended[1]
     883                      ) {
     884                          $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
     885                          $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
     886                          $extended = implode($matchesExtended[1] . '(', $extended);
     887                          $extended = [ [ $extended ]];
     888                          array_pop($out);
     889                      }
     890                  }
     891              }
     892          }
     893          $out[] = $extended;
     894      }
     895  
     896      /**
     897       * Match extends single
     898       *
     899       * @param array   $rawSingle
     900       * @param array   $outOrigin
     901       * @param boolean $initial
     902       *
     903       * @return boolean
     904       */
     905      protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
     906      {
     907          $counts = [];
     908          $single = [];
     909  
     910          // simple usual cases, no need to do the whole trick
     911          if (\in_array($rawSingle, [['>'],['+'],['~']])) {
     912              return false;
     913          }
     914  
     915          foreach ($rawSingle as $part) {
     916              // matches Number
     917              if (! \is_string($part)) {
     918                  return false;
     919              }
     920  
     921              if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
     922                  $single[\count($single) - 1] .= $part;
     923              } else {
     924                  $single[] = $part;
     925              }
     926          }
     927  
     928          $extendingDecoratedTag = false;
     929  
     930          if (\count($single) > 1) {
     931              $matches = null;
     932              $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
     933          }
     934  
     935          $outOrigin = [];
     936          $found = false;
     937  
     938          foreach ($single as $k => $part) {
     939              if (isset($this->extendsMap[$part])) {
     940                  foreach ($this->extendsMap[$part] as $idx) {
     941                      $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
     942                  }
     943              }
     944  
     945              if (
     946                  $initial &&
     947                  $this->isPseudoSelector($part, $matches) &&
     948                  ! \in_array($matches[1], [ 'not' ])
     949              ) {
     950                  $buffer    = $matches[2];
     951                  $parser    = $this->parserFactory(__METHOD__);
     952  
     953                  if ($parser->parseSelector($buffer, $subSelectors, false)) {
     954                      foreach ($subSelectors as $ksub => $subSelector) {
     955                          $subExtended = [];
     956                          $this->matchExtends($subSelector, $subExtended, 0, false);
     957  
     958                          if ($subExtended) {
     959                              $subSelectorsExtended = $subSelectors;
     960                              $subSelectorsExtended[$ksub] = $subExtended;
     961  
     962                              foreach ($subSelectorsExtended as $ksse => $sse) {
     963                                  $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
     964                              }
     965  
     966                              $subSelectorsExtended = implode(', ', $subSelectorsExtended);
     967                              $singleExtended = $single;
     968                              $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
     969                              $outOrigin[] = [ $singleExtended ];
     970                              $found = true;
     971                          }
     972                      }
     973                  }
     974              }
     975          }
     976  
     977          foreach ($counts as $idx => $count) {
     978              list($target, $origin, /* $block */) = $this->extends[$idx];
     979  
     980              $origin = $this->glueFunctionSelectors($origin);
     981  
     982              // check count
     983              if ($count !== \count($target)) {
     984                  continue;
     985              }
     986  
     987              $this->extends[$idx][3] = true;
     988  
     989              $rem = array_diff($single, $target);
     990  
     991              foreach ($origin as $j => $new) {
     992                  // prevent infinite loop when target extends itself
     993                  if ($this->isSelfExtend($single, $origin) && ! $initial) {
     994                      return false;
     995                  }
     996  
     997                  $replacement = end($new);
     998  
     999                  // Extending a decorated tag with another tag is not possible.
    1000                  if (
    1001                      $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
    1002                      preg_match('/^[a-z0-9]+$/i', $replacement[0])
    1003                  ) {
    1004                      unset($origin[$j]);
    1005                      continue;
    1006                  }
    1007  
    1008                  $combined = $this->combineSelectorSingle($replacement, $rem);
    1009  
    1010                  if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
    1011                      $origin[$j][\count($origin[$j]) - 1] = $combined;
    1012                  }
    1013              }
    1014  
    1015              $outOrigin = array_merge($outOrigin, $origin);
    1016  
    1017              $found = true;
    1018          }
    1019  
    1020          return $found;
    1021      }
    1022  
    1023      /**
    1024       * Extract a relationship from the fragment.
    1025       *
    1026       * When extracting the last portion of a selector we will be left with a
    1027       * fragment which may end with a direction relationship combinator. This
    1028       * method will extract the relationship fragment and return it along side
    1029       * the rest.
    1030       *
    1031       * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
    1032       *
    1033       * @return array The selector without the relationship fragment if any, the relationship fragment.
    1034       */
    1035      protected function extractRelationshipFromFragment(array $fragment)
    1036      {
    1037          $parents = [];
    1038          $children = [];
    1039  
    1040          $j = $i = \count($fragment);
    1041  
    1042          for (;;) {
    1043              $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
    1044              $parents  = \array_slice($fragment, 0, $j);
    1045              $slice    = end($parents);
    1046  
    1047              if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
    1048                  break;
    1049              }
    1050  
    1051              $j -= 2;
    1052          }
    1053  
    1054          return [$parents, $children];
    1055      }
    1056  
    1057      /**
    1058       * Combine selector single
    1059       *
    1060       * @param array $base
    1061       * @param array $other
    1062       *
    1063       * @return array
    1064       */
    1065      protected function combineSelectorSingle($base, $other)
    1066      {
    1067          $tag    = [];
    1068          $out    = [];
    1069          $wasTag = false;
    1070          $pseudo = [];
    1071  
    1072          while (\count($other) && strpos(end($other), ':') === 0) {
    1073              array_unshift($pseudo, array_pop($other));
    1074          }
    1075  
    1076          foreach ([array_reverse($base), array_reverse($other)] as $single) {
    1077              $rang = count($single);
    1078  
    1079              foreach ($single as $part) {
    1080                  if (preg_match('/^[\[:]/', $part)) {
    1081                      $out[] = $part;
    1082                      $wasTag = false;
    1083                  } elseif (preg_match('/^[\.#]/', $part)) {
    1084                      array_unshift($out, $part);
    1085                      $wasTag = false;
    1086                  } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
    1087                      $tag[] = $part;
    1088                      $wasTag = true;
    1089                  } elseif ($wasTag) {
    1090                      $tag[\count($tag) - 1] .= $part;
    1091                  } else {
    1092                      array_unshift($out, $part);
    1093                  }
    1094                  $rang--;
    1095              }
    1096          }
    1097  
    1098          if (\count($tag)) {
    1099              array_unshift($out, $tag[0]);
    1100          }
    1101  
    1102          while (\count($pseudo)) {
    1103              $out[] = array_shift($pseudo);
    1104          }
    1105  
    1106          return $out;
    1107      }
    1108  
    1109      /**
    1110       * Compile media
    1111       *
    1112       * @param \ScssPhp\ScssPhp\Block $media
    1113       *
    1114       * @return void
    1115       */
    1116      protected function compileMedia(Block $media)
    1117      {
    1118          $this->pushEnv($media);
    1119  
    1120          $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
    1121  
    1122          if (! empty($mediaQueries) && $mediaQueries) {
    1123              $previousScope = $this->scope;
    1124              $parentScope = $this->mediaParent($this->scope);
    1125  
    1126              foreach ($mediaQueries as $mediaQuery) {
    1127                  $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
    1128  
    1129                  $parentScope->children[] = $this->scope;
    1130                  $parentScope = $this->scope;
    1131              }
    1132  
    1133              // top level properties in a media cause it to be wrapped
    1134              $needsWrap = false;
    1135  
    1136              foreach ($media->children as $child) {
    1137                  $type = $child[0];
    1138  
    1139                  if (
    1140                      $type !== Type::T_BLOCK &&
    1141                      $type !== Type::T_MEDIA &&
    1142                      $type !== Type::T_DIRECTIVE &&
    1143                      $type !== Type::T_IMPORT
    1144                  ) {
    1145                      $needsWrap = true;
    1146                      break;
    1147                  }
    1148              }
    1149  
    1150              if ($needsWrap) {
    1151                  $wrapped = new Block();
    1152                  $wrapped->sourceName   = $media->sourceName;
    1153                  $wrapped->sourceIndex  = $media->sourceIndex;
    1154                  $wrapped->sourceLine   = $media->sourceLine;
    1155                  $wrapped->sourceColumn = $media->sourceColumn;
    1156                  $wrapped->selectors    = [];
    1157                  $wrapped->comments     = [];
    1158                  $wrapped->parent       = $media;
    1159                  $wrapped->children     = $media->children;
    1160  
    1161                  $media->children = [[Type::T_BLOCK, $wrapped]];
    1162              }
    1163  
    1164              $this->compileChildrenNoReturn($media->children, $this->scope);
    1165  
    1166              $this->scope = $previousScope;
    1167          }
    1168  
    1169          $this->popEnv();
    1170      }
    1171  
    1172      /**
    1173       * Media parent
    1174       *
    1175       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
    1176       *
    1177       * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
    1178       */
    1179      protected function mediaParent(OutputBlock $scope)
    1180      {
    1181          while (! empty($scope->parent)) {
    1182              if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
    1183                  break;
    1184              }
    1185  
    1186              $scope = $scope->parent;
    1187          }
    1188  
    1189          return $scope;
    1190      }
    1191  
    1192      /**
    1193       * Compile directive
    1194       *
    1195       * @param \ScssPhp\ScssPhp\Block|array $block
    1196       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    1197       *
    1198       * @return void
    1199       */
    1200      protected function compileDirective($directive, OutputBlock $out)
    1201      {
    1202          if (\is_array($directive)) {
    1203              $directiveName = $this->compileDirectiveName($directive[0]);
    1204              $s = '@' . $directiveName;
    1205  
    1206              if (! empty($directive[1])) {
    1207                  $s .= ' ' . $this->compileValue($directive[1]);
    1208              }
    1209              // sass-spec compliance on newline after directives, a bit tricky :/
    1210              $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
    1211              if (\is_array($directive[0]) && empty($directive[1])) {
    1212                  $appendNewLine = "\n";
    1213              }
    1214  
    1215              if (empty($directive[3])) {
    1216                  $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
    1217              } else {
    1218                  $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
    1219              }
    1220          } else {
    1221              $directive->name = $this->compileDirectiveName($directive->name);
    1222              $s = '@' . $directive->name;
    1223  
    1224              if (! empty($directive->value)) {
    1225                  $s .= ' ' . $this->compileValue($directive->value);
    1226              }
    1227  
    1228              if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
    1229                  $this->compileKeyframeBlock($directive, [$s]);
    1230              } else {
    1231                  $this->compileNestedBlock($directive, [$s]);
    1232              }
    1233          }
    1234      }
    1235  
    1236      /**
    1237       * directive names can include some interpolation
    1238       *
    1239       * @param string|array $directiveName
    1240       * @return array|string
    1241       * @throws CompilerException
    1242       */
    1243      protected function compileDirectiveName($directiveName)
    1244      {
    1245          if (is_string($directiveName)) {
    1246              return $directiveName;
    1247          }
    1248  
    1249          return $this->compileValue($directiveName);
    1250      }
    1251  
    1252      /**
    1253       * Compile at-root
    1254       *
    1255       * @param \ScssPhp\ScssPhp\Block $block
    1256       *
    1257       * @return void
    1258       */
    1259      protected function compileAtRoot(Block $block)
    1260      {
    1261          $env     = $this->pushEnv($block);
    1262          $envs    = $this->compactEnv($env);
    1263          list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
    1264  
    1265          // wrap inline selector
    1266          if ($block->selector) {
    1267              $wrapped = new Block();
    1268              $wrapped->sourceName   = $block->sourceName;
    1269              $wrapped->sourceIndex  = $block->sourceIndex;
    1270              $wrapped->sourceLine   = $block->sourceLine;
    1271              $wrapped->sourceColumn = $block->sourceColumn;
    1272              $wrapped->selectors    = $block->selector;
    1273              $wrapped->comments     = [];
    1274              $wrapped->parent       = $block;
    1275              $wrapped->children     = $block->children;
    1276              $wrapped->selfParent   = $block->selfParent;
    1277  
    1278              $block->children = [[Type::T_BLOCK, $wrapped]];
    1279              $block->selector = null;
    1280          }
    1281  
    1282          $selfParent = $block->selfParent;
    1283  
    1284          if (
    1285              ! $block->selfParent->selectors &&
    1286              isset($block->parent) && $block->parent &&
    1287              isset($block->parent->selectors) && $block->parent->selectors
    1288          ) {
    1289              $selfParent = $block->parent;
    1290          }
    1291  
    1292          $this->env = $this->filterWithWithout($envs, $with, $without);
    1293  
    1294          $saveScope   = $this->scope;
    1295          $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
    1296  
    1297          // propagate selfParent to the children where they still can be useful
    1298          $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
    1299  
    1300          $this->scope = $this->completeScope($this->scope, $saveScope);
    1301          $this->scope = $saveScope;
    1302          $this->env   = $this->extractEnv($envs);
    1303  
    1304          $this->popEnv();
    1305      }
    1306  
    1307      /**
    1308       * Filter at-root scope depending of with/without option
    1309       *
    1310       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
    1311       * @param array                                  $with
    1312       * @param array                                  $without
    1313       *
    1314       * @return OutputBlock
    1315       */
    1316      protected function filterScopeWithWithout($scope, $with, $without)
    1317      {
    1318          $filteredScopes = [];
    1319          $childStash = [];
    1320  
    1321          if ($scope->type === TYPE::T_ROOT) {
    1322              return $scope;
    1323          }
    1324  
    1325          // start from the root
    1326          while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
    1327              array_unshift($childStash, $scope);
    1328              $scope = $scope->parent;
    1329          }
    1330  
    1331          for (;;) {
    1332              if (! $scope) {
    1333                  break;
    1334              }
    1335  
    1336              if ($this->isWith($scope, $with, $without)) {
    1337                  $s = clone $scope;
    1338                  $s->children = [];
    1339                  $s->lines    = [];
    1340                  $s->parent   = null;
    1341  
    1342                  if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
    1343                      $s->selectors = [];
    1344                  }
    1345  
    1346                  $filteredScopes[] = $s;
    1347              }
    1348  
    1349              if (\count($childStash)) {
    1350                  $scope = array_shift($childStash);
    1351              } elseif ($scope->children) {
    1352                  $scope = end($scope->children);
    1353              } else {
    1354                  $scope = null;
    1355              }
    1356          }
    1357  
    1358          if (! \count($filteredScopes)) {
    1359              return $this->rootBlock;
    1360          }
    1361  
    1362          $newScope = array_shift($filteredScopes);
    1363          $newScope->parent = $this->rootBlock;
    1364  
    1365          $this->rootBlock->children[] = $newScope;
    1366  
    1367          $p = &$newScope;
    1368  
    1369          while (\count($filteredScopes)) {
    1370              $s = array_shift($filteredScopes);
    1371              $s->parent = $p;
    1372              $p->children[] = $s;
    1373              $newScope = &$p->children[0];
    1374              $p = &$p->children[0];
    1375          }
    1376  
    1377          return $newScope;
    1378      }
    1379  
    1380      /**
    1381       * found missing selector from a at-root compilation in the previous scope
    1382       * (if at-root is just enclosing a property, the selector is in the parent tree)
    1383       *
    1384       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
    1385       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
    1386       *
    1387       * @return OutputBlock
    1388       */
    1389      protected function completeScope($scope, $previousScope)
    1390      {
    1391          if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
    1392              $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
    1393          }
    1394  
    1395          if ($scope->children) {
    1396              foreach ($scope->children as $k => $c) {
    1397                  $scope->children[$k] = $this->completeScope($c, $previousScope);
    1398              }
    1399          }
    1400  
    1401          return $scope;
    1402      }
    1403  
    1404      /**
    1405       * Find a selector by the depth node in the scope
    1406       *
    1407       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
    1408       * @param integer                                $depth
    1409       *
    1410       * @return array
    1411       */
    1412      protected function findScopeSelectors($scope, $depth)
    1413      {
    1414          if ($scope->depth === $depth && $scope->selectors) {
    1415              return $scope->selectors;
    1416          }
    1417  
    1418          if ($scope->children) {
    1419              foreach (array_reverse($scope->children) as $c) {
    1420                  if ($s = $this->findScopeSelectors($c, $depth)) {
    1421                      return $s;
    1422                  }
    1423              }
    1424          }
    1425  
    1426          return [];
    1427      }
    1428  
    1429      /**
    1430       * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
    1431       *
    1432       * @param array $withCondition
    1433       *
    1434       * @return array
    1435       */
    1436      protected function compileWith($withCondition)
    1437      {
    1438          // just compile what we have in 2 lists
    1439          $with = [];
    1440          $without = ['rule' => true];
    1441  
    1442          if ($withCondition) {
    1443              if ($withCondition[0] === Type::T_INTERPOLATE) {
    1444                  $w = $this->compileValue($withCondition);
    1445  
    1446                  $buffer = "($w)";
    1447                  $parser = $this->parserFactory(__METHOD__);
    1448  
    1449                  if ($parser->parseValue($buffer, $reParsedWith)) {
    1450                      $withCondition = $reParsedWith;
    1451                  }
    1452              }
    1453  
    1454              if ($this->libMapHasKey([$withCondition, static::$with])) {
    1455                  $without = []; // cancel the default
    1456                  $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
    1457  
    1458                  foreach ($list[2] as $item) {
    1459                      $keyword = $this->compileStringContent($this->coerceString($item));
    1460  
    1461                      $with[$keyword] = true;
    1462                  }
    1463              }
    1464  
    1465              if ($this->libMapHasKey([$withCondition, static::$without])) {
    1466                  $without = []; // cancel the default
    1467                  $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
    1468  
    1469                  foreach ($list[2] as $item) {
    1470                      $keyword = $this->compileStringContent($this->coerceString($item));
    1471  
    1472                      $without[$keyword] = true;
    1473                  }
    1474              }
    1475          }
    1476  
    1477          return [$with, $without];
    1478      }
    1479  
    1480      /**
    1481       * Filter env stack
    1482       *
    1483       * @param Environment[] $envs
    1484       * @param array $with
    1485       * @param array $without
    1486       *
    1487       * @return Environment
    1488       *
    1489       * @phpstan-param  non-empty-array<Environment> $envs
    1490       */
    1491      protected function filterWithWithout($envs, $with, $without)
    1492      {
    1493          $filtered = [];
    1494  
    1495          foreach ($envs as $e) {
    1496              if ($e->block && ! $this->isWith($e->block, $with, $without)) {
    1497                  $ec = clone $e;
    1498                  $ec->block     = null;
    1499                  $ec->selectors = [];
    1500  
    1501                  $filtered[] = $ec;
    1502              } else {
    1503                  $filtered[] = $e;
    1504              }
    1505          }
    1506  
    1507          return $this->extractEnv($filtered);
    1508      }
    1509  
    1510      /**
    1511       * Filter WITH rules
    1512       *
    1513       * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
    1514       * @param array                                                         $with
    1515       * @param array                                                         $without
    1516       *
    1517       * @return boolean
    1518       */
    1519      protected function isWith($block, $with, $without)
    1520      {
    1521          if (isset($block->type)) {
    1522              if ($block->type === Type::T_MEDIA) {
    1523                  return $this->testWithWithout('media', $with, $without);
    1524              }
    1525  
    1526              if ($block->type === Type::T_DIRECTIVE) {
    1527                  if (isset($block->name)) {
    1528                      return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
    1529                  } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
    1530                      return $this->testWithWithout($m[1], $with, $without);
    1531                  } else {
    1532                      return $this->testWithWithout('???', $with, $without);
    1533                  }
    1534              }
    1535          } elseif (isset($block->selectors)) {
    1536              // a selector starting with number is a keyframe rule
    1537              if (\count($block->selectors)) {
    1538                  $s = reset($block->selectors);
    1539  
    1540                  while (\is_array($s)) {
    1541                      $s = reset($s);
    1542                  }
    1543  
    1544                  if (\is_object($s) && $s instanceof Number) {
    1545                      return $this->testWithWithout('keyframes', $with, $without);
    1546                  }
    1547              }
    1548  
    1549              return $this->testWithWithout('rule', $with, $without);
    1550          }
    1551  
    1552          return true;
    1553      }
    1554  
    1555      /**
    1556       * Test a single type of block against with/without lists
    1557       *
    1558       * @param string $what
    1559       * @param array  $with
    1560       * @param array  $without
    1561       *
    1562       * @return boolean
    1563       *   true if the block should be kept, false to reject
    1564       */
    1565      protected function testWithWithout($what, $with, $without)
    1566      {
    1567          // if without, reject only if in the list (or 'all' is in the list)
    1568          if (\count($without)) {
    1569              return (isset($without[$what]) || isset($without['all'])) ? false : true;
    1570          }
    1571  
    1572          // otherwise reject all what is not in the with list
    1573          return (isset($with[$what]) || isset($with['all'])) ? true : false;
    1574      }
    1575  
    1576  
    1577      /**
    1578       * Compile keyframe block
    1579       *
    1580       * @param \ScssPhp\ScssPhp\Block $block
    1581       * @param array                  $selectors
    1582       *
    1583       * @return void
    1584       */
    1585      protected function compileKeyframeBlock(Block $block, $selectors)
    1586      {
    1587          $env = $this->pushEnv($block);
    1588  
    1589          $envs = $this->compactEnv($env);
    1590  
    1591          $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
    1592              return ! isset($e->block->selectors);
    1593          }));
    1594  
    1595          $this->scope = $this->makeOutputBlock($block->type, $selectors);
    1596          $this->scope->depth = 1;
    1597          $this->scope->parent->children[] = $this->scope;
    1598  
    1599          $this->compileChildrenNoReturn($block->children, $this->scope);
    1600  
    1601          $this->scope = $this->scope->parent;
    1602          $this->env   = $this->extractEnv($envs);
    1603  
    1604          $this->popEnv();
    1605      }
    1606  
    1607      /**
    1608       * Compile nested properties lines
    1609       *
    1610       * @param \ScssPhp\ScssPhp\Block                 $block
    1611       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    1612       *
    1613       * @return void
    1614       */
    1615      protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
    1616      {
    1617          $prefix = $this->compileValue($block->prefix) . '-';
    1618  
    1619          $nested = $this->makeOutputBlock($block->type);
    1620          $nested->parent = $out;
    1621  
    1622          if ($block->hasValue) {
    1623              $nested->depth = $out->depth + 1;
    1624          }
    1625  
    1626          $out->children[] = $nested;
    1627  
    1628          foreach ($block->children as $child) {
    1629              switch ($child[0]) {
    1630                  case Type::T_ASSIGN:
    1631                      array_unshift($child[1][2], $prefix);
    1632                      break;
    1633  
    1634                  case Type::T_NESTED_PROPERTY:
    1635                      array_unshift($child[1]->prefix[2], $prefix);
    1636                      break;
    1637              }
    1638  
    1639              $this->compileChild($child, $nested);
    1640          }
    1641      }
    1642  
    1643      /**
    1644       * Compile nested block
    1645       *
    1646       * @param \ScssPhp\ScssPhp\Block $block
    1647       * @param array                  $selectors
    1648       *
    1649       * @return void
    1650       */
    1651      protected function compileNestedBlock(Block $block, $selectors)
    1652      {
    1653          $this->pushEnv($block);
    1654  
    1655          $this->scope = $this->makeOutputBlock($block->type, $selectors);
    1656          $this->scope->parent->children[] = $this->scope;
    1657  
    1658          // wrap assign children in a block
    1659          // except for @font-face
    1660          if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') {
    1661              // need wrapping?
    1662              $needWrapping = false;
    1663  
    1664              foreach ($block->children as $child) {
    1665                  if ($child[0] === Type::T_ASSIGN) {
    1666                      $needWrapping = true;
    1667                      break;
    1668                  }
    1669              }
    1670  
    1671              if ($needWrapping) {
    1672                  $wrapped = new Block();
    1673                  $wrapped->sourceName   = $block->sourceName;
    1674                  $wrapped->sourceIndex  = $block->sourceIndex;
    1675                  $wrapped->sourceLine   = $block->sourceLine;
    1676                  $wrapped->sourceColumn = $block->sourceColumn;
    1677                  $wrapped->selectors    = [];
    1678                  $wrapped->comments     = [];
    1679                  $wrapped->parent       = $block;
    1680                  $wrapped->children     = $block->children;
    1681                  $wrapped->selfParent   = $block->selfParent;
    1682  
    1683                  $block->children = [[Type::T_BLOCK, $wrapped]];
    1684              }
    1685          }
    1686  
    1687          $this->compileChildrenNoReturn($block->children, $this->scope);
    1688  
    1689          $this->scope = $this->scope->parent;
    1690  
    1691          $this->popEnv();
    1692      }
    1693  
    1694      /**
    1695       * Recursively compiles a block.
    1696       *
    1697       * A block is analogous to a CSS block in most cases. A single SCSS document
    1698       * is encapsulated in a block when parsed, but it does not have parent tags
    1699       * so all of its children appear on the root level when compiled.
    1700       *
    1701       * Blocks are made up of selectors and children.
    1702       *
    1703       * The children of a block are just all the blocks that are defined within.
    1704       *
    1705       * Compiling the block involves pushing a fresh environment on the stack,
    1706       * and iterating through the props, compiling each one.
    1707       *
    1708       * @see Compiler::compileChild()
    1709       *
    1710       * @param \ScssPhp\ScssPhp\Block $block
    1711       *
    1712       * @return void
    1713       */
    1714      protected function compileBlock(Block $block)
    1715      {
    1716          $env = $this->pushEnv($block);
    1717          $env->selectors = $this->evalSelectors($block->selectors);
    1718  
    1719          $out = $this->makeOutputBlock(null);
    1720  
    1721          $this->scope->children[] = $out;
    1722  
    1723          if (\count($block->children)) {
    1724              $out->selectors = $this->multiplySelectors($env, $block->selfParent);
    1725  
    1726              // propagate selfParent to the children where they still can be useful
    1727              $selfParentSelectors = null;
    1728  
    1729              if (isset($block->selfParent->selectors)) {
    1730                  $selfParentSelectors = $block->selfParent->selectors;
    1731                  $block->selfParent->selectors = $out->selectors;
    1732              }
    1733  
    1734              $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
    1735  
    1736              // and revert for the following children of the same block
    1737              if ($selfParentSelectors) {
    1738                  $block->selfParent->selectors = $selfParentSelectors;
    1739              }
    1740          }
    1741  
    1742          $this->popEnv();
    1743      }
    1744  
    1745  
    1746      /**
    1747       * Compile the value of a comment that can have interpolation
    1748       *
    1749       * @param array   $value
    1750       * @param boolean $pushEnv
    1751       *
    1752       * @return string
    1753       */
    1754      protected function compileCommentValue($value, $pushEnv = false)
    1755      {
    1756          $c = $value[1];
    1757  
    1758          if (isset($value[2])) {
    1759              if ($pushEnv) {
    1760                  $this->pushEnv();
    1761              }
    1762  
    1763              $ignoreCallStackMessage = $this->ignoreCallStackMessage;
    1764              $this->ignoreCallStackMessage = true;
    1765  
    1766              try {
    1767                  $c = $this->compileValue($value[2]);
    1768              } catch (\Exception $e) {
    1769                  // ignore error in comment compilation which are only interpolation
    1770              }
    1771  
    1772              $this->ignoreCallStackMessage = $ignoreCallStackMessage;
    1773  
    1774              if ($pushEnv) {
    1775                  $this->popEnv();
    1776              }
    1777          }
    1778  
    1779          return $c;
    1780      }
    1781  
    1782      /**
    1783       * Compile root level comment
    1784       *
    1785       * @param array $block
    1786       *
    1787       * @return void
    1788       */
    1789      protected function compileComment($block)
    1790      {
    1791          $out = $this->makeOutputBlock(Type::T_COMMENT);
    1792          $out->lines[] = $this->compileCommentValue($block, true);
    1793  
    1794          $this->scope->children[] = $out;
    1795      }
    1796  
    1797      /**
    1798       * Evaluate selectors
    1799       *
    1800       * @param array $selectors
    1801       *
    1802       * @return array
    1803       */
    1804      protected function evalSelectors($selectors)
    1805      {
    1806          $this->shouldEvaluate = false;
    1807  
    1808          $selectors = array_map([$this, 'evalSelector'], $selectors);
    1809  
    1810          // after evaluating interpolates, we might need a second pass
    1811          if ($this->shouldEvaluate) {
    1812              $selectors = $this->replaceSelfSelector($selectors, '&');
    1813              $buffer    = $this->collapseSelectors($selectors);
    1814              $parser    = $this->parserFactory(__METHOD__);
    1815  
    1816              try {
    1817                  $isValid = $parser->parseSelector($buffer, $newSelectors, true);
    1818              } catch (ParserException $e) {
    1819                  throw $this->error($e->getMessage());
    1820              }
    1821  
    1822              if ($isValid) {
    1823                  $selectors = array_map([$this, 'evalSelector'], $newSelectors);
    1824              }
    1825          }
    1826  
    1827          return $selectors;
    1828      }
    1829  
    1830      /**
    1831       * Evaluate selector
    1832       *
    1833       * @param array $selector
    1834       *
    1835       * @return array
    1836       */
    1837      protected function evalSelector($selector)
    1838      {
    1839          return array_map([$this, 'evalSelectorPart'], $selector);
    1840      }
    1841  
    1842      /**
    1843       * Evaluate selector part; replaces all the interpolates, stripping quotes
    1844       *
    1845       * @param array $part
    1846       *
    1847       * @return array
    1848       */
    1849      protected function evalSelectorPart($part)
    1850      {
    1851          foreach ($part as &$p) {
    1852              if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
    1853                  $p = $this->compileValue($p);
    1854  
    1855                  // force re-evaluation if self char or non standard char
    1856                  if (preg_match(',[^\w-],', $p)) {
    1857                      $this->shouldEvaluate = true;
    1858                  }
    1859              } elseif (
    1860                  \is_string($p) && \strlen($p) >= 2 &&
    1861                  ($first = $p[0]) && ($first === '"' || $first === "'") &&
    1862                  substr($p, -1) === $first
    1863              ) {
    1864                  $p = substr($p, 1, -1);
    1865              }
    1866          }
    1867  
    1868          return $this->flattenSelectorSingle($part);
    1869      }
    1870  
    1871      /**
    1872       * Collapse selectors
    1873       *
    1874       * @param array   $selectors
    1875       * @param boolean $selectorFormat
    1876       *   if false return a collapsed string
    1877       *   if true return an array description of a structured selector
    1878       *
    1879       * @return string
    1880       */
    1881      protected function collapseSelectors($selectors, $selectorFormat = false)
    1882      {
    1883          $parts = [];
    1884  
    1885          foreach ($selectors as $selector) {
    1886              $output = [];
    1887              $glueNext = false;
    1888  
    1889              foreach ($selector as $node) {
    1890                  $compound = '';
    1891  
    1892                  array_walk_recursive(
    1893                      $node,
    1894                      function ($value, $key) use (&$compound) {
    1895                          $compound .= $value;
    1896                      }
    1897                  );
    1898  
    1899                  if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
    1900                      if (\count($output)) {
    1901                          $output[\count($output) - 1] .= ' ' . $compound;
    1902                      } else {
    1903                          $output[] = $compound;
    1904                      }
    1905  
    1906                      $glueNext = true;
    1907                  } elseif ($glueNext) {
    1908                      $output[\count($output) - 1] .= ' ' . $compound;
    1909                      $glueNext = false;
    1910                  } else {
    1911                      $output[] = $compound;
    1912                  }
    1913              }
    1914  
    1915              if ($selectorFormat) {
    1916                  foreach ($output as &$o) {
    1917                      $o = [Type::T_STRING, '', [$o]];
    1918                  }
    1919  
    1920                  $output = [Type::T_LIST, ' ', $output];
    1921              } else {
    1922                  $output = implode(' ', $output);
    1923              }
    1924  
    1925              $parts[] = $output;
    1926          }
    1927  
    1928          if ($selectorFormat) {
    1929              $parts = [Type::T_LIST, ',', $parts];
    1930          } else {
    1931              $parts = implode(', ', $parts);
    1932          }
    1933  
    1934          return $parts;
    1935      }
    1936  
    1937      /**
    1938       * Parse down the selector and revert [self] to "&" before a reparsing
    1939       *
    1940       * @param array $selectors
    1941       *
    1942       * @return array
    1943       */
    1944      protected function replaceSelfSelector($selectors, $replace = null)
    1945      {
    1946          foreach ($selectors as &$part) {
    1947              if (\is_array($part)) {
    1948                  if ($part === [Type::T_SELF]) {
    1949                      if (\is_null($replace)) {
    1950                          $replace = $this->reduce([Type::T_SELF]);
    1951                          $replace = $this->compileValue($replace);
    1952                      }
    1953                      $part = $replace;
    1954                  } else {
    1955                      $part = $this->replaceSelfSelector($part, $replace);
    1956                  }
    1957              }
    1958          }
    1959  
    1960          return $selectors;
    1961      }
    1962  
    1963      /**
    1964       * Flatten selector single; joins together .classes and #ids
    1965       *
    1966       * @param array $single
    1967       *
    1968       * @return array
    1969       */
    1970      protected function flattenSelectorSingle($single)
    1971      {
    1972          $joined = [];
    1973  
    1974          foreach ($single as $part) {
    1975              if (
    1976                  empty($joined) ||
    1977                  ! \is_string($part) ||
    1978                  preg_match('/[\[.:#%]/', $part)
    1979              ) {
    1980                  $joined[] = $part;
    1981                  continue;
    1982              }
    1983  
    1984              if (\is_array(end($joined))) {
    1985                  $joined[] = $part;
    1986              } else {
    1987                  $joined[\count($joined) - 1] .= $part;
    1988              }
    1989          }
    1990  
    1991          return $joined;
    1992      }
    1993  
    1994      /**
    1995       * Compile selector to string; self(&) should have been replaced by now
    1996       *
    1997       * @param string|array $selector
    1998       *
    1999       * @return string
    2000       */
    2001      protected function compileSelector($selector)
    2002      {
    2003          if (! \is_array($selector)) {
    2004              return $selector; // media and the like
    2005          }
    2006  
    2007          return implode(
    2008              ' ',
    2009              array_map(
    2010                  [$this, 'compileSelectorPart'],
    2011                  $selector
    2012              )
    2013          );
    2014      }
    2015  
    2016      /**
    2017       * Compile selector part
    2018       *
    2019       * @param array $piece
    2020       *
    2021       * @return string
    2022       */
    2023      protected function compileSelectorPart($piece)
    2024      {
    2025          foreach ($piece as &$p) {
    2026              if (! \is_array($p)) {
    2027                  continue;
    2028              }
    2029  
    2030              switch ($p[0]) {
    2031                  case Type::T_SELF:
    2032                      $p = '&';
    2033                      break;
    2034  
    2035                  default:
    2036                      $p = $this->compileValue($p);
    2037                      break;
    2038              }
    2039          }
    2040  
    2041          return implode($piece);
    2042      }
    2043  
    2044      /**
    2045       * Has selector placeholder?
    2046       *
    2047       * @param array $selector
    2048       *
    2049       * @return boolean
    2050       */
    2051      protected function hasSelectorPlaceholder($selector)
    2052      {
    2053          if (! \is_array($selector)) {
    2054              return false;
    2055          }
    2056  
    2057          foreach ($selector as $parts) {
    2058              foreach ($parts as $part) {
    2059                  if (\strlen($part) && '%' === $part[0]) {
    2060                      return true;
    2061                  }
    2062              }
    2063          }
    2064  
    2065          return false;
    2066      }
    2067  
    2068      /**
    2069       * @param string $name
    2070       *
    2071       * @return void
    2072       */
    2073      protected function pushCallStack($name = '')
    2074      {
    2075          $this->callStack[] = [
    2076            'n' => $name,
    2077            Parser::SOURCE_INDEX => $this->sourceIndex,
    2078            Parser::SOURCE_LINE => $this->sourceLine,
    2079            Parser::SOURCE_COLUMN => $this->sourceColumn
    2080          ];
    2081  
    2082          // infinite calling loop
    2083          if (\count($this->callStack) > 25000) {
    2084              // not displayed but you can var_dump it to deep debug
    2085              $msg = $this->callStackMessage(true, 100);
    2086              $msg = 'Infinite calling loop';
    2087  
    2088              throw $this->error($msg);
    2089          }
    2090      }
    2091  
    2092      /**
    2093       * @return void
    2094       */
    2095      protected function popCallStack()
    2096      {
    2097          array_pop($this->callStack);
    2098      }
    2099  
    2100      /**
    2101       * Compile children and return result
    2102       *
    2103       * @param array                                  $stms
    2104       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2105       * @param string                                 $traceName
    2106       *
    2107       * @return array|null
    2108       */
    2109      protected function compileChildren($stms, OutputBlock $out, $traceName = '')
    2110      {
    2111          $this->pushCallStack($traceName);
    2112  
    2113          foreach ($stms as $stm) {
    2114              $ret = $this->compileChild($stm, $out);
    2115  
    2116              if (isset($ret)) {
    2117                  $this->popCallStack();
    2118  
    2119                  return $ret;
    2120              }
    2121          }
    2122  
    2123          $this->popCallStack();
    2124  
    2125          return null;
    2126      }
    2127  
    2128      /**
    2129       * Compile children and throw exception if unexpected `@return`
    2130       *
    2131       * @param array                                  $stms
    2132       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2133       * @param \ScssPhp\ScssPhp\Block                 $selfParent
    2134       * @param string                                 $traceName
    2135       *
    2136       * @return void
    2137       *
    2138       * @throws \Exception
    2139       */
    2140      protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
    2141      {
    2142          $this->pushCallStack($traceName);
    2143  
    2144          foreach ($stms as $stm) {
    2145              if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
    2146                  $stm[1]->selfParent = $selfParent;
    2147                  $ret = $this->compileChild($stm, $out);
    2148                  $stm[1]->selfParent = null;
    2149              } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
    2150                  $stm['selfParent'] = $selfParent;
    2151                  $ret = $this->compileChild($stm, $out);
    2152                  unset($stm['selfParent']);
    2153              } else {
    2154                  $ret = $this->compileChild($stm, $out);
    2155              }
    2156  
    2157              if (isset($ret)) {
    2158                  throw $this->error('@return may only be used within a function');
    2159              }
    2160          }
    2161  
    2162          $this->popCallStack();
    2163      }
    2164  
    2165  
    2166      /**
    2167       * evaluate media query : compile internal value keeping the structure unchanged
    2168       *
    2169       * @param array $queryList
    2170       *
    2171       * @return array
    2172       */
    2173      protected function evaluateMediaQuery($queryList)
    2174      {
    2175          static $parser = null;
    2176  
    2177          $outQueryList = [];
    2178  
    2179          foreach ($queryList as $kql => $query) {
    2180              $shouldReparse = false;
    2181  
    2182              foreach ($query as $kq => $q) {
    2183                  for ($i = 1; $i < \count($q); $i++) {
    2184                      $value = $this->compileValue($q[$i]);
    2185  
    2186                      // the parser had no mean to know if media type or expression if it was an interpolation
    2187                      // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
    2188                      if (
    2189                          $q[0] == Type::T_MEDIA_TYPE &&
    2190                          (strpos($value, '(') !== false ||
    2191                          strpos($value, ')') !== false ||
    2192                          strpos($value, ':') !== false ||
    2193                          strpos($value, ',') !== false)
    2194                      ) {
    2195                          $shouldReparse = true;
    2196                      }
    2197  
    2198                      $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
    2199                  }
    2200              }
    2201  
    2202              if ($shouldReparse) {
    2203                  if (\is_null($parser)) {
    2204                      $parser = $this->parserFactory(__METHOD__);
    2205                  }
    2206  
    2207                  $queryString = $this->compileMediaQuery([$queryList[$kql]]);
    2208                  $queryString = reset($queryString);
    2209  
    2210                  if (strpos($queryString, '@media ') === 0) {
    2211                      $queryString = substr($queryString, 7);
    2212                      $queries = [];
    2213  
    2214                      if ($parser->parseMediaQueryList($queryString, $queries)) {
    2215                          $queries = $this->evaluateMediaQuery($queries[2]);
    2216  
    2217                          while (\count($queries)) {
    2218                              $outQueryList[] = array_shift($queries);
    2219                          }
    2220  
    2221                          continue;
    2222                      }
    2223                  }
    2224              }
    2225  
    2226              $outQueryList[] = $queryList[$kql];
    2227          }
    2228  
    2229          return $outQueryList;
    2230      }
    2231  
    2232      /**
    2233       * Compile media query
    2234       *
    2235       * @param array $queryList
    2236       *
    2237       * @return array
    2238       */
    2239      protected function compileMediaQuery($queryList)
    2240      {
    2241          $start   = '@media ';
    2242          $default = trim($start);
    2243          $out     = [];
    2244          $current = '';
    2245  
    2246          foreach ($queryList as $query) {
    2247              $type = null;
    2248              $parts = [];
    2249  
    2250              $mediaTypeOnly = true;
    2251  
    2252              foreach ($query as $q) {
    2253                  if ($q[0] !== Type::T_MEDIA_TYPE) {
    2254                      $mediaTypeOnly = false;
    2255                      break;
    2256                  }
    2257              }
    2258  
    2259              foreach ($query as $q) {
    2260                  switch ($q[0]) {
    2261                      case Type::T_MEDIA_TYPE:
    2262                          $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
    2263  
    2264                          // combining not and anything else than media type is too risky and should be avoided
    2265                          if (! $mediaTypeOnly) {
    2266                              if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
    2267                                  if ($type) {
    2268                                      array_unshift($parts, implode(' ', array_filter($type)));
    2269                                  }
    2270  
    2271                                  if (! empty($parts)) {
    2272                                      if (\strlen($current)) {
    2273                                          $current .= $this->formatter->tagSeparator;
    2274                                      }
    2275  
    2276                                      $current .= implode(' and ', $parts);
    2277                                  }
    2278  
    2279                                  if ($current) {
    2280                                      $out[] = $start . $current;
    2281                                  }
    2282  
    2283                                  $current = '';
    2284                                  $type    = null;
    2285                                  $parts   = [];
    2286                              }
    2287                          }
    2288  
    2289                          if ($newType === ['all'] && $default) {
    2290                              $default = $start . 'all';
    2291                          }
    2292  
    2293                          // all can be safely ignored and mixed with whatever else
    2294                          if ($newType !== ['all']) {
    2295                              if ($type) {
    2296                                  $type = $this->mergeMediaTypes($type, $newType);
    2297  
    2298                                  if (empty($type)) {
    2299                                      // merge failed : ignore this query that is not valid, skip to the next one
    2300                                      $parts = [];
    2301                                      $default = ''; // if everything fail, no @media at all
    2302                                      continue 3;
    2303                                  }
    2304                              } else {
    2305                                  $type = $newType;
    2306                              }
    2307                          }
    2308                          break;
    2309  
    2310                      case Type::T_MEDIA_EXPRESSION:
    2311                          if (isset($q[2])) {
    2312                              $parts[] = '('
    2313                                  . $this->compileValue($q[1])
    2314                                  . $this->formatter->assignSeparator
    2315                                  . $this->compileValue($q[2])
    2316                                  . ')';
    2317                          } else {
    2318                              $parts[] = '('
    2319                                  . $this->compileValue($q[1])
    2320                                  . ')';
    2321                          }
    2322                          break;
    2323  
    2324                      case Type::T_MEDIA_VALUE:
    2325                          $parts[] = $this->compileValue($q[1]);
    2326                          break;
    2327                  }
    2328              }
    2329  
    2330              if ($type) {
    2331                  array_unshift($parts, implode(' ', array_filter($type)));
    2332              }
    2333  
    2334              if (! empty($parts)) {
    2335                  if (\strlen($current)) {
    2336                      $current .= $this->formatter->tagSeparator;
    2337                  }
    2338  
    2339                  $current .= implode(' and ', $parts);
    2340              }
    2341          }
    2342  
    2343          if ($current) {
    2344              $out[] = $start . $current;
    2345          }
    2346  
    2347          // no @media type except all, and no conflict?
    2348          if (! $out && $default) {
    2349              $out[] = $default;
    2350          }
    2351  
    2352          return $out;
    2353      }
    2354  
    2355      /**
    2356       * Merge direct relationships between selectors
    2357       *
    2358       * @param array $selectors1
    2359       * @param array $selectors2
    2360       *
    2361       * @return array
    2362       */
    2363      protected function mergeDirectRelationships($selectors1, $selectors2)
    2364      {
    2365          if (empty($selectors1) || empty($selectors2)) {
    2366              return array_merge($selectors1, $selectors2);
    2367          }
    2368  
    2369          $part1 = end($selectors1);
    2370          $part2 = end($selectors2);
    2371  
    2372          if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
    2373              return array_merge($selectors1, $selectors2);
    2374          }
    2375  
    2376          $merged = [];
    2377  
    2378          do {
    2379              $part1 = array_pop($selectors1);
    2380              $part2 = array_pop($selectors2);
    2381  
    2382              if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
    2383                  if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
    2384                      array_unshift($merged, [$part1[0] . $part2[0]]);
    2385                      $merged = array_merge($selectors1, $selectors2, $merged);
    2386                  } else {
    2387                      $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
    2388                  }
    2389  
    2390                  break;
    2391              }
    2392  
    2393              array_unshift($merged, $part1);
    2394          } while (! empty($selectors1) && ! empty($selectors2));
    2395  
    2396          return $merged;
    2397      }
    2398  
    2399      /**
    2400       * Merge media types
    2401       *
    2402       * @param array $type1
    2403       * @param array $type2
    2404       *
    2405       * @return array|null
    2406       */
    2407      protected function mergeMediaTypes($type1, $type2)
    2408      {
    2409          if (empty($type1)) {
    2410              return $type2;
    2411          }
    2412  
    2413          if (empty($type2)) {
    2414              return $type1;
    2415          }
    2416  
    2417          if (\count($type1) > 1) {
    2418              $m1 = strtolower($type1[0]);
    2419              $t1 = strtolower($type1[1]);
    2420          } else {
    2421              $m1 = '';
    2422              $t1 = strtolower($type1[0]);
    2423          }
    2424  
    2425          if (\count($type2) > 1) {
    2426              $m2 = strtolower($type2[0]);
    2427              $t2 = strtolower($type2[1]);
    2428          } else {
    2429              $m2 = '';
    2430              $t2 = strtolower($type2[0]);
    2431          }
    2432  
    2433          if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
    2434              if ($t1 === $t2) {
    2435                  return null;
    2436              }
    2437  
    2438              return [
    2439                  $m1 === Type::T_NOT ? $m2 : $m1,
    2440                  $m1 === Type::T_NOT ? $t2 : $t1,
    2441              ];
    2442          }
    2443  
    2444          if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
    2445              // CSS has no way of representing "neither screen nor print"
    2446              if ($t1 !== $t2) {
    2447                  return null;
    2448              }
    2449  
    2450              return [Type::T_NOT, $t1];
    2451          }
    2452  
    2453          if ($t1 !== $t2) {
    2454              return null;
    2455          }
    2456  
    2457          // t1 == t2, neither m1 nor m2 are "not"
    2458          return [empty($m1) ? $m2 : $m1, $t1];
    2459      }
    2460  
    2461      /**
    2462       * Compile import; returns true if the value was something that could be imported
    2463       *
    2464       * @param array                                  $rawPath
    2465       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2466       * @param boolean                                $once
    2467       *
    2468       * @return boolean
    2469       */
    2470      protected function compileImport($rawPath, OutputBlock $out, $once = false)
    2471      {
    2472          if ($rawPath[0] === Type::T_STRING) {
    2473              $path = $this->compileStringContent($rawPath);
    2474  
    2475              if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
    2476                  if (! $once || ! \in_array($path, $this->importedFiles)) {
    2477                      $this->importFile($path, $out);
    2478                      $this->importedFiles[] = $path;
    2479                  }
    2480  
    2481                  return true;
    2482              }
    2483  
    2484              $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
    2485  
    2486              return false;
    2487          }
    2488  
    2489          if ($rawPath[0] === Type::T_LIST) {
    2490              // handle a list of strings
    2491              if (\count($rawPath[2]) === 0) {
    2492                  return false;
    2493              }
    2494  
    2495              foreach ($rawPath[2] as $path) {
    2496                  if ($path[0] !== Type::T_STRING) {
    2497                      $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
    2498  
    2499                      return false;
    2500                  }
    2501              }
    2502  
    2503              foreach ($rawPath[2] as $path) {
    2504                  $this->compileImport($path, $out, $once);
    2505              }
    2506  
    2507              return true;
    2508          }
    2509  
    2510          $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
    2511  
    2512          return false;
    2513      }
    2514  
    2515      /**
    2516       * @param array $rawPath
    2517       * @return string
    2518       * @throws CompilerException
    2519       */
    2520      protected function compileImportPath($rawPath)
    2521      {
    2522          $path = $this->compileValue($rawPath);
    2523  
    2524          // case url() without quotes : suppress \r \n remaining in the path
    2525          // if this is a real string there can not be CR or LF char
    2526          if (strpos($path, 'url(') === 0) {
    2527              $path = str_replace(array("\r", "\n"), array('', ' '), $path);
    2528          } else {
    2529              // if this is a file name in a string, spaces should be escaped
    2530              $path = $this->reduce($rawPath);
    2531              $path = $this->escapeImportPathString($path);
    2532              $path = $this->compileValue($path);
    2533          }
    2534  
    2535          return $path;
    2536      }
    2537  
    2538      /**
    2539       * @param array $path
    2540       * @return array
    2541       * @throws CompilerException
    2542       */
    2543      protected function escapeImportPathString($path)
    2544      {
    2545          switch ($path[0]) {
    2546              case Type::T_LIST:
    2547                  foreach ($path[2] as $k => $v) {
    2548                      $path[2][$k] = $this->escapeImportPathString($v);
    2549                  }
    2550                  break;
    2551              case Type::T_STRING:
    2552                  if ($path[1]) {
    2553                      $path = $this->compileValue($path);
    2554                      $path = str_replace(' ', '\\ ', $path);
    2555                      $path = [Type::T_KEYWORD, $path];
    2556                  }
    2557                  break;
    2558          }
    2559  
    2560          return $path;
    2561      }
    2562  
    2563      /**
    2564       * Append a root directive like @import or @charset as near as the possible from the source code
    2565       * (keeping before comments, @import and @charset coming before in the source code)
    2566       *
    2567       * @param string                                 $line
    2568       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2569       * @param array                                  $allowed
    2570       *
    2571       * @return void
    2572       */
    2573      protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
    2574      {
    2575          $root = $out;
    2576  
    2577          while ($root->parent) {
    2578              $root = $root->parent;
    2579          }
    2580  
    2581          $i = 0;
    2582  
    2583          while ($i < \count($root->children)) {
    2584              if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
    2585                  break;
    2586              }
    2587  
    2588              $i++;
    2589          }
    2590  
    2591          // remove incompatible children from the bottom of the list
    2592          $saveChildren = [];
    2593  
    2594          while ($i < \count($root->children)) {
    2595              $saveChildren[] = array_pop($root->children);
    2596          }
    2597  
    2598          // insert the directive as a comment
    2599          $child = $this->makeOutputBlock(Type::T_COMMENT);
    2600          $child->lines[]      = $line;
    2601          $child->sourceName   = $this->sourceNames[$this->sourceIndex];
    2602          $child->sourceLine   = $this->sourceLine;
    2603          $child->sourceColumn = $this->sourceColumn;
    2604  
    2605          $root->children[] = $child;
    2606  
    2607          // repush children
    2608          while (\count($saveChildren)) {
    2609              $root->children[] = array_pop($saveChildren);
    2610          }
    2611      }
    2612  
    2613      /**
    2614       * Append lines to the current output block:
    2615       * directly to the block or through a child if necessary
    2616       *
    2617       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2618       * @param string                                 $type
    2619       * @param string|mixed                           $line
    2620       *
    2621       * @return void
    2622       */
    2623      protected function appendOutputLine(OutputBlock $out, $type, $line)
    2624      {
    2625          $outWrite = &$out;
    2626  
    2627          // check if it's a flat output or not
    2628          if (\count($out->children)) {
    2629              $lastChild = &$out->children[\count($out->children) - 1];
    2630  
    2631              if (
    2632                  $lastChild->depth === $out->depth &&
    2633                  \is_null($lastChild->selectors) &&
    2634                  ! \count($lastChild->children)
    2635              ) {
    2636                  $outWrite = $lastChild;
    2637              } else {
    2638                  $nextLines = $this->makeOutputBlock($type);
    2639                  $nextLines->parent = $out;
    2640                  $nextLines->depth  = $out->depth;
    2641  
    2642                  $out->children[] = $nextLines;
    2643                  $outWrite = &$nextLines;
    2644              }
    2645          }
    2646  
    2647          $outWrite->lines[] = $line;
    2648      }
    2649  
    2650      /**
    2651       * Compile child; returns a value to halt execution
    2652       *
    2653       * @param array                                  $child
    2654       * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
    2655       *
    2656       * @return array|Number|null
    2657       */
    2658      protected function compileChild($child, OutputBlock $out)
    2659      {
    2660          if (isset($child[Parser::SOURCE_LINE])) {
    2661              $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
    2662              $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
    2663              $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
    2664          } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
    2665              $this->sourceIndex  = $child[1]->sourceIndex;
    2666              $this->sourceLine   = $child[1]->sourceLine;
    2667              $this->sourceColumn = $child[1]->sourceColumn;
    2668          } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
    2669              $this->sourceLine   = $out->sourceLine;
    2670              $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
    2671              $this->sourceColumn = $out->sourceColumn;
    2672  
    2673              if ($this->sourceIndex === false) {
    2674                  $this->sourceIndex = null;
    2675              }
    2676          }
    2677  
    2678          switch ($child[0]) {
    2679              case Type::T_SCSSPHP_IMPORT_ONCE:
    2680                  $rawPath = $this->reduce($child[1]);
    2681  
    2682                  $this->compileImport($rawPath, $out, true);
    2683                  break;
    2684  
    2685              case Type::T_IMPORT:
    2686                  $rawPath = $this->reduce($child[1]);
    2687  
    2688                  $this->compileImport($rawPath, $out);
    2689                  break;
    2690  
    2691              case Type::T_DIRECTIVE:
    2692                  $this->compileDirective($child[1], $out);
    2693                  break;
    2694  
    2695              case Type::T_AT_ROOT:
    2696                  $this->compileAtRoot($child[1]);
    2697                  break;
    2698  
    2699              case Type::T_MEDIA:
    2700                  $this->compileMedia($child[1]);
    2701                  break;
    2702  
    2703              case Type::T_BLOCK:
    2704                  $this->compileBlock($child[1]);
    2705                  break;
    2706  
    2707              case Type::T_CHARSET:
    2708                  if (! $this->charsetSeen) {
    2709                      $this->charsetSeen = true;
    2710                      $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
    2711                  }
    2712                  break;
    2713  
    2714              case Type::T_CUSTOM_PROPERTY:
    2715                  list(, $name, $value) = $child;
    2716                  $compiledName = $this->compileValue($name);
    2717  
    2718                  // if the value reduces to null from something else then
    2719                  // the property should be discarded
    2720                  if ($value[0] !== Type::T_NULL) {
    2721                      $value = $this->reduce($value);
    2722  
    2723                      if ($value[0] === Type::T_NULL || $value === static::$nullString) {
    2724                          break;
    2725                      }
    2726                  }
    2727  
    2728                  $compiledValue = $this->compileValue($value);
    2729  
    2730                  $line = $this->formatter->customProperty(
    2731                      $compiledName,
    2732                      $compiledValue
    2733                  );
    2734  
    2735                  $this->appendOutputLine($out, Type::T_ASSIGN, $line);
    2736                  break;
    2737  
    2738              case Type::T_ASSIGN:
    2739                  list(, $name, $value) = $child;
    2740  
    2741                  if ($name[0] === Type::T_VARIABLE) {
    2742                      $flags     = isset($child[3]) ? $child[3] : [];
    2743                      $isDefault = \in_array('!default', $flags);
    2744                      $isGlobal  = \in_array('!global', $flags);
    2745  
    2746                      if ($isGlobal) {
    2747                          $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
    2748                          break;
    2749                      }
    2750  
    2751                      $shouldSet = $isDefault &&
    2752                          (\is_null($result = $this->get($name[1], false)) ||
    2753                          $result === static::$null);
    2754  
    2755                      if (! $isDefault || $shouldSet) {
    2756                          $this->set($name[1], $this->reduce($value), true, null, $value);
    2757                      }
    2758                      break;
    2759                  }
    2760  
    2761                  $compiledName = $this->compileValue($name);
    2762  
    2763                  // handle shorthand syntaxes : size / line-height...
    2764                  if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
    2765                      if ($value[0] === Type::T_VARIABLE) {
    2766                          // if the font value comes from variable, the content is already reduced
    2767                          // (i.e., formulas were already calculated), so we need the original unreduced value
    2768                          $value = $this->get($value[1], true, null, true);
    2769                      }
    2770  
    2771                      $shorthandValue=&$value;
    2772  
    2773                      $shorthandDividerNeedsUnit = false;
    2774                      $maxListElements           = null;
    2775                      $maxShorthandDividers      = 1;
    2776  
    2777                      switch ($compiledName) {
    2778                          case 'border-radius':
    2779                              $maxListElements = 4;
    2780                              $shorthandDividerNeedsUnit = true;
    2781                              break;
    2782                      }
    2783  
    2784                      if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
    2785                          // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
    2786                          // we need to handle the first list element
    2787                          $shorthandValue=&$value[2][0];
    2788                      }
    2789  
    2790                      if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
    2791                          $revert = true;
    2792  
    2793                          if ($shorthandDividerNeedsUnit) {
    2794                              $divider = $shorthandValue[3];
    2795  
    2796                              if (\is_array($divider)) {
    2797                                  $divider = $this->reduce($divider, true);
    2798                              }
    2799  
    2800                              if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
    2801                                  $revert = false;
    2802                              }
    2803                          }
    2804  
    2805                          if ($revert) {
    2806                              $shorthandValue = $this->expToString($shorthandValue);
    2807                          }
    2808                      } elseif ($shorthandValue[0] === Type::T_LIST) {
    2809                          foreach ($shorthandValue[2] as &$item) {
    2810                              if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
    2811                                  if ($maxShorthandDividers > 0) {
    2812                                      $revert = true;
    2813  
    2814                                      // if the list of values is too long, this has to be a shorthand,
    2815                                      // otherwise it could be a real division
    2816                                      if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
    2817                                          if ($shorthandDividerNeedsUnit) {
    2818                                              $divider = $item[3];
    2819  
    2820                                              if (\is_array($divider)) {
    2821                                                  $divider = $this->reduce($divider, true);
    2822                                              }
    2823  
    2824                                              if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
    2825                                                  $revert = false;
    2826                                              }
    2827                                          }
    2828                                      }
    2829  
    2830                                      if ($revert) {
    2831                                          $item = $this->expToString($item);
    2832                                          $maxShorthandDividers--;
    2833                                      }
    2834                                  }
    2835                              }
    2836                          }
    2837                      }
    2838                  }
    2839  
    2840                  // if the value reduces to null from something else then
    2841                  // the property should be discarded
    2842                  if ($value[0] !== Type::T_NULL) {
    2843                      $value = $this->reduce($value);
    2844  
    2845                      if ($value[0] === Type::T_NULL || $value === static::$nullString) {
    2846                          break;
    2847                      }
    2848                  }
    2849  
    2850                  $compiledValue = $this->compileValue($value);
    2851  
    2852                  // ignore empty value
    2853                  if (\strlen($compiledValue)) {
    2854                      $line = $this->formatter->property(
    2855                          $compiledName,
    2856                          $compiledValue
    2857                      );
    2858                      $this->appendOutputLine($out, Type::T_ASSIGN, $line);
    2859                  }
    2860                  break;
    2861  
    2862              case Type::T_COMMENT:
    2863                  if ($out->type === Type::T_ROOT) {
    2864                      $this->compileComment($child);
    2865                      break;
    2866                  }
    2867  
    2868                  $line = $this->compileCommentValue($child, true);
    2869                  $this->appendOutputLine($out, Type::T_COMMENT, $line);
    2870                  break;
    2871  
    2872              case Type::T_MIXIN:
    2873              case Type::T_FUNCTION:
    2874                  list(, $block) = $child;
    2875                  // the block need to be able to go up to it's parent env to resolve vars
    2876                  $block->parentEnv = $this->getStoreEnv();
    2877                  $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
    2878                  break;
    2879  
    2880              case Type::T_EXTEND:
    2881                  foreach ($child[1] as $sel) {
    2882                      $sel = $this->replaceSelfSelector($sel);
    2883                      $results = $this->evalSelectors([$sel]);
    2884  
    2885                      foreach ($results as $result) {
    2886                          // only use the first one
    2887                          $result = current($result);
    2888                          $selectors = $out->selectors;
    2889  
    2890                          if (! $selectors && isset($child['selfParent'])) {
    2891                              $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
    2892                          }
    2893  
    2894                          $this->pushExtends($result, $selectors, $child);
    2895                      }
    2896                  }
    2897                  break;
    2898  
    2899              case Type::T_IF:
    2900                  list(, $if) = $child;
    2901  
    2902                  if ($this->isTruthy($this->reduce($if->cond, true))) {
    2903                      return $this->compileChildren($if->children, $out);
    2904                  }
    2905  
    2906                  foreach ($if->cases as $case) {
    2907                      if (
    2908                          $case->type === Type::T_ELSE ||
    2909                          $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
    2910                      ) {
    2911                          return $this->compileChildren($case->children, $out);
    2912                      }
    2913                  }
    2914                  break;
    2915  
    2916              case Type::T_EACH:
    2917                  list(, $each) = $child;
    2918  
    2919                  $list = $this->coerceList($this->reduce($each->list), ',', true);
    2920  
    2921                  $this->pushEnv();
    2922  
    2923                  foreach ($list[2] as $item) {
    2924                      if (\count($each->vars) === 1) {
    2925                          $this->set($each->vars[0], $item, true);
    2926                      } else {
    2927                          list(,, $values) = $this->coerceList($item);
    2928  
    2929                          foreach ($each->vars as $i => $var) {
    2930                              $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
    2931                          }
    2932                      }
    2933  
    2934                      $ret = $this->compileChildren($each->children, $out);
    2935  
    2936                      if ($ret) {
    2937                          $store = $this->env->store;
    2938                          $this->popEnv();
    2939                          $this->backPropagateEnv($store, $each->vars);
    2940  
    2941                          return $ret;
    2942                      }
    2943                  }
    2944                  $store = $this->env->store;
    2945                  $this->popEnv();
    2946                  $this->backPropagateEnv($store, $each->vars);
    2947  
    2948                  break;
    2949  
    2950              case Type::T_WHILE:
    2951                  list(, $while) = $child;
    2952  
    2953                  while ($this->isTruthy($this->reduce($while->cond, true))) {
    2954                      $ret = $this->compileChildren($while->children, $out);
    2955  
    2956                      if ($ret) {
    2957                          return $ret;
    2958                      }
    2959                  }
    2960                  break;
    2961  
    2962              case Type::T_FOR:
    2963                  list(, $for) = $child;
    2964  
    2965                  $start = $this->reduce($for->start, true);
    2966                  $end   = $this->reduce($for->end, true);
    2967  
    2968                  if (! $start instanceof Number) {
    2969                      throw $this->error('%s is not a number', $start[0]);
    2970                  }
    2971  
    2972                  if (! $end instanceof Number) {
    2973                      throw $this->error('%s is not a number', $end[0]);
    2974                  }
    2975  
    2976                  $start->assertSameUnitOrUnitless($end);
    2977  
    2978                  $numeratorUnits = $start->getNumeratorUnits();
    2979                  $denominatorUnits = $start->getDenominatorUnits();
    2980  
    2981                  $start = $start->getDimension();
    2982                  $end   = $end->getDimension();
    2983  
    2984                  $d = $start < $end ? 1 : -1;
    2985  
    2986                  $this->pushEnv();
    2987  
    2988                  for (;;) {
    2989                      if (
    2990                          (! $for->until && $start - $d == $end) ||
    2991                          ($for->until && $start == $end)
    2992                      ) {
    2993                          break;
    2994                      }
    2995  
    2996                      $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
    2997                      $start += $d;
    2998  
    2999                      $ret = $this->compileChildren($for->children, $out);
    3000  
    3001                      if ($ret) {
    3002                          $store = $this->env->store;
    3003                          $this->popEnv();
    3004                          $this->backPropagateEnv($store, [$for->var]);
    3005  
    3006                          return $ret;
    3007                      }
    3008                  }
    3009  
    3010                  $store = $this->env->store;
    3011                  $this->popEnv();
    3012                  $this->backPropagateEnv($store, [$for->var]);
    3013  
    3014                  break;
    3015  
    3016              case Type::T_RETURN:
    3017                  return $this->reduce($child[1], true);
    3018  
    3019              case Type::T_NESTED_PROPERTY:
    3020                  $this->compileNestedPropertiesBlock($child[1], $out);
    3021                  break;
    3022  
    3023              case Type::T_INCLUDE:
    3024                  // including a mixin
    3025                  list(, $name, $argValues, $content, $argUsing) = $child;
    3026  
    3027                  $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
    3028  
    3029                  if (! $mixin) {
    3030                      throw $this->error("Undefined mixin $name");
    3031                  }
    3032  
    3033                  $callingScope = $this->getStoreEnv();
    3034  
    3035                  // push scope, apply args
    3036                  $this->pushEnv();
    3037                  $this->env->depth--;
    3038  
    3039                  // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
    3040                  // and assign this fake parent to childs
    3041                  $selfParent = null;
    3042  
    3043                  if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
    3044                      $selfParent = $child['selfParent'];
    3045                  } else {
    3046                      $parentSelectors = $this->multiplySelectors($this->env);
    3047  
    3048                      if ($parentSelectors) {
    3049                          $parent = new Block();
    3050                          $parent->selectors = $parentSelectors;
    3051  
    3052                          foreach ($mixin->children as $k => $child) {
    3053                              if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
    3054                                  $mixin->children[$k][1]->parent = $parent;
    3055                              }
    3056                          }
    3057                      }
    3058                  }
    3059  
    3060                  // clone the stored content to not have its scope spoiled by a further call to the same mixin
    3061                  // i.e., recursive @include of the same mixin
    3062                  if (isset($content)) {
    3063                      $copyContent = clone $content;
    3064                      $copyContent->scope = clone $callingScope;
    3065  
    3066                      $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
    3067                  } else {
    3068                      $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
    3069                  }
    3070  
    3071                  // save the "using" argument list for applying it to when "@content" is invoked
    3072                  if (isset($argUsing)) {
    3073                      $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
    3074                  } else {
    3075                      $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
    3076                  }
    3077  
    3078                  if (isset($mixin->args)) {
    3079                      $this->applyArguments($mixin->args, $argValues);
    3080                  }
    3081  
    3082                  $this->env->marker = 'mixin';
    3083  
    3084                  if (! empty($mixin->parentEnv)) {
    3085                      $this->env->declarationScopeParent = $mixin->parentEnv;
    3086                  } else {
    3087                      throw $this->error("@mixin $name() without parentEnv");
    3088                  }
    3089  
    3090                  $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
    3091  
    3092                  $this->popEnv();
    3093                  break;
    3094  
    3095              case Type::T_MIXIN_CONTENT:
    3096                  $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
    3097                  $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
    3098                  $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
    3099                  $argContent = $child[1];
    3100  
    3101                  if (! $content) {
    3102                      break;
    3103                  }
    3104  
    3105                  $storeEnv = $this->storeEnv;
    3106                  $varsUsing = [];
    3107  
    3108                  if (isset($argUsing) && isset($argContent)) {
    3109                      // Get the arguments provided for the content with the names provided in the "using" argument list
    3110                      $this->storeEnv = null;
    3111                      $varsUsing = $this->applyArguments($argUsing, $argContent, false);
    3112                  }
    3113  
    3114                  // restore the scope from the @content
    3115                  $this->storeEnv = $content->scope;
    3116  
    3117                  // append the vars from using if any
    3118                  foreach ($varsUsing as $name => $val) {
    3119                      $this->set($name, $val, true, $this->storeEnv);
    3120                  }
    3121  
    3122                  $this->compileChildrenNoReturn($content->children, $out);
    3123  
    3124                  $this->storeEnv = $storeEnv;
    3125                  break;
    3126  
    3127              case Type::T_DEBUG:
    3128                  list(, $value) = $child;
    3129  
    3130                  $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
    3131                  $line  = $this->sourceLine;
    3132                  $value = $this->compileDebugValue($value);
    3133  
    3134                  fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
    3135                  break;
    3136  
    3137              case Type::T_WARN:
    3138                  list(, $value) = $child;
    3139  
    3140                  $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
    3141                  $line  = $this->sourceLine;
    3142                  $value = $this->compileDebugValue($value);
    3143  
    3144                  fwrite($this->stderr, "WARNING: $value\n         on line $line of $fname\n\n");
    3145                  break;
    3146  
    3147              case Type::T_ERROR:
    3148                  list(, $value) = $child;
    3149  
    3150                  $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
    3151                  $line  = $this->sourceLine;
    3152                  $value = $this->compileValue($this->reduce($value, true));
    3153  
    3154                  throw $this->error("File $fname on line $line ERROR: $value\n");
    3155  
    3156              default:
    3157                  throw $this->error("unknown child type: $child[0]");
    3158          }
    3159      }
    3160  
    3161      /**
    3162       * Reduce expression to string
    3163       *
    3164       * @param array $exp
    3165       * @param bool $keepParens
    3166       *
    3167       * @return array
    3168       */
    3169      protected function expToString($exp, $keepParens = false)
    3170      {
    3171          list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
    3172  
    3173          $content = [];
    3174  
    3175          if ($keepParens && $inParens) {
    3176              $content[] = '(';
    3177          }
    3178  
    3179          $content[] = $this->reduce($left);
    3180  
    3181          if ($whiteLeft) {
    3182              $content[] = ' ';
    3183          }
    3184  
    3185          $content[] = $op;
    3186  
    3187          if ($whiteRight) {
    3188              $content[] = ' ';
    3189          }
    3190  
    3191          $content[] = $this->reduce($right);
    3192  
    3193          if ($keepParens && $inParens) {
    3194              $content[] = ')';
    3195          }
    3196  
    3197          return [Type::T_STRING, '', $content];
    3198      }
    3199  
    3200      /**
    3201       * Is truthy?
    3202       *
    3203       * @param array|Number $value
    3204       *
    3205       * @return boolean
    3206       */
    3207      protected function isTruthy($value)
    3208      {
    3209          return $value !== static::$false && $value !== static::$null;
    3210      }
    3211  
    3212      /**
    3213       * Is the value a direct relationship combinator?
    3214       *
    3215       * @param string $value
    3216       *
    3217       * @return boolean
    3218       */
    3219      protected function isImmediateRelationshipCombinator($value)
    3220      {
    3221          return $value === '>' || $value === '+' || $value === '~';
    3222      }
    3223  
    3224      /**
    3225       * Should $value cause its operand to eval
    3226       *
    3227       * @param array $value
    3228       *
    3229       * @return boolean
    3230       */
    3231      protected function shouldEval($value)
    3232      {
    3233          switch ($value[0]) {
    3234              case Type::T_EXPRESSION:
    3235                  if ($value[1] === '/') {
    3236                      return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
    3237                  }
    3238  
    3239                  // fall-thru
    3240              case Type::T_VARIABLE:
    3241              case Type::T_FUNCTION_CALL:
    3242                  return true;
    3243          }
    3244  
    3245          return false;
    3246      }
    3247  
    3248      /**
    3249       * Reduce value
    3250       *
    3251       * @param array|Number $value
    3252       * @param boolean $inExp
    3253       *
    3254       * @return null|string|array|Number
    3255       */
    3256      protected function reduce($value, $inExp = false)
    3257      {
    3258          if (\is_null($value)) {
    3259              return null;
    3260          }
    3261  
    3262          switch ($value[0]) {
    3263              case Type::T_EXPRESSION:
    3264                  list(, $op, $left, $right, $inParens) = $value;
    3265  
    3266                  $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
    3267                  $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
    3268  
    3269                  $left = $this->reduce($left, true);
    3270  
    3271                  if ($op !== 'and' && $op !== 'or') {
    3272                      $right = $this->reduce($right, true);
    3273                  }
    3274  
    3275                  // special case: looks like css shorthand
    3276                  if (
    3277                      $opName == 'div' && ! $inParens && ! $inExp &&
    3278                      (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
    3279                      ($right[0] === Type::T_NUMBER && ! $right->unitless()))
    3280                  ) {
    3281                      return $this->expToString($value);
    3282                  }
    3283  
    3284                  $left  = $this->coerceForExpression($left);
    3285                  $right = $this->coerceForExpression($right);
    3286                  $ltype = $left[0];
    3287                  $rtype = $right[0];
    3288  
    3289                  $ucOpName = ucfirst($opName);
    3290                  $ucLType  = ucfirst($ltype);
    3291                  $ucRType  = ucfirst($rtype);
    3292  
    3293                  // this tries:
    3294                  // 1. op[op name][left type][right type]
    3295                  // 2. op[left type][right type] (passing the op as first arg
    3296                  // 3. op[op name]
    3297                  $fn = "op$ucOpName}$ucLType}$ucRType}";
    3298  
    3299                  if (
    3300                      \is_callable([$this, $fn]) ||
    3301                      (($fn = "op$ucLType}$ucRType}") &&
    3302                          \is_callable([$this, $fn]) &&
    3303                          $passOp = true) ||
    3304                      (($fn = "op$ucOpName}") &&
    3305                          \is_callable([$this, $fn]) &&
    3306                          $genOp = true)
    3307                  ) {
    3308                      $shouldEval = $inParens || $inExp;
    3309  
    3310                      if (isset($passOp)) {
    3311                          $out = $this->$fn($op, $left, $right, $shouldEval);
    3312                      } else {
    3313                          $out = $this->$fn($left, $right, $shouldEval);
    3314                      }
    3315  
    3316                      if (isset($out)) {
    3317                          return $out;
    3318                      }
    3319                  }
    3320  
    3321                  return $this->expToString($value);
    3322  
    3323              case Type::T_UNARY:
    3324                  list(, $op, $exp, $inParens) = $value;
    3325  
    3326                  $inExp = $inExp || $this->shouldEval($exp);
    3327                  $exp = $this->reduce($exp);
    3328  
    3329                  if ($exp instanceof Number) {
    3330                      switch ($op) {
    3331                          case '+':
    3332                              return $exp;
    3333  
    3334                          case '-':
    3335                              return $exp->unaryMinus();
    3336                      }
    3337                  }
    3338  
    3339                  if ($op === 'not') {
    3340                      if ($inExp || $inParens) {
    3341                          if ($exp === static::$false || $exp === static::$null) {
    3342                              return static::$true;
    3343                          }
    3344  
    3345                          return static::$false;
    3346                      }
    3347  
    3348                      $op = $op . ' ';
    3349                  }
    3350  
    3351                  return [Type::T_STRING, '', [$op, $exp]];
    3352  
    3353              case Type::T_VARIABLE:
    3354                  return $this->reduce($this->get($value[1]));
    3355  
    3356              case Type::T_LIST:
    3357                  foreach ($value[2] as &$item) {
    3358                      $item = $this->reduce($item);
    3359                  }
    3360  
    3361                  return $value;
    3362  
    3363              case Type::T_MAP:
    3364                  foreach ($value[1] as &$item) {
    3365                      $item = $this->reduce($item);
    3366                  }
    3367  
    3368                  foreach ($value[2] as &$item) {
    3369                      $item = $this->reduce($item);
    3370                  }
    3371  
    3372                  return $value;
    3373  
    3374              case Type::T_STRING:
    3375                  foreach ($value[2] as &$item) {
    3376                      if (\is_array($item) || $item instanceof \ArrayAccess) {
    3377                          $item = $this->reduce($item);
    3378                      }
    3379                  }
    3380  
    3381                  return $value;
    3382  
    3383              case Type::T_INTERPOLATE:
    3384                  $value[1] = $this->reduce($value[1]);
    3385  
    3386                  if ($inExp) {
    3387                      return $value[1];
    3388                  }
    3389  
    3390                  return $value;
    3391  
    3392              case Type::T_FUNCTION_CALL:
    3393                  return $this->fncall($value[1], $value[2]);
    3394  
    3395              case Type::T_SELF:
    3396                  $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
    3397                  $selfSelector = $this->multiplySelectors($this->env, $selfParent);
    3398                  $selfSelector = $this->collapseSelectors($selfSelector, true);
    3399  
    3400                  return $selfSelector;
    3401  
    3402              default:
    3403                  return $value;
    3404          }
    3405      }
    3406  
    3407      /**
    3408       * Function caller
    3409       *
    3410       * @param string $name
    3411       * @param array  $argValues
    3412       *
    3413       * @return array|Number
    3414       */
    3415      protected function fncall($functionReference, $argValues)
    3416      {
    3417          // a string means this is a static hard reference coming from the parsing
    3418          if (is_string($functionReference)) {
    3419              $name = $functionReference;
    3420  
    3421              $functionReference = $this->getFunctionReference($name);
    3422              if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
    3423                  $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
    3424              }
    3425          }
    3426  
    3427          // a function type means we just want a plain css function call
    3428          if ($functionReference[0] === Type::T_FUNCTION) {
    3429              // for CSS functions, simply flatten the arguments into a list
    3430              $listArgs = [];
    3431  
    3432              foreach ((array) $argValues as $arg) {
    3433                  if (empty($arg[0]) || count($argValues) === 1) {
    3434                      $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
    3435                  }
    3436              }
    3437  
    3438              return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
    3439          }
    3440  
    3441          if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
    3442              return static::$defaultValue;
    3443          }
    3444  
    3445  
    3446          switch ($functionReference[1]) {
    3447              // SCSS @function
    3448              case 'scss':
    3449                  return $this->callScssFunction($functionReference[3], $argValues);
    3450  
    3451              // native PHP functions
    3452              case 'user':
    3453              case 'native':
    3454                  list(,,$name, $fn, $prototype) = $functionReference;
    3455  
    3456                  // special cases of css valid functions min/max
    3457                  $name = strtolower($name);
    3458                  if (\in_array($name, ['min', 'max'])) {
    3459                      $cssFunction = $this->cssValidArg(
    3460                          [Type::T_FUNCTION_CALL, $name, $argValues],
    3461                          ['min', 'max', 'calc', 'env', 'var']
    3462                      );
    3463                      if ($cssFunction !== false) {
    3464                          return $cssFunction;
    3465                      }
    3466                  }
    3467                  $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
    3468  
    3469                  if (! isset($returnValue)) {
    3470                      return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
    3471                  }
    3472  
    3473                  return $returnValue;
    3474  
    3475              default:
    3476                  return static::$defaultValue;
    3477          }
    3478      }
    3479  
    3480      protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
    3481      {
    3482          switch ($arg[0]) {
    3483              case Type::T_INTERPOLATE:
    3484                  return [Type::T_KEYWORD, $this->CompileValue($arg)];
    3485  
    3486              case Type::T_FUNCTION:
    3487                  if (! \in_array($arg[1], $allowed_function)) {
    3488                      return false;
    3489                  }
    3490                  if ($arg[2][0] === Type::T_LIST) {
    3491                      foreach ($arg[2][2] as $k => $subarg) {
    3492                          $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
    3493                          if ($arg[2][2][$k] === false) {
    3494                              return false;
    3495                          }
    3496                      }
    3497                  }
    3498                  return $arg;
    3499  
    3500              case Type::T_FUNCTION_CALL:
    3501                  if (! \in_array($arg[1], $allowed_function)) {
    3502                      return false;
    3503                  }
    3504                  $cssArgs = [];
    3505                  foreach ($arg[2] as $argValue) {
    3506                      if ($argValue === static::$null) {
    3507                          return false;
    3508                      }
    3509                      $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
    3510                      if (empty($argValue[0]) && $cssArg !== false) {
    3511                          $cssArgs[] = [$argValue[0], $cssArg];
    3512                      } else {
    3513                          return false;
    3514                      }
    3515                  }
    3516  
    3517                  return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
    3518  
    3519              case Type::T_STRING:
    3520              case Type::T_KEYWORD:
    3521                  if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
    3522                      return false;
    3523                  }
    3524                  return $this->stringifyFncallArgs($arg);
    3525  
    3526              case Type::T_NUMBER:
    3527                  return $this->stringifyFncallArgs($arg);
    3528  
    3529              case Type::T_LIST:
    3530                  if (!$inFunction) {
    3531                      return false;
    3532                  }
    3533                  if (empty($arg['enclosing']) and $arg[1] === '') {
    3534                      foreach ($arg[2] as $k => $subarg) {
    3535                          $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
    3536                          if ($arg[2][$k] === false) {
    3537                              return false;
    3538                          }
    3539                      }
    3540                      $arg[0] = Type::T_STRING;
    3541                      return $arg;
    3542                  }
    3543                  return false;
    3544  
    3545              case Type::T_EXPRESSION:
    3546                  if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
    3547                      return false;
    3548                  }
    3549                  $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
    3550                  $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
    3551                  if ($arg[2] === false || $arg[3] === false) {
    3552                      return false;
    3553                  }
    3554                  return $this->expToString($arg, true);
    3555  
    3556              case Type::T_VARIABLE:
    3557              case Type::T_SELF:
    3558              default:
    3559                  return false;
    3560          }
    3561      }
    3562  
    3563  
    3564      /**
    3565       * Reformat fncall arguments to proper css function output
    3566       *
    3567       * @param $arg
    3568       *
    3569       * @return array|\ArrayAccess|Number|string|null
    3570       */
    3571      protected function stringifyFncallArgs($arg)
    3572      {
    3573  
    3574          switch ($arg[0]) {
    3575              case Type::T_LIST:
    3576                  foreach ($arg[2] as $k => $v) {
    3577                      $arg[2][$k] = $this->stringifyFncallArgs($v);
    3578                  }
    3579                  break;
    3580  
    3581              case Type::T_EXPRESSION:
    3582                  if ($arg[1] === '/') {
    3583                      $arg[2] = $this->stringifyFncallArgs($arg[2]);
    3584                      $arg[3] = $this->stringifyFncallArgs($arg[3]);
    3585                      $arg[5] = $arg[6] = false; // no space around /
    3586                      $arg = $this->expToString($arg);
    3587                  }
    3588                  break;
    3589  
    3590              case Type::T_FUNCTION_CALL:
    3591                  $name = strtolower($arg[1]);
    3592  
    3593                  if (in_array($name, ['max', 'min', 'calc'])) {
    3594                      $args = $arg[2];
    3595                      $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
    3596                  }
    3597                  break;
    3598          }
    3599  
    3600          return $arg;
    3601      }
    3602  
    3603      /**
    3604       * Find a function reference
    3605       * @param string $name
    3606       * @param bool $safeCopy
    3607       * @return array
    3608       */
    3609      protected function getFunctionReference($name, $safeCopy = false)
    3610      {
    3611          // SCSS @function
    3612          if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
    3613              if ($safeCopy) {
    3614                  $func = clone $func;
    3615              }
    3616  
    3617              return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
    3618          }
    3619  
    3620          // native PHP functions
    3621  
    3622          // try to find a native lib function
    3623          $normalizedName = $this->normalizeName($name);
    3624          $libName = null;
    3625  
    3626          if (isset($this->userFunctions[$normalizedName])) {
    3627              // see if we can find a user function
    3628              list($f, $prototype) = $this->userFunctions[$normalizedName];
    3629  
    3630              return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
    3631          }
    3632  
    3633          if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
    3634              $libName   = $f[1];
    3635              $prototype = isset(static::$$libName) ? static::$$libName : null;
    3636  
    3637              return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
    3638          }
    3639  
    3640          return static::$null;
    3641      }
    3642  
    3643  
    3644      /**
    3645       * Normalize name
    3646       *
    3647       * @param string $name
    3648       *
    3649       * @return string
    3650       */
    3651      protected function normalizeName($name)
    3652      {
    3653          return str_replace('-', '_', $name);
    3654      }
    3655  
    3656      /**
    3657       * Normalize value
    3658       *
    3659       * @param array|Number $value
    3660       *
    3661       * @return array|Number
    3662       */
    3663      public function normalizeValue($value)
    3664      {
    3665          $value = $this->coerceForExpression($this->reduce($value));
    3666  
    3667          switch ($value[0]) {
    3668              case Type::T_LIST:
    3669                  $value = $this->extractInterpolation($value);
    3670  
    3671                  if ($value[0] !== Type::T_LIST) {
    3672                      return [Type::T_KEYWORD, $this->compileValue($value)];
    3673                  }
    3674  
    3675                  foreach ($value[2] as $key => $item) {
    3676                      $value[2][$key] = $this->normalizeValue($item);
    3677                  }
    3678  
    3679                  if (! empty($value['enclosing'])) {
    3680                      unset($value['enclosing']);
    3681                  }
    3682  
    3683                  return $value;
    3684  
    3685              case Type::T_STRING:
    3686                  return [$value[0], '"', [$this->compileStringContent($value)]];
    3687  
    3688              case Type::T_INTERPOLATE:
    3689                  return [Type::T_KEYWORD, $this->compileValue($value)];
    3690  
    3691              default:
    3692                  return $value;
    3693          }
    3694      }
    3695  
    3696      /**
    3697       * Add numbers
    3698       *
    3699       * @param Number $left
    3700       * @param Number $right
    3701       *
    3702       * @return Number
    3703       */
    3704      protected function opAddNumberNumber(Number $left, Number $right)
    3705      {
    3706          return $left->plus($right);
    3707      }
    3708  
    3709      /**
    3710       * Multiply numbers
    3711       *
    3712       * @param Number $left
    3713       * @param Number $right
    3714       *
    3715       * @return Number
    3716       */
    3717      protected function opMulNumberNumber(Number $left, Number $right)
    3718      {
    3719          return $left->times($right);
    3720      }
    3721  
    3722      /**
    3723       * Subtract numbers
    3724       *
    3725       * @param Number $left
    3726       * @param Number $right
    3727       *
    3728       * @return Number
    3729       */
    3730      protected function opSubNumberNumber(Number $left, Number $right)
    3731      {
    3732          return $left->minus($right);
    3733      }
    3734  
    3735      /**
    3736       * Divide numbers
    3737       *
    3738       * @param Number $left
    3739       * @param Number $right
    3740       *
    3741       * @return Number
    3742       */
    3743      protected function opDivNumberNumber(Number $left, Number $right)
    3744      {
    3745          return $left->dividedBy($right);
    3746      }
    3747  
    3748      /**
    3749       * Mod numbers
    3750       *
    3751       * @param Number $left
    3752       * @param Number $right
    3753       *
    3754       * @return Number
    3755       */
    3756      protected function opModNumberNumber(Number $left, Number $right)
    3757      {
    3758          return $left->modulo($right);
    3759      }
    3760  
    3761      /**
    3762       * Add strings
    3763       *
    3764       * @param array $left
    3765       * @param array $right
    3766       *
    3767       * @return array|null
    3768       */
    3769      protected function opAdd($left, $right)
    3770      {
    3771          if ($strLeft = $this->coerceString($left)) {
    3772              if ($right[0] === Type::T_STRING) {
    3773                  $right[1] = '';
    3774              }
    3775  
    3776              $strLeft[2][] = $right;
    3777  
    3778              return $strLeft;
    3779          }
    3780  
    3781          if ($strRight = $this->coerceString($right)) {
    3782              if ($left[0] === Type::T_STRING) {
    3783                  $left[1] = '';
    3784              }
    3785  
    3786              array_unshift($strRight[2], $left);
    3787  
    3788              return $strRight;
    3789          }
    3790  
    3791          return null;
    3792      }
    3793  
    3794      /**
    3795       * Boolean and
    3796       *
    3797       * @param array|Number $left
    3798       * @param array|Number  $right
    3799       * @param boolean $shouldEval
    3800       *
    3801       * @return array|Number|null
    3802       */
    3803      protected function opAnd($left, $right, $shouldEval)
    3804      {
    3805          $truthy = ($left === static::$null || $right === static::$null) ||
    3806                    ($left === static::$false || $left === static::$true) &&
    3807                    ($right === static::$false || $right === static::$true);
    3808  
    3809          if (! $shouldEval) {
    3810              if (! $truthy) {
    3811                  return null;
    3812              }
    3813          }
    3814  
    3815          if ($left !== static::$false && $left !== static::$null) {
    3816              return $this->reduce($right, true);
    3817          }
    3818  
    3819          return $left;
    3820      }
    3821  
    3822      /**
    3823       * Boolean or
    3824       *
    3825       * @param array|Number $left
    3826       * @param array|Number $right
    3827       * @param boolean $shouldEval
    3828       *
    3829       * @return array|Number|null
    3830       */
    3831      protected function opOr($left, $right, $shouldEval)
    3832      {
    3833          $truthy = ($left === static::$null || $right === static::$null) ||
    3834                    ($left === static::$false || $left === static::$true) &&
    3835                    ($right === static::$false || $right === static::$true);
    3836  
    3837          if (! $shouldEval) {
    3838              if (! $truthy) {
    3839                  return null;
    3840              }
    3841          }
    3842  
    3843          if ($left !== static::$false && $left !== static::$null) {
    3844              return $left;
    3845          }
    3846  
    3847          return $this->reduce($right, true);
    3848      }
    3849  
    3850      /**
    3851       * Compare colors
    3852       *
    3853       * @param string $op
    3854       * @param array  $left
    3855       * @param array  $right
    3856       *
    3857       * @return array
    3858       */
    3859      protected function opColorColor($op, $left, $right)
    3860      {
    3861          if ($op !== '==' && $op !== '!=') {
    3862              $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
    3863                  . "Consider using Sass's color functions instead.";
    3864              $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
    3865              $line  = $this->sourceLine;
    3866  
    3867              fwrite($this->stderr, "DEPRECATION WARNING: $warning\n         on line $line of $fname\n\n");
    3868          }
    3869  
    3870          $out = [Type::T_COLOR];
    3871  
    3872          foreach ([1, 2, 3] as $i) {
    3873              $lval = isset($left[$i]) ? $left[$i] : 0;
    3874              $rval = isset($right[$i]) ? $right[$i] : 0;
    3875  
    3876              switch ($op) {
    3877                  case '+':
    3878                      $out[] = $lval + $rval;
    3879                      break;
    3880  
    3881                  case '-':
    3882                      $out[] = $lval - $rval;
    3883                      break;
    3884  
    3885                  case '*':
    3886                      $out[] = $lval * $rval;
    3887                      break;
    3888  
    3889                  case '%':
    3890                      if ($rval == 0) {
    3891                          throw $this->error("color: Can't take modulo by zero");
    3892                      }
    3893  
    3894                      $out[] = $lval % $rval;
    3895                      break;
    3896  
    3897                  case '/':
    3898                      if ($rval == 0) {
    3899                          throw $this->error("color: Can't divide by zero");
    3900                      }
    3901  
    3902                      $out[] = (int) ($lval / $rval);
    3903                      break;
    3904  
    3905                  case '==':
    3906                      return $this->opEq($left, $right);
    3907  
    3908                  case '!=':
    3909                      return $this->opNeq($left, $right);
    3910  
    3911                  default:
    3912                      throw $this->error("color: unknown op $op");
    3913              }
    3914          }
    3915  
    3916          if (isset($left[4])) {
    3917              $out[4] = $left[4];
    3918          } elseif (isset($right[4])) {
    3919              $out[4] = $right[4];
    3920          }
    3921  
    3922          return $this->fixColor($out);
    3923      }
    3924  
    3925      /**
    3926       * Compare color and number
    3927       *
    3928       * @param string $op
    3929       * @param array  $left
    3930       * @param Number  $right
    3931       *
    3932       * @return array
    3933       */
    3934      protected function opColorNumber($op, $left, Number $right)
    3935      {
    3936          if ($op === '==') {
    3937              return static::$false;
    3938          }
    3939  
    3940          if ($op === '!=') {
    3941              return static::$true;
    3942          }
    3943  
    3944          $value = $right->getDimension();
    3945  
    3946          return $this->opColorColor(
    3947              $op,
    3948              $left,
    3949              [Type::T_COLOR, $value, $value, $value]
    3950          );
    3951      }
    3952  
    3953      /**
    3954       * Compare number and color
    3955       *
    3956       * @param string $op
    3957       * @param Number  $left
    3958       * @param array  $right
    3959       *
    3960       * @return array
    3961       */
    3962      protected function opNumberColor($op, Number $left, $right)
    3963      {
    3964          if ($op === '==') {
    3965              return static::$false;
    3966          }
    3967  
    3968          if ($op === '!=') {
    3969              return static::$true;
    3970          }
    3971  
    3972          $value = $left->getDimension();
    3973  
    3974          return $this->opColorColor(
    3975              $op,
    3976              [Type::T_COLOR, $value, $value, $value],
    3977              $right
    3978          );
    3979      }
    3980  
    3981      /**
    3982       * Compare number1 == number2
    3983       *
    3984       * @param array|Number $left
    3985       * @param array|Number $right
    3986       *
    3987       * @return array
    3988       */
    3989      protected function opEq($left, $right)
    3990      {
    3991          if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
    3992              $lStr[1] = '';
    3993              $rStr[1] = '';
    3994  
    3995              $left = $this->compileValue($lStr);
    3996              $right = $this->compileValue($rStr);
    3997          }
    3998  
    3999          return $this->toBool($left === $right);
    4000      }
    4001  
    4002      /**
    4003       * Compare number1 != number2
    4004       *
    4005       * @param array|Number $left
    4006       * @param array|Number $right
    4007       *
    4008       * @return array
    4009       */
    4010      protected function opNeq($left, $right)
    4011      {
    4012          if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
    4013              $lStr[1] = '';
    4014              $rStr[1] = '';
    4015  
    4016              $left = $this->compileValue($lStr);
    4017              $right = $this->compileValue($rStr);
    4018          }
    4019  
    4020          return $this->toBool($left !== $right);
    4021      }
    4022  
    4023      /**
    4024       * Compare number1 == number2
    4025       *
    4026       * @param Number $left
    4027       * @param Number $right
    4028       *
    4029       * @return array
    4030       */
    4031      protected function opEqNumberNumber(Number $left, Number $right)
    4032      {
    4033          return $this->toBool($left->equals($right));
    4034      }
    4035  
    4036      /**
    4037       * Compare number1 != number2
    4038       *
    4039       * @param Number $left
    4040       * @param Number $right
    4041       *
    4042       * @return array
    4043       */
    4044      protected function opNeqNumberNumber(Number $left, Number $right)
    4045      {
    4046          return $this->toBool(!$left->equals($right));
    4047      }
    4048  
    4049      /**
    4050       * Compare number1 >= number2
    4051       *
    4052       * @param Number $left
    4053       * @param Number $right
    4054       *
    4055       * @return array
    4056       */
    4057      protected function opGteNumberNumber(Number $left, Number $right)
    4058      {
    4059          return $this->toBool($left->greaterThanOrEqual($right));
    4060      }
    4061  
    4062      /**
    4063       * Compare number1 > number2
    4064       *
    4065       * @param Number $left
    4066       * @param Number $right
    4067       *
    4068       * @return array
    4069       */
    4070      protected function opGtNumberNumber(Number $left, Number $right)
    4071      {
    4072          return $this->toBool($left->greaterThan($right));
    4073      }
    4074  
    4075      /**
    4076       * Compare number1 <= number2
    4077       *
    4078       * @param Number $left
    4079       * @param Number $right
    4080       *
    4081       * @return array
    4082       */
    4083      protected function opLteNumberNumber(Number $left, Number $right)
    4084      {
    4085          return $this->toBool($left->lessThanOrEqual($right));
    4086      }
    4087  
    4088      /**
    4089       * Compare number1 < number2
    4090       *
    4091       * @param Number $left
    4092       * @param Number $right
    4093       *
    4094       * @return array
    4095       */
    4096      protected function opLtNumberNumber(Number $left, Number $right)
    4097      {
    4098          return $this->toBool($left->lessThan($right));
    4099      }
    4100  
    4101      /**
    4102       * Cast to boolean
    4103       *
    4104       * @api
    4105       *
    4106       * @param mixed $thing
    4107       *
    4108       * @return array
    4109       */
    4110      public function toBool($thing)
    4111      {
    4112          return $thing ? static::$true : static::$false;
    4113      }
    4114  
    4115      /**
    4116       * Escape non printable chars in strings output as in dart-sass
    4117       * @param string $string
    4118       * @return string
    4119       */
    4120      public function escapeNonPrintableChars($string, $inKeyword = false)
    4121      {
    4122          static $replacement = [];
    4123          if (empty($replacement[$inKeyword])) {
    4124              for ($i = 0; $i < 32; $i++) {
    4125                  if ($i !== 9 || $inKeyword) {
    4126                      $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
    4127                  }
    4128              }
    4129          }
    4130          $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
    4131          // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
    4132          if (strpos($string, chr(0)) !== false) {
    4133              if (substr($string, -1) === chr(0)) {
    4134                  $string = substr($string, 0, -1);
    4135              }
    4136              $string = str_replace(
    4137                  [chr(0) . '\\',chr(0) . ' '],
    4138                  [ '\\', ' '],
    4139                  $string
    4140              );
    4141              if (strpos($string, chr(0)) !== false) {
    4142                  $parts = explode(chr(0), $string);
    4143                  $string = array_shift($parts);
    4144                  while (count($parts)) {
    4145                      $next = array_shift($parts);
    4146                      if (strpos("