Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
<?php
>
/** * SCSSPHP *
< * @copyright 2012-2019 Leaf Corcoran
> * @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; < use ScssPhp\ScssPhp\Cache; < use ScssPhp\ScssPhp\Compiler;
> 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\Node; < use ScssPhp\ScssPhp\Type;
> 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
> * @var array<string, int>
*/ protected static $precedence = [ '=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3,
< '<=>' => 3,
'<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6, ];
> /** protected static $commentPattern; > * @var string protected static $operatorPattern; > */
protected static $whitePattern;
> /** > * @var string protected $cache; > */
> /** private $sourceName; > * @var string private $sourceIndex; > */
private $sourcePositions;
> /** private $charset; > * @var Cache|null private $count; > */
private $env;
> /** private $inParens; > * @var array<int, int> private $eatWhiteDefault; > */
< private $charset;
> /** > * The current offset in the buffer > * > * @var int > */
private $buffer;
> /** private $utf8; > * @var Block|null private $encoding; > */
private $patternModifiers;
> /** private $commentsSeen; > * @var bool > */
/**
> /** * Constructor > * @var bool * > */
* @api
> /** * > * @var bool * @param string $sourceName > */
* @param integer $sourceIndex
> private $allowVars; * @param string $encoding > /** * @param \ScssPhp\ScssPhp\Cache $cache > * @var string */ > */
public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
> /** { > * @var string|null $this->sourceName = $sourceName ?: '(stdin)'; > */
$this->sourceIndex = $sourceIndex;
> private $cssOnly; $this->charset = null; > $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; > /** $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; > * @var LoggerInterface $this->commentsSeen = []; > */ $this->discardComments = false; > private $logger; >
< * @param string $sourceName < * @param integer $sourceIndex < * @param string $encoding < * @param \ScssPhp\ScssPhp\Cache $cache
> * @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 = null)
> public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null)
< $this->charset = null;
< $this->discardComments = false;
> $this->allowVars = true; > $this->cssOnly = $cssOnly; > $this->logger = $logger ?: new QuietLogger();
< static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
> static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
static::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; }
< if ($cache) {
$this->cache = $cache; }
< }
/** * Get source file name * * @api * * @return string */ public function getSourceName() { return $this->sourceName; } /** * Throw parser error * * @api * * @param string $msg *
< * @throws \ScssPhp\ScssPhp\Exception\ParserException
> * @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( list($line, $column) = $this->getSourcePosition($this->count); > 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', > E_USER_DEPRECATED $loc = empty($this->sourceName) > ); ? "line: $line, column: $column" > : "$this->sourceName on line $line, at column $column"; > throw $this->parseError($msg); > } if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { > throw new ParserException("$msg: failed at `$m[1]` $loc"); > /** } > * Creates a parser error > * throw new ParserException("$msg: $loc"); > * @api } > * > * @param string $msg /** > * * Parser buffer > * @return ParserException * > */ * @api > public function parseError($msg = 'parse error') * > {
< if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { < throw new ParserException("$msg: failed at `$m[1]` $loc");
> 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;
< throw new ParserException("$msg: $loc");
> $this->restoreEncoding(); > > $e = new ParserException("$msg: $loc"); > $e->setSourcePosition([$this->sourceName, $line, $column]); > > return $e;
< * @return \ScssPhp\ScssPhp\Block
> * @return Block
public function parse($buffer) { if ($this->cache) {
< $cacheKey = $this->sourceName . ":" . md5($buffer);
> $cacheKey = $this->sourceName . ':' . md5($buffer);
$parseOptions = [
< 'charset' => $this->charset,
'utf8' => $this->utf8, ];
< $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
> $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
< if (! is_null($v)) {
> 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); $this->pushBlock(null); // root block $this->whitespace(); $this->pushBlock(null); $this->popBlock(); while ($this->parseChunk()) { ; }
< if ($this->count !== strlen($this->buffer)) { < $this->throwParseError();
> if ($this->count !== \strlen($this->buffer)) { > throw $this->parseError();
} if (! empty($this->env->parent)) {
< $this->throwParseError('unclosed block'); < } < < if ($this->charset) { < array_unshift($this->env->children, $this->charset);
> throw $this->parseError('unclosed block');
} $this->restoreEncoding();
> assert($this->env !== null);
if ($this->cache) {
< $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
> $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 boolean
> * @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); $this->restoreEncoding(); return $list; } /** * Parse a selector or selector list * * @api * * @param string $buffer * @param string|array $out
> * @param bool $shouldValidate
*
< * @return boolean
> * @return bool
*/
< public function parseSelector($buffer, &$out)
> 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); > $selector = $this->selectors($out); > // discard space/comments at the start > $this->discardComments = true; $this->restoreEncoding(); > $this->whitespace(); > $this->discardComments = false;
return $selector;
> if ($shouldValidate && $this->count !== strlen($buffer)) { } > throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); > } /** >
* Parse a media Query * * @api * * @param string $buffer
< * @param string|array $out
> * @param array $out
*
< * @return boolean
> * @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 boolean
> * @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) &&
> if ( > $this->literal('@at-root', 8) &&
($this->selectors($selector) || true) && ($this->map($with) || true) &&
< (($this->matchChar('(') < && $this->interpolation($with) < && $this->matchChar(')')) || true) &&
> (($this->matchChar('(') && > $this->interpolation($with) && > $this->matchChar(')')) || true) &&
$this->matchChar('{', false) ) {
< $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
> ! $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 = $this->pushSpecialBlock(Type::T_MEDIA, $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) &&
> if ( > $this->literal('@mixin', 6) &&
$this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->matchChar('{', false) ) {
< $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
> ! $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) &&
> if ( > ($this->literal('@include', 8) &&
$this->keyword($mixinName) && ($this->matchChar('(') && ($this->argValues($argValues) || true) && $this->matchChar(')') || true) &&
< ($this->end() ||
> ($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 = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
> $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) &&
> if ( > $this->literal('@scssphp-import-once', 20) &&
$this->valueList($importPath) && $this->end() ) {
> ! $this->cssOnly || $this->assertPlainCssValid(false, $s); $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); > > list($line, $column) = $this->getSourcePosition($s); return true; > $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->seek($s);
< if ($this->literal('@import', 7) &&
> if ( > $this->literal('@import', 7) &&
$this->valueList($importPath) &&
> $importPath[0] !== Type::T_FUNCTION_CALL &&
$this->end() ) {
> if ($this->cssOnly) { $this->append([Type::T_IMPORT, $importPath], $s); > $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); > $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); return true; > return true; } > } >
$this->seek($s);
< if ($this->literal('@import', 7) &&
> if ( > $this->literal('@import', 7) &&
$this->url($importPath) && $this->end() ) {
> if ($this->cssOnly) { $this->append([Type::T_IMPORT, $importPath], $s); > $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); > $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); return true; > return true; } > } >
$this->seek($s);
< if ($this->literal('@extend', 7) &&
> 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) &&
> if ( > $this->literal('@function', 9) &&
$this->keyword($fnName) && $this->argumentDef($args) && $this->matchChar('{', false) ) {
< $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
> ! $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('@break', 6) && $this->end()) { < $this->append([Type::T_BREAK], $s); < < return true; < } < < $this->seek($s); < < if ($this->literal('@continue', 9) && $this->end()) { < $this->append([Type::T_CONTINUE], $s); < < return true; < } < < $this->seek($s);
> if ( > $this->literal('@return', 7) && > ($this->valueList($retVal) || true) && > $this->end() > ) { > ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
< if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
$this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); return true; } $this->seek($s);
< if ($this->literal('@each', 5) &&
> if ( > $this->literal('@each', 5) &&
$this->genericList($varNames, 'variable', ',', false) && $this->literal('in', 2) && $this->valueList($list) && $this->matchChar('{', false) ) {
< $each = $this->pushSpecialBlock(Type::T_EACH, $s);
> ! $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) &&
> if ( > $this->literal('@while', 6) &&
$this->expression($cond) && $this->matchChar('{', false) ) {
< $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
> ! $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) &&
> 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) ) {
< $for = $this->pushSpecialBlock(Type::T_FOR, $s);
> ! $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->valueList($cond) && $this->matchChar('{', false)) { < $if = $this->pushSpecialBlock(Type::T_IF, $s); < while ($cond[0] === Type::T_LIST < && !empty($cond['enclosing']) < && $cond['enclosing'] === 'parent' < && count($cond[2]) == 1) {
> 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->valueList($value) && < $this->end()
> 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->valueList($value) && < $this->end()
> 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->valueList($value) && < $this->end()
> 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)) < < if ($this->literal('@content', 8) &&
> if ( > $this->literal('@content', 8) &&
($this->end() || $this->matchChar('(') && $this->argValues($argContent) && $this->matchChar(')') &&
< $this->end())) {
> $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 = $this->pushSpecialBlock(Type::T_ELSE, $s); < } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) { < $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
> $else = new ElseBlock(); > } elseif ( > $this->literal('if', 2) && > $this->functionCallArgumentsList($cond, false, '{', false) > ) { > $else = new ElseifBlock();
$else->cond = $cond; } if (isset($else)) {
< $else->dontAppend = true;
> $this->registerPushedBlock($else, $s);
$if->cases[] = $else; return true; } } $this->seek($s); } // only retain the first @charset directive encountered
< if ($this->literal('@charset', 8) &&
> if ( > $this->literal('@charset', 8) &&
$this->valueList($charset) && $this->end() ) {
< if (! isset($this->charset)) { < $statement = [Type::T_CHARSET, $charset]; < < list($line, $column) = $this->getSourcePosition($s); < < $statement[static::SOURCE_LINE] = $line; < $statement[static::SOURCE_COLUMN] = $column; < $statement[static::SOURCE_INDEX] = $this->sourceIndex; < < $this->charset = $statement; < } <
return true; } $this->seek($s);
< if ($this->literal('@supports', 9) &&
> if ( > $this->literal('@supports', 9) &&
($t1=$this->supportsQuery($supportQuery)) && ($t2=$this->matchChar('{', false)) ) {
< $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
> $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->keyword($dirName) && < ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && < $this->matchChar('{', false)
> if ( > $this->matchChar('@', false) && > $this->mixedKeyword($dirName) && > $this->directiveValue($dirValue, '{')
) {
> if (count($dirName) === 1 && is_string(reset($dirName))) { if ($dirName === 'media') { > $dirName = reset($dirName); $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); > } else { } else { > $dirName = [Type::T_STRING, '', $dirName]; $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); > }
< $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
> $directive = new MediaBlock();
< $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
> $directive = new DirectiveBlock();
> $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->keyword($dirName) && < $this->valueList($dirValue) && < $this->end()
> if ( > $this->matchChar('@', false) && > $this->mixedKeyword($dirName) && > ! $this->isKnownGenericDirective($dirName) && > ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))
) {
< $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
> 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; // property shortcut > if ($this->cssOnly) { // captures most properties before having to parse a selector > $inCssSelector = (! empty($this->env->parent) && if ($this->keyword($name, false) && > ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); $this->literal(': ', 2) && > } $this->valueList($value) && > // custom properties : right part is static $this->end() > if (($this->customProperty($name) ) && $this->matchChar(':', false)) { ) { > $start = $this->count; $name = [Type::T_STRING, '', [$name]]; > $this->append([Type::T_ASSIGN, $name, $value], $s); > // but can be complex and finish with ; or } > foreach ([';','}'] as $ending) { return true; > if ( } > $this->openString($ending, $stringValue, '(', ')', false) && > $this->end() $this->seek($s); > ) { > $end = $this->count; // variable assigns > $value = $stringValue; if ($this->variable($name) && > $this->matchChar(':') && > // check if we have only a partial value due to nested [] or { } to take in account $this->valueList($value) && > $nestingPairs = [['[', ']'], ['{', '}']]; $this->end() > ) { > foreach ($nestingPairs as $nestingPair) { // check for '!flag' > $p = strpos($this->buffer, $nestingPair[0], $start); $assignmentFlags = $this->stripAssignmentFlags($value); > $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); > if ($p && $p < $end) { > $this->seek($start); return true; > } > if ( > $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && $this->seek($s); > $this->end() && > $this->count > $end // misc > ) { if ($this->literal('-->', 3)) { > $end = $this->count; return true; > $value = $stringValue; } > } > } // opening css block > } if ($this->selectors($selectors) && $this->matchChar('{', false)) { > $this->pushBlock($selectors, $s); > $this->seek($end); > $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); if ($this->eatWhiteDefault) { > $this->whitespace(); > return true; $this->append(null); // collect comments at the beginning if needed > } } > } > return true; > // TODO: output an error here if nothing found according to sass spec } > } > $this->seek($s); > $this->seek($s); >
< if ($this->keyword($name, false) &&
> if ( > $this->keyword($name, false) &&
< if ($this->variable($name) &&
> if ( > $this->variable($name) &&
$foundSomething = false;
> ! $this->cssOnly || $this->assertPlainCssValid(false, $s); >
< // misc < if ($this->literal('-->', 3)) { < return true; < } <
< if ($this->selectors($selectors) && $this->matchChar('{', false)) {
> if ( > $this->selectors($selectors) && > $this->matchChar('{', false) > ) { > ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); >
< if ($this->propertyName($name) && $this->matchChar(':')) {
> if ( > $this->propertyName($name) && > $this->matchChar(':') > ) {
< $this->throwParseError('expected "{"');
> throw $this->parseError('expected "{"');
if ($this->matchChar('{', false)) {
< $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
> ! $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 (isset($block->type) && $block->type === Type::T_INCLUDE) {
> if ($block instanceof ContentBlock) {
$include = $block->child;
> assert(\is_array($include));
unset($block->child); $include[3] = $block; $this->append($include, $s);
< } elseif (empty($block->dontAppend)) {
> } 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(';') || < $this->literal('<!--', 4) < ) {
> if ($this->matchChar(';')) {
return true; } return false; } /** * Push block onto parse tree *
< * @param array $selectors < * @param integer $pos
> * @param array|null $selectors > * @param int $pos
*
< * @return \ScssPhp\ScssPhp\Block
> * @return Block
*/ protected function pushBlock($selectors, $pos = 0) {
> $b = new Block(); list($line, $column) = $this->getSourcePosition($pos); > $b->selectors = $selectors; > $b = new Block; > $this->registerPushedBlock($b, $pos); $b->sourceName = $this->sourceName; > $b->sourceLine = $line; > return $b; $b->sourceColumn = $column; > } $b->sourceIndex = $this->sourceIndex; > $b->selectors = $selectors; > /** $b->comments = []; > * @param Block $b $b->parent = $this->env; > * @param int $pos > * if (! $this->env) { > * @return void $b->children = []; > */ } elseif (empty($this->env->children)) { > private function registerPushedBlock(Block $b, $pos) $this->env->children = $this->env->comments; > {
< $b = new Block;
< $b->selectors = $selectors;
} 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); } }
< < return $b;
} /** * Push special (named) block onto parse tree *
> * @deprecated * @param string $type > *
< * @param integer $pos
> * @param int $pos
*
< * @return \ScssPhp\ScssPhp\Block
> * @return Block
*/ protected function pushSpecialBlock($type, $pos) { $block = $this->pushBlock(null, $pos); $block->type = $type; return $block; } /** * Pop scope and return last block *
< * @return \ScssPhp\ScssPhp\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)) {
< $this->throwParseError('unexpected }');
> 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 integer $from
> * @param int $from
*
< * @return integer
> * @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, null, $from);
> $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 integer $where
> * @param int $where > * > * @return void
*/ protected function seek($where) { $this->count = $where; } /**
> * Assert a parsed part is plain CSS Valid * Match string looking for either ending delim, escape, or string interpolation > * * > * @param array|false $parsed * {@internal This is a workaround for preg_match's 250K string match limit. }} > * @param int $startPos * > * * @param array $m Matches (passed by reference) > * @return array * @param string $delim Delimeter > * * > * @throws ParserException * @return boolean True if match; false otherwise > */ */ > protected function assertPlainCssValid($parsed, $startPos = null) protected function matchString(&$m, $delim) > { { > $type = ''; $token = null; > if ($parsed) { > $type = $parsed[0]; $end = strlen($this->buffer); > $parsed = $this->isPlainCssValidElement($parsed); > } // look for either ending delim, escape, or string interpolation > if (! $parsed) { foreach (['#{', '\\', $delim] as $lookahead) { > if (! \is_null($startPos)) { $pos = strpos($this->buffer, $lookahead, $this->count); > $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos)); > $message = "Error : `{$plain}` isn't allowed in plain CSS"; if ($pos !== false && $pos < $end) { > } else { $end = $pos; > $message = 'Error: SCSS syntax not allowed in CSS file'; $token = $lookahead; > } } > if ($type) { } > $message .= " ($type)"; > } if (! isset($token)) { > throw $this->parseError($message); return false; > } } > > return $parsed; $match = substr($this->buffer, $this->count, $end - $this->count); > } $m = [ > $match . $token, > /** $match, > * Check a parsed element is plain CSS Valid $token > * ]; > * @param array $parsed $this->count = $end + strlen($token); > * @param bool $allowExpression > * return true; > * @return array|false } > */ > protected function isPlainCssValidElement($parsed, $allowExpression = false) /** > { * Try to match something on head of buffer > // keep string as is * > if (is_string($parsed)) { * @param string $regex > return $parsed; * @param array $out > } * @param boolean $eatWhitespace > * > if ( * @return boolean > \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && */ > !\in_array($parsed[1], [ protected function match($regex, &$out, $eatWhitespace = null) > 'alpha', { > 'attr', $r = '/' . $regex . '/' . $this->patternModifiers; > 'calc', > 'cubic-bezier', if (! preg_match($r, $this->buffer, $out, null, $this->count)) { > 'env', return false; > 'grayscale', } > 'hsl', > 'hsla', $this->count += strlen($out[0]); > 'hwb', > 'invert', if (! isset($eatWhitespace)) { > 'linear-gradient', $eatWhitespace = $this->eatWhiteDefault; > 'min', } > 'max', > 'radial-gradient', if ($eatWhitespace) { > 'repeating-linear-gradient', $this->whitespace(); > 'repeating-radial-gradient', } > 'rgb', > 'rgba', return true; > 'rotate', } > 'saturate', > 'var', /** > ]) && * Match a single string > Compiler::isNativeFunction($parsed[1]) * > ) { * @param string $char > return false; * @param boolean $eatWhitespace > } * > * @return boolean > switch ($parsed[0]) { */ > case Type::T_BLOCK: protected function matchChar($char, $eatWhitespace = null) > case Type::T_KEYWORD: { > case Type::T_NULL: if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { > case Type::T_NUMBER: return false; > case Type::T_MEDIA: } > return $parsed; > $this->count++; > case Type::T_COMMENT: > if (isset($parsed[2])) { if (! isset($eatWhitespace)) { > return false; $eatWhitespace = $this->eatWhiteDefault; > } } > return $parsed; > if ($eatWhitespace) { > case Type::T_DIRECTIVE: $this->whitespace(); > if (\is_array($parsed[1])) { } > $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]); > if (! $parsed[1][1]) { return true; > return false; } > } > } /** > * Match literal string > return $parsed; * > * @param string $what > case Type::T_IMPORT: * @param integer $len > if ($parsed[1][0] === Type::T_LIST) { * @param boolean $eatWhitespace > return false; * > } * @return boolean > $parsed[1] = $this->isPlainCssValidElement($parsed[1]); */ > if ($parsed[1] === false) { protected function literal($what, $len, $eatWhitespace = null) > return false; { > } if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { > return $parsed; return false; > } > case Type::T_STRING: > foreach ($parsed[2] as $k => $substr) { $this->count += $len; > if (\is_array($substr)) { > $parsed[2][$k] = $this->isPlainCssValidElement($substr); if (! isset($eatWhitespace)) { > if (! $parsed[2][$k]) { $eatWhitespace = $this->eatWhiteDefault; > return false; } > } > } if ($eatWhitespace) { > } $this->whitespace(); > return $parsed; } > > case Type::T_LIST: return true; > if (!empty($parsed['enclosing'])) { } > return false; > } /** > foreach ($parsed[2] as $k => $listElement) { * Match some whitespace > $parsed[2][$k] = $this->isPlainCssValidElement($listElement); * > if (! $parsed[2][$k]) { * @return boolean > return false; */ > } protected function whitespace() > } { > return $parsed; $gotWhite = false; > > case Type::T_ASSIGN: while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { > foreach ([1, 2, 3] as $k) { if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { > if (! empty($parsed[$k])) { // comment that are kept in the output CSS > $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]); $comment = []; > if (! $parsed[$k]) { $startCommentCount = $this->count; > return false; $endCommentCount = $this->count + strlen($m[1]); > } > } // find interpolations in comment > } $p = strpos($this->buffer, '#{', $this->count); > return $parsed; > while ($p !== false && $p < $endCommentCount) { > case Type::T_EXPRESSION: $c = substr($this->buffer, $this->count, $p - $this->count); > list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; $comment[] = $c; > if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { $this->count = $p; > return false; $out = null; > } > $lhs = $this->isPlainCssValidElement($lhs, true); if ($this->interpolation($out)) { > if (! $lhs) { // keep right spaces in the following string part > return false; if ($out[3]) { > } while ($this->buffer[$this->count-1] !== '}') { > $rhs = $this->isPlainCssValidElement($rhs, true); $this->count--; > if (! $rhs) { } > return false; > } $out[3] = ''; > } > return [ > Type::T_STRING, $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out]; > '', [ } else { > $this->inParens ? '(' : '', $comment[] = substr($this->buffer, $this->count, 2); > $lhs, > ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''), $this->count += 2; > $rhs, } > $this->inParens ? ')' : '' > ] $p = strpos($this->buffer, '#{', $this->count); > ]; } > > case Type::T_CUSTOM_PROPERTY: // remaining part > case Type::T_UNARY: $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); > $parsed[2] = $this->isPlainCssValidElement($parsed[2]); > if (! $parsed[2]) { if (! $comment) { > return false; // single part static comment > } $this->appendComment([Type::T_COMMENT, $c]); > return $parsed; } else { > $comment[] = $c; > case Type::T_FUNCTION: $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount); > $argsList = $parsed[2]; $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]); > foreach ($argsList[2] as $argElement) { } > if (! $this->isPlainCssValidElement($argElement)) { > return false; $this->commentsSeen[$startCommentCount] = true; > } $this->count = $endCommentCount; > } } else { > return $parsed; // comment that are ignored and not kept in the output css > $this->count += strlen($m[0]); > case Type::T_FUNCTION_CALL: } > $parsed[0] = Type::T_FUNCTION; > $argsList = [Type::T_LIST, ',', []]; $gotWhite = true; > foreach ($parsed[2] as $arg) { } > if ($arg[0] || ! empty($arg[2])) { > // no named arguments possible in a css function call return $gotWhite; > // nor ... argument } > return false; > } /** > $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc'); * Append comment to current block > if (! $arg) { * > return false; * @param array $comment > } */ > $argsList[2][] = $arg; protected function appendComment($comment) > } { > $parsed[2] = $argsList; if (! $this->discardComments) { > return $parsed; if ($comment[0] === Type::T_COMMENT) { > } if (is_string($comment[1])) { > $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1); > return false; } > } if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) { > foreach ($comment[2][2] as $k => $v) { > /**
< * @param string $delim Delimeter
> * @param string $delim Delimiter
< * @return boolean True if match; false otherwise
> * @return bool True if match; false otherwise > * > * @phpstan-impure
< $end = strlen($this->buffer);
> $end = \strlen($this->buffer);
< foreach (['#{', '\\', $delim] as $lookahead) {
> foreach (['#{', '\\', "\r", $delim] as $lookahead) {
< $this->count = $end + strlen($token);
> $this->count = $end + \strlen($token);
< * @param boolean $eatWhitespace
> * @param bool $eatWhitespace > * > * @return bool
< * @return boolean
> * @phpstan-impure
< if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
> if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {
< $this->count += strlen($out[0]);
> $this->count += \strlen($out[0]);
< * @param boolean $eatWhitespace
> * @param bool $eatWhitespace
< * @return boolean
> * @return bool > * > * @phpstan-impure
< * @param integer $len < * @param boolean $eatWhitespace
> * @param int $len > * @param bool $eatWhitespace > * > * @return bool
< * @return boolean
> * @phpstan-impure
< * @return boolean
> * @return bool > * > * @phpstan-impure
< while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
> while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
< $endCommentCount = $this->count + strlen($m[1]);
> $endCommentCount = $this->count + \strlen($m[1]);
*
> list($line, $column) = $this->getSourcePosition($this->count); * @param array $statement > $file = $this->sourceName; * @param integer $pos > 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); protected function append($statement, $pos = null) > }
< $this->appendComment([Type::T_COMMENT, $c]);
> $commentStatement = [Type::T_COMMENT, $c];
< $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
> $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];
if (! is_null($pos)) {
> list($line, $column) = $this->getSourcePosition($startCommentCount); list($line, $column) = $this->getSourcePosition($pos); > $commentStatement[self::SOURCE_LINE] = $line; > $commentStatement[self::SOURCE_COLUMN] = $column; $statement[static::SOURCE_LINE] = $line; > $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; $statement[static::SOURCE_COLUMN] = $column; > $statement[static::SOURCE_INDEX] = $this->sourceIndex; > $this->appendComment($commentStatement); } >
< $this->count += strlen($m[0]);
> $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]));
$this->env->children[] = $statement;
> * } > * @return void
< if (! $this->discardComments) { < if ($comment[0] === Type::T_COMMENT) { < if (is_string($comment[1])) { < $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1); < } < if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) { < foreach ($comment[2][2] as $k => $v) { < if (is_string($v)) { < $p = strpos($v, "\n"); < if ($p !== false) { < $comment[2][2][$k] = substr($v, 0, $p + 1) < . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1)); < } < } < } < } < }
> assert($this->env !== null);
> if (! $this->discardComments) {
< * @param array $statement < * @param integer $pos
> * @param array|null $statement > * @param int $pos > * > * @return void
< if (! is_null($statement)) { < if (! is_null($pos)) {
> assert($this->env !== null); > > if (! \is_null($statement)) { > ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); > > if (! \is_null($pos)) {
< $i = count($this->env->children) - 1;
> assert($this->env !== null); > > $i = \count($this->env->children) - 1;
/**
> * Parse media query list > return null;
* * @param array $out *
< * @return boolean
> * @return bool
*/ protected function mediaQueryList(&$out) { return $this->genericList($out, 'mediaQuery', ',', false); } /** * Parse media query * * @param array $out *
< * @return boolean
> * @return bool
*/ protected function mediaQuery(&$out) { $expressions = null; $parts = [];
< if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
> 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)) {
> 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)) {
> if (\is_array($expressions)) {
$parts = array_merge($parts, $expressions[2]); } } $out = $parts; return true; } /** * Parse supports query * * @param array $out *
< * @return boolean
> * @return bool
*/ protected function supportsQuery(&$out) { $expressions = null; $parts = []; $s = $this->count; $not = false;
< if (($this->literal('not', 3) && ($not = true) || true) &&
> 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('(') &&
> 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) &&
> 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) &&
> 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)) {
> 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)) {
> 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)) {
> 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 (\count($parts)) {
if ($this->eatWhiteDefault) { $this->whitespace(); } $out = [Type::T_STRING, '', $parts]; return true; } return false; } /** * Parse media expression * * @param array $out *
< * @return boolean
> * @return bool
*/ protected function mediaExpression(&$out) { $s = $this->count; $value = null;
< if ($this->matchChar('(') &&
> if ( > $this->matchChar('(') &&
$this->expression($feature) &&
< ($this->matchChar(':') && $this->expression($value) || true) &&
> ($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 boolean
> * @return bool
*/ protected function argValues(&$out) {
> $discardComments = $this->discardComments; if ($this->genericList($list, 'argValue', ',', false)) { > $this->discardComments = true; $out = $list[2]; >
> $this->discardComments = $discardComments; return true; >
}
> $this->discardComments = $discardComments; return false; >
} /** * Parse argument value * * @param array $out *
< * @return boolean
> * @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')) {
> 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 * Parse comma separated value list > * @param mixed $directiveName * > * @return bool * @param array $out > */ * > protected function isKnownGenericDirective($directiveName) * @return boolean > { */ > if (\is_array($directiveName) && \is_string(reset($directiveName))) { protected function valueList(&$out) > $directiveName = reset($directiveName); { > } $discardComments = $this->discardComments; > if (! \is_string($directiveName)) { $this->discardComments = true; > return false; $res = $this->genericList($out, 'spaceList', ','); > } $this->discardComments = $discardComments; > if ( > \in_array($directiveName, [ return $res; > 'at-root', } > 'media', > 'mixin', /** > 'include', * Parse space separated value list > 'scssphp-import-once', * > 'import', * @param array $out > 'extend', * > 'function', * @return boolean > 'break', */ > 'continue', protected function spaceList(&$out) > 'return', { > 'each', return $this->genericList($out, 'expression'); > 'while', } > 'for', > 'if', /** > 'debug', * Parse generic list > 'warn', * > 'error', * @param array $out > 'content', * @param callable $parseItem > 'else', * @param string $delim > 'charset', * @param boolean $flatten > 'supports', * > // Todo * @return boolean > 'use', */ > 'forward', protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) > ]) { > ) { $s = $this->count; > return true; $items = []; > } $value = null; > return false; > } while ($this->$parseItem($value)) { > $trailing_delim = false; > /** $items[] = $value; > * Parse directive value list that considers $vars as keyword > * if ($delim) { > * @param array $out if (! $this->literal($delim, strlen($delim))) { > * @param string|false $endChar break; > * } > * @return bool $trailing_delim = true; > * } > * @phpstan-impure } > */ > protected function directiveValue(&$out, $endChar = false) if (! $items) { > { $this->seek($s); > $s = $this->count; > return false; > if ($this->variable($out)) { } > if ($endChar && $this->matchChar($endChar, false)) { > return true; if ($trailing_delim) { > } $items[] = [Type::T_NULL]; > } > if (! $endChar && $this->end()) { if ($flatten && count($items) === 1) { > return true; $out = $items[0]; > } } else { > } $out = [Type::T_LIST, $delim, $items]; > } > $this->seek($s); > return true; > if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) { } > if ($endChar && $this->matchChar($endChar, false)) { > return true; /** > } * Parse expression > $ss = $this->count; * > if (!$endChar && $this->end()) { * @param array $out > $this->seek($ss); * @param bool $listOnly > return true; * @param bool $lookForExp > } * > } * @return boolean > */ > $this->seek($s); protected function expression(&$out, $listOnly = false, $lookForExp = true) > { > $allowVars = $this->allowVars; $s = $this->count; > $this->allowVars = false; $discard = $this->discardComments; > $this->discardComments = true; > $res = $this->genericList($out, 'spaceList', ','); $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]); > $this->allowVars = $allowVars; > if ($this->matchChar('(')) { > if ($res) { if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) { > if ($endChar && $this->matchChar($endChar, false)) { if ($lookForExp) { > return true; $out = $this->expHelper($lhs, 0); > } } else { > $out = $lhs; > if (! $endChar && $this->end()) { } > return true; > } $this->discardComments = $discard; > } > return true; > $this->seek($s); } > > if ($endChar && $this->matchChar($endChar, false)) { $this->seek($s); > return true; } > } > if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { > return false; if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) { > } if ($lookForExp) { > $out = $this->expHelper($lhs, 0); > /**
< * @return boolean
> * @return bool
$out = $lhs;
> * Parse a function call, where externals () are part of the call } > * and not of the value list $this->discardComments = $discard; > * > * @param array $out return true; > * @param bool $mandatoryEnclos } > * @param null|string $charAfter > * @param null|bool $eatWhiteSp $this->seek($s); > * } > * @return bool > */ if (!$listOnly && $this->value($lhs)) { > protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) if ($lookForExp) { > { $out = $this->expHelper($lhs, 0); > $s = $this->count; } else { > $out = $lhs; > if ( } > $this->matchChar('(') && > $this->valueList($out) && $this->discardComments = $discard; > $this->matchChar(')') && > ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) return true; > ) { } > return true; > } $this->discardComments = $discard; > return false; > if (! $mandatoryEnclos) { } > $this->seek($s); > /** > if ( * Parse expression specifically checking for lists in parenthesis or brackets > $this->valueList($out) && * > ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end()) * @param array $out > ) { * @param integer $s > return true; * @param string $closingParen > } * @param array $allowedTypes > } * > * @return boolean > $this->seek($s); */ > protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP]) > return false; { > } if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) { > $out = [Type::T_LIST, '', []]; > /**
< * @return boolean
> * @return bool
< * @param callable $parseItem
> * @param string $parseItem The name of the method used to parse items
< * @param boolean $flatten
> * @param bool $flatten
< * @return boolean
> * @return bool
case "]":
> /** @var array|Number|null $value */
< if (! $this->literal($delim, strlen($delim))) {
> if (! $this->literal($delim, \strlen($delim))) {
break;
>
}
> } else { return true; > 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 ($this->valueList($out) && $this->matchChar($closingParen) > if ($value[0] === Type::T_KEYWORD) { && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD]) > assert(\is_array($value)); && in_array(Type::T_LIST, $allowedTypes)) { > /** @var string $word */ if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) { > $word = $value[1]; $out = [Type::T_LIST, '', [$out]]; > } > $last_char = substr($word, -1); switch ($closingParen) { > case ")": > if ( $out['enclosing'] = 'parent'; // parenthesis list > strlen($word) > 1 && break; > in_array($last_char, [ "'", '"']) && case "]": > substr($word, -2, 1) !== '\\' $out['enclosing'] = 'bracket'; // bracketed list > ) { break; > // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake } > $word = str_replace('\\' . $last_char, '\\\\', $word); return true; > if (strpos($word, $last_char) < strlen($word) - 1) { } > continue; > } $this->seek($s); > > $currentCount = $this->count; if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { > return true; > // let's try to rewind to previous char and try a parse } > $this->count--; > // in case the keyword also eat spaces return false; > while (substr($this->buffer, $this->count, 1) !== $last_char) { } > $this->count--; > } /** > * Parse left-hand side of subexpression > /** @var array|Number|null $nextValue */ * > $nextValue = null; * @param array $lhs > if ($this->$parseItem($nextValue)) { * @param integer $minP > assert(\is_array($nextValue) || $nextValue instanceof Number); * > if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) { * @return array > // bad try, forget it */ > $this->seek($currentCount); protected function expHelper($lhs, $minP) > continue; { > } $operators = static::$operatorPattern; > if ($nextValue[0] !== Type::T_STRING) { > // bad try, forget it $ss = $this->count; > $this->seek($currentCount); $whiteBefore = isset($this->buffer[$this->count - 1]) && > continue; ctype_space($this->buffer[$this->count - 1]); > } > while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { > // OK it was a good idea $whiteAfter = isset($this->buffer[$this->count]) && > $value[1] = substr($value[1], 0, -1); ctype_space($this->buffer[$this->count]); > array_pop($items); $varAfter = isset($this->buffer[$this->count]) && > $items[] = $value; $this->buffer[$this->count] === '$'; > $items[] = $nextValue; > } else { $this->whitespace(); > // bad try, forget it > $this->seek($currentCount); $op = $m[1]; > continue; > } // don't turn negative numbers into expressions > } if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { > }
< if ($flatten && count($items) === 1) {
> > if ($flatten && \count($items) === 1) {
< * @return boolean
> * @return bool > * > * @phpstan-impure
< if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
> if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
< if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { < if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
> if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) { > if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
}
>
>
< * @param integer $s
> * @param int $s
< * @param array $allowedTypes
> * @param string[] $allowedTypes > * > * @return bool
< * @return boolean
> * @phpstan-param array<Type::*> $allowedTypes
< protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
> protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP])
< if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) {
> if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {
$lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
>
< case ")":
> case ')':
< case "]":
> > case ']':
ctype_space($this->buffer[$this->count - 1]);
>
< if ($this->valueList($out) && $this->matchChar($closingParen) < && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD]) < && in_array(Type::T_LIST, $allowedTypes)) {
> if ( > $this->valueList($out) && > $this->matchChar($closingParen) && ! ($closingParen === ')' && > \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) && > \in_array(Type::T_LIST, $allowedTypes) > ) {
>
< case ")":
> case ')':
< case "]":
> > case ']':
>
< if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
> if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
< * @param integer $minP
> * @param int $minP
< while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
> while ($this->match($operators, $m, false) && static::$precedence[strtolower($m[1])] >= $minP) {
< // peek and see if rhs belongs to next operator < if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) { < $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
> if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) { > break;
*/
> // consume higher-precedence operators on the right-hand side protected function value(&$out) > $rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1); { >
if (! isset($this->buffer[$this->count])) {
>
< * @return boolean
> * @return bool
} $s = $this->count; $char = $this->buffer[$this->count];
< if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
> 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)) {
> 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)) {
> 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; }
< $this->count--;
> if ($follow_white) { > $out = [Type::T_KEYWORD, $char]; > return true; > } > > $this->seek($s);
return false; } // negation if ($char === '-') {
> if ($this->customProperty($out)) { $this->count++; > return true; > } if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { >
$out = [Type::T_UNARY, '-', $inner, $this->inParens];
> $follow_white = $this->whitespace(); >
return true; }
< $this->count--;
> 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->matchChar('&', true)) { > if ($this->keyword($keyword)) { $out = [Type::T_SELF]; > $out = [Type::T_KEYWORD, '#' . $keyword]; > return true; > return true; } > } > if ($char === '$' && $this->variable($out)) { > $this->count--;
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)) {
> 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 boolean
> * @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(')')) {
> 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 boolean
> * @return bool
*/ protected function progid(&$out) { $s = $this->count;
< if ($this->literal('progid:', 7, false) &&
> 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 boolean
> * @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 ( > $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) &&
> 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 boolean
> * @return bool
*/ protected function argumentList(&$out) { $s = $this->count; $this->matchChar('('); $args = []; while ($this->keyword($var)) {
< if ($this->matchChar('=') && $this->expression($exp)) {
> 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 boolean
> * @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')) {
> 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(')')) {
< $this->throwParseError('... has to be after the final argument');
> 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 boolean
> * @return bool
*/ protected function map(&$out) { $s = $this->count; if (! $this->matchChar('(')) { return false; } $keys = []; $values = [];
< while ($this->genericList($key, 'expression') && $this->matchChar(':') && < $this->genericList($value, 'expression')
> 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 boolean
> * @return bool
*/ protected function color(&$out) { $s = $this->count;
< if ($this->match('(#([0-9a-f]+))', $m)) { < if (in_array(strlen($m[2]), [3,4,6,8])) {
> 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 boolean
> * @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])) {
> 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 boolean
> * @return bool
*/
< protected function string(&$out)
> 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]);
> $this->count -= \strlen($m[2]);
if ($this->interpolation($inter, false)) { $content[] = $inter; $hasInterpolation = true; } else {
< $this->count += strlen($m[2]);
> $this->count += \strlen($m[2]);
$content[] = '#{'; // ignore it }
> } elseif ($m[2] === "\r") { } elseif ($m[2] === '\\') { > $content[] = chr(10); if ($this->matchChar('"', false)) { > // TODO : warning $content[] = $m[2] . '"'; > # DEPRECATION WARNING on line x, column y of zzz: } elseif ($this->matchChar("'", false)) { > # Unescaped multiline strings are deprecated and will be removed in a future version of Sass. $content[] = $m[2] . "'"; > # To include a newline in a string, use "\a" or "\a " as in CSS. } elseif ($this->literal("\\", 1, false)) { > if ($this->matchChar("\n", false)) { $content[] = $m[2] . "\\"; > $content[] = ' '; } elseif ($this->literal("\r\n", 2, false) || > }
< if ($this->matchChar('"', false)) { < $content[] = $m[2] . '"'; < } elseif ($this->matchChar("'", false)) { < $content[] = $m[2] . "'"; < } elseif ($this->literal("\\", 1, false)) { < $content[] = $m[2] . "\\"; < } elseif ($this->literal("\r\n", 2, false) ||
> if ( > $this->literal("\r\n", 2, false) ||
}
> } elseif ($this->matchEscapeCharacter($c)) { } else { > $content[] = $c;
< $content[] = $m[2];
> throw $this->parseError('Unterminated escape sequence');
< $this->count -= strlen($delim);
> $this->count -= \strlen($delim);
} } $this->eatWhiteDefault = $oldWhite;
< if ($this->literal($delim, strlen($delim))) { < if ($hasInterpolation) {
> if ($this->literal($delim, \strlen($delim))) { > if ($hasInterpolation && ! $keepDelimWithInterpolation) {
$delim = '"';
> } > foreach ($content as &$string) { > $out = [Type::T_STRING, $delim, $content];
< foreach ($content as &$string) { < if ($string === "\\\\") { < $string = "\\"; < } elseif ($string === "\\'") { < $string = "'"; < } elseif ($string === '\\"') { < $string = '"';
> return true;
}
> } > $this->seek($s); > $out = [Type::T_STRING, $delim, $content]; > return false; > } return true; > } > /** > * @param string $out $this->seek($s); > * @param bool $inKeywords > * return false; > * @return bool } > */ > protected function matchEscapeCharacter(&$out, $inKeywords = false) /** > { * Parse keyword or interpolation > $s = $this->count; * > if ($this->match('[a-f0-9]', $m, false)) { * @param array $out > $hex = $m[0]; * @param boolean $restricted > * > for ($i = 5; $i--;) { * @return boolean > if ($this->match('[a-f0-9]', $m, false)) { */ > $hex .= $m[0]; protected function mixedKeyword(&$out, $restricted = false) > } else { { > break;
< $out = [Type::T_STRING, $delim, $content];
> // 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); > }
> if ($this->match('.', $m, false)) { $oldWhite = $this->eatWhiteDefault; > if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
$this->eatWhiteDefault = false;
> return false; > } for (;;) { > $out = $m[0]; if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) { > $parts[] = $key; > return true; continue; > }
< * @param boolean $restricted
> * @param bool $restricted
< * @return boolean
> * @return bool
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 $nestingOpen
> * @param string $nestOpen > * @param string $nestClose > * @param bool $rtrim > * @param string $disallow
*
< * @return boolean
> * @return bool
*/
< protected function openString($end, &$out, $nestingOpen = null)
> protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
{ $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false;
< $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
> 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 ($nestingOpen) { < $nestingLevel += substr_count($m[1], $nestingOpen);
> if ($nestOpen) { > $nestingLevel += substr_count($m[1], $nestOpen);
} } $tok = $m[2];
< $this->count-= strlen($tok);
> $this->count -= \strlen($tok);
< if ($tok === $end && ! $nestingLevel--) {
> if ($tok === $end && ! $nestingLevel) {
break; }
< if (($tok === "'" || $tok === '"') && $this->string($str)) {
> 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->count += \strlen($tok);
} $this->eatWhiteDefault = $oldWhite;
< if (! $content) {
> if (! $content || $tok !== $end) {
return false; } // trim the end
< if (is_string(end($content))) { < $content[count($content) - 1] = rtrim(end($content));
> 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 boolean $lookWhite save information about whitespace before and after
> * @param bool $lookWhite save information about whitespace before and after
*
< * @return boolean
> * @return bool
*/ protected function interpolation(&$out, $lookWhite = true) { $oldWhite = $this->eatWhiteDefault;
> $allowVars = $this->allowVars; $this->eatWhiteDefault = true; > $this->allowVars = true;
$s = $this->count;
< if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
> 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 = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
> $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 boolean
> * @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, < null, < $this->count < )) {
> if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
if (! empty($m[0])) { $parts[] = $m[0];
< $this->count += strlen($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) * Parse comma separated selector list > * * > * @param array $out * @param array $out > * * @param boolean $subSelector > * @return bool * > */ * @return boolean > protected function customProperty(&$out) */ > { protected function selectors(&$out, $subSelector = false) > $s = $this->count; { > $s = $this->count; > if (! $this->literal('--', 2, false)) { $selectors = []; > return false; > } while ($this->selector($sel, $subSelector)) { > $selectors[] = $sel; > $parts = ['--']; > if (! $this->matchChar(',', true)) { > $oldWhite = $this->eatWhiteDefault; break; > $this->eatWhiteDefault = false; } > > for (;;) { while ($this->matchChar(',', true)) { > if ($this->interpolation($inter)) { ; // ignore extra > $parts[] = $inter; } > continue; } > } > if (! $selectors) { > if ($this->matchChar('&', false)) { $this->seek($s); > $parts[] = [Type::T_SELF]; > continue; return false; > } } > > if ($this->variable($var)) { $out = $selectors; > $parts[] = $var; > continue; return true; > } } > > if ($this->keyword($text)) { /** > $parts[] = $text; * Parse whitespace separated selector list > continue; * > } * @param array $out > * @param boolean $subSelector > break; * > } * @return boolean > */ > $this->eatWhiteDefault = $oldWhite; protected function selector(&$out, $subSelector = false) > { > if (\count($parts) == 1) { $selector = []; > $this->seek($s); > for (;;) { > return false; $s = $this->count; > } > if ($this->match('[>+~]+', $m, true)) { > $this->whitespace(); // get any extra whitespace if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0 && > $m[0] === '+' && $this->match("(\d+|n\b)", $counter) > $out = [Type::T_STRING, '', $parts]; ) { > $this->seek($s); > return true; } else { > } $selector[] = [$m[0]]; > continue; > /**
< * @param boolean $subSelector
> * @param string|bool $subSelector
< * @return boolean
> * @return bool
< * @param boolean $subSelector
> * @param string|bool $subSelector
< * @return boolean
> * @return bool
$selector[] = $part;
> $discardComments = $this->discardComments; $this->match('\s+', $m); > $this->discardComments = true; continue; >
< if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
> if ( > $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
< $this->match('\s+', $m); < continue; < } < < if ($this->match('\/[^\/]+\/', $m, true)) { < $selector[] = [$m[0]];
> $this->whitespace();
break; }
> $this->discardComments = $discardComments; if (! $selector) { >
return false; } $out = $selector; return true; } /**
> * parsing escaped chars in selectors: * Parse the parts that make up a selector > * - 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) * {@internal > * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder > * normalized to lowercase * }} > * * > * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars, * @param array $out > * and escaping added when printing in the Compiler, where/if it's mandatory * @param boolean $subSelector > * - but this require a better formal selector representation instead of the array we have now * > * * @return boolean > * @param string $out */ > * @param bool $keepEscapedNumber protected function selectorSingle(&$out, $subSelector = false) > * { > * @return bool $oldWhite = $this->eatWhiteDefault; > */ $this->eatWhiteDefault = false; > protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) > { $parts = []; > $s_escape = $this->count; > if ($this->match('\\\\', $m)) { if ($this->matchChar('*', false)) { > $out = '\\' . $m[0]; $parts[] = '*'; > return true; } > } > for (;;) { > if ($this->matchEscapeCharacter($escapedout, true)) { if (! isset($this->buffer[$this->count])) { > if (strlen($escapedout) === 1) { break; > if (!preg_match(",\w,", $escapedout)) { } > $out = '\\' . $escapedout; > return true; $s = $this->count; > } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) { $char = $this->buffer[$this->count]; > $out = $escapedout; > return true; // see if we can stop early > } if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') { > } break; > $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape)); } > if (strlen($escape_sequence) < 6) { > $escape_sequence .= ' '; // parsing a sub selector in () stop with the closing ) > } if ($subSelector && $char === ')') { > $out = '\\' . strtolower($escape_sequence); break; > return true; } > } > if ($this->match('\\S', $m)) { //self > $out = '\\' . $m[0]; switch ($char) { > return true; case '&': > } $parts[] = Compiler::$selfSelector; > $this->count++; > continue 2; > return false; > } case '.': > $parts[] = '.'; > /**
< * @param boolean $subSelector
> * @param string|bool $subSelector
< * @return boolean
> * @return bool
> ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
case '|': $parts[] = '|'; $this->count++; continue 2; }
< if ($char === '\\' && $this->match('\\\\\S', $m)) { < $parts[] = $m[0];
> // 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'] ||
> 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']
> $nameParts === ['nth-child'] || > $nameParts === ['nth-last-child'] || > $nameParts === ['nth-of-type'] || > $nameParts === ['nth-last-of-type']
) {
< if ($this->matchChar('(', true) &&
> 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)) {
> if (\count($sub) && reset($sub)) {
$parts[] = ' '; } }
< if (count($subs) && reset($subs)) {
> if (\count($subs) && reset($subs)) {
$parts[] = ', '; } } $parts[] = ')'; } else { $this->seek($ss); }
< } else { < if ($this->matchChar('(') &&
> } 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 ($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 === '[' &&
> 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)) {
> 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 boolean
> * @return bool
*/ protected function variable(&$out) { $s = $this->count;
< if ($this->matchChar('$', false) && $this->keyword($name)) {
> 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 boolean $eatWhitespace
> * @param bool $eatWhitespace > * @param bool $inSelector
*
< * @return boolean
> * @return bool
*/
< protected function keyword(&$word, $eatWhitespace = null)
> protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
{
< if ($this->match(
> $s = $this->count; > $match = $this->match(
$this->utf8
< ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)' < : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
> ? '(([\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,
< $eatWhitespace < )) {
> false > ); > > if ($match) {
$word = $m[1];
> // handling of escaping in keyword : get the escaped char return true; > if (strpos($word, '\\') !== false) { } > $send = $this->count; > $escapedWord = []; return false; > $this->seek($s); } > $previousEscape = false; > while ($this->count < $send) { /** > $char = $this->buffer[$this->count]; * Parse a keyword that should not start with a number > $this->count++; * > if ( * @param string $word > $this->count < $send * @param boolean $eatWhitespace > && $char === '\\' * > && !$previousEscape * @return boolean > && ( */ > $inSelector ? protected function restrictedKeyword(&$word, $eatWhitespace = null) > $this->matchEscapeCharacterInSelector($out) { > : $s = $this->count; > $this->matchEscapeCharacter($out, true) > ) if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) { > ) { return true; > $escapedWord[] = $out; } > } else { > if ($previousEscape) { $this->seek($s); > $previousEscape = false; > } elseif ($char === '\\') { return false; > $previousEscape = true; } > } > $escapedWord[] = $char; /** > } * Parse a placeholder > } * > * @param string|array $placeholder > $word = implode('', $escapedWord); * > } * @return boolean > */ > if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) { protected function placeholder(&$placeholder) > $this->whitespace(); { > } if ($this->match( >
< * @param boolean $eatWhitespace
> * @param bool $eatWhitespace > * @param bool $inSelector
< * @return boolean
> * @return bool
< protected function restrictedKeyword(&$word, $eatWhitespace = null)
> protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
< if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
> if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
< * @return boolean
> * @return bool
< if ($this->match(
> $match = $this->match(
< )) {
> ); > > if ($match) {
return true; } if ($this->interpolation($placeholder)) { return true; } return false; } /** * Parse a url * * @param array $out *
< * @return boolean
> * @return bool
*/ protected function url(&$out) {
< if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) { < $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
> 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); return false; > } > if ( > $this->openString(')', $out) && /** > $this->matchChar(')') * Consume an end of statement delimiter > ) { * > $out = [Type::T_STRING, '', ['url(', $out, ')']]; * @return boolean > */ > return true; protected function end() > } { > } if ($this->matchChar(';')) { >
return true;
> * @param bool $eatWhitespace
< * @return boolean
> * @return bool
< protected function end()
> protected function end($eatWhitespace = null)
< if ($this->matchChar(';')) {
> if ($this->matchChar(';', $eatWhitespace)) {
< if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
> if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {
return true; } return false; } /** * Strip assignment flag from the list * * @param array $value *
< * @return array
> * @return string[]
*/ protected function stripAssignmentFlags(&$value) { $flags = [];
< for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
> 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'])) {
> 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 string
> * @return bool
*/ protected function stripOptionalFlag(&$selectors) { $optional = false; $selector = end($selectors); $part = end($selector); if ($part === ['!optional']) {
< array_pop($selectors[count($selectors) - 1]);
> 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) {
> if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {
return $this->flattenList($value[2][0]); } return $value; } /**
< * @deprecated < * < * {@internal < * advance counter to next occurrence of $what < * $until - don't include $what in advance < * $allowNewline, if string, will be used as valid char set < * }} < */ < protected function to($what, &$out, $until = false, $allowNewline = false) < { < if (is_string($allowNewline)) { < $validChars = $allowNewline; < } else { < $validChars = $allowNewline ? '.' : "[^\n]"; < } < < $m = null; < < if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) { < return false; < } < < if ($until) { < $this->count -= strlen($what); // give back $what < } < < $out = $m[1]; < < return true; < } < < /** < * @deprecated < */ < protected function show() < { < if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { < return $m[1]; < } < < return ''; < } < < /**
* 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);
> $this->sourcePositions[] = \strlen($buffer);
if (substr($buffer, -1) !== "\n") {
< $this->sourcePositions[] = strlen($buffer) + 1;
> $this->sourcePositions[] = \strlen($buffer) + 1;
} } /** * Get source line number and column (given character position in the buffer) *
< * @param integer $pos
> * @param int $pos
* * @return array
> * @phpstan-return array{int, int}
*/ private function getSourcePosition($pos) { $low = 0;
< $high = count($this->sourcePositions);
> $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
> * 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 (version_compare(PHP_VERSION, '7.2.0') >= 0) { < return; < } < < // deprecated in PHP 7.2 < $iniDirective = 'mbstring.func_overload'; < < if (extension_loaded('mbstring') && ini_get($iniDirective) & 2) {
> 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) {
> if (\extension_loaded('mbstring') && $this->encoding) {
mb_internal_encoding($this->encoding); } } }