Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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