%PDF- %PDF-
Direktori : /home/q/g/b/qgbqkvz/www/wp-content/plugins/wp-scss/scssphp/src/Parser/ |
Current File : /home/q/g/b/qgbqkvz/www/wp-content/plugins/wp-scss/scssphp/src/Parser/SelectorParser.php |
<?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Parser; use ScssPhp\ScssPhp\Ast\Selector\AttributeOperator; use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector; use ScssPhp\ScssPhp\Ast\Selector\ClassSelector; use ScssPhp\ScssPhp\Ast\Selector\Combinator; use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector; use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent; use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector; use ScssPhp\ScssPhp\Ast\Selector\IDSelector; use ScssPhp\ScssPhp\Ast\Selector\ParentSelector; use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector; use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector; use ScssPhp\ScssPhp\Ast\Selector\QualifiedName; use ScssPhp\ScssPhp\Ast\Selector\SelectorList; use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector; use ScssPhp\ScssPhp\Ast\Selector\TypeSelector; use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector; use ScssPhp\ScssPhp\Logger\LoggerInterface; use ScssPhp\ScssPhp\Util; use ScssPhp\ScssPhp\Util\Character; /** * A parser for selectors. * * @internal */ final class SelectorParser extends Parser { /** * Pseudo-class selectors that take unadorned selectors as arguments. */ private const SELECTOR_PSEUDO_CLASSES = ['not', 'is', 'matches', 'where', 'current', 'any', 'has', 'host', 'host-context']; /** * Pseudo-element selectors that take unadorned selectors as arguments. */ private const SELECTOR_PSEUDO_ELEMENTS = ['slotted']; /** * @var bool * @readonly */ private $allowParent; /** * @var bool * @readonly */ private $allowPlaceholder; public function __construct(string $contents, ?LoggerInterface $logger = null, ?string $url = null, bool $allowParent = true, bool $allowPlaceholder = true) { $this->allowParent = $allowParent; $this->allowPlaceholder = $allowPlaceholder; parent::__construct($contents, $logger, $url); } public function parse(): SelectorList { try { $selector = $this->selectorList(); if (!$this->scanner->isDone()) { $this->scanner->error('expected selector.'); } return $selector; } catch (FormatException $e) { throw $this->wrapException($e); } } public function parseComplexSelector(): ComplexSelector { try { $complex = $this->complexSelector(); if (!$this->scanner->isDone()) { $this->scanner->error('expected selector.'); } return $complex; } catch (FormatException $e) { throw $this->wrapException($e); } } public function parseCompoundSelector(): CompoundSelector { try { $compound = $this->compoundSelector(); if (!$this->scanner->isDone()) { $this->scanner->error('expected selector.'); } return $compound; } catch (FormatException $e) { throw $this->wrapException($e); } } public function parseSimpleSelector(): SimpleSelector { try { $simple = $this->simpleSelector(); if (!$this->scanner->isDone()) { $this->scanner->error('unexpected token.'); } return $simple; } catch (FormatException $e) { throw $this->wrapException($e); } } /** * Consumes a selector list. */ private function selectorList(): SelectorList { $previousLine = $this->scanner->getLine(); $components = [$this->complexSelector()]; $this->whitespace(); while ($this->scanner->scanChar(',')) { $this->whitespace(); $next = $this->scanner->peekChar(); if ($next === ',') { continue; } if ($this->scanner->isDone()) { break; } $lineBreak = $this->scanner->getLine() !== $previousLine; if ($lineBreak) { $previousLine = $this->scanner->getLine(); } $components[] = $this->complexSelector($lineBreak); } return new SelectorList($components); } /** * Consumes a complex selector. * * If $lineBreak is `true`, that indicates that there was a line break * before this selector. */ private function complexSelector(bool $lineBreak = false): ComplexSelector { $lastCompound = null; $combinators = []; $initialCombinators = null; $components = []; while (true) { $this->whitespace(); $next = $this->scanner->peekChar(); switch ($next) { case '+': $this->scanner->readChar(); $combinators[] = Combinator::NEXT_SIBLING; break; case '>': $this->scanner->readChar(); $combinators[] = Combinator::CHILD; break; case '~': $this->scanner->readChar(); $combinators[] = Combinator::FOLLOWING_SIBLING; break; default: if ($next === null || (!\in_array($next, ['[', '.', '#', '%', ':', '&', '*', '|'], true) && !$this->lookingAtIdentifier())) { break 2; } if ($lastCompound !== null) { $components[] = new ComplexSelectorComponent($lastCompound, $combinators); } elseif (\count($combinators) !== 0) { \assert($initialCombinators === null); $initialCombinators = $combinators; } $lastCompound = $this->compoundSelector(); $combinators = []; if ($this->scanner->peekChar() === '&') { $this->scanner->error('"&" may only used at the beginning of a compound selector.'); } break; } } if ($lastCompound !== null) { $components[] = new ComplexSelectorComponent($lastCompound, $combinators); } elseif (\count($combinators) !== 0) { $initialCombinators = $combinators; } else { $this->scanner->error('expected selector.'); } return new ComplexSelector($initialCombinators ?? [], $components, $lineBreak); } /** * Consumes a compound selector. */ private function compoundSelector(): CompoundSelector { $components = [$this->simpleSelector()]; while (Character::isSimpleSelectorStart($this->scanner->peekChar())) { $components[] = $this->simpleSelector(false); } return new CompoundSelector($components); } /** * Consumes a simple selector. * * If $allowParent is passed, it controls whether the parent selector `&` is * allowed. Otherwise, it defaults to {@see allowParent}. */ private function simpleSelector(?bool $allowParent = null): SimpleSelector { $start = $this->scanner->getPosition(); $allowParent = $allowParent ?? $this->allowParent; switch ($this->scanner->peekChar()) { case '[': return $this->attributeSelector(); case '.': return $this->classSelector(); case '#': return $this->idSelector(); case '%': $selector = $this->placeholderSelector(); if (!$this->allowPlaceholder) { $this->error("Placeholder selectors aren't allowed here.", $this->scanner->spanFrom($start)); } return $selector; case ':': return $this->pseudoSelector(); case '&': $selector = $this->parentSelector(); if (!$allowParent) { $this->error("Parent selectors aren't allowed here.", $this->scanner->spanFrom($start)); } return $selector; default: return $this->typeOrUniversalSelector(); } } /** * Consumes an attribute selector. */ private function attributeSelector(): AttributeSelector { $this->scanner->expectChar('['); $this->whitespace(); $name = $this->attributeName(); $this->whitespace(); if ($this->scanner->scanChar(']')) { return AttributeSelector::create($name); } $operator = $this->attributeOperator(); $this->whitespace(); $next = $this->scanner->peekChar(); $value = $next === "'" || $next === '"' ? $this->string() : $this->identifier(); $this->whitespace(); $next = $this->scanner->peekChar(); $modifier = $next !== null && Character::isAlphabetic($next) ? $this->scanner->readChar() : null; $this->scanner->expectChar(']'); return AttributeSelector::withOperator($name, $operator, $value, $modifier); } /** * Consumes a qualified name as part of an attribute selector. */ private function attributeName(): QualifiedName { if ($this->scanner->scanChar('*')) { $this->scanner->expectChar('|'); return new QualifiedName($this->identifier(), '*'); } $nameOrNamespace = $this->identifier(); if ($this->scanner->peekChar() !== '|' || $this->scanner->peekChar(1) === '=') { return new QualifiedName($nameOrNamespace); } $this->scanner->readChar(); return new QualifiedName($this->identifier(), $nameOrNamespace); } /** * Consumes an attribute selector's operator. * * @phpstan-return AttributeOperator::* */ private function attributeOperator(): string { $start = $this->scanner->getPosition(); switch ($this->scanner->readChar()) { case '=': return AttributeOperator::EQUAL; case '~': $this->scanner->expectChar('='); return AttributeOperator::INCLUDE; case '|': $this->scanner->expectChar('='); return AttributeOperator::DASH; case '^': $this->scanner->expectChar('='); return AttributeOperator::PREFIX; case '$': $this->scanner->expectChar('='); return AttributeOperator::SUFFIX; case '*': $this->scanner->expectChar('='); return AttributeOperator::SUBSTRING; default: $this->scanner->error('Expected "]".', $start); } } /** * Consumes a class selector. */ private function classSelector(): ClassSelector { $this->scanner->expectChar('.'); $name = $this->identifier(); return new ClassSelector($name); } /** * Consumes an ID selector. */ private function idSelector(): IDSelector { $this->scanner->expectChar('#'); $name = $this->identifier(); return new IDSelector($name); } /** * Consumes a placeholder selector. */ private function placeholderSelector(): PlaceholderSelector { $this->scanner->expectChar('%'); $name = $this->identifier(); return new PlaceholderSelector($name); } /** * Consumes a parent selector. */ private function parentSelector(): ParentSelector { $this->scanner->expectChar('&'); $suffix = $this->lookingAtIdentifierBody() ? $this->identifierBody() : null; return new ParentSelector($suffix); } /** * Consumes a pseudo selector. */ private function pseudoSelector(): PseudoSelector { $this->scanner->expectChar(':'); $element = $this->scanner->scanChar(':'); $name = $this->identifier(); if (!$this->scanner->scanChar('(')) { return new PseudoSelector($name, $element); } $this->whitespace(); $unvendored = Util::unvendor($name); $argument = null; $selector = null; if ($element) { if (\in_array($unvendored, self::SELECTOR_PSEUDO_ELEMENTS, true)) { $selector = $this->selectorList(); } else { $argument = $this->declarationValue(true); } } elseif (\in_array($unvendored, self::SELECTOR_PSEUDO_CLASSES, true)) { $selector = $this->selectorList(); } elseif ($unvendored === 'nth-child' || $unvendored === 'nth-last-child') { $argument = $this->aNPlusB(); $this->whitespace(); if (Character::isWhitespace($this->scanner->peekChar(-1)) && $this->scanner->peekChar() !== ')') { $this->expectIdentifier('of'); $argument .= ' of'; $this->whitespace(); $selector = $this->selectorList(); } } else { $argument = rtrim($this->declarationValue(true)); } $this->scanner->expectChar(')'); return new PseudoSelector($name, $element, $argument, $selector); } /** * Consumes an [`An+B` production][An+B] and returns its text. * * [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax */ private function aNPlusB(): string { $buffer = ''; switch ($this->scanner->peekChar()) { case 'e': case 'E': $this->expectIdentifier('even'); return 'even'; case 'o': case 'O': $this->expectIdentifier('odd'); return 'odd'; case '+': case '-': $buffer .= $this->scanner->readChar(); break; } $first = $this->scanner->peekChar(); if ($first !== null && Character::isDigit($first)) { while (Character::isDigit($this->scanner->peekChar())) { $buffer .= $this->scanner->readChar(); } $this->whitespace(); if (!$this->scanIdentChar('n')) { return $buffer; } } else { $this->expectIdentChar('n'); } $buffer .= 'n'; $this->whitespace(); $next = $this->scanner->peekChar(); if ($next !== '+' && $next !== '-') { return $buffer; } $buffer .= $this->scanner->readChar(); $this->whitespace(); $last = $this->scanner->peekChar(); if ($last === null || !Character::isDigit($last)) { $this->scanner->error('Expected a number.'); } while (Character::isDigit($this->scanner->peekChar())) { $buffer .= $this->scanner->readChar(); } return $buffer; } /** * Consumes a type selector or a universal selector. * * These are combined because either one could start with `*`. */ private function typeOrUniversalSelector(): SimpleSelector { $first = $this->scanner->peekChar(); if ($first === '*') { $this->scanner->readChar(); if (!$this->scanner->scanChar('|')) { return new UniversalSelector(); } if ($this->scanner->scanChar('*')) { return new UniversalSelector('*'); } return new TypeSelector(new QualifiedName($this->identifier(), '*')); } if ($first === '|') { $this->scanner->readChar(); if ($this->scanner->scanChar('*')) { return new UniversalSelector(''); } return new TypeSelector(new QualifiedName($this->identifier(), '')); } $nameOrNamespace = $this->identifier(); if (!$this->scanner->scanChar('|')) { return new TypeSelector(new QualifiedName($nameOrNamespace)); } if ($this->scanner->scanChar('*')) { return new UniversalSelector($nameOrNamespace); } return new TypeSelector(new QualifiedName($this->identifier(), $nameOrNamespace)); } }