vendor/scssphp/scssphp/src/Parser.php line 2098

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