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\Block;
  15  use ScssPhp\ScssPhp\Cache;
  16  use ScssPhp\ScssPhp\Compiler;
  17  use ScssPhp\ScssPhp\Exception\ParserException;
  18  use ScssPhp\ScssPhp\Node;
  19  use ScssPhp\ScssPhp\Type;
  20  
  21  /**
  22   * Parser
  23   *
  24   * @author Leaf Corcoran <leafot@gmail.com>
  25   */
  26  class Parser
  27  {
  28      const SOURCE_INDEX  = -1;
  29      const SOURCE_LINE   = -2;
  30      const SOURCE_COLUMN = -3;
  31  
  32      /**
  33       * @var array
  34       */
  35      protected static $precedence = [
  36          '='   => 0,
  37          'or'  => 1,
  38          'and' => 2,
  39          '=='  => 3,
  40          '!='  => 3,
  41          '<=>' => 3,
  42          '<='  => 4,
  43          '>='  => 4,
  44          '<'   => 4,
  45          '>'   => 4,
  46          '+'   => 5,
  47          '-'   => 5,
  48          '*'   => 6,
  49          '/'   => 6,
  50          '%'   => 6,
  51      ];
  52  
  53      protected static $commentPattern;
  54      protected static $operatorPattern;
  55      protected static $whitePattern;
  56  
  57      protected $cache;
  58  
  59      private $sourceName;
  60      private $sourceIndex;
  61      private $sourcePositions;
  62      private $charset;
  63      private $count;
  64      private $env;
  65      private $inParens;
  66      private $eatWhiteDefault;
  67      private $discardComments;
  68      private $buffer;
  69      private $utf8;
  70      private $encoding;
  71      private $patternModifiers;
  72      private $commentsSeen;
  73  
  74      /**
  75       * Constructor
  76       *
  77       * @api
  78       *
  79       * @param string                 $sourceName
  80       * @param integer                $sourceIndex
  81       * @param string                 $encoding
  82       * @param \ScssPhp\ScssPhp\Cache $cache
  83       */
  84      public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
  85      {
  86          $this->sourceName       = $sourceName ?: '(stdin)';
  87          $this->sourceIndex      = $sourceIndex;
  88          $this->charset          = null;
  89          $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
  90          $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
  91          $this->commentsSeen     = [];
  92          $this->discardComments  = false;
  93  
  94          if (empty(static::$operatorPattern)) {
  95              static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
  96  
  97              $commentSingle      = '\/\/';
  98              $commentMultiLeft   = '\/\*';
  99              $commentMultiRight  = '\*\/';
 100  
 101              static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
 102              static::$whitePattern = $this->utf8
 103                  ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
 104                  : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
 105          }
 106  
 107          if ($cache) {
 108              $this->cache = $cache;
 109          }
 110      }
 111  
 112      /**
 113       * Get source file name
 114       *
 115       * @api
 116       *
 117       * @return string
 118       */
 119      public function getSourceName()
 120      {
 121          return $this->sourceName;
 122      }
 123  
 124      /**
 125       * Throw parser error
 126       *
 127       * @api
 128       *
 129       * @param string $msg
 130       *
 131       * @throws \ScssPhp\ScssPhp\Exception\ParserException
 132       */
 133      public function throwParseError($msg = 'parse error')
 134      {
 135          list($line, $column) = $this->getSourcePosition($this->count);
 136  
 137          $loc = empty($this->sourceName)
 138               ? "line: $line, column: $column"
 139               : "$this->sourceName on line $line, at column $column";
 140  
 141          if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
 142              throw new ParserException("$msg: failed at `$m[1]` $loc");
 143          }
 144  
 145          throw new ParserException("$msg: $loc");
 146      }
 147  
 148      /**
 149       * Parser buffer
 150       *
 151       * @api
 152       *
 153       * @param string $buffer
 154       *
 155       * @return \ScssPhp\ScssPhp\Block
 156       */
 157      public function parse($buffer)
 158      {
 159          if ($this->cache) {
 160              $cacheKey = $this->sourceName . ":" . md5($buffer);
 161              $parseOptions = [
 162                  'charset' => $this->charset,
 163                  'utf8' => $this->utf8,
 164              ];
 165              $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
 166  
 167              if (! is_null($v)) {
 168                  return $v;
 169              }
 170          }
 171  
 172          // strip BOM (byte order marker)
 173          if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
 174              $buffer = substr($buffer, 3);
 175          }
 176  
 177          $this->buffer          = rtrim($buffer, "\x00..\x1f");
 178          $this->count           = 0;
 179          $this->env             = null;
 180          $this->inParens        = false;
 181          $this->eatWhiteDefault = true;
 182  
 183          $this->saveEncoding();
 184          $this->extractLineNumbers($buffer);
 185  
 186          $this->pushBlock(null); // root block
 187          $this->whitespace();
 188          $this->pushBlock(null);
 189          $this->popBlock();
 190  
 191          while ($this->parseChunk()) {
 192              ;
 193          }
 194  
 195          if ($this->count !== strlen($this->buffer)) {
 196              $this->throwParseError();
 197          }
 198  
 199          if (! empty($this->env->parent)) {
 200              $this->throwParseError('unclosed block');
 201          }
 202  
 203          if ($this->charset) {
 204              array_unshift($this->env->children, $this->charset);
 205          }
 206  
 207          $this->restoreEncoding();
 208  
 209          if ($this->cache) {
 210              $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
 211          }
 212  
 213          return $this->env;
 214      }
 215  
 216      /**
 217       * Parse a value or value list
 218       *
 219       * @api
 220       *
 221       * @param string       $buffer
 222       * @param string|array $out
 223       *
 224       * @return boolean
 225       */
 226      public function parseValue($buffer, &$out)
 227      {
 228          $this->count           = 0;
 229          $this->env             = null;
 230          $this->inParens        = false;
 231          $this->eatWhiteDefault = true;
 232          $this->buffer          = (string) $buffer;
 233  
 234          $this->saveEncoding();
 235  
 236          $list = $this->valueList($out);
 237  
 238          $this->restoreEncoding();
 239  
 240          return $list;
 241      }
 242  
 243      /**
 244       * Parse a selector or selector list
 245       *
 246       * @api
 247       *
 248       * @param string       $buffer
 249       * @param string|array $out
 250       *
 251       * @return boolean
 252       */
 253      public function parseSelector($buffer, &$out)
 254      {
 255          $this->count           = 0;
 256          $this->env             = null;
 257          $this->inParens        = false;
 258          $this->eatWhiteDefault = true;
 259          $this->buffer          = (string) $buffer;
 260  
 261          $this->saveEncoding();
 262  
 263          $selector = $this->selectors($out);
 264  
 265          $this->restoreEncoding();
 266  
 267          return $selector;
 268      }
 269  
 270      /**
 271       * Parse a media Query
 272       *
 273       * @api
 274       *
 275       * @param string       $buffer
 276       * @param string|array $out
 277       *
 278       * @return boolean
 279       */
 280      public function parseMediaQueryList($buffer, &$out)
 281      {
 282          $this->count           = 0;
 283          $this->env             = null;
 284          $this->inParens        = false;
 285          $this->eatWhiteDefault = true;
 286          $this->buffer          = (string) $buffer;
 287  
 288          $this->saveEncoding();
 289  
 290          $isMediaQuery = $this->mediaQueryList($out);
 291  
 292          $this->restoreEncoding();
 293  
 294          return $isMediaQuery;
 295      }
 296  
 297      /**
 298       * Parse a single chunk off the head of the buffer and append it to the
 299       * current parse environment.
 300       *
 301       * Returns false when the buffer is empty, or when there is an error.
 302       *
 303       * This function is called repeatedly until the entire document is
 304       * parsed.
 305       *
 306       * This parser is most similar to a recursive descent parser. Single
 307       * functions represent discrete grammatical rules for the language, and
 308       * they are able to capture the text that represents those rules.
 309       *
 310       * Consider the function Compiler::keyword(). (All parse functions are
 311       * structured the same.)
 312       *
 313       * The function takes a single reference argument. When calling the
 314       * function it will attempt to match a keyword on the head of the buffer.
 315       * If it is successful, it will place the keyword in the referenced
 316       * argument, advance the position in the buffer, and return true. If it
 317       * fails then it won't advance the buffer and it will return false.
 318       *
 319       * All of these parse functions are powered by Compiler::match(), which behaves
 320       * the same way, but takes a literal regular expression. Sometimes it is
 321       * more convenient to use match instead of creating a new function.
 322       *
 323       * Because of the format of the functions, to parse an entire string of
 324       * grammatical rules, you can chain them together using &&.
 325       *
 326       * But, if some of the rules in the chain succeed before one fails, then
 327       * the buffer position will be left at an invalid state. In order to
 328       * avoid this, Compiler::seek() is used to remember and set buffer positions.
 329       *
 330       * Before parsing a chain, use $s = $this->count to remember the current
 331       * position into $s. Then if a chain fails, use $this->seek($s) to
 332       * go back where we started.
 333       *
 334       * @return boolean
 335       */
 336      protected function parseChunk()
 337      {
 338          $s = $this->count;
 339  
 340          // the directives
 341          if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
 342              if ($this->literal('@at-root', 8) &&
 343                  ($this->selectors($selector) || true) &&
 344                  ($this->map($with) || true) &&
 345                  (($this->matchChar('(')
 346                      && $this->interpolation($with)
 347                      && $this->matchChar(')')) || true) &&
 348                  $this->matchChar('{', false)
 349              ) {
 350                  $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
 351                  $atRoot->selector = $selector;
 352                  $atRoot->with     = $with;
 353  
 354                  return true;
 355              }
 356  
 357              $this->seek($s);
 358  
 359              if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
 360                  $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
 361                  $media->queryList = $mediaQueryList[2];
 362  
 363                  return true;
 364              }
 365  
 366              $this->seek($s);
 367  
 368              if ($this->literal('@mixin', 6) &&
 369                  $this->keyword($mixinName) &&
 370                  ($this->argumentDef($args) || true) &&
 371                  $this->matchChar('{', false)
 372              ) {
 373                  $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
 374                  $mixin->name = $mixinName;
 375                  $mixin->args = $args;
 376  
 377                  return true;
 378              }
 379  
 380              $this->seek($s);
 381  
 382              if ($this->literal('@include', 8) &&
 383                  $this->keyword($mixinName) &&
 384                  ($this->matchChar('(') &&
 385                      ($this->argValues($argValues) || true) &&
 386                      $this->matchChar(')') || true) &&
 387                  ($this->end() ||
 388                      ($this->literal('using', 5) &&
 389                          $this->argumentDef($argUsing) &&
 390                          ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
 391                      $this->matchChar('{') && $hasBlock = true)
 392              ) {
 393                  $child = [
 394                      Type::T_INCLUDE,
 395                      $mixinName,
 396                      isset($argValues) ? $argValues : null,
 397                      null,
 398                      isset($argUsing) ? $argUsing : null
 399                  ];
 400  
 401                  if (! empty($hasBlock)) {
 402                      $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
 403                      $include->child = $child;
 404                  } else {
 405                      $this->append($child, $s);
 406                  }
 407  
 408                  return true;
 409              }
 410  
 411              $this->seek($s);
 412  
 413              if ($this->literal('@scssphp-import-once', 20) &&
 414                  $this->valueList($importPath) &&
 415                  $this->end()
 416              ) {
 417                  $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
 418  
 419                  return true;
 420              }
 421  
 422              $this->seek($s);
 423  
 424              if ($this->literal('@import', 7) &&
 425                  $this->valueList($importPath) &&
 426                  $this->end()
 427              ) {
 428                  $this->append([Type::T_IMPORT, $importPath], $s);
 429  
 430                  return true;
 431              }
 432  
 433              $this->seek($s);
 434  
 435              if ($this->literal('@import', 7) &&
 436                  $this->url($importPath) &&
 437                  $this->end()
 438              ) {
 439                  $this->append([Type::T_IMPORT, $importPath], $s);
 440  
 441                  return true;
 442              }
 443  
 444              $this->seek($s);
 445  
 446              if ($this->literal('@extend', 7) &&
 447                  $this->selectors($selectors) &&
 448                  $this->end()
 449              ) {
 450                  // check for '!flag'
 451                  $optional = $this->stripOptionalFlag($selectors);
 452                  $this->append([Type::T_EXTEND, $selectors, $optional], $s);
 453  
 454                  return true;
 455              }
 456  
 457              $this->seek($s);
 458  
 459              if ($this->literal('@function', 9) &&
 460                  $this->keyword($fnName) &&
 461                  $this->argumentDef($args) &&
 462                  $this->matchChar('{', false)
 463              ) {
 464                  $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
 465                  $func->name = $fnName;
 466                  $func->args = $args;
 467  
 468                  return true;
 469              }
 470  
 471              $this->seek($s);
 472  
 473              if ($this->literal('@break', 6) && $this->end()) {
 474                  $this->append([Type::T_BREAK], $s);
 475  
 476                  return true;
 477              }
 478  
 479              $this->seek($s);
 480  
 481              if ($this->literal('@continue', 9) && $this->end()) {
 482                  $this->append([Type::T_CONTINUE], $s);
 483  
 484                  return true;
 485              }
 486  
 487              $this->seek($s);
 488  
 489              if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
 490                  $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
 491  
 492                  return true;
 493              }
 494  
 495              $this->seek($s);
 496  
 497              if ($this->literal('@each', 5) &&
 498                  $this->genericList($varNames, 'variable', ',', false) &&
 499                  $this->literal('in', 2) &&
 500                  $this->valueList($list) &&
 501                  $this->matchChar('{', false)
 502              ) {
 503                  $each = $this->pushSpecialBlock(Type::T_EACH, $s);
 504  
 505                  foreach ($varNames[2] as $varName) {
 506                      $each->vars[] = $varName[1];
 507                  }
 508  
 509                  $each->list = $list;
 510  
 511                  return true;
 512              }
 513  
 514              $this->seek($s);
 515  
 516              if ($this->literal('@while', 6) &&
 517                  $this->expression($cond) &&
 518                  $this->matchChar('{', false)
 519              ) {
 520                  $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
 521                  $while->cond = $cond;
 522  
 523                  return true;
 524              }
 525  
 526              $this->seek($s);
 527  
 528              if ($this->literal('@for', 4) &&
 529                  $this->variable($varName) &&
 530                  $this->literal('from', 4) &&
 531                  $this->expression($start) &&
 532                  ($this->literal('through', 7) ||
 533                      ($forUntil = true && $this->literal('to', 2))) &&
 534                  $this->expression($end) &&
 535                  $this->matchChar('{', false)
 536              ) {
 537                  $for = $this->pushSpecialBlock(Type::T_FOR, $s);
 538                  $for->var   = $varName[1];
 539                  $for->start = $start;
 540                  $for->end   = $end;
 541                  $for->until = isset($forUntil);
 542  
 543                  return true;
 544              }
 545  
 546              $this->seek($s);
 547  
 548              if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
 549                  $if = $this->pushSpecialBlock(Type::T_IF, $s);
 550                  while ($cond[0] === Type::T_LIST
 551                      && !empty($cond['enclosing'])
 552                      && $cond['enclosing'] === 'parent'
 553                      && count($cond[2]) == 1) {
 554                      $cond = reset($cond[2]);
 555                  }
 556                  $if->cond  = $cond;
 557                  $if->cases = [];
 558  
 559                  return true;
 560              }
 561  
 562              $this->seek($s);
 563  
 564              if ($this->literal('@debug', 6) &&
 565                  $this->valueList($value) &&
 566                  $this->end()
 567              ) {
 568                  $this->append([Type::T_DEBUG, $value], $s);
 569  
 570                  return true;
 571              }
 572  
 573              $this->seek($s);
 574  
 575              if ($this->literal('@warn', 5) &&
 576                  $this->valueList($value) &&
 577                  $this->end()
 578              ) {
 579                  $this->append([Type::T_WARN, $value], $s);
 580  
 581                  return true;
 582              }
 583  
 584              $this->seek($s);
 585  
 586              if ($this->literal('@error', 6) &&
 587                  $this->valueList($value) &&
 588                  $this->end()
 589              ) {
 590                  $this->append([Type::T_ERROR, $value], $s);
 591  
 592                  return true;
 593              }
 594  
 595              $this->seek($s);
 596  
 597              #if ($this->literal('@content', 8))
 598  
 599              if ($this->literal('@content', 8) &&
 600                  ($this->end() ||
 601                      $this->matchChar('(') &&
 602                          $this->argValues($argContent) &&
 603                          $this->matchChar(')') &&
 604                      $this->end())) {
 605                  $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
 606  
 607                  return true;
 608              }
 609  
 610              $this->seek($s);
 611  
 612              $last = $this->last();
 613  
 614              if (isset($last) && $last[0] === Type::T_IF) {
 615                  list(, $if) = $last;
 616  
 617                  if ($this->literal('@else', 5)) {
 618                      if ($this->matchChar('{', false)) {
 619                          $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
 620                      } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
 621                          $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
 622                          $else->cond = $cond;
 623                      }
 624  
 625                      if (isset($else)) {
 626                          $else->dontAppend = true;
 627                          $if->cases[] = $else;
 628  
 629                          return true;
 630                      }
 631                  }
 632  
 633                  $this->seek($s);
 634              }
 635  
 636              // only retain the first @charset directive encountered
 637              if ($this->literal('@charset', 8) &&
 638                  $this->valueList($charset) &&
 639                  $this->end()
 640              ) {
 641                  if (! isset($this->charset)) {
 642                      $statement = [Type::T_CHARSET, $charset];
 643  
 644                      list($line, $column) = $this->getSourcePosition($s);
 645  
 646                      $statement[static::SOURCE_LINE]   = $line;
 647                      $statement[static::SOURCE_COLUMN] = $column;
 648                      $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
 649  
 650                      $this->charset = $statement;
 651                  }
 652  
 653                  return true;
 654              }
 655  
 656              $this->seek($s);
 657  
 658              if ($this->literal('@supports', 9) &&
 659                  ($t1=$this->supportsQuery($supportQuery)) &&
 660                  ($t2=$this->matchChar('{', false))
 661              ) {
 662                  $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
 663                  $directive->name  = 'supports';
 664                  $directive->value = $supportQuery;
 665  
 666                  return true;
 667              }
 668  
 669              $this->seek($s);
 670  
 671              // doesn't match built in directive, do generic one
 672              if ($this->matchChar('@', false) &&
 673                  $this->keyword($dirName) &&
 674                  ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
 675                  $this->matchChar('{', false)
 676              ) {
 677                  if ($dirName === 'media') {
 678                      $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
 679                  } else {
 680                      $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
 681                      $directive->name = $dirName;
 682                  }
 683  
 684                  if (isset($dirValue)) {
 685                      $directive->value = $dirValue;
 686                  }
 687  
 688                  return true;
 689              }
 690  
 691              $this->seek($s);
 692  
 693              // maybe it's a generic blockless directive
 694              if ($this->matchChar('@', false) &&
 695                  $this->keyword($dirName) &&
 696                  $this->valueList($dirValue) &&
 697                  $this->end()
 698              ) {
 699                  $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
 700  
 701                  return true;
 702              }
 703  
 704              $this->seek($s);
 705  
 706              return false;
 707          }
 708  
 709          // property shortcut
 710          // captures most properties before having to parse a selector
 711          if ($this->keyword($name, false) &&
 712              $this->literal(': ', 2) &&
 713              $this->valueList($value) &&
 714              $this->end()
 715          ) {
 716              $name = [Type::T_STRING, '', [$name]];
 717              $this->append([Type::T_ASSIGN, $name, $value], $s);
 718  
 719              return true;
 720          }
 721  
 722          $this->seek($s);
 723  
 724          // variable assigns
 725          if ($this->variable($name) &&
 726              $this->matchChar(':') &&
 727              $this->valueList($value) &&
 728              $this->end()
 729          ) {
 730              // check for '!flag'
 731              $assignmentFlags = $this->stripAssignmentFlags($value);
 732              $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
 733  
 734              return true;
 735          }
 736  
 737          $this->seek($s);
 738  
 739          // misc
 740          if ($this->literal('-->', 3)) {
 741              return true;
 742          }
 743  
 744          // opening css block
 745          if ($this->selectors($selectors) && $this->matchChar('{', false)) {
 746              $this->pushBlock($selectors, $s);
 747  
 748              if ($this->eatWhiteDefault) {
 749                  $this->whitespace();
 750                  $this->append(null); // collect comments at the beginning if needed
 751              }
 752  
 753              return true;
 754          }
 755  
 756          $this->seek($s);
 757  
 758          // property assign, or nested assign
 759          if ($this->propertyName($name) && $this->matchChar(':')) {
 760              $foundSomething = false;
 761  
 762              if ($this->valueList($value)) {
 763                  if (empty($this->env->parent)) {
 764                      $this->throwParseError('expected "{"');
 765                  }
 766  
 767                  $this->append([Type::T_ASSIGN, $name, $value], $s);
 768                  $foundSomething = true;
 769              }
 770  
 771              if ($this->matchChar('{', false)) {
 772                  $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
 773                  $propBlock->prefix = $name;
 774                  $propBlock->hasValue = $foundSomething;
 775  
 776                  $foundSomething = true;
 777              } elseif ($foundSomething) {
 778                  $foundSomething = $this->end();
 779              }
 780  
 781              if ($foundSomething) {
 782                  return true;
 783              }
 784          }
 785  
 786          $this->seek($s);
 787  
 788          // closing a block
 789          if ($this->matchChar('}', false)) {
 790              $block = $this->popBlock();
 791  
 792              if (! isset($block->type) || $block->type !== Type::T_IF) {
 793                  if ($this->env->parent) {
 794                      $this->append(null); // collect comments before next statement if needed
 795                  }
 796              }
 797  
 798              if (isset($block->type) && $block->type === Type::T_INCLUDE) {
 799                  $include = $block->child;
 800                  unset($block->child);
 801                  $include[3] = $block;
 802                  $this->append($include, $s);
 803              } elseif (empty($block->dontAppend)) {
 804                  $type = isset($block->type) ? $block->type : Type::T_BLOCK;
 805                  $this->append([$type, $block], $s);
 806              }
 807  
 808              // collect comments just after the block closing if needed
 809              if ($this->eatWhiteDefault) {
 810                  $this->whitespace();
 811  
 812                  if ($this->env->comments) {
 813                      $this->append(null);
 814                  }
 815              }
 816  
 817              return true;
 818          }
 819  
 820          // extra stuff
 821          if ($this->matchChar(';') ||
 822              $this->literal('<!--', 4)
 823          ) {
 824              return true;
 825          }
 826  
 827          return false;
 828      }
 829  
 830      /**
 831       * Push block onto parse tree
 832       *
 833       * @param array   $selectors
 834       * @param integer $pos
 835       *
 836       * @return \ScssPhp\ScssPhp\Block
 837       */
 838      protected function pushBlock($selectors, $pos = 0)
 839      {
 840          list($line, $column) = $this->getSourcePosition($pos);
 841  
 842          $b = new Block;
 843          $b->sourceName   = $this->sourceName;
 844          $b->sourceLine   = $line;
 845          $b->sourceColumn = $column;
 846          $b->sourceIndex  = $this->sourceIndex;
 847          $b->selectors    = $selectors;
 848          $b->comments     = [];
 849          $b->parent       = $this->env;
 850  
 851          if (! $this->env) {
 852              $b->children = [];
 853          } elseif (empty($this->env->children)) {
 854              $this->env->children = $this->env->comments;
 855              $b->children = [];
 856              $this->env->comments = [];
 857          } else {
 858              $b->children = $this->env->comments;
 859              $this->env->comments = [];
 860          }
 861  
 862          $this->env = $b;
 863  
 864          // collect comments at the beginning of a block if needed
 865          if ($this->eatWhiteDefault) {
 866              $this->whitespace();
 867  
 868              if ($this->env->comments) {
 869                  $this->append(null);
 870              }
 871          }
 872  
 873          return $b;
 874      }
 875  
 876      /**
 877       * Push special (named) block onto parse tree
 878       *
 879       * @param string  $type
 880       * @param integer $pos
 881       *
 882       * @return \ScssPhp\ScssPhp\Block
 883       */
 884      protected function pushSpecialBlock($type, $pos)
 885      {
 886          $block = $this->pushBlock(null, $pos);
 887          $block->type = $type;
 888  
 889          return $block;
 890      }
 891  
 892      /**
 893       * Pop scope and return last block
 894       *
 895       * @return \ScssPhp\ScssPhp\Block
 896       *
 897       * @throws \Exception
 898       */
 899      protected function popBlock()
 900      {
 901  
 902          // collect comments ending just before of a block closing
 903          if ($this->env->comments) {
 904              $this->append(null);
 905          }
 906  
 907          // pop the block
 908          $block = $this->env;
 909  
 910          if (empty($block->parent)) {
 911              $this->throwParseError('unexpected }');
 912          }
 913  
 914          if ($block->type == Type::T_AT_ROOT) {
 915              // keeps the parent in case of self selector &
 916              $block->selfParent = $block->parent;
 917          }
 918  
 919          $this->env = $block->parent;
 920  
 921          unset($block->parent);
 922  
 923          return $block;
 924      }
 925  
 926      /**
 927       * Peek input stream
 928       *
 929       * @param string  $regex
 930       * @param array   $out
 931       * @param integer $from
 932       *
 933       * @return integer
 934       */
 935      protected function peek($regex, &$out, $from = null)
 936      {
 937          if (! isset($from)) {
 938              $from = $this->count;
 939          }
 940  
 941          $r = '/' . $regex . '/' . $this->patternModifiers;
 942          $result = preg_match($r, $this->buffer, $out, null, $from);
 943  
 944          return $result;
 945      }
 946  
 947      /**
 948       * Seek to position in input stream (or return current position in input stream)
 949       *
 950       * @param integer $where
 951       */
 952      protected function seek($where)
 953      {
 954          $this->count = $where;
 955      }
 956  
 957      /**
 958       * Match string looking for either ending delim, escape, or string interpolation
 959       *
 960       * {@internal This is a workaround for preg_match's 250K string match limit. }}
 961       *
 962       * @param array  $m     Matches (passed by reference)
 963       * @param string $delim Delimeter
 964       *
 965       * @return boolean True if match; false otherwise
 966       */
 967      protected function matchString(&$m, $delim)
 968      {
 969          $token = null;
 970  
 971          $end = strlen($this->buffer);
 972  
 973          // look for either ending delim, escape, or string interpolation
 974          foreach (['#{', '\\', $delim] as $lookahead) {
 975              $pos = strpos($this->buffer, $lookahead, $this->count);
 976  
 977              if ($pos !== false && $pos < $end) {
 978                  $end = $pos;
 979                  $token = $lookahead;
 980              }
 981          }
 982  
 983          if (! isset($token)) {
 984              return false;
 985          }
 986  
 987          $match = substr($this->buffer, $this->count, $end - $this->count);
 988          $m = [
 989              $match . $token,
 990              $match,
 991              $token
 992          ];
 993          $this->count = $end + strlen($token);
 994  
 995          return true;
 996      }
 997  
 998      /**
 999       * Try to match something on head of buffer
1000       *
1001       * @param string  $regex
1002       * @param array   $out
1003       * @param boolean $eatWhitespace
1004       *
1005       * @return boolean
1006       */
1007      protected function match($regex, &$out, $eatWhitespace = null)
1008      {
1009          $r = '/' . $regex . '/' . $this->patternModifiers;
1010  
1011          if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
1012              return false;
1013          }
1014  
1015          $this->count += strlen($out[0]);
1016  
1017          if (! isset($eatWhitespace)) {
1018              $eatWhitespace = $this->eatWhiteDefault;
1019          }
1020  
1021          if ($eatWhitespace) {
1022              $this->whitespace();
1023          }
1024  
1025          return true;
1026      }
1027  
1028      /**
1029       * Match a single string
1030       *
1031       * @param string  $char
1032       * @param boolean $eatWhitespace
1033       *
1034       * @return boolean
1035       */
1036      protected function matchChar($char, $eatWhitespace = null)
1037      {
1038          if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
1039              return false;
1040          }
1041  
1042          $this->count++;
1043  
1044          if (! isset($eatWhitespace)) {
1045              $eatWhitespace = $this->eatWhiteDefault;
1046          }
1047  
1048          if ($eatWhitespace) {
1049              $this->whitespace();
1050          }
1051  
1052          return true;
1053      }
1054  
1055      /**
1056       * Match literal string
1057       *
1058       * @param string  $what
1059       * @param integer $len
1060       * @param boolean $eatWhitespace
1061       *
1062       * @return boolean
1063       */
1064      protected function literal($what, $len, $eatWhitespace = null)
1065      {
1066          if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
1067              return false;
1068          }
1069  
1070          $this->count += $len;
1071  
1072          if (! isset($eatWhitespace)) {
1073              $eatWhitespace = $this->eatWhiteDefault;
1074          }
1075  
1076          if ($eatWhitespace) {
1077              $this->whitespace();
1078          }
1079  
1080          return true;
1081      }
1082  
1083      /**
1084       * Match some whitespace
1085       *
1086       * @return boolean
1087       */
1088      protected function whitespace()
1089      {
1090          $gotWhite = false;
1091  
1092          while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
1093              if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1094                  // comment that are kept in the output CSS
1095                  $comment = [];
1096                  $startCommentCount = $this->count;
1097                  $endCommentCount = $this->count + strlen($m[1]);
1098  
1099                  // find interpolations in comment
1100                  $p = strpos($this->buffer, '#{', $this->count);
1101  
1102                  while ($p !== false && $p < $endCommentCount) {
1103                      $c           = substr($this->buffer, $this->count, $p - $this->count);
1104                      $comment[]   = $c;
1105                      $this->count = $p;
1106                      $out         = null;
1107  
1108                      if ($this->interpolation($out)) {
1109                          // keep right spaces in the following string part
1110                          if ($out[3]) {
1111                              while ($this->buffer[$this->count-1] !== '}') {
1112                                  $this->count--;
1113                              }
1114  
1115                              $out[3] = '';
1116                          }
1117  
1118                          $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
1119                      } else {
1120                          $comment[] = substr($this->buffer, $this->count, 2);
1121  
1122                          $this->count += 2;
1123                      }
1124  
1125                      $p = strpos($this->buffer, '#{', $this->count);
1126                  }
1127  
1128                  // remaining part
1129                  $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
1130  
1131                  if (! $comment) {
1132                      // single part static comment
1133                      $this->appendComment([Type::T_COMMENT, $c]);
1134                  } else {
1135                      $comment[] = $c;
1136                      $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
1137                      $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
1138                  }
1139  
1140                  $this->commentsSeen[$startCommentCount] = true;
1141                  $this->count = $endCommentCount;
1142              } else {
1143                  // comment that are ignored and not kept in the output css
1144                  $this->count += strlen($m[0]);
1145              }
1146  
1147              $gotWhite = true;
1148          }
1149  
1150          return $gotWhite;
1151      }
1152  
1153      /**
1154       * Append comment to current block
1155       *
1156       * @param array $comment
1157       */
1158      protected function appendComment($comment)
1159      {
1160          if (! $this->discardComments) {
1161              if ($comment[0] === Type::T_COMMENT) {
1162                  if (is_string($comment[1])) {
1163                      $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
1164                  }
1165                  if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
1166                      foreach ($comment[2][2] as $k => $v) {
1167                          if (is_string($v)) {
1168                              $p = strpos($v, "\n");
1169                              if ($p !== false) {
1170                                  $comment[2][2][$k] = substr($v, 0, $p + 1)
1171                                      . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
1172                              }
1173                          }
1174                      }
1175                  }
1176              }
1177  
1178              $this->env->comments[] = $comment;
1179          }
1180      }
1181  
1182      /**
1183       * Append statement to current block
1184       *
1185       * @param array   $statement
1186       * @param integer $pos
1187       */
1188      protected function append($statement, $pos = null)
1189      {
1190          if (! is_null($statement)) {
1191              if (! is_null($pos)) {
1192                  list($line, $column) = $this->getSourcePosition($pos);
1193  
1194                  $statement[static::SOURCE_LINE]   = $line;
1195                  $statement[static::SOURCE_COLUMN] = $column;
1196                  $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
1197              }
1198  
1199              $this->env->children[] = $statement;
1200          }
1201  
1202          $comments = $this->env->comments;
1203  
1204          if ($comments) {
1205              $this->env->children = array_merge($this->env->children, $comments);
1206              $this->env->comments = [];
1207          }
1208      }
1209  
1210      /**
1211       * Returns last child was appended
1212       *
1213       * @return array|null
1214       */
1215      protected function last()
1216      {
1217          $i = count($this->env->children) - 1;
1218  
1219          if (isset($this->env->children[$i])) {
1220              return $this->env->children[$i];
1221          }
1222      }
1223  
1224      /**
1225       * Parse media query list
1226       *
1227       * @param array $out
1228       *
1229       * @return boolean
1230       */
1231      protected function mediaQueryList(&$out)
1232      {
1233          return $this->genericList($out, 'mediaQuery', ',', false);
1234      }
1235  
1236      /**
1237       * Parse media query
1238       *
1239       * @param array $out
1240       *
1241       * @return boolean
1242       */
1243      protected function mediaQuery(&$out)
1244      {
1245          $expressions = null;
1246          $parts = [];
1247  
1248          if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
1249              $this->mixedKeyword($mediaType)
1250          ) {
1251              $prop = [Type::T_MEDIA_TYPE];
1252  
1253              if (isset($only)) {
1254                  $prop[] = [Type::T_KEYWORD, 'only'];
1255              }
1256  
1257              if (isset($not)) {
1258                  $prop[] = [Type::T_KEYWORD, 'not'];
1259              }
1260  
1261              $media = [Type::T_LIST, '', []];
1262  
1263              foreach ((array) $mediaType as $type) {
1264                  if (is_array($type)) {
1265                      $media[2][] = $type;
1266                  } else {
1267                      $media[2][] = [Type::T_KEYWORD, $type];
1268                  }
1269              }
1270  
1271              $prop[]  = $media;
1272              $parts[] = $prop;
1273          }
1274  
1275          if (empty($parts) || $this->literal('and', 3)) {
1276              $this->genericList($expressions, 'mediaExpression', 'and', false);
1277  
1278              if (is_array($expressions)) {
1279                  $parts = array_merge($parts, $expressions[2]);
1280              }
1281          }
1282  
1283          $out = $parts;
1284  
1285          return true;
1286      }
1287  
1288      /**
1289       * Parse supports query
1290       *
1291       * @param array $out
1292       *
1293       * @return boolean
1294       */
1295      protected function supportsQuery(&$out)
1296      {
1297          $expressions = null;
1298          $parts = [];
1299  
1300          $s = $this->count;
1301  
1302          $not = false;
1303  
1304          if (($this->literal('not', 3) && ($not = true) || true) &&
1305              $this->matchChar('(') &&
1306              ($this->expression($property)) &&
1307              $this->literal(': ', 2) &&
1308              $this->valueList($value) &&
1309              $this->matchChar(')')
1310           ) {
1311              $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1312              $support[2][] = $property;
1313              $support[2][] = [Type::T_KEYWORD, ': '];
1314              $support[2][] = $value;
1315              $support[2][] = [Type::T_KEYWORD, ')'];
1316  
1317              $parts[] = $support;
1318              $s = $this->count;
1319          } else {
1320              $this->seek($s);
1321          }
1322  
1323          if ($this->matchChar('(') &&
1324              $this->supportsQuery($subQuery) &&
1325              $this->matchChar(')')
1326          ) {
1327              $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1328              $s = $this->count;
1329          } else {
1330              $this->seek($s);
1331          }
1332  
1333          if ($this->literal('not', 3) &&
1334              $this->supportsQuery($subQuery)
1335          ) {
1336              $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1337              $s = $this->count;
1338          } else {
1339              $this->seek($s);
1340          }
1341  
1342          if ($this->literal('selector(', 9) &&
1343              $this->selector($selector) &&
1344              $this->matchChar(')')
1345          ) {
1346              $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1347  
1348              $selectorList = [Type::T_LIST, '', []];
1349  
1350              foreach ($selector as $sc) {
1351                  $compound = [Type::T_STRING, '', []];
1352  
1353                  foreach ($sc as $scp) {
1354                      if (is_array($scp)) {
1355                          $compound[2][] = $scp;
1356                      } else {
1357                          $compound[2][] = [Type::T_KEYWORD, $scp];
1358                      }
1359                  }
1360  
1361                  $selectorList[2][] = $compound;
1362              }
1363              $support[2][] = $selectorList;
1364              $support[2][] = [Type::T_KEYWORD, ')'];
1365              $parts[] = $support;
1366              $s = $this->count;
1367          } else {
1368              $this->seek($s);
1369          }
1370  
1371          if ($this->variable($var) or $this->interpolation($var)) {
1372              $parts[] = $var;
1373              $s = $this->count;
1374          } else {
1375              $this->seek($s);
1376          }
1377  
1378          if ($this->literal('and', 3) &&
1379              $this->genericList($expressions, 'supportsQuery', ' and', false)) {
1380              array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1381  
1382              $parts = [$expressions];
1383              $s = $this->count;
1384          } else {
1385              $this->seek($s);
1386          }
1387  
1388          if ($this->literal('or', 2) &&
1389              $this->genericList($expressions, 'supportsQuery', ' or', false)) {
1390              array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1391  
1392              $parts = [$expressions];
1393              $s = $this->count;
1394          } else {
1395              $this->seek($s);
1396          }
1397  
1398          if (count($parts)) {
1399              if ($this->eatWhiteDefault) {
1400                  $this->whitespace();
1401              }
1402  
1403              $out = [Type::T_STRING, '', $parts];
1404  
1405              return true;
1406          }
1407  
1408          return false;
1409      }
1410  
1411  
1412      /**
1413       * Parse media expression
1414       *
1415       * @param array $out
1416       *
1417       * @return boolean
1418       */
1419      protected function mediaExpression(&$out)
1420      {
1421          $s = $this->count;
1422          $value = null;
1423  
1424          if ($this->matchChar('(') &&
1425              $this->expression($feature) &&
1426              ($this->matchChar(':') && $this->expression($value) || true) &&
1427              $this->matchChar(')')
1428          ) {
1429              $out = [Type::T_MEDIA_EXPRESSION, $feature];
1430  
1431              if ($value) {
1432                  $out[] = $value;
1433              }
1434  
1435              return true;
1436          }
1437  
1438          $this->seek($s);
1439  
1440          return false;
1441      }
1442  
1443      /**
1444       * Parse argument values
1445       *
1446       * @param array $out
1447       *
1448       * @return boolean
1449       */
1450      protected function argValues(&$out)
1451      {
1452          if ($this->genericList($list, 'argValue', ',', false)) {
1453              $out = $list[2];
1454  
1455              return true;
1456          }
1457  
1458          return false;
1459      }
1460  
1461      /**
1462       * Parse argument value
1463       *
1464       * @param array $out
1465       *
1466       * @return boolean
1467       */
1468      protected function argValue(&$out)
1469      {
1470          $s = $this->count;
1471  
1472          $keyword = null;
1473  
1474          if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1475              $this->seek($s);
1476  
1477              $keyword = null;
1478          }
1479  
1480          if ($this->genericList($value, 'expression')) {
1481              $out = [$keyword, $value, false];
1482              $s = $this->count;
1483  
1484              if ($this->literal('...', 3)) {
1485                  $out[2] = true;
1486              } else {
1487                  $this->seek($s);
1488              }
1489  
1490              return true;
1491          }
1492  
1493          return false;
1494      }
1495  
1496      /**
1497       * Parse comma separated value list
1498       *
1499       * @param array $out
1500       *
1501       * @return boolean
1502       */
1503      protected function valueList(&$out)
1504      {
1505          $discardComments = $this->discardComments;
1506          $this->discardComments = true;
1507          $res = $this->genericList($out, 'spaceList', ',');
1508          $this->discardComments = $discardComments;
1509  
1510          return $res;
1511      }
1512  
1513      /**
1514       * Parse space separated value list
1515       *
1516       * @param array $out
1517       *
1518       * @return boolean
1519       */
1520      protected function spaceList(&$out)
1521      {
1522          return $this->genericList($out, 'expression');
1523      }
1524  
1525      /**
1526       * Parse generic list
1527       *
1528       * @param array    $out
1529       * @param callable $parseItem
1530       * @param string   $delim
1531       * @param boolean  $flatten
1532       *
1533       * @return boolean
1534       */
1535      protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1536      {
1537          $s     = $this->count;
1538          $items = [];
1539          $value = null;
1540  
1541          while ($this->$parseItem($value)) {
1542              $trailing_delim = false;
1543              $items[] = $value;
1544  
1545              if ($delim) {
1546                  if (! $this->literal($delim, strlen($delim))) {
1547                      break;
1548                  }
1549                  $trailing_delim = true;
1550              }
1551          }
1552  
1553          if (! $items) {
1554              $this->seek($s);
1555  
1556              return false;
1557          }
1558  
1559          if ($trailing_delim) {
1560              $items[] = [Type::T_NULL];
1561          }
1562          if ($flatten && count($items) === 1) {
1563              $out = $items[0];
1564          } else {
1565              $out = [Type::T_LIST, $delim, $items];
1566          }
1567  
1568          return true;
1569      }
1570  
1571      /**
1572       * Parse expression
1573       *
1574       * @param array $out
1575       * @param bool $listOnly
1576       * @param bool $lookForExp
1577       *
1578       * @return boolean
1579       */
1580      protected function expression(&$out, $listOnly = false, $lookForExp = true)
1581      {
1582          $s = $this->count;
1583          $discard = $this->discardComments;
1584          $this->discardComments = true;
1585          $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
1586  
1587          if ($this->matchChar('(')) {
1588              if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
1589                  if ($lookForExp) {
1590                      $out = $this->expHelper($lhs, 0);
1591                  } else {
1592                      $out = $lhs;
1593                  }
1594  
1595                  $this->discardComments = $discard;
1596  
1597                  return true;
1598              }
1599  
1600              $this->seek($s);
1601          }
1602  
1603          if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
1604              if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
1605                  if ($lookForExp) {
1606                      $out = $this->expHelper($lhs, 0);
1607                  } else {
1608                      $out = $lhs;
1609                  }
1610                  $this->discardComments = $discard;
1611  
1612                  return true;
1613              }
1614  
1615              $this->seek($s);
1616          }
1617  
1618          if (!$listOnly && $this->value($lhs)) {
1619              if ($lookForExp) {
1620                  $out = $this->expHelper($lhs, 0);
1621              } else {
1622                  $out = $lhs;
1623              }
1624  
1625              $this->discardComments = $discard;
1626  
1627              return true;
1628          }
1629  
1630          $this->discardComments = $discard;
1631          return false;
1632      }
1633  
1634      /**
1635       * Parse expression specifically checking for lists in parenthesis or brackets
1636       *
1637       * @param array   $out
1638       * @param integer $s
1639       * @param string  $closingParen
1640       * @param array   $allowedTypes
1641       *
1642       * @return boolean
1643       */
1644      protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
1645      {
1646          if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) {
1647              $out = [Type::T_LIST, '', []];
1648              switch ($closingParen) {
1649                  case ")":
1650                      $out['enclosing'] = 'parent'; // parenthesis list
1651                      break;
1652                  case "]":
1653                      $out['enclosing'] = 'bracket'; // bracketed list
1654                      break;
1655              }
1656              return true;
1657          }
1658  
1659          if ($this->valueList($out) && $this->matchChar($closingParen)
1660              && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD])
1661              && in_array(Type::T_LIST, $allowedTypes)) {
1662              if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
1663                  $out = [Type::T_LIST, '', [$out]];
1664              }
1665              switch ($closingParen) {
1666                  case ")":
1667                      $out['enclosing'] = 'parent'; // parenthesis list
1668                      break;
1669                  case "]":
1670                      $out['enclosing'] = 'bracket'; // bracketed list
1671                      break;
1672              }
1673              return true;
1674          }
1675  
1676          $this->seek($s);
1677  
1678          if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
1679              return true;
1680          }
1681  
1682          return false;
1683      }
1684  
1685      /**
1686       * Parse left-hand side of subexpression
1687       *
1688       * @param array   $lhs
1689       * @param integer $minP
1690       *
1691       * @return array
1692       */
1693      protected function expHelper($lhs, $minP)
1694      {
1695          $operators = static::$operatorPattern;
1696  
1697          $ss = $this->count;
1698          $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1699              ctype_space($this->buffer[$this->count - 1]);
1700  
1701          while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
1702              $whiteAfter = isset($this->buffer[$this->count]) &&
1703                  ctype_space($this->buffer[$this->count]);
1704              $varAfter = isset($this->buffer[$this->count]) &&
1705                  $this->buffer[$this->count] === '$';
1706  
1707              $this->whitespace();
1708  
1709              $op = $m[1];
1710  
1711              // don't turn negative numbers into expressions
1712              if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1713                  break;
1714              }
1715  
1716              if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
1717                  break;
1718              }
1719  
1720              // peek and see if rhs belongs to next operator
1721              if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
1722                  $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
1723              }
1724  
1725              $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1726              $ss = $this->count;
1727              $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1728                  ctype_space($this->buffer[$this->count - 1]);
1729          }
1730  
1731          $this->seek($ss);
1732  
1733          return $lhs;
1734      }
1735  
1736      /**
1737       * Parse value
1738       *
1739       * @param array $out
1740       *
1741       * @return boolean
1742       */
1743      protected function value(&$out)
1744      {
1745          if (! isset($this->buffer[$this->count])) {
1746              return false;
1747          }
1748  
1749          $s = $this->count;
1750          $char = $this->buffer[$this->count];
1751  
1752          if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
1753              $len = strspn(
1754                  $this->buffer,
1755                  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
1756                  $this->count
1757              );
1758  
1759              $this->count += $len;
1760  
1761              if ($this->matchChar(')')) {
1762                  $content = substr($this->buffer, $s, $this->count - $s);
1763                  $out = [Type::T_KEYWORD, $content];
1764  
1765                  return true;
1766              }
1767          }
1768  
1769          $this->seek($s);
1770  
1771          if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {
1772              $content = 'url(' . $m[1];
1773  
1774              if ($this->matchChar(')')) {
1775                  $content .= ')';
1776                  $out = [Type::T_KEYWORD, $content];
1777  
1778                  return true;
1779              }
1780          }
1781  
1782          $this->seek($s);
1783  
1784          // not
1785          if ($char === 'n' && $this->literal('not', 3, false)) {
1786              if ($this->whitespace() && $this->value($inner)) {
1787                  $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1788  
1789                  return true;
1790              }
1791  
1792              $this->seek($s);
1793  
1794              if ($this->parenValue($inner)) {
1795                  $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1796  
1797                  return true;
1798              }
1799  
1800              $this->seek($s);
1801          }
1802  
1803          // addition
1804          if ($char === '+') {
1805              $this->count++;
1806  
1807              if ($this->value($inner)) {
1808                  $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1809  
1810                  return true;
1811              }
1812  
1813              $this->count--;
1814  
1815              return false;
1816          }
1817  
1818          // negation
1819          if ($char === '-') {
1820              $this->count++;
1821  
1822              if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
1823                  $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1824  
1825                  return true;
1826              }
1827  
1828              $this->count--;
1829          }
1830  
1831          // paren
1832          if ($char === '(' && $this->parenValue($out)) {
1833              return true;
1834          }
1835  
1836          if ($char === '#') {
1837              if ($this->interpolation($out) || $this->color($out)) {
1838                  return true;
1839              }
1840          }
1841  
1842          if ($this->matchChar('&', true)) {
1843              $out = [Type::T_SELF];
1844  
1845              return true;
1846          }
1847  
1848          if ($char === '$' && $this->variable($out)) {
1849              return true;
1850          }
1851  
1852          if ($char === 'p' && $this->progid($out)) {
1853              return true;
1854          }
1855  
1856          if (($char === '"' || $char === "'") && $this->string($out)) {
1857              return true;
1858          }
1859  
1860          if ($this->unit($out)) {
1861              return true;
1862          }
1863  
1864          // unicode range with wildcards
1865          if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
1866              $out = [Type::T_KEYWORD, 'U+' . $m[0]];
1867  
1868              return true;
1869          }
1870  
1871          if ($this->keyword($keyword, false)) {
1872              if ($this->func($keyword, $out)) {
1873                  return true;
1874              }
1875  
1876              $this->whitespace();
1877  
1878              if ($keyword === 'null') {
1879                  $out = [Type::T_NULL];
1880              } else {
1881                  $out = [Type::T_KEYWORD, $keyword];
1882              }
1883  
1884              return true;
1885          }
1886  
1887          return false;
1888      }
1889  
1890      /**
1891       * Parse parenthesized value
1892       *
1893       * @param array $out
1894       *
1895       * @return boolean
1896       */
1897      protected function parenValue(&$out)
1898      {
1899          $s = $this->count;
1900  
1901          $inParens = $this->inParens;
1902  
1903          if ($this->matchChar('(')) {
1904              if ($this->matchChar(')')) {
1905                  $out = [Type::T_LIST, '', []];
1906  
1907                  return true;
1908              }
1909  
1910              $this->inParens = true;
1911  
1912              if ($this->expression($exp) && $this->matchChar(')')) {
1913                  $out = $exp;
1914                  $this->inParens = $inParens;
1915  
1916                  return true;
1917              }
1918          }
1919  
1920          $this->inParens = $inParens;
1921          $this->seek($s);
1922  
1923          return false;
1924      }
1925  
1926      /**
1927       * Parse "progid:"
1928       *
1929       * @param array $out
1930       *
1931       * @return boolean
1932       */
1933      protected function progid(&$out)
1934      {
1935          $s = $this->count;
1936  
1937          if ($this->literal('progid:', 7, false) &&
1938              $this->openString('(', $fn) &&
1939              $this->matchChar('(')
1940          ) {
1941              $this->openString(')', $args, '(');
1942  
1943              if ($this->matchChar(')')) {
1944                  $out = [Type::T_STRING, '', [
1945                      'progid:', $fn, '(', $args, ')'
1946                  ]];
1947  
1948                  return true;
1949              }
1950          }
1951  
1952          $this->seek($s);
1953  
1954          return false;
1955      }
1956  
1957      /**
1958       * Parse function call
1959       *
1960       * @param string $name
1961       * @param array  $func
1962       *
1963       * @return boolean
1964       */
1965      protected function func($name, &$func)
1966      {
1967          $s = $this->count;
1968  
1969          if ($this->matchChar('(')) {
1970              if ($name === 'alpha' && $this->argumentList($args)) {
1971                  $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1972  
1973                  return true;
1974              }
1975  
1976              if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1977                  $ss = $this->count;
1978  
1979                  if ($this->argValues($args) && $this->matchChar(')')) {
1980                      $func = [Type::T_FUNCTION_CALL, $name, $args];
1981  
1982                      return true;
1983                  }
1984  
1985                  $this->seek($ss);
1986              }
1987  
1988              if (($this->openString(')', $str, '(') || true) &&
1989                  $this->matchChar(')')
1990              ) {
1991                  $args = [];
1992  
1993                  if (! empty($str)) {
1994                      $args[] = [null, [Type::T_STRING, '', [$str]]];
1995                  }
1996  
1997                  $func = [Type::T_FUNCTION_CALL, $name, $args];
1998  
1999                  return true;
2000              }
2001          }
2002  
2003          $this->seek($s);
2004  
2005          return false;
2006      }
2007  
2008      /**
2009       * Parse function call argument list
2010       *
2011       * @param array $out
2012       *
2013       * @return boolean
2014       */
2015      protected function argumentList(&$out)
2016      {
2017          $s = $this->count;
2018          $this->matchChar('(');
2019  
2020          $args = [];
2021  
2022          while ($this->keyword($var)) {
2023              if ($this->matchChar('=') && $this->expression($exp)) {
2024                  $args[] = [Type::T_STRING, '', [$var . '=']];
2025                  $arg = $exp;
2026              } else {
2027                  break;
2028              }
2029  
2030              $args[] = $arg;
2031  
2032              if (! $this->matchChar(',')) {
2033                  break;
2034              }
2035  
2036              $args[] = [Type::T_STRING, '', [', ']];
2037          }
2038  
2039          if (! $this->matchChar(')') || ! $args) {
2040              $this->seek($s);
2041  
2042              return false;
2043          }
2044  
2045          $out = $args;
2046  
2047          return true;
2048      }
2049  
2050      /**
2051       * Parse mixin/function definition  argument list
2052       *
2053       * @param array $out
2054       *
2055       * @return boolean
2056       */
2057      protected function argumentDef(&$out)
2058      {
2059          $s = $this->count;
2060          $this->matchChar('(');
2061  
2062          $args = [];
2063  
2064          while ($this->variable($var)) {
2065              $arg = [$var[1], null, false];
2066  
2067              $ss = $this->count;
2068  
2069              if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
2070                  $arg[1] = $defaultVal;
2071              } else {
2072                  $this->seek($ss);
2073              }
2074  
2075              $ss = $this->count;
2076  
2077              if ($this->literal('...', 3)) {
2078                  $sss = $this->count;
2079  
2080                  if (! $this->matchChar(')')) {
2081                      $this->throwParseError('... has to be after the final argument');
2082                  }
2083  
2084                  $arg[2] = true;
2085                  $this->seek($sss);
2086              } else {
2087                  $this->seek($ss);
2088              }
2089  
2090              $args[] = $arg;
2091  
2092              if (! $this->matchChar(',')) {
2093                  break;
2094              }
2095          }
2096  
2097          if (! $this->matchChar(')')) {
2098              $this->seek($s);
2099  
2100              return false;
2101          }
2102  
2103          $out = $args;
2104  
2105          return true;
2106      }
2107  
2108      /**
2109       * Parse map
2110       *
2111       * @param array $out
2112       *
2113       * @return boolean
2114       */
2115      protected function map(&$out)
2116      {
2117          $s = $this->count;
2118  
2119          if (! $this->matchChar('(')) {
2120              return false;
2121          }
2122  
2123          $keys = [];
2124          $values = [];
2125  
2126          while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
2127              $this->genericList($value, 'expression')
2128          ) {
2129              $keys[] = $key;
2130              $values[] = $value;
2131  
2132              if (! $this->matchChar(',')) {
2133                  break;
2134              }
2135          }
2136  
2137          if (! $keys || ! $this->matchChar(')')) {
2138              $this->seek($s);
2139  
2140              return false;
2141          }
2142  
2143          $out = [Type::T_MAP, $keys, $values];
2144  
2145          return true;
2146      }
2147  
2148      /**
2149       * Parse color
2150       *
2151       * @param array $out
2152       *
2153       * @return boolean
2154       */
2155      protected function color(&$out)
2156      {
2157          $s = $this->count;
2158  
2159          if ($this->match('(#([0-9a-f]+))', $m)) {
2160              if (in_array(strlen($m[2]), [3,4,6,8])) {
2161                  $out = [Type::T_KEYWORD, $m[0]];
2162                  return true;
2163              }
2164  
2165              $this->seek($s);
2166              return false;
2167          }
2168  
2169          return false;
2170      }
2171  
2172      /**
2173       * Parse number with unit
2174       *
2175       * @param array $unit
2176       *
2177       * @return boolean
2178       */
2179      protected function unit(&$unit)
2180      {
2181          $s = $this->count;
2182  
2183          if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2184              if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2185                  $this->whitespace();
2186  
2187                  $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
2188  
2189                  return true;
2190              }
2191  
2192              $this->seek($s);
2193          }
2194  
2195          return false;
2196      }
2197  
2198      /**
2199       * Parse string
2200       *
2201       * @param array $out
2202       *
2203       * @return boolean
2204       */
2205      protected function string(&$out)
2206      {
2207          $s = $this->count;
2208  
2209          if ($this->matchChar('"', false)) {
2210              $delim = '"';
2211          } elseif ($this->matchChar("'", false)) {
2212              $delim = "'";
2213          } else {
2214              return false;
2215          }
2216  
2217          $content = [];
2218          $oldWhite = $this->eatWhiteDefault;
2219          $this->eatWhiteDefault = false;
2220          $hasInterpolation = false;
2221  
2222          while ($this->matchString($m, $delim)) {
2223              if ($m[1] !== '') {
2224                  $content[] = $m[1];
2225              }
2226  
2227              if ($m[2] === '#{') {
2228                  $this->count -= strlen($m[2]);
2229  
2230                  if ($this->interpolation($inter, false)) {
2231                      $content[] = $inter;
2232                      $hasInterpolation = true;
2233                  } else {
2234                      $this->count += strlen($m[2]);
2235                      $content[] = '#{'; // ignore it
2236                  }
2237              } elseif ($m[2] === '\\') {
2238                  if ($this->matchChar('"', false)) {
2239                      $content[] = $m[2] . '"';
2240                  } elseif ($this->matchChar("'", false)) {
2241                      $content[] = $m[2] . "'";
2242                  } elseif ($this->literal("\\", 1, false)) {
2243                      $content[] = $m[2] . "\\";
2244                  } elseif ($this->literal("\r\n", 2, false) ||
2245                    $this->matchChar("\r", false) ||
2246                    $this->matchChar("\n", false) ||
2247                    $this->matchChar("\f", false)
2248                  ) {
2249                      // this is a continuation escaping, to be ignored
2250                  } else {
2251                      $content[] = $m[2];
2252                  }
2253              } else {
2254                  $this->count -= strlen($delim);
2255                  break; // delim
2256              }
2257          }
2258  
2259          $this->eatWhiteDefault = $oldWhite;
2260  
2261          if ($this->literal($delim, strlen($delim))) {
2262              if ($hasInterpolation) {
2263                  $delim = '"';
2264  
2265                  foreach ($content as &$string) {
2266                      if ($string === "\\\\") {
2267                          $string = "\\";
2268                      } elseif ($string === "\\'") {
2269                          $string = "'";
2270                      } elseif ($string === '\\"') {
2271                          $string = '"';
2272                      }
2273                  }
2274              }
2275  
2276              $out = [Type::T_STRING, $delim, $content];
2277  
2278              return true;
2279          }
2280  
2281          $this->seek($s);
2282  
2283          return false;
2284      }
2285  
2286      /**
2287       * Parse keyword or interpolation
2288       *
2289       * @param array   $out
2290       * @param boolean $restricted
2291       *
2292       * @return boolean
2293       */
2294      protected function mixedKeyword(&$out, $restricted = false)
2295      {
2296          $parts = [];
2297  
2298          $oldWhite = $this->eatWhiteDefault;
2299          $this->eatWhiteDefault = false;
2300  
2301          for (;;) {
2302              if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
2303                  $parts[] = $key;
2304                  continue;
2305              }
2306  
2307              if ($this->interpolation($inter)) {
2308                  $parts[] = $inter;
2309                  continue;
2310              }
2311  
2312              break;
2313          }
2314  
2315          $this->eatWhiteDefault = $oldWhite;
2316  
2317          if (! $parts) {
2318              return false;
2319          }
2320  
2321          if ($this->eatWhiteDefault) {
2322              $this->whitespace();
2323          }
2324  
2325          $out = $parts;
2326  
2327          return true;
2328      }
2329  
2330      /**
2331       * Parse an unbounded string stopped by $end
2332       *
2333       * @param string $end
2334       * @param array  $out
2335       * @param string $nestingOpen
2336       *
2337       * @return boolean
2338       */
2339      protected function openString($end, &$out, $nestingOpen = null)
2340      {
2341          $oldWhite = $this->eatWhiteDefault;
2342          $this->eatWhiteDefault = false;
2343  
2344          $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
2345  
2346          $nestingLevel = 0;
2347  
2348          $content = [];
2349  
2350          while ($this->match($patt, $m, false)) {
2351              if (isset($m[1]) && $m[1] !== '') {
2352                  $content[] = $m[1];
2353  
2354                  if ($nestingOpen) {
2355                      $nestingLevel += substr_count($m[1], $nestingOpen);
2356                  }
2357              }
2358  
2359              $tok = $m[2];
2360  
2361              $this->count-= strlen($tok);
2362  
2363              if ($tok === $end && ! $nestingLevel--) {
2364                  break;
2365              }
2366  
2367              if (($tok === "'" || $tok === '"') && $this->string($str)) {
2368                  $content[] = $str;
2369                  continue;
2370              }
2371  
2372              if ($tok === '#{' && $this->interpolation($inter)) {
2373                  $content[] = $inter;
2374                  continue;
2375              }
2376  
2377              $content[] = $tok;
2378              $this->count+= strlen($tok);
2379          }
2380  
2381          $this->eatWhiteDefault = $oldWhite;
2382  
2383          if (! $content) {
2384              return false;
2385          }
2386  
2387          // trim the end
2388          if (is_string(end($content))) {
2389              $content[count($content) - 1] = rtrim(end($content));
2390          }
2391  
2392          $out = [Type::T_STRING, '', $content];
2393  
2394          return true;
2395      }
2396  
2397      /**
2398       * Parser interpolation
2399       *
2400       * @param string|array $out
2401       * @param boolean      $lookWhite save information about whitespace before and after
2402       *
2403       * @return boolean
2404       */
2405      protected function interpolation(&$out, $lookWhite = true)
2406      {
2407          $oldWhite = $this->eatWhiteDefault;
2408          $this->eatWhiteDefault = true;
2409  
2410          $s = $this->count;
2411  
2412          if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
2413              if ($value === [Type::T_SELF]) {
2414                  $out = $value;
2415              } else {
2416                  if ($lookWhite) {
2417                      $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
2418                      $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
2419                  } else {
2420                      $left = $right = false;
2421                  }
2422  
2423                  $out = [Type::T_INTERPOLATE, $value, $left, $right];
2424              }
2425  
2426              $this->eatWhiteDefault = $oldWhite;
2427  
2428              if ($this->eatWhiteDefault) {
2429                  $this->whitespace();
2430              }
2431  
2432              return true;
2433          }
2434  
2435          $this->seek($s);
2436  
2437          $this->eatWhiteDefault = $oldWhite;
2438  
2439          return false;
2440      }
2441  
2442      /**
2443       * Parse property name (as an array of parts or a string)
2444       *
2445       * @param array $out
2446       *
2447       * @return boolean
2448       */
2449      protected function propertyName(&$out)
2450      {
2451          $parts = [];
2452  
2453          $oldWhite = $this->eatWhiteDefault;
2454          $this->eatWhiteDefault = false;
2455  
2456          for (;;) {
2457              if ($this->interpolation($inter)) {
2458                  $parts[] = $inter;
2459                  continue;
2460              }
2461  
2462              if ($this->keyword($text)) {
2463                  $parts[] = $text;
2464                  continue;
2465              }
2466  
2467              if (! $parts && $this->match('[:.#]', $m, false)) {
2468                  // css hacks
2469                  $parts[] = $m[0];
2470                  continue;
2471              }
2472  
2473              break;
2474          }
2475  
2476          $this->eatWhiteDefault = $oldWhite;
2477  
2478          if (! $parts) {
2479              return false;
2480          }
2481  
2482          // match comment hack
2483          if (preg_match(
2484              static::$whitePattern,
2485              $this->buffer,
2486              $m,
2487              null,
2488              $this->count
2489          )) {
2490              if (! empty($m[0])) {
2491                  $parts[] = $m[0];
2492                  $this->count += strlen($m[0]);
2493              }
2494          }
2495  
2496          $this->whitespace(); // get any extra whitespace
2497  
2498          $out = [Type::T_STRING, '', $parts];
2499  
2500          return true;
2501      }
2502  
2503      /**
2504       * Parse comma separated selector list
2505       *
2506       * @param array   $out
2507       * @param boolean $subSelector
2508       *
2509       * @return boolean
2510       */
2511      protected function selectors(&$out, $subSelector = false)
2512      {
2513          $s = $this->count;
2514          $selectors = [];
2515  
2516          while ($this->selector($sel, $subSelector)) {
2517              $selectors[] = $sel;
2518  
2519              if (! $this->matchChar(',', true)) {
2520                  break;
2521              }
2522  
2523              while ($this->matchChar(',', true)) {
2524                  ; // ignore extra
2525              }
2526          }
2527  
2528          if (! $selectors) {
2529              $this->seek($s);
2530  
2531              return false;
2532          }
2533  
2534          $out = $selectors;
2535  
2536          return true;
2537      }
2538  
2539      /**
2540       * Parse whitespace separated selector list
2541       *
2542       * @param array   $out
2543       * @param boolean $subSelector
2544       *
2545       * @return boolean
2546       */
2547      protected function selector(&$out, $subSelector = false)
2548      {
2549          $selector = [];
2550  
2551          for (;;) {
2552              $s = $this->count;
2553  
2554              if ($this->match('[>+~]+', $m, true)) {
2555                  if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
2556                      $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
2557                  ) {
2558                      $this->seek($s);
2559                  } else {
2560                      $selector[] = [$m[0]];
2561                      continue;
2562                  }
2563              }
2564  
2565              if ($this->selectorSingle($part, $subSelector)) {
2566                  $selector[] = $part;
2567                  $this->match('\s+', $m);
2568                  continue;
2569              }
2570  
2571              if ($this->match('\/[^\/]+\/', $m, true)) {
2572                  $selector[] = [$m[0]];
2573                  continue;
2574              }
2575  
2576              break;
2577          }
2578  
2579          if (! $selector) {
2580              return false;
2581          }
2582  
2583          $out = $selector;
2584  
2585          return true;
2586      }
2587  
2588      /**
2589       * Parse the parts that make up a selector
2590       *
2591       * {@internal
2592       *     div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2593       * }}
2594       *
2595       * @param array   $out
2596       * @param boolean $subSelector
2597       *
2598       * @return boolean
2599       */
2600      protected function selectorSingle(&$out, $subSelector = false)
2601      {
2602          $oldWhite = $this->eatWhiteDefault;
2603          $this->eatWhiteDefault = false;
2604  
2605          $parts = [];
2606  
2607          if ($this->matchChar('*', false)) {
2608              $parts[] = '*';
2609          }
2610  
2611          for (;;) {
2612              if (! isset($this->buffer[$this->count])) {
2613                  break;
2614              }
2615  
2616              $s = $this->count;
2617              $char = $this->buffer[$this->count];
2618  
2619              // see if we can stop early
2620              if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
2621                  break;
2622              }
2623  
2624              // parsing a sub selector in () stop with the closing )
2625              if ($subSelector && $char === ')') {
2626                  break;
2627              }
2628  
2629              //self
2630              switch ($char) {
2631                  case '&':
2632                      $parts[] = Compiler::$selfSelector;
2633                      $this->count++;
2634                      continue 2;
2635  
2636                  case '.':
2637                      $parts[] = '.';
2638                      $this->count++;
2639                      continue 2;
2640  
2641                  case '|':
2642                      $parts[] = '|';
2643                      $this->count++;
2644                      continue 2;
2645              }
2646  
2647              if ($char === '\\' && $this->match('\\\\\S', $m)) {
2648                  $parts[] = $m[0];
2649                  continue;
2650              }
2651  
2652              if ($char === '%') {
2653                  $this->count++;
2654  
2655                  if ($this->placeholder($placeholder)) {
2656                      $parts[] = '%';
2657                      $parts[] = $placeholder;
2658                      continue;
2659                  }
2660  
2661                  break;
2662              }
2663  
2664              if ($char === '#') {
2665                  if ($this->interpolation($inter)) {
2666                      $parts[] = $inter;
2667                      continue;
2668                  }
2669  
2670                  $parts[] = '#';
2671                  $this->count++;
2672                  continue;
2673              }
2674  
2675              // a pseudo selector
2676              if ($char === ':') {
2677                  if ($this->buffer[$this->count + 1] === ':') {
2678                      $this->count += 2;
2679                      $part = '::';
2680                  } else {
2681                      $this->count++;
2682                      $part = ':';
2683                  }
2684  
2685                  if ($this->mixedKeyword($nameParts, true)) {
2686                      $parts[] = $part;
2687  
2688                      foreach ($nameParts as $sub) {
2689                          $parts[] = $sub;
2690                      }
2691  
2692                      $ss = $this->count;
2693  
2694                      if ($nameParts === ['not'] || $nameParts === ['is'] ||
2695                          $nameParts === ['has'] || $nameParts === ['where'] ||
2696                          $nameParts === ['slotted'] ||
2697                          $nameParts === ['nth-child'] || $nameParts == ['nth-last-child'] ||
2698                          $nameParts === ['nth-of-type'] || $nameParts == ['nth-last-of-type']
2699                      ) {
2700                          if ($this->matchChar('(', true) &&
2701                            ($this->selectors($subs, reset($nameParts)) || true) &&
2702                            $this->matchChar(')')
2703                          ) {
2704                              $parts[] = '(';
2705  
2706                              while ($sub = array_shift($subs)) {
2707                                  while ($ps = array_shift($sub)) {
2708                                      foreach ($ps as &$p) {
2709                                          $parts[] = $p;
2710                                      }
2711  
2712                                      if (count($sub) && reset($sub)) {
2713                                          $parts[] = ' ';
2714                                      }
2715                                  }
2716  
2717                                  if (count($subs) && reset($subs)) {
2718                                      $parts[] = ', ';
2719                                  }
2720                              }
2721  
2722                              $parts[] = ')';
2723                          } else {
2724                              $this->seek($ss);
2725                          }
2726                      } else {
2727                          if ($this->matchChar('(') &&
2728                            ($this->openString(')', $str, '(') || true) &&
2729                            $this->matchChar(')')
2730                          ) {
2731                              $parts[] = '(';
2732  
2733                              if (! empty($str)) {
2734                                  $parts[] = $str;
2735                              }
2736  
2737                              $parts[] = ')';
2738                          } else {
2739                              $this->seek($ss);
2740                          }
2741                      }
2742  
2743                      continue;
2744                  }
2745              }
2746  
2747              $this->seek($s);
2748  
2749              // 2n+1
2750              if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
2751                  if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
2752                      $parts[] = $counter[0];
2753                      //$parts[] = str_replace(' ', '', $counter[0]);
2754                      continue;
2755                  }
2756              }
2757  
2758              $this->seek($s);
2759  
2760              // attribute selector
2761              if ($char === '[' &&
2762                  $this->matchChar('[') &&
2763                  ($this->openString(']', $str, '[') || true) &&
2764                  $this->matchChar(']')
2765              ) {
2766                  $parts[] = '[';
2767  
2768                  if (! empty($str)) {
2769                      $parts[] = $str;
2770                  }
2771  
2772                  $parts[] = ']';
2773                  continue;
2774              }
2775  
2776              $this->seek($s);
2777  
2778              // for keyframes
2779              if ($this->unit($unit)) {
2780                  $parts[] = $unit;
2781                  continue;
2782              }
2783  
2784              if ($this->restrictedKeyword($name)) {
2785                  $parts[] = $name;
2786                  continue;
2787              }
2788  
2789              break;
2790          }
2791  
2792          $this->eatWhiteDefault = $oldWhite;
2793  
2794          if (! $parts) {
2795              return false;
2796          }
2797  
2798          $out = $parts;
2799  
2800          return true;
2801      }
2802  
2803      /**
2804       * Parse a variable
2805       *
2806       * @param array $out
2807       *
2808       * @return boolean
2809       */
2810      protected function variable(&$out)
2811      {
2812          $s = $this->count;
2813  
2814          if ($this->matchChar('$', false) && $this->keyword($name)) {
2815              $out = [Type::T_VARIABLE, $name];
2816  
2817              return true;
2818          }
2819  
2820          $this->seek($s);
2821  
2822          return false;
2823      }
2824  
2825      /**
2826       * Parse a keyword
2827       *
2828       * @param string  $word
2829       * @param boolean $eatWhitespace
2830       *
2831       * @return boolean
2832       */
2833      protected function keyword(&$word, $eatWhitespace = null)
2834      {
2835          if ($this->match(
2836              $this->utf8
2837                  ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
2838                  : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2839              $m,
2840              $eatWhitespace
2841          )) {
2842              $word = $m[1];
2843  
2844              return true;
2845          }
2846  
2847          return false;
2848      }
2849  
2850      /**
2851       * Parse a keyword that should not start with a number
2852       *
2853       * @param string  $word
2854       * @param boolean $eatWhitespace
2855       *
2856       * @return boolean
2857       */
2858      protected function restrictedKeyword(&$word, $eatWhitespace = null)
2859      {
2860          $s = $this->count;
2861  
2862          if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
2863              return true;
2864          }
2865  
2866          $this->seek($s);
2867  
2868          return false;
2869      }
2870  
2871      /**
2872       * Parse a placeholder
2873       *
2874       * @param string|array $placeholder
2875       *
2876       * @return boolean
2877       */
2878      protected function placeholder(&$placeholder)
2879      {
2880          if ($this->match(
2881              $this->utf8
2882                  ? '([\pL\w\-_]+)'
2883                  : '([\w\-_]+)',
2884              $m
2885          )) {
2886              $placeholder = $m[1];
2887  
2888              return true;
2889          }
2890  
2891          if ($this->interpolation($placeholder)) {
2892              return true;
2893          }
2894  
2895          return false;
2896      }
2897  
2898      /**
2899       * Parse a url
2900       *
2901       * @param array $out
2902       *
2903       * @return boolean
2904       */
2905      protected function url(&$out)
2906      {
2907          if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2908              $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2909  
2910              return true;
2911          }
2912  
2913          return false;
2914      }
2915  
2916      /**
2917       * Consume an end of statement delimiter
2918       *
2919       * @return boolean
2920       */
2921      protected function end()
2922      {
2923          if ($this->matchChar(';')) {
2924              return true;
2925          }
2926  
2927          if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2928              // if there is end of file or a closing block next then we don't need a ;
2929              return true;
2930          }
2931  
2932          return false;
2933      }
2934  
2935      /**
2936       * Strip assignment flag from the list
2937       *
2938       * @param array $value
2939       *
2940       * @return array
2941       */
2942      protected function stripAssignmentFlags(&$value)
2943      {
2944          $flags = [];
2945  
2946          for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2947              $lastNode = &$token[2][$s - 1];
2948  
2949              while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2950                  array_pop($token[2]);
2951  
2952                  $node     = end($token[2]);
2953                  $token    = $this->flattenList($token);
2954                  $flags[]  = $lastNode[1];
2955                  $lastNode = $node;
2956              }
2957          }
2958  
2959          return $flags;
2960      }
2961  
2962      /**
2963       * Strip optional flag from selector list
2964       *
2965       * @param array $selectors
2966       *
2967       * @return string
2968       */
2969      protected function stripOptionalFlag(&$selectors)
2970      {
2971          $optional = false;
2972          $selector = end($selectors);
2973          $part     = end($selector);
2974  
2975          if ($part === ['!optional']) {
2976              array_pop($selectors[count($selectors) - 1]);
2977  
2978              $optional = true;
2979          }
2980  
2981          return $optional;
2982      }
2983  
2984      /**
2985       * Turn list of length 1 into value type
2986       *
2987       * @param array $value
2988       *
2989       * @return array
2990       */
2991      protected function flattenList($value)
2992      {
2993          if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2994              return $this->flattenList($value[2][0]);
2995          }
2996  
2997          return $value;
2998      }
2999  
3000      /**
3001       * @deprecated
3002       *
3003       * {@internal
3004       *     advance counter to next occurrence of $what
3005       *     $until - don't include $what in advance
3006       *     $allowNewline, if string, will be used as valid char set
3007       * }}
3008       */
3009      protected function to($what, &$out, $until = false, $allowNewline = false)
3010      {
3011          if (is_string($allowNewline)) {
3012              $validChars = $allowNewline;
3013          } else {
3014              $validChars = $allowNewline ? '.' : "[^\n]";
3015          }
3016  
3017          $m = null;
3018  
3019          if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
3020              return false;
3021          }
3022  
3023          if ($until) {
3024              $this->count -= strlen($what); // give back $what
3025          }
3026  
3027          $out = $m[1];
3028  
3029          return true;
3030      }
3031  
3032      /**
3033       * @deprecated
3034       */
3035      protected function show()
3036      {
3037          if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
3038              return $m[1];
3039          }
3040  
3041          return '';
3042      }
3043  
3044      /**
3045       * Quote regular expression
3046       *
3047       * @param string $what
3048       *
3049       * @return string
3050       */
3051      private function pregQuote($what)
3052      {
3053          return preg_quote($what, '/');
3054      }
3055  
3056      /**
3057       * Extract line numbers from buffer
3058       *
3059       * @param string $buffer
3060       */
3061      private function extractLineNumbers($buffer)
3062      {
3063          $this->sourcePositions = [0 => 0];
3064          $prev = 0;
3065  
3066          while (($pos = strpos($buffer, "\n", $prev)) !== false) {
3067              $this->sourcePositions[] = $pos;
3068              $prev = $pos + 1;
3069          }
3070  
3071          $this->sourcePositions[] = strlen($buffer);
3072  
3073          if (substr($buffer, -1) !== "\n") {
3074              $this->sourcePositions[] = strlen($buffer) + 1;
3075          }
3076      }
3077  
3078      /**
3079       * Get source line number and column (given character position in the buffer)
3080       *
3081       * @param integer $pos
3082       *
3083       * @return array
3084       */
3085      private function getSourcePosition($pos)
3086      {
3087          $low = 0;
3088          $high = count($this->sourcePositions);
3089  
3090          while ($low < $high) {
3091              $mid = (int) (($high + $low) / 2);
3092  
3093              if ($pos < $this->sourcePositions[$mid]) {
3094                  $high = $mid - 1;
3095                  continue;
3096              }
3097  
3098              if ($pos >= $this->sourcePositions[$mid + 1]) {
3099                  $low = $mid + 1;
3100                  continue;
3101              }
3102  
3103              return [$mid + 1, $pos - $this->sourcePositions[$mid]];
3104          }
3105  
3106          return [$low + 1, $pos - $this->sourcePositions[$low]];
3107      }
3108  
3109      /**
3110       * Save internal encoding
3111       */
3112      private function saveEncoding()
3113      {
3114          if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
3115              return;
3116          }
3117  
3118          // deprecated in PHP 7.2
3119          $iniDirective = 'mbstring.func_overload';
3120  
3121          if (extension_loaded('mbstring') && ini_get($iniDirective) & 2) {
3122              $this->encoding = mb_internal_encoding();
3123  
3124              mb_internal_encoding('iso-8859-1');
3125          }
3126      }
3127  
3128      /**
3129       * Restore internal encoding
3130       */
3131      private function restoreEncoding()
3132      {
3133          if (extension_loaded('mbstring') && $this->encoding) {
3134              mb_internal_encoding($this->encoding);
3135          }
3136      }
3137  }