AutorÃa | Ultima modificación | Ver Log |
<?php/*** SCSSPHP** @copyright 2012-2020 Leaf Corcoran** @license http://opensource.org/licenses/MIT MIT** @link http://scssphp.github.io/scssphp*/namespace ScssPhp\ScssPhp;use ScssPhp\ScssPhp\Block\AtRootBlock;use ScssPhp\ScssPhp\Block\CallableBlock;use ScssPhp\ScssPhp\Block\ContentBlock;use ScssPhp\ScssPhp\Block\DirectiveBlock;use ScssPhp\ScssPhp\Block\EachBlock;use ScssPhp\ScssPhp\Block\ElseBlock;use ScssPhp\ScssPhp\Block\ElseifBlock;use ScssPhp\ScssPhp\Block\ForBlock;use ScssPhp\ScssPhp\Block\IfBlock;use ScssPhp\ScssPhp\Block\MediaBlock;use ScssPhp\ScssPhp\Block\NestedPropertyBlock;use ScssPhp\ScssPhp\Block\WhileBlock;use ScssPhp\ScssPhp\Exception\ParserException;use ScssPhp\ScssPhp\Logger\LoggerInterface;use ScssPhp\ScssPhp\Logger\QuietLogger;use ScssPhp\ScssPhp\Node\Number;/*** Parser** @author Leaf Corcoran <leafot@gmail.com>** @internal*/class Parser{const SOURCE_INDEX = -1;const SOURCE_LINE = -2;const SOURCE_COLUMN = -3;/*** @var array<string, int>*/protected static $precedence = ['=' => 0,'or' => 1,'and' => 2,'==' => 3,'!=' => 3,'<=' => 4,'>=' => 4,'<' => 4,'>' => 4,'+' => 5,'-' => 5,'*' => 6,'/' => 6,'%' => 6,];/*** @var string*/protected static $commentPattern;/*** @var string*/protected static $operatorPattern;/*** @var string*/protected static $whitePattern;/*** @var Cache|null*/protected $cache;private $sourceName;private $sourceIndex;/*** @var array<int, int>*/private $sourcePositions;/*** The current offset in the buffer** @var int*/private $count;/*** @var Block|null*/private $env;/*** @var bool*/private $inParens;/*** @var bool*/private $eatWhiteDefault;/*** @var bool*/private $discardComments;private $allowVars;/*** @var string*/private $buffer;private $utf8;/*** @var string|null*/private $encoding;private $patternModifiers;private $commentsSeen;private $cssOnly;/*** @var LoggerInterface*/private $logger;/*** Constructor** @api** @param string|null $sourceName* @param int $sourceIndex* @param string|null $encoding* @param Cache|null $cache* @param bool $cssOnly* @param LoggerInterface|null $logger*/public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null){$this->sourceName = $sourceName ?: '(stdin)';$this->sourceIndex = $sourceIndex;$this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8';$this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';$this->commentsSeen = [];$this->allowVars = true;$this->cssOnly = $cssOnly;$this->logger = $logger ?: new QuietLogger();if (empty(static::$operatorPattern)) {static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';$commentSingle = '\/\/';$commentMultiLeft = '\/\*';$commentMultiRight = '\*\/';static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;static::$whitePattern = $this->utf8? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS': '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';}$this->cache = $cache;}/*** Get source file name** @api** @return string*/public function getSourceName(){return $this->sourceName;}/*** Throw parser error** @api** @param string $msg** @phpstan-return never-return** @throws ParserException** @deprecated use "parseError" and throw the exception in the caller instead.*/public function throwParseError($msg = 'parse error'){@trigger_error('The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',E_USER_DEPRECATED);throw $this->parseError($msg);}/*** Creates a parser error** @api** @param string $msg** @return ParserException*/public function parseError($msg = 'parse error'){list($line, $column) = $this->getSourcePosition($this->count);$loc = empty($this->sourceName)? "line: $line, column: $column": "$this->sourceName on line $line, at column $column";if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {$this->restoreEncoding();$e = new ParserException("$msg: failed at `$m[1]` $loc");$e->setSourcePosition([$this->sourceName, $line, $column]);return $e;}$this->restoreEncoding();$e = new ParserException("$msg: $loc");$e->setSourcePosition([$this->sourceName, $line, $column]);return $e;}/*** Parser buffer** @api** @param string $buffer** @return Block*/public function parse($buffer){if ($this->cache) {$cacheKey = $this->sourceName . ':' . md5($buffer);$parseOptions = ['utf8' => $this->utf8,];$v = $this->cache->getCache('parse', $cacheKey, $parseOptions);if (! \is_null($v)) {return $v;}}// strip BOM (byte order marker)if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {$buffer = substr($buffer, 3);}$this->buffer = rtrim($buffer, "\x00..\x1f");$this->count = 0;$this->env = null;$this->inParens = false;$this->eatWhiteDefault = true;$this->saveEncoding();$this->extractLineNumbers($buffer);if ($this->utf8 && !preg_match('//u', $buffer)) {$message = $this->sourceName ? 'Invalid UTF-8 file: ' . $this->sourceName : 'Invalid UTF-8 file';throw new ParserException($message);}$this->pushBlock(null); // root block$this->whitespace();$this->pushBlock(null);$this->popBlock();while ($this->parseChunk()) {;}if ($this->count !== \strlen($this->buffer)) {throw $this->parseError();}if (! empty($this->env->parent)) {throw $this->parseError('unclosed block');}$this->restoreEncoding();assert($this->env !== null);if ($this->cache) {$this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);}return $this->env;}/*** Parse a value or value list** @api** @param string $buffer* @param string|array $out** @return bool*/public function parseValue($buffer, &$out){$this->count = 0;$this->env = null;$this->inParens = false;$this->eatWhiteDefault = true;$this->buffer = (string) $buffer;$this->saveEncoding();$this->extractLineNumbers($this->buffer);$list = $this->valueList($out);if ($this->count !== \strlen($this->buffer)) {$error = $this->parseError('Expected end of value');$message = 'Passing trailing content after the expression when parsing a value is deprecated since Scssphp 1.12.0 and will be an error in 2.0. ' . $error->getMessage();@trigger_error($message, E_USER_DEPRECATED);}$this->restoreEncoding();return $list;}/*** Parse a selector or selector list** @api** @param string $buffer* @param string|array $out* @param bool $shouldValidate** @return bool*/public function parseSelector($buffer, &$out, $shouldValidate = true){$this->count = 0;$this->env = null;$this->inParens = false;$this->eatWhiteDefault = true;$this->buffer = (string) $buffer;$this->saveEncoding();$this->extractLineNumbers($this->buffer);// discard space/comments at the start$this->discardComments = true;$this->whitespace();$this->discardComments = false;$selector = $this->selectors($out);$this->restoreEncoding();if ($shouldValidate && $this->count !== strlen($buffer)) {throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`");}return $selector;}/*** Parse a media Query** @api** @param string $buffer* @param array $out** @return bool*/public function parseMediaQueryList($buffer, &$out){$this->count = 0;$this->env = null;$this->inParens = false;$this->eatWhiteDefault = true;$this->buffer = (string) $buffer;$this->discardComments = true;$this->saveEncoding();$this->extractLineNumbers($this->buffer);$this->whitespace();$isMediaQuery = $this->mediaQueryList($out);$this->restoreEncoding();return $isMediaQuery;}/*** Parse a single chunk off the head of the buffer and append it to the* current parse environment.** Returns false when the buffer is empty, or when there is an error.** This function is called repeatedly until the entire document is* parsed.** This parser is most similar to a recursive descent parser. Single* functions represent discrete grammatical rules for the language, and* they are able to capture the text that represents those rules.** Consider the function Compiler::keyword(). (All parse functions are* structured the same.)** The function takes a single reference argument. When calling the* function it will attempt to match a keyword on the head of the buffer.* If it is successful, it will place the keyword in the referenced* argument, advance the position in the buffer, and return true. If it* fails then it won't advance the buffer and it will return false.** All of these parse functions are powered by Compiler::match(), which behaves* the same way, but takes a literal regular expression. Sometimes it is* more convenient to use match instead of creating a new function.** Because of the format of the functions, to parse an entire string of* grammatical rules, you can chain them together using &&.** But, if some of the rules in the chain succeed before one fails, then* the buffer position will be left at an invalid state. In order to* avoid this, Compiler::seek() is used to remember and set buffer positions.** Before parsing a chain, use $s = $this->count to remember the current* position into $s. Then if a chain fails, use $this->seek($s) to* go back where we started.** @return bool*/protected function parseChunk(){$s = $this->count;// the directivesif (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {if ($this->literal('@at-root', 8) &&($this->selectors($selector) || true) &&($this->map($with) || true) &&(($this->matchChar('(') &&$this->interpolation($with) &&$this->matchChar(')')) || true) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$atRoot = new AtRootBlock();$this->registerPushedBlock($atRoot, $s);$atRoot->selector = $selector;$atRoot->with = $with;return true;}$this->seek($s);if ($this->literal('@media', 6) &&$this->mediaQueryList($mediaQueryList) &&$this->matchChar('{', false)) {$media = new MediaBlock();$this->registerPushedBlock($media, $s);$media->queryList = $mediaQueryList[2];return true;}$this->seek($s);if ($this->literal('@mixin', 6) &&$this->keyword($mixinName) &&($this->argumentDef($args) || true) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$mixin = new CallableBlock(Type::T_MIXIN);$this->registerPushedBlock($mixin, $s);$mixin->name = $mixinName;$mixin->args = $args;return true;}$this->seek($s);if (($this->literal('@include', 8) &&$this->keyword($mixinName) &&($this->matchChar('(') &&($this->argValues($argValues) || true) &&$this->matchChar(')') || true) &&($this->end()) ||($this->literal('using', 5) &&$this->argumentDef($argUsing) &&($this->end() || $this->matchChar('{') && $hasBlock = true)) ||$this->matchChar('{') && $hasBlock = true)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$child = [Type::T_INCLUDE,$mixinName,isset($argValues) ? $argValues : null,null,isset($argUsing) ? $argUsing : null];if (! empty($hasBlock)) {$include = new ContentBlock();$this->registerPushedBlock($include, $s);$include->child = $child;} else {$this->append($child, $s);}return true;}$this->seek($s);if ($this->literal('@scssphp-import-once', 20) &&$this->valueList($importPath) &&$this->end()) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);list($line, $column) = $this->getSourcePosition($s);$file = $this->sourceName;$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);$this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);return true;}$this->seek($s);if ($this->literal('@import', 7) &&$this->valueList($importPath) &&$importPath[0] !== Type::T_FUNCTION_CALL &&$this->end()) {if ($this->cssOnly) {$this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);$this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);return true;}$this->append([Type::T_IMPORT, $importPath], $s);return true;}$this->seek($s);if ($this->literal('@import', 7) &&$this->url($importPath) &&$this->end()) {if ($this->cssOnly) {$this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);$this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);return true;}$this->append([Type::T_IMPORT, $importPath], $s);return true;}$this->seek($s);if ($this->literal('@extend', 7) &&$this->selectors($selectors) &&$this->end()) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);// check for '!flag'$optional = $this->stripOptionalFlag($selectors);$this->append([Type::T_EXTEND, $selectors, $optional], $s);return true;}$this->seek($s);if ($this->literal('@function', 9) &&$this->keyword($fnName) &&$this->argumentDef($args) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$func = new CallableBlock(Type::T_FUNCTION);$this->registerPushedBlock($func, $s);$func->name = $fnName;$func->args = $args;return true;}$this->seek($s);if ($this->literal('@return', 7) &&($this->valueList($retVal) || true) &&$this->end()) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);return true;}$this->seek($s);if ($this->literal('@each', 5) &&$this->genericList($varNames, 'variable', ',', false) &&$this->literal('in', 2) &&$this->valueList($list) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$each = new EachBlock();$this->registerPushedBlock($each, $s);foreach ($varNames[2] as $varName) {$each->vars[] = $varName[1];}$each->list = $list;return true;}$this->seek($s);if ($this->literal('@while', 6) &&$this->expression($cond) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);while ($cond[0] === Type::T_LIST &&! empty($cond['enclosing']) &&$cond['enclosing'] === 'parent' &&\count($cond[2]) == 1) {$cond = reset($cond[2]);}$while = new WhileBlock();$this->registerPushedBlock($while, $s);$while->cond = $cond;return true;}$this->seek($s);if ($this->literal('@for', 4) &&$this->variable($varName) &&$this->literal('from', 4) &&$this->expression($start) &&($this->literal('through', 7) ||($forUntil = true && $this->literal('to', 2))) &&$this->expression($end) &&$this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$for = new ForBlock();$this->registerPushedBlock($for, $s);$for->var = $varName[1];$for->start = $start;$for->end = $end;$for->until = isset($forUntil);return true;}$this->seek($s);if ($this->literal('@if', 3) &&$this->functionCallArgumentsList($cond, false, '{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$if = new IfBlock();$this->registerPushedBlock($if, $s);while ($cond[0] === Type::T_LIST &&! empty($cond['enclosing']) &&$cond['enclosing'] === 'parent' &&\count($cond[2]) == 1) {$cond = reset($cond[2]);}$if->cond = $cond;$if->cases = [];return true;}$this->seek($s);if ($this->literal('@debug', 6) &&$this->functionCallArgumentsList($value, false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$this->append([Type::T_DEBUG, $value], $s);return true;}$this->seek($s);if ($this->literal('@warn', 5) &&$this->functionCallArgumentsList($value, false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$this->append([Type::T_WARN, $value], $s);return true;}$this->seek($s);if ($this->literal('@error', 6) &&$this->functionCallArgumentsList($value, false)) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$this->append([Type::T_ERROR, $value], $s);return true;}$this->seek($s);if ($this->literal('@content', 8) &&($this->end() ||$this->matchChar('(') &&$this->argValues($argContent) &&$this->matchChar(')') &&$this->end())) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);$this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);return true;}$this->seek($s);$last = $this->last();if (isset($last) && $last[0] === Type::T_IF) {list(, $if) = $last;assert($if instanceof IfBlock);if ($this->literal('@else', 5)) {if ($this->matchChar('{', false)) {$else = new ElseBlock();} elseif ($this->literal('if', 2) &&$this->functionCallArgumentsList($cond, false, '{', false)) {$else = new ElseifBlock();$else->cond = $cond;}if (isset($else)) {$this->registerPushedBlock($else, $s);$if->cases[] = $else;return true;}}$this->seek($s);}// only retain the first @charset directive encounteredif ($this->literal('@charset', 8) &&$this->valueList($charset) &&$this->end()) {return true;}$this->seek($s);if ($this->literal('@supports', 9) &&($t1 = $this->supportsQuery($supportQuery)) &&($t2 = $this->matchChar('{', false))) {$directive = new DirectiveBlock();$this->registerPushedBlock($directive, $s);$directive->name = 'supports';$directive->value = $supportQuery;return true;}$this->seek($s);// doesn't match built in directive, do generic oneif ($this->matchChar('@', false) &&$this->mixedKeyword($dirName) &&$this->directiveValue($dirValue, '{')) {if (count($dirName) === 1 && is_string(reset($dirName))) {$dirName = reset($dirName);} else {$dirName = [Type::T_STRING, '', $dirName];}if ($dirName === 'media') {$directive = new MediaBlock();} else {$directive = new DirectiveBlock();$directive->name = $dirName;}$this->registerPushedBlock($directive, $s);if (isset($dirValue)) {! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));$directive->value = $dirValue;}return true;}$this->seek($s);// maybe it's a generic blockless directiveif ($this->matchChar('@', false) &&$this->mixedKeyword($dirName) &&! $this->isKnownGenericDirective($dirName) &&($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))) {if (\count($dirName) === 1 && \is_string(\reset($dirName))) {$dirName = \reset($dirName);} else {$dirName = [Type::T_STRING, '', $dirName];}if (! empty($this->env->parent) &&$this->env->type &&! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])) {$plain = \trim(\substr($this->buffer, $s, $this->count - $s));throw $this->parseError("Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block");}// blockless directives with a blank line after keeps their blank lines after// sass-spec compliance purpose$s = $this->count;$hasBlankLine = false;if ($this->match('\s*?\n\s*\n', $out, false)) {$hasBlankLine = true;$this->seek($s);}$isNotRoot = ! empty($this->env->parent);$this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);$this->whitespace();return true;}$this->seek($s);return false;}$inCssSelector = null;if ($this->cssOnly) {$inCssSelector = (! empty($this->env->parent) &&! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));}// custom properties : right part is staticif (($this->customProperty($name) ) && $this->matchChar(':', false)) {$start = $this->count;// but can be complex and finish with ; or }foreach ([';','}'] as $ending) {if ($this->openString($ending, $stringValue, '(', ')', false) &&$this->end()) {$end = $this->count;$value = $stringValue;// check if we have only a partial value due to nested [] or { } to take in account$nestingPairs = [['[', ']'], ['{', '}']];foreach ($nestingPairs as $nestingPair) {$p = strpos($this->buffer, $nestingPair[0], $start);if ($p && $p < $end) {$this->seek($start);if ($this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&$this->end() &&$this->count > $end) {$end = $this->count;$value = $stringValue;}}}$this->seek($end);$this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s);return true;}}// TODO: output an error here if nothing found according to sass spec}$this->seek($s);// property shortcut// captures most properties before having to parse a selectorif ($this->keyword($name, false) &&$this->literal(': ', 2) &&$this->valueList($value) &&$this->end()) {$name = [Type::T_STRING, '', [$name]];$this->append([Type::T_ASSIGN, $name, $value], $s);return true;}$this->seek($s);// variable assignsif ($this->variable($name) &&$this->matchChar(':') &&$this->valueList($value) &&$this->end()) {! $this->cssOnly || $this->assertPlainCssValid(false, $s);// check for '!flag'$assignmentFlags = $this->stripAssignmentFlags($value);$this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);return true;}$this->seek($s);// opening css blockif ($this->selectors($selectors) &&$this->matchChar('{', false)) {! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);$this->pushBlock($selectors, $s);if ($this->eatWhiteDefault) {$this->whitespace();$this->append(null); // collect comments at the beginning if needed}return true;}$this->seek($s);// property assign, or nested assignif ($this->propertyName($name) &&$this->matchChar(':')) {$foundSomething = false;if ($this->valueList($value)) {if (empty($this->env->parent)) {throw $this->parseError('expected "{"');}$this->append([Type::T_ASSIGN, $name, $value], $s);$foundSomething = true;}if ($this->matchChar('{', false)) {! $this->cssOnly || $this->assertPlainCssValid(false);$propBlock = new NestedPropertyBlock();$this->registerPushedBlock($propBlock, $s);$propBlock->prefix = $name;$propBlock->hasValue = $foundSomething;$foundSomething = true;} elseif ($foundSomething) {$foundSomething = $this->end();}if ($foundSomething) {return true;}}$this->seek($s);// closing a blockif ($this->matchChar('}', false)) {$block = $this->popBlock();if (! isset($block->type) || $block->type !== Type::T_IF) {assert($this->env !== null);if ($this->env->parent) {$this->append(null); // collect comments before next statement if needed}}if ($block instanceof ContentBlock) {$include = $block->child;assert(\is_array($include));unset($block->child);$include[3] = $block;$this->append($include, $s);} elseif (!$block instanceof ElseBlock && !$block instanceof ElseifBlock) {$type = isset($block->type) ? $block->type : Type::T_BLOCK;$this->append([$type, $block], $s);}// collect comments just after the block closing if neededif ($this->eatWhiteDefault) {$this->whitespace();assert($this->env !== null);if ($this->env->comments) {$this->append(null);}}return true;}// extra stuffif ($this->matchChar(';')) {return true;}return false;}/*** Push block onto parse tree** @param array|null $selectors* @param int $pos** @return Block*/protected function pushBlock($selectors, $pos = 0){$b = new Block();$b->selectors = $selectors;$this->registerPushedBlock($b, $pos);return $b;}/*** @param Block $b* @param int $pos** @return void*/private function registerPushedBlock(Block $b, $pos){list($line, $column) = $this->getSourcePosition($pos);$b->sourceName = $this->sourceName;$b->sourceLine = $line;$b->sourceColumn = $column;$b->sourceIndex = $this->sourceIndex;$b->comments = [];$b->parent = $this->env;if (! $this->env) {$b->children = [];} elseif (empty($this->env->children)) {$this->env->children = $this->env->comments;$b->children = [];$this->env->comments = [];} else {$b->children = $this->env->comments;$this->env->comments = [];}$this->env = $b;// collect comments at the beginning of a block if neededif ($this->eatWhiteDefault) {$this->whitespace();assert($this->env !== null);if ($this->env->comments) {$this->append(null);}}}/*** Push special (named) block onto parse tree** @deprecated** @param string $type* @param int $pos** @return Block*/protected function pushSpecialBlock($type, $pos){$block = $this->pushBlock(null, $pos);$block->type = $type;return $block;}/*** Pop scope and return last block** @return Block** @throws \Exception*/protected function popBlock(){assert($this->env !== null);// collect comments ending just before of a block closingif ($this->env->comments) {$this->append(null);}// pop the block$block = $this->env;if (empty($block->parent)) {throw $this->parseError('unexpected }');}if ($block->type == Type::T_AT_ROOT) {// keeps the parent in case of self selector &$block->selfParent = $block->parent;}$this->env = $block->parent;unset($block->parent);return $block;}/*** Peek input stream** @param string $regex* @param array $out* @param int $from** @return int*/protected function peek($regex, &$out, $from = null){if (! isset($from)) {$from = $this->count;}$r = '/' . $regex . '/' . $this->patternModifiers;$result = preg_match($r, $this->buffer, $out, 0, $from);return $result;}/*** Seek to position in input stream (or return current position in input stream)** @param int $where** @return void*/protected function seek($where){$this->count = $where;}/*** Assert a parsed part is plain CSS Valid** @param array|false $parsed* @param int $startPos** @return array** @throws ParserException*/protected function assertPlainCssValid($parsed, $startPos = null){$type = '';if ($parsed) {$type = $parsed[0];$parsed = $this->isPlainCssValidElement($parsed);}if (! $parsed) {if (! \is_null($startPos)) {$plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos));$message = "Error : `{$plain}` isn't allowed in plain CSS";} else {$message = 'Error: SCSS syntax not allowed in CSS file';}if ($type) {$message .= " ($type)";}throw $this->parseError($message);}return $parsed;}/*** Check a parsed element is plain CSS Valid** @param array $parsed* @param bool $allowExpression** @return array|false*/protected function isPlainCssValidElement($parsed, $allowExpression = false){// keep string as isif (is_string($parsed)) {return $parsed;}if (\in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&!\in_array($parsed[1], ['alpha','attr','calc','cubic-bezier','env','grayscale','hsl','hsla','hwb','invert','linear-gradient','min','max','radial-gradient','repeating-linear-gradient','repeating-radial-gradient','rgb','rgba','rotate','saturate','var',]) &&Compiler::isNativeFunction($parsed[1])) {return false;}switch ($parsed[0]) {case Type::T_BLOCK:case Type::T_KEYWORD:case Type::T_NULL:case Type::T_NUMBER:case Type::T_MEDIA:return $parsed;case Type::T_COMMENT:if (isset($parsed[2])) {return false;}return $parsed;case Type::T_DIRECTIVE:if (\is_array($parsed[1])) {$parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);if (! $parsed[1][1]) {return false;}}return $parsed;case Type::T_IMPORT:if ($parsed[1][0] === Type::T_LIST) {return false;}$parsed[1] = $this->isPlainCssValidElement($parsed[1]);if ($parsed[1] === false) {return false;}return $parsed;case Type::T_STRING:foreach ($parsed[2] as $k => $substr) {if (\is_array($substr)) {$parsed[2][$k] = $this->isPlainCssValidElement($substr);if (! $parsed[2][$k]) {return false;}}}return $parsed;case Type::T_LIST:if (!empty($parsed['enclosing'])) {return false;}foreach ($parsed[2] as $k => $listElement) {$parsed[2][$k] = $this->isPlainCssValidElement($listElement);if (! $parsed[2][$k]) {return false;}}return $parsed;case Type::T_ASSIGN:foreach ([1, 2, 3] as $k) {if (! empty($parsed[$k])) {$parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);if (! $parsed[$k]) {return false;}}}return $parsed;case Type::T_EXPRESSION:list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) {return false;}$lhs = $this->isPlainCssValidElement($lhs, true);if (! $lhs) {return false;}$rhs = $this->isPlainCssValidElement($rhs, true);if (! $rhs) {return false;}return [Type::T_STRING,'', [$this->inParens ? '(' : '',$lhs,($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''),$rhs,$this->inParens ? ')' : '']];case Type::T_CUSTOM_PROPERTY:case Type::T_UNARY:$parsed[2] = $this->isPlainCssValidElement($parsed[2]);if (! $parsed[2]) {return false;}return $parsed;case Type::T_FUNCTION:$argsList = $parsed[2];foreach ($argsList[2] as $argElement) {if (! $this->isPlainCssValidElement($argElement)) {return false;}}return $parsed;case Type::T_FUNCTION_CALL:$parsed[0] = Type::T_FUNCTION;$argsList = [Type::T_LIST, ',', []];foreach ($parsed[2] as $arg) {if ($arg[0] || ! empty($arg[2])) {// no named arguments possible in a css function call// nor ... argumentreturn false;}$arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');if (! $arg) {return false;}$argsList[2][] = $arg;}$parsed[2] = $argsList;return $parsed;}return false;}/*** Match string looking for either ending delim, escape, or string interpolation** {@internal This is a workaround for preg_match's 250K string match limit. }}** @param array $m Matches (passed by reference)* @param string $delim Delimiter** @return bool True if match; false otherwise** @phpstan-impure*/protected function matchString(&$m, $delim){$token = null;$end = \strlen($this->buffer);// look for either ending delim, escape, or string interpolationforeach (['#{', '\\', "\r", $delim] as $lookahead) {$pos = strpos($this->buffer, $lookahead, $this->count);if ($pos !== false && $pos < $end) {$end = $pos;$token = $lookahead;}}if (! isset($token)) {return false;}$match = substr($this->buffer, $this->count, $end - $this->count);$m = [$match . $token,$match,$token];$this->count = $end + \strlen($token);return true;}/*** Try to match something on head of buffer** @param string $regex* @param array $out* @param bool $eatWhitespace** @return bool** @phpstan-impure*/protected function match($regex, &$out, $eatWhitespace = null){$r = '/' . $regex . '/' . $this->patternModifiers;if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {return false;}$this->count += \strlen($out[0]);if (! isset($eatWhitespace)) {$eatWhitespace = $this->eatWhiteDefault;}if ($eatWhitespace) {$this->whitespace();}return true;}/*** Match a single string** @param string $char* @param bool $eatWhitespace** @return bool** @phpstan-impure*/protected function matchChar($char, $eatWhitespace = null){if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {return false;}$this->count++;if (! isset($eatWhitespace)) {$eatWhitespace = $this->eatWhiteDefault;}if ($eatWhitespace) {$this->whitespace();}return true;}/*** Match literal string** @param string $what* @param int $len* @param bool $eatWhitespace** @return bool** @phpstan-impure*/protected function literal($what, $len, $eatWhitespace = null){if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {return false;}$this->count += $len;if (! isset($eatWhitespace)) {$eatWhitespace = $this->eatWhiteDefault;}if ($eatWhitespace) {$this->whitespace();}return true;}/*** Match some whitespace** @return bool** @phpstan-impure*/protected function whitespace(){$gotWhite = false;while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {// comment that are kept in the output CSS$comment = [];$startCommentCount = $this->count;$endCommentCount = $this->count + \strlen($m[1]);// find interpolations in comment$p = strpos($this->buffer, '#{', $this->count);while ($p !== false && $p < $endCommentCount) {$c = substr($this->buffer, $this->count, $p - $this->count);$comment[] = $c;$this->count = $p;$out = null;if ($this->interpolation($out)) {// keep right spaces in the following string partif ($out[3]) {while ($this->buffer[$this->count - 1] !== '}') {$this->count--;}$out[3] = '';}$comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];} else {list($line, $column) = $this->getSourcePosition($this->count);$file = $this->sourceName;if (!$this->discardComments) {$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);}$comment[] = substr($this->buffer, $this->count, 2);$this->count += 2;}$p = strpos($this->buffer, '#{', $this->count);}// remaining part$c = substr($this->buffer, $this->count, $endCommentCount - $this->count);if (! $comment) {// single part static comment$commentStatement = [Type::T_COMMENT, $c];} else {$comment[] = $c;$staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);$commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];}list($line, $column) = $this->getSourcePosition($startCommentCount);$commentStatement[self::SOURCE_LINE] = $line;$commentStatement[self::SOURCE_COLUMN] = $column;$commentStatement[self::SOURCE_INDEX] = $this->sourceIndex;$this->appendComment($commentStatement);$this->commentsSeen[$startCommentCount] = true;$this->count = $endCommentCount;} else {// comment that are ignored and not kept in the output css$this->count += \strlen($m[0]);// silent comments are not allowed in plain CSS files! $this->cssOnly|| ! \strlen(trim($m[0]))|| $this->assertPlainCssValid(false, $this->count - \strlen($m[0]));}$gotWhite = true;}return $gotWhite;}/*** Append comment to current block** @param array $comment** @return void*/protected function appendComment($comment){if (! $this->discardComments) {assert($this->env !== null);$this->env->comments[] = $comment;}}/*** Append statement to current block** @param array|null $statement* @param int $pos** @return void*/protected function append($statement, $pos = null){assert($this->env !== null);if (! \is_null($statement)) {! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));if (! \is_null($pos)) {list($line, $column) = $this->getSourcePosition($pos);$statement[static::SOURCE_LINE] = $line;$statement[static::SOURCE_COLUMN] = $column;$statement[static::SOURCE_INDEX] = $this->sourceIndex;}$this->env->children[] = $statement;}$comments = $this->env->comments;if ($comments) {$this->env->children = array_merge($this->env->children, $comments);$this->env->comments = [];}}/*** Returns last child was appended** @return array|null*/protected function last(){assert($this->env !== null);$i = \count($this->env->children) - 1;if (isset($this->env->children[$i])) {return $this->env->children[$i];}return null;}/*** Parse media query list** @param array $out** @return bool*/protected function mediaQueryList(&$out){return $this->genericList($out, 'mediaQuery', ',', false);}/*** Parse media query** @param array $out** @return bool*/protected function mediaQuery(&$out){$expressions = null;$parts = [];if (($this->literal('only', 4) && ($only = true) ||$this->literal('not', 3) && ($not = true) || true) &&$this->mixedKeyword($mediaType)) {$prop = [Type::T_MEDIA_TYPE];if (isset($only)) {$prop[] = [Type::T_KEYWORD, 'only'];}if (isset($not)) {$prop[] = [Type::T_KEYWORD, 'not'];}$media = [Type::T_LIST, '', []];foreach ((array) $mediaType as $type) {if (\is_array($type)) {$media[2][] = $type;} else {$media[2][] = [Type::T_KEYWORD, $type];}}$prop[] = $media;$parts[] = $prop;}if (empty($parts) || $this->literal('and', 3)) {$this->genericList($expressions, 'mediaExpression', 'and', false);if (\is_array($expressions)) {$parts = array_merge($parts, $expressions[2]);}}$out = $parts;return true;}/*** Parse supports query** @param array $out** @return bool*/protected function supportsQuery(&$out){$expressions = null;$parts = [];$s = $this->count;$not = false;if (($this->literal('not', 3) && ($not = true) || true) &&$this->matchChar('(') &&($this->expression($property)) &&$this->literal(': ', 2) &&$this->valueList($value) &&$this->matchChar(')')) {$support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];$support[2][] = $property;$support[2][] = [Type::T_KEYWORD, ': '];$support[2][] = $value;$support[2][] = [Type::T_KEYWORD, ')'];$parts[] = $support;$s = $this->count;} else {$this->seek($s);}if ($this->matchChar('(') &&$this->supportsQuery($subQuery) &&$this->matchChar(')')) {$parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];$s = $this->count;} else {$this->seek($s);}if ($this->literal('not', 3) &&$this->supportsQuery($subQuery)) {$parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];$s = $this->count;} else {$this->seek($s);}if ($this->literal('selector(', 9) &&$this->selector($selector) &&$this->matchChar(')')) {$support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];$selectorList = [Type::T_LIST, '', []];foreach ($selector as $sc) {$compound = [Type::T_STRING, '', []];foreach ($sc as $scp) {if (\is_array($scp)) {$compound[2][] = $scp;} else {$compound[2][] = [Type::T_KEYWORD, $scp];}}$selectorList[2][] = $compound;}$support[2][] = $selectorList;$support[2][] = [Type::T_KEYWORD, ')'];$parts[] = $support;$s = $this->count;} else {$this->seek($s);}if ($this->variable($var) or $this->interpolation($var)) {$parts[] = $var;$s = $this->count;} else {$this->seek($s);}if ($this->literal('and', 3) &&$this->genericList($expressions, 'supportsQuery', ' and', false)) {array_unshift($expressions[2], [Type::T_STRING, '', $parts]);$parts = [$expressions];$s = $this->count;} else {$this->seek($s);}if ($this->literal('or', 2) &&$this->genericList($expressions, 'supportsQuery', ' or', false)) {array_unshift($expressions[2], [Type::T_STRING, '', $parts]);$parts = [$expressions];$s = $this->count;} else {$this->seek($s);}if (\count($parts)) {if ($this->eatWhiteDefault) {$this->whitespace();}$out = [Type::T_STRING, '', $parts];return true;}return false;}/*** Parse media expression** @param array $out** @return bool*/protected function mediaExpression(&$out){$s = $this->count;$value = null;if ($this->matchChar('(') &&$this->expression($feature) &&($this->matchChar(':') &&$this->expression($value) || true) &&$this->matchChar(')')) {$out = [Type::T_MEDIA_EXPRESSION, $feature];if ($value) {$out[] = $value;}return true;}$this->seek($s);return false;}/*** Parse argument values** @param array $out** @return bool*/protected function argValues(&$out){$discardComments = $this->discardComments;$this->discardComments = true;if ($this->genericList($list, 'argValue', ',', false)) {$out = $list[2];$this->discardComments = $discardComments;return true;}$this->discardComments = $discardComments;return false;}/*** Parse argument value** @param array $out** @return bool*/protected function argValue(&$out){$s = $this->count;$keyword = null;if (! $this->variable($keyword) || ! $this->matchChar(':')) {$this->seek($s);$keyword = null;}if ($this->genericList($value, 'expression', '', true)) {$out = [$keyword, $value, false];$s = $this->count;if ($this->literal('...', 3)) {$out[2] = true;} else {$this->seek($s);}return true;}return false;}/*** Check if a generic directive is known to be able to allow almost any syntax or not* @param mixed $directiveName* @return bool*/protected function isKnownGenericDirective($directiveName){if (\is_array($directiveName) && \is_string(reset($directiveName))) {$directiveName = reset($directiveName);}if (! \is_string($directiveName)) {return false;}if (\in_array($directiveName, ['at-root','media','mixin','include','scssphp-import-once','import','extend','function','break','continue','return','each','while','for','if','debug','warn','error','content','else','charset','supports',// Todo'use','forward',])) {return true;}return false;}/*** Parse directive value list that considers $vars as keyword** @param array $out* @param string|false $endChar** @return bool** @phpstan-impure*/protected function directiveValue(&$out, $endChar = false){$s = $this->count;if ($this->variable($out)) {if ($endChar && $this->matchChar($endChar, false)) {return true;}if (! $endChar && $this->end()) {return true;}}$this->seek($s);if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) {if ($endChar && $this->matchChar($endChar, false)) {return true;}$ss = $this->count;if (!$endChar && $this->end()) {$this->seek($ss);return true;}}$this->seek($s);$allowVars = $this->allowVars;$this->allowVars = false;$res = $this->genericList($out, 'spaceList', ',');$this->allowVars = $allowVars;if ($res) {if ($endChar && $this->matchChar($endChar, false)) {return true;}if (! $endChar && $this->end()) {return true;}}$this->seek($s);if ($endChar && $this->matchChar($endChar, false)) {return true;}return false;}/*** Parse comma separated value list** @param array $out** @return bool*/protected function valueList(&$out){$discardComments = $this->discardComments;$this->discardComments = true;$res = $this->genericList($out, 'spaceList', ',');$this->discardComments = $discardComments;return $res;}/*** Parse a function call, where externals () are part of the call* and not of the value list** @param array $out* @param bool $mandatoryEnclos* @param null|string $charAfter* @param null|bool $eatWhiteSp** @return bool*/protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null){$s = $this->count;if ($this->matchChar('(') &&$this->valueList($out) &&$this->matchChar(')') &&($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())) {return true;}if (! $mandatoryEnclos) {$this->seek($s);if ($this->valueList($out) &&($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())) {return true;}}$this->seek($s);return false;}/*** Parse space separated value list** @param array $out** @return bool*/protected function spaceList(&$out){return $this->genericList($out, 'expression');}/*** Parse generic list** @param array $out* @param string $parseItem The name of the method used to parse items* @param string $delim* @param bool $flatten** @return bool*/protected function genericList(&$out, $parseItem, $delim = '', $flatten = true){$s = $this->count;$items = [];/** @var array|Number|null $value */$value = null;while ($this->$parseItem($value)) {$trailing_delim = false;$items[] = $value;if ($delim) {if (! $this->literal($delim, \strlen($delim))) {break;}$trailing_delim = true;} else {assert(\is_array($value) || $value instanceof Number);// if no delim watch that a keyword didn't eat the single/double quote// from the following starting stringif ($value[0] === Type::T_KEYWORD) {assert(\is_array($value));/** @var string $word */$word = $value[1];$last_char = substr($word, -1);if (strlen($word) > 1 &&in_array($last_char, [ "'", '"']) &&substr($word, -2, 1) !== '\\') {// if there is a non escaped opening quote in the keyword, this seems unlikely a mistake$word = str_replace('\\' . $last_char, '\\\\', $word);if (strpos($word, $last_char) < strlen($word) - 1) {continue;}$currentCount = $this->count;// let's try to rewind to previous char and try a parse$this->count--;// in case the keyword also eat spaceswhile (substr($this->buffer, $this->count, 1) !== $last_char) {$this->count--;}/** @var array|Number|null $nextValue */$nextValue = null;if ($this->$parseItem($nextValue)) {assert(\is_array($nextValue) || $nextValue instanceof Number);if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {// bad try, forget it$this->seek($currentCount);continue;}if ($nextValue[0] !== Type::T_STRING) {// bad try, forget it$this->seek($currentCount);continue;}// OK it was a good idea$value[1] = substr($value[1], 0, -1);array_pop($items);$items[] = $value;$items[] = $nextValue;} else {// bad try, forget it$this->seek($currentCount);continue;}}}}}if (! $items) {$this->seek($s);return false;}if ($trailing_delim) {$items[] = [Type::T_NULL];}if ($flatten && \count($items) === 1) {$out = $items[0];} else {$out = [Type::T_LIST, $delim, $items];}return true;}/*** Parse expression** @param array $out* @param bool $listOnly* @param bool $lookForExp** @return bool** @phpstan-impure*/protected function expression(&$out, $listOnly = false, $lookForExp = true){$s = $this->count;$discard = $this->discardComments;$this->discardComments = true;$allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);if ($this->matchChar('(')) {if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {if ($lookForExp) {$out = $this->expHelper($lhs, 0);} else {$out = $lhs;}$this->discardComments = $discard;return true;}$this->seek($s);}if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {if ($lookForExp) {$out = $this->expHelper($lhs, 0);} else {$out = $lhs;}$this->discardComments = $discard;return true;}$this->seek($s);}if (! $listOnly && $this->value($lhs)) {if ($lookForExp) {$out = $this->expHelper($lhs, 0);} else {$out = $lhs;}$this->discardComments = $discard;return true;}$this->discardComments = $discard;return false;}/*** Parse expression specifically checking for lists in parenthesis or brackets** @param array $out* @param int $s* @param string $closingParen* @param string[] $allowedTypes** @return bool** @phpstan-param array<Type::*> $allowedTypes*/protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]){if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {$out = [Type::T_LIST, '', []];switch ($closingParen) {case ')':$out['enclosing'] = 'parent'; // parenthesis listbreak;case ']':$out['enclosing'] = 'bracket'; // bracketed listbreak;}return true;}if ($this->valueList($out) &&$this->matchChar($closingParen) && ! ($closingParen === ')' &&\in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&\in_array(Type::T_LIST, $allowedTypes)) {if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {$out = [Type::T_LIST, '', [$out]];}switch ($closingParen) {case ')':$out['enclosing'] = 'parent'; // parenthesis listbreak;case ']':$out['enclosing'] = 'bracket'; // bracketed listbreak;}return true;}$this->seek($s);if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {return true;}return false;}/*** Parse left-hand side of subexpression** @param array $lhs* @param int $minP** @return array*/protected function expHelper($lhs, $minP){$operators = static::$operatorPattern;$ss = $this->count;$whiteBefore = isset($this->buffer[$this->count - 1]) &&ctype_space($this->buffer[$this->count - 1]);while ($this->match($operators, $m, false) && static::$precedence[strtolower($m[1])] >= $minP) {$whiteAfter = isset($this->buffer[$this->count]) &&ctype_space($this->buffer[$this->count]);$varAfter = isset($this->buffer[$this->count]) &&$this->buffer[$this->count] === '$';$this->whitespace();$op = $m[1];// don't turn negative numbers into expressionsif ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {break;}if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {break;}if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {break;}// consume higher-precedence operators on the right-hand side$rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1);$lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];$ss = $this->count;$whiteBefore = isset($this->buffer[$this->count - 1]) &&ctype_space($this->buffer[$this->count - 1]);}$this->seek($ss);return $lhs;}/*** Parse value** @param array $out** @return bool*/protected function value(&$out){if (! isset($this->buffer[$this->count])) {return false;}$s = $this->count;$char = $this->buffer[$this->count];if ($this->literal('url(', 4) &&$this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {$len = strspn($this->buffer,'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',$this->count);$this->count += $len;if ($this->matchChar(')')) {$content = substr($this->buffer, $s, $this->count - $s);$out = [Type::T_KEYWORD, $content];return true;}}$this->seek($s);if ($this->literal('url(', 4, false) &&$this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {$content = 'url(' . $m[1];if ($this->matchChar(')')) {$content .= ')';$out = [Type::T_KEYWORD, $content];return true;}}$this->seek($s);// notif ($char === 'n' && $this->literal('not', 3, false)) {if ($this->whitespace() &&$this->value($inner)) {$out = [Type::T_UNARY, 'not', $inner, $this->inParens];return true;}$this->seek($s);if ($this->parenValue($inner)) {$out = [Type::T_UNARY, 'not', $inner, $this->inParens];return true;}$this->seek($s);}// additionif ($char === '+') {$this->count++;$follow_white = $this->whitespace();if ($this->value($inner)) {$out = [Type::T_UNARY, '+', $inner, $this->inParens];return true;}if ($follow_white) {$out = [Type::T_KEYWORD, $char];return true;}$this->seek($s);return false;}// negationif ($char === '-') {if ($this->customProperty($out)) {return true;}$this->count++;$follow_white = $this->whitespace();if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {$out = [Type::T_UNARY, '-', $inner, $this->inParens];return true;}if ($this->keyword($inner) &&! $this->func($inner, $out)) {$out = [Type::T_UNARY, '-', $inner, $this->inParens];return true;}if ($follow_white) {$out = [Type::T_KEYWORD, $char];return true;}$this->seek($s);}// parenif ($char === '(' && $this->parenValue($out)) {return true;}if ($char === '#') {if ($this->interpolation($out) || $this->color($out)) {return true;}$this->count++;if ($this->keyword($keyword)) {$out = [Type::T_KEYWORD, '#' . $keyword];return true;}$this->count--;}if ($this->matchChar('&', true)) {$out = [Type::T_SELF];return true;}if ($char === '$' && $this->variable($out)) {return true;}if ($char === 'p' && $this->progid($out)) {return true;}if (($char === '"' || $char === "'") && $this->string($out)) {return true;}if ($this->unit($out)) {return true;}// unicode range with wildcardsif ($this->literal('U+', 2) &&$this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)) {$unicode = explode('-', $m[0]);if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {$out = [Type::T_KEYWORD, 'U+' . $m[0]];return true;}$this->count -= strlen($m[0]) + 2;}if ($this->keyword($keyword, false)) {if ($this->func($keyword, $out)) {return true;}$this->whitespace();if ($keyword === 'null') {$out = [Type::T_NULL];} else {$out = [Type::T_KEYWORD, $keyword];}return true;}return false;}/*** Parse parenthesized value** @param array $out** @return bool*/protected function parenValue(&$out){$s = $this->count;$inParens = $this->inParens;if ($this->matchChar('(')) {if ($this->matchChar(')')) {$out = [Type::T_LIST, '', []];return true;}$this->inParens = true;if ($this->expression($exp) &&$this->matchChar(')')) {$out = $exp;$this->inParens = $inParens;return true;}}$this->inParens = $inParens;$this->seek($s);return false;}/*** Parse "progid:"** @param array $out** @return bool*/protected function progid(&$out){$s = $this->count;if ($this->literal('progid:', 7, false) &&$this->openString('(', $fn) &&$this->matchChar('(')) {$this->openString(')', $args, '(');if ($this->matchChar(')')) {$out = [Type::T_STRING, '', ['progid:', $fn, '(', $args, ')']];return true;}}$this->seek($s);return false;}/*** Parse function call** @param string $name* @param array $func** @return bool*/protected function func($name, &$func){$s = $this->count;if ($this->matchChar('(')) {if ($name === 'alpha' && $this->argumentList($args)) {$func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];return true;}if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {$ss = $this->count;if ($this->argValues($args) &&$this->matchChar(')')) {if (strtolower($name) === 'var' && \count($args) === 2 && $args[1][0] === Type::T_NULL) {$args[1] = [null, [Type::T_STRING, '', [' ']], false];}$func = [Type::T_FUNCTION_CALL, $name, $args];return true;}$this->seek($ss);}if (($this->openString(')', $str, '(') || true) &&$this->matchChar(')')) {$args = [];if (! empty($str)) {$args[] = [null, [Type::T_STRING, '', [$str]]];}$func = [Type::T_FUNCTION_CALL, $name, $args];return true;}}$this->seek($s);return false;}/*** Parse function call argument list** @param array $out** @return bool*/protected function argumentList(&$out){$s = $this->count;$this->matchChar('(');$args = [];while ($this->keyword($var)) {if ($this->matchChar('=') &&$this->expression($exp)) {$args[] = [Type::T_STRING, '', [$var . '=']];$arg = $exp;} else {break;}$args[] = $arg;if (! $this->matchChar(',')) {break;}$args[] = [Type::T_STRING, '', [', ']];}if (! $this->matchChar(')') || ! $args) {$this->seek($s);return false;}$out = $args;return true;}/*** Parse mixin/function definition argument list** @param array $out** @return bool*/protected function argumentDef(&$out){$s = $this->count;$this->matchChar('(');$args = [];while ($this->variable($var)) {$arg = [$var[1], null, false];$ss = $this->count;if ($this->matchChar(':') &&$this->genericList($defaultVal, 'expression', '', true)) {$arg[1] = $defaultVal;} else {$this->seek($ss);}$ss = $this->count;if ($this->literal('...', 3)) {$sss = $this->count;if (! $this->matchChar(')')) {throw $this->parseError('... has to be after the final argument');}$arg[2] = true;$this->seek($sss);} else {$this->seek($ss);}$args[] = $arg;if (! $this->matchChar(',')) {break;}}if (! $this->matchChar(')')) {$this->seek($s);return false;}$out = $args;return true;}/*** Parse map** @param array $out** @return bool*/protected function map(&$out){$s = $this->count;if (! $this->matchChar('(')) {return false;}$keys = [];$values = [];while ($this->genericList($key, 'expression', '', true) &&$this->matchChar(':') &&$this->genericList($value, 'expression', '', true)) {$keys[] = $key;$values[] = $value;if (! $this->matchChar(',')) {break;}}if (! $keys || ! $this->matchChar(')')) {$this->seek($s);return false;}$out = [Type::T_MAP, $keys, $values];return true;}/*** Parse color** @param array $out** @return bool*/protected function color(&$out){$s = $this->count;if ($this->match('(#([0-9a-f]+)\b)', $m)) {if (\in_array(\strlen($m[2]), [3,4,6,8])) {$out = [Type::T_KEYWORD, $m[0]];return true;}$this->seek($s);return false;}return false;}/*** Parse number with unit** @param array $unit** @return bool*/protected function unit(&$unit){$s = $this->count;if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {$this->whitespace();$unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);return true;}$this->seek($s);}return false;}/*** Parse string** @param array $out* @param bool $keepDelimWithInterpolation** @return bool*/protected function string(&$out, $keepDelimWithInterpolation = false){$s = $this->count;if ($this->matchChar('"', false)) {$delim = '"';} elseif ($this->matchChar("'", false)) {$delim = "'";} else {return false;}$content = [];$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;$hasInterpolation = false;while ($this->matchString($m, $delim)) {if ($m[1] !== '') {$content[] = $m[1];}if ($m[2] === '#{') {$this->count -= \strlen($m[2]);if ($this->interpolation($inter, false)) {$content[] = $inter;$hasInterpolation = true;} else {$this->count += \strlen($m[2]);$content[] = '#{'; // ignore it}} elseif ($m[2] === "\r") {$content[] = chr(10);// TODO : warning# DEPRECATION WARNING on line x, column y of zzz:# Unescaped multiline strings are deprecated and will be removed in a future version of Sass.# To include a newline in a string, use "\a" or "\a " as in CSS.if ($this->matchChar("\n", false)) {$content[] = ' ';}} elseif ($m[2] === '\\') {if ($this->literal("\r\n", 2, false) ||$this->matchChar("\r", false) ||$this->matchChar("\n", false) ||$this->matchChar("\f", false)) {// this is a continuation escaping, to be ignored} elseif ($this->matchEscapeCharacter($c)) {$content[] = $c;} else {throw $this->parseError('Unterminated escape sequence');}} else {$this->count -= \strlen($delim);break; // delim}}$this->eatWhiteDefault = $oldWhite;if ($this->literal($delim, \strlen($delim))) {if ($hasInterpolation && ! $keepDelimWithInterpolation) {$delim = '"';}$out = [Type::T_STRING, $delim, $content];return true;}$this->seek($s);return false;}/*** @param string $out* @param bool $inKeywords** @return bool*/protected function matchEscapeCharacter(&$out, $inKeywords = false){$s = $this->count;if ($this->match('[a-f0-9]', $m, false)) {$hex = $m[0];for ($i = 5; $i--;) {if ($this->match('[a-f0-9]', $m, false)) {$hex .= $m[0];} else {break;}}// CSS allows Unicode escape sequences to be followed by a delimiter space// (necessary in some cases for shorter sequences to disambiguate their end)$this->matchChar(' ', false);$value = hexdec($hex);if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) {$out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5} elseif ($value < 0x20) {$out = Util::mbChr($value);} else {$out = Util::mbChr($value);}return true;}if ($this->match('.', $m, false)) {if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {$this->seek($s);return false;}$out = $m[0];return true;}return false;}/*** Parse keyword or interpolation** @param array $out* @param bool $restricted** @return bool*/protected function mixedKeyword(&$out, $restricted = false){$parts = [];$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;for (;;) {if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {$parts[] = $key;continue;}if ($this->interpolation($inter)) {$parts[] = $inter;continue;}break;}$this->eatWhiteDefault = $oldWhite;if (! $parts) {return false;}if ($this->eatWhiteDefault) {$this->whitespace();}$out = $parts;return true;}/*** Parse an unbounded string stopped by $end** @param string $end* @param array $out* @param string $nestOpen* @param string $nestClose* @param bool $rtrim* @param string $disallow** @return bool*/protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null){$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;if ($nestOpen && ! $nestClose) {$nestClose = $end;}$patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');$patt = '(' . $patt . '*?)([\'"]|#\{|'. $this->pregQuote($end) . '|'. (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : ''). static::$commentPattern . ')';$nestingLevel = 0;$content = [];while ($this->match($patt, $m, false)) {if (isset($m[1]) && $m[1] !== '') {$content[] = $m[1];if ($nestOpen) {$nestingLevel += substr_count($m[1], $nestOpen);}}$tok = $m[2];$this->count -= \strlen($tok);if ($tok === $end && ! $nestingLevel) {break;}if ($tok === $nestClose) {$nestingLevel--;}if (($tok === "'" || $tok === '"') && $this->string($str, true)) {$content[] = $str;continue;}if ($tok === '#{' && $this->interpolation($inter)) {$content[] = $inter;continue;}$content[] = $tok;$this->count += \strlen($tok);}$this->eatWhiteDefault = $oldWhite;if (! $content || $tok !== $end) {return false;}// trim the endif ($rtrim && \is_string(end($content))) {$content[\count($content) - 1] = rtrim(end($content));}$out = [Type::T_STRING, '', $content];return true;}/*** Parser interpolation** @param string|array $out* @param bool $lookWhite save information about whitespace before and after** @return bool*/protected function interpolation(&$out, $lookWhite = true){$oldWhite = $this->eatWhiteDefault;$allowVars = $this->allowVars;$this->allowVars = true;$this->eatWhiteDefault = true;$s = $this->count;if ($this->literal('#{', 2) &&$this->valueList($value) &&$this->matchChar('}', false)) {if ($value === [Type::T_SELF]) {$out = $value;} else {if ($lookWhite) {$left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';$right = (! empty($this->buffer[$this->count]) &&preg_match('/\s/', $this->buffer[$this->count])) ? ' ' : '';} else {$left = $right = false;}$out = [Type::T_INTERPOLATE, $value, $left, $right];}$this->eatWhiteDefault = $oldWhite;$this->allowVars = $allowVars;if ($this->eatWhiteDefault) {$this->whitespace();}return true;}$this->seek($s);$this->eatWhiteDefault = $oldWhite;$this->allowVars = $allowVars;return false;}/*** Parse property name (as an array of parts or a string)** @param array $out** @return bool*/protected function propertyName(&$out){$parts = [];$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;for (;;) {if ($this->interpolation($inter)) {$parts[] = $inter;continue;}if ($this->keyword($text)) {$parts[] = $text;continue;}if (! $parts && $this->match('[:.#]', $m, false)) {// css hacks$parts[] = $m[0];continue;}break;}$this->eatWhiteDefault = $oldWhite;if (! $parts) {return false;}// match comment hackif (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {if (! empty($m[0])) {$parts[] = $m[0];$this->count += \strlen($m[0]);}}$this->whitespace(); // get any extra whitespace$out = [Type::T_STRING, '', $parts];return true;}/*** Parse custom property name (as an array of parts or a string)** @param array $out** @return bool*/protected function customProperty(&$out){$s = $this->count;if (! $this->literal('--', 2, false)) {return false;}$parts = ['--'];$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;for (;;) {if ($this->interpolation($inter)) {$parts[] = $inter;continue;}if ($this->matchChar('&', false)) {$parts[] = [Type::T_SELF];continue;}if ($this->variable($var)) {$parts[] = $var;continue;}if ($this->keyword($text)) {$parts[] = $text;continue;}break;}$this->eatWhiteDefault = $oldWhite;if (\count($parts) == 1) {$this->seek($s);return false;}$this->whitespace(); // get any extra whitespace$out = [Type::T_STRING, '', $parts];return true;}/*** Parse comma separated selector list** @param array $out* @param string|bool $subSelector** @return bool*/protected function selectors(&$out, $subSelector = false){$s = $this->count;$selectors = [];while ($this->selector($sel, $subSelector)) {$selectors[] = $sel;if (! $this->matchChar(',', true)) {break;}while ($this->matchChar(',', true)) {; // ignore extra}}if (! $selectors) {$this->seek($s);return false;}$out = $selectors;return true;}/*** Parse whitespace separated selector list** @param array $out* @param string|bool $subSelector** @return bool*/protected function selector(&$out, $subSelector = false){$selector = [];$discardComments = $this->discardComments;$this->discardComments = true;for (;;) {$s = $this->count;if ($this->match('[>+~]+', $m, true)) {if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&$m[0] === '+' && $this->match("(\d+|n\b)", $counter)) {$this->seek($s);} else {$selector[] = [$m[0]];continue;}}if ($this->selectorSingle($part, $subSelector)) {$selector[] = $part;$this->whitespace();continue;}break;}$this->discardComments = $discardComments;if (! $selector) {return false;}$out = $selector;return true;}/*** parsing escaped chars in selectors:* - escaped single chars are kept escaped in the selector but in a normalized form* (if not in 0-9a-f range as this would be ambigous)* - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,* normalized to lowercase** TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,* and escaping added when printing in the Compiler, where/if it's mandatory* - but this require a better formal selector representation instead of the array we have now** @param string $out* @param bool $keepEscapedNumber** @return bool*/protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false){$s_escape = $this->count;if ($this->match('\\\\', $m)) {$out = '\\' . $m[0];return true;}if ($this->matchEscapeCharacter($escapedout, true)) {if (strlen($escapedout) === 1) {if (!preg_match(",\w,", $escapedout)) {$out = '\\' . $escapedout;return true;} elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) {$out = $escapedout;return true;}}$escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape));if (strlen($escape_sequence) < 6) {$escape_sequence .= ' ';}$out = '\\' . strtolower($escape_sequence);return true;}if ($this->match('\\S', $m)) {$out = '\\' . $m[0];return true;}return false;}/*** Parse the parts that make up a selector** {@internal* div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder* }}** @param array $out* @param string|bool $subSelector** @return bool*/protected function selectorSingle(&$out, $subSelector = false){$oldWhite = $this->eatWhiteDefault;$this->eatWhiteDefault = false;$parts = [];if ($this->matchChar('*', false)) {$parts[] = '*';}for (;;) {if (! isset($this->buffer[$this->count])) {break;}$s = $this->count;$char = $this->buffer[$this->count];// see if we can stop earlyif ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {break;}// parsing a sub selector in () stop with the closing )if ($subSelector && $char === ')') {break;}//selfswitch ($char) {case '&':$parts[] = Compiler::$selfSelector;$this->count++;! $this->cssOnly || $this->assertPlainCssValid(false, $s);continue 2;case '.':$parts[] = '.';$this->count++;continue 2;case '|':$parts[] = '|';$this->count++;continue 2;}// handling of escaping in selectors : get the escaped charif ($char === '\\') {$this->count++;if ($this->matchEscapeCharacterInSelector($escaped, true)) {$parts[] = $escaped;continue;}$this->count--;}if ($char === '%') {$this->count++;if ($this->placeholder($placeholder)) {$parts[] = '%';$parts[] = $placeholder;! $this->cssOnly || $this->assertPlainCssValid(false, $s);continue;}break;}if ($char === '#') {if ($this->interpolation($inter)) {$parts[] = $inter;! $this->cssOnly || $this->assertPlainCssValid(false, $s);continue;}$parts[] = '#';$this->count++;continue;}// a pseudo selectorif ($char === ':') {if ($this->buffer[$this->count + 1] === ':') {$this->count += 2;$part = '::';} else {$this->count++;$part = ':';}if ($this->mixedKeyword($nameParts, true)) {$parts[] = $part;foreach ($nameParts as $sub) {$parts[] = $sub;}$ss = $this->count;if ($nameParts === ['not'] ||$nameParts === ['is'] ||$nameParts === ['has'] ||$nameParts === ['where'] ||$nameParts === ['slotted'] ||$nameParts === ['nth-child'] ||$nameParts === ['nth-last-child'] ||$nameParts === ['nth-of-type'] ||$nameParts === ['nth-last-of-type']) {if ($this->matchChar('(', true) &&($this->selectors($subs, reset($nameParts)) || true) &&$this->matchChar(')')) {$parts[] = '(';while ($sub = array_shift($subs)) {while ($ps = array_shift($sub)) {foreach ($ps as &$p) {$parts[] = $p;}if (\count($sub) && reset($sub)) {$parts[] = ' ';}}if (\count($subs) && reset($subs)) {$parts[] = ', ';}}$parts[] = ')';} else {$this->seek($ss);}} elseif ($this->matchChar('(', true) &&($this->openString(')', $str, '(') || true) &&$this->matchChar(')')) {$parts[] = '(';if (! empty($str)) {$parts[] = $str;}$parts[] = ')';} else {$this->seek($ss);}continue;}}$this->seek($s);// 2n+1if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {$parts[] = $counter[0];//$parts[] = str_replace(' ', '', $counter[0]);continue;}}$this->seek($s);// attribute selectorif ($char === '[' &&$this->matchChar('[') &&($this->openString(']', $str, '[') || true) &&$this->matchChar(']')) {$parts[] = '[';if (! empty($str)) {$parts[] = $str;}$parts[] = ']';continue;}$this->seek($s);// for keyframesif ($this->unit($unit)) {$parts[] = $unit;continue;}if ($this->restrictedKeyword($name, false, true)) {$parts[] = $name;continue;}break;}$this->eatWhiteDefault = $oldWhite;if (! $parts) {return false;}$out = $parts;return true;}/*** Parse a variable** @param array $out** @return bool*/protected function variable(&$out){$s = $this->count;if ($this->matchChar('$', false) &&$this->keyword($name)) {if ($this->allowVars) {$out = [Type::T_VARIABLE, $name];} else {$out = [Type::T_KEYWORD, '$' . $name];}return true;}$this->seek($s);return false;}/*** Parse a keyword** @param string $word* @param bool $eatWhitespace* @param bool $inSelector** @return bool*/protected function keyword(&$word, $eatWhitespace = null, $inSelector = false){$s = $this->count;$match = $this->match($this->utf8? '(([\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]) ?|[\\\\].)*)': '(([\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]) ?|[\\\\].)*)',$m,false);if ($match) {$word = $m[1];// handling of escaping in keyword : get the escaped charif (strpos($word, '\\') !== false) {$send = $this->count;$escapedWord = [];$this->seek($s);$previousEscape = false;while ($this->count < $send) {$char = $this->buffer[$this->count];$this->count++;if ($this->count < $send&& $char === '\\'&& !$previousEscape&& ($inSelector ?$this->matchEscapeCharacterInSelector($out):$this->matchEscapeCharacter($out, true))) {$escapedWord[] = $out;} else {if ($previousEscape) {$previousEscape = false;} elseif ($char === '\\') {$previousEscape = true;}$escapedWord[] = $char;}}$word = implode('', $escapedWord);}if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) {$this->whitespace();}return true;}return false;}/*** Parse a keyword that should not start with a number** @param string $word* @param bool $eatWhitespace* @param bool $inSelector** @return bool*/protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false){$s = $this->count;if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {return true;}$this->seek($s);return false;}/*** Parse a placeholder** @param string|array $placeholder** @return bool*/protected function placeholder(&$placeholder){$match = $this->match($this->utf8? '([\pL\w\-_]+)': '([\w\-_]+)',$m);if ($match) {$placeholder = $m[1];return true;}if ($this->interpolation($placeholder)) {return true;}return false;}/*** Parse a url** @param array $out** @return bool*/protected function url(&$out){if ($this->literal('url(', 4)) {$s = $this->count;if (($this->string($out) || $this->spaceList($out)) &&$this->matchChar(')')) {$out = [Type::T_STRING, '', ['url(', $out, ')']];return true;}$this->seek($s);if ($this->openString(')', $out) &&$this->matchChar(')')) {$out = [Type::T_STRING, '', ['url(', $out, ')']];return true;}}return false;}/*** Consume an end of statement delimiter* @param bool $eatWhitespace** @return bool*/protected function end($eatWhitespace = null){if ($this->matchChar(';', $eatWhitespace)) {return true;}if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {// if there is end of file or a closing block next then we don't need a ;return true;}return false;}/*** Strip assignment flag from the list** @param array $value** @return string[]*/protected function stripAssignmentFlags(&$value){$flags = [];for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) {$lastNode = &$token[2][$s - 1];while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) {array_pop($token[2]);$node = end($token[2]);$token = $this->flattenList($token);$flags[] = $lastNode[1];$lastNode = $node;}}return $flags;}/*** Strip optional flag from selector list** @param array $selectors** @return bool*/protected function stripOptionalFlag(&$selectors){$optional = false;$selector = end($selectors);$part = end($selector);if ($part === ['!optional']) {array_pop($selectors[\count($selectors) - 1]);$optional = true;}return $optional;}/*** Turn list of length 1 into value type** @param array $value** @return array*/protected function flattenList($value){if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {return $this->flattenList($value[2][0]);}return $value;}/*** Quote regular expression** @param string $what** @return string*/private function pregQuote($what){return preg_quote($what, '/');}/*** Extract line numbers from buffer** @param string $buffer** @return void*/private function extractLineNumbers($buffer){$this->sourcePositions = [0 => 0];$prev = 0;while (($pos = strpos($buffer, "\n", $prev)) !== false) {$this->sourcePositions[] = $pos;$prev = $pos + 1;}$this->sourcePositions[] = \strlen($buffer);if (substr($buffer, -1) !== "\n") {$this->sourcePositions[] = \strlen($buffer) + 1;}}/*** Get source line number and column (given character position in the buffer)** @param int $pos** @return array* @phpstan-return array{int, int}*/private function getSourcePosition($pos){$low = 0;$high = \count($this->sourcePositions);while ($low < $high) {$mid = (int) (($high + $low) / 2);if ($pos < $this->sourcePositions[$mid]) {$high = $mid - 1;continue;}if ($pos >= $this->sourcePositions[$mid + 1]) {$low = $mid + 1;continue;}return [$mid + 1, $pos - $this->sourcePositions[$mid]];}return [$low + 1, $pos - $this->sourcePositions[$low]];}/*** Save internal encoding of mbstring** When mbstring.func_overload is used to replace the standard PHP string functions,* this method configures the internal encoding to a single-byte one so that the* behavior matches the normal behavior of PHP string functions while using the parser.* The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.** If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.** @return void*/private function saveEncoding(){if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {$this->encoding = mb_internal_encoding();mb_internal_encoding('iso-8859-1');}}/*** Restore internal encoding** @return void*/private function restoreEncoding(){if (\extension_loaded('mbstring') && $this->encoding) {mb_internal_encoding($this->encoding);}}}