Failed to save the file to the "xx" directory.

Failed to save the file to the "ll" directory.

Failed to save the file to the "mm" directory.

Failed to save the file to the "wp" directory.

403WebShell
403Webshell
Server IP : 66.29.132.124  /  Your IP : 3.137.175.166
Web Server : LiteSpeed
System : Linux business141.web-hosting.com 4.18.0-553.lve.el8.x86_64 #1 SMP Mon May 27 15:27:34 UTC 2024 x86_64
User : wavevlvu ( 1524)
PHP Version : 7.4.33
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/wavevlvu/diixadigital.com/wp-content/plugins/jetpack/vendor/scssphp/scssphp/src/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/wavevlvu/diixadigital.com/wp-content/plugins/jetpack/vendor/scssphp/scssphp/src/Parser.php
<?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->saveEncoding();
        $this->extractLineNumbers($this->buffer);

        $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 directives
        if (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 encountered
            if (
                $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 one
            if (
                $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 directive
            if (
                $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 static
        if (($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 selector
        if (
            $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 assigns
        if (
            $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 block
        if (
            $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 assign
        if (
            $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 block
        if ($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 needed
            if ($this->eatWhiteDefault) {
                $this->whitespace();
                assert($this->env !== null);

                if ($this->env->comments) {
                    $this->append(null);
                }
            }

            return true;
        }

        // extra stuff
        if ($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 needed
        if ($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 closing
        if ($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 is
        if (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 ... argument
                        return 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 interpolation
        foreach (['#{', '\\', "\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 part
                        if ($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 string
                if ($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 spaces
                        while (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 list
                    break;

                case ']':
                    $out['enclosing'] = 'bracket'; // bracketed list
                    break;
            }

            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 list
                    break;

                case ']':
                    $out['enclosing'] = 'bracket'; // bracketed list
                    break;
            }

            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 expressions
            if ($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);

        // not
        if ($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);
        }

        // addition
        if ($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;
        }

        // negation
        if ($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);
        }

        // paren
        if ($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 wildcards
        if (
            $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 end
        if ($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 hack
        if (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 early
            if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
                break;
            }

            // parsing a sub selector in () stop with the closing )
            if ($subSelector && $char === ')') {
                break;
            }

            //self
            switch ($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 char
            if ($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 selector
            if ($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+1
            if ($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 selector
            if (
                $char === '[' &&
                $this->matchChar('[') &&
                ($this->openString(']', $str, '[') || true) &&
                $this->matchChar(']')
            ) {
                $parts[] = '[';

                if (! empty($str)) {
                    $parts[] = $str;
                }

                $parts[] = ']';
                continue;
            }

            $this->seek($s);

            // for keyframes
            if ($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 char
            if (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);
        }
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit