update lock clucknut
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 14s
Build, Push and Deploy / build-and-push (push) Successful in 3m14s
Build, Push and Deploy / deploy-staging (push) Successful in 25s
Build, Push and Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-18 20:32:18 +07:00
parent 4554035227
commit dcaf267458
3359 changed files with 153185 additions and 205489 deletions

View File

@@ -16,11 +16,19 @@ interface AtRule extends CSSListItem
* Since there are more set rules than block rules,
* were whitelisting the block rules and have anything else be treated as a set rule.
*
* @var non-empty-string
*
* @internal since 8.5.2
*/
public const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
public const BLOCK_RULES = [
'media',
'document',
'supports',
'region-style',
'font-feature-values',
'container',
'layer',
'scope',
'starting-style',
];
/**
* @return non-empty-string

View File

@@ -8,6 +8,7 @@
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
@@ -18,6 +19,7 @@ class CSSNamespace implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var CSSString|URL
@@ -94,4 +96,19 @@ public function atRuleArgs(): array
}
return $result;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
// We're using `uri` here instead of `url` to better match the spec.
'uri' => $this->url->getArrayRepresentation(),
'prefix' => $this->prefix,
];
}
}

View File

@@ -8,6 +8,7 @@
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\CSSString;
/**
@@ -22,6 +23,7 @@ class Charset implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var CSSString
@@ -71,4 +73,17 @@ public function atRuleArgs(): CSSString
{
return $this->charset;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'charset' => $this->charset->getArrayRepresentation(),
];
}
}

View File

@@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
use function Safe\preg_match;
/**
* `Declaration`s just have a string key (the property name) and a 'Value'.
*
* In CSS, `Declaration`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];
*/
class Declaration implements Commentable, CSSElement, Positionable
{
use CommentContainer;
use Position;
/**
* @var non-empty-string
*/
private $propertyName;
/**
* @var RuleValueList|string|null
*/
private $value;
/**
* @var bool
*/
private $isImportant = false;
/**
* @param non-empty-string $propertyName
* @param int<1, max>|null $lineNumber
* @param int<0, max>|null $columnNumber
*/
public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null)
{
$this->propertyName = $propertyName;
$this->setPosition($lineNumber, $columnNumber);
}
/**
* @param list<Comment> $commentsBefore
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $parserState, array $commentsBefore = []): self
{
$comments = $commentsBefore;
$parserState->consumeWhiteSpace($comments);
$declaration = new self(
$parserState->parseIdentifier(!$parserState->comes('--')),
$parserState->currentLine(),
$parserState->currentColumn()
);
$parserState->consumeWhiteSpace($comments);
$declaration->setComments($comments);
$parserState->consume(':');
$value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($declaration->getPropertyName()));
$declaration->setValue($value);
$parserState->consumeWhiteSpace();
if ($parserState->comes('!')) {
$parserState->consume('!');
$parserState->consumeWhiteSpace();
$parserState->consume('important');
$declaration->setIsImportant(true);
}
$parserState->consumeWhiteSpace();
while ($parserState->comes(';')) {
$parserState->consume(';');
}
return $declaration;
}
/**
* Returns a list of delimiters (or separators).
* The first item is the innermost separator (or, put another way, the highest-precedence operator).
* The sequence continues to the outermost separator (or lowest-precedence operator).
*
* @param non-empty-string $propertyName
*
* @return list<non-empty-string>
*/
private static function getDelimitersForPropertyValue(string $propertyName): array
{
if (preg_match('/^font($|-)/', $propertyName) === 1) {
return [',', '/', ' '];
}
switch ($propertyName) {
case 'src':
return [' ', ','];
default:
return [',', ' ', '/'];
}
}
/**
* @param non-empty-string $propertyName
*/
public function setPropertyName(string $propertyName): void
{
$this->propertyName = $propertyName;
}
/**
* @return non-empty-string
*/
public function getPropertyName(): string
{
return $this->propertyName;
}
/**
* @param non-empty-string $propertyName
*
* @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead.
*/
public function setRule(string $propertyName): void
{
$this->propertyName = $propertyName;
}
/**
* @return non-empty-string
*
* @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead.
*/
public function getRule(): string
{
return $this->propertyName;
}
/**
* @return RuleValueList|string|null
*/
public function getValue()
{
return $this->value;
}
/**
* @param RuleValueList|string|null $value
*/
public function setValue($value): void
{
$this->value = $value;
}
/**
* Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type.
* Otherwise, the existing value will be wrapped by one.
*
* @param RuleValueList|array<int, RuleValueList> $value
*/
public function addValue($value, string $type = ' '): void
{
if (!\is_array($value)) {
$value = [$value];
}
if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) {
$currentValue = $this->value;
$this->value = new RuleValueList($type, $this->getLineNumber());
if ($currentValue !== null && $currentValue !== '') {
$this->value->addListComponent($currentValue);
}
}
foreach ($value as $valueItem) {
$this->value->addListComponent($valueItem);
}
}
public function setIsImportant(bool $isImportant): void
{
$this->isImportant = $isImportant;
}
public function getIsImportant(): bool
{
return $this->isImportant;
}
/**
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
$formatter = $outputFormat->getFormatter();
$result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}";
if ($this->value instanceof Value) { // Can also be a ValueList
$result .= $this->value->render($outputFormat);
} else {
$result .= $this->value;
}
if ($this->isImportant) {
$result .= ' !important';
}
$result .= ';';
return $result;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
}

View File

@@ -8,6 +8,7 @@
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\URL;
/**
@@ -17,6 +18,7 @@ class Import implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var URL
@@ -82,4 +84,19 @@ public function getMediaQuery(): ?string
{
return $this->mediaQuery;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
// We're using the term "uri" here to match the wording used in the specs:
// https://www.w3.org/TR/CSS22/cascade.html#at-import
'uri' => $this->location->getArrayRepresentation(),
];
}
}

View File

@@ -11,8 +11,6 @@ class KeyframeSelector extends Selector
* - comma is not allowed unless escaped or quoted;
* - percentage value is allowed by itself.
*
* @var non-empty-string
*
* @internal since 8.5.2
*/
public const SELECTOR_VALIDATION_RX = '/

View File

@@ -4,9 +4,16 @@
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\Selector\Combinator;
use Sabberworm\CSS\Property\Selector\Component;
use Sabberworm\CSS\Property\Selector\CompoundSelector;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
@@ -16,13 +23,15 @@
*/
class Selector implements Renderable
{
use ShortClassNameProvider;
/**
* @var non-empty-string
*
* @internal since 8.5.2
*/
public const SELECTOR_VALIDATION_RX = '/
^(
# not whitespace only
(?!\\s*+$)
(?:
# any sequence of valid unescaped characters, except quotes
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
@@ -49,9 +58,9 @@ class Selector implements Renderable
/ux';
/**
* @var string
* @var non-empty-list<Component>
*/
private $selector;
private $components;
/**
* @internal since V8.8.0
@@ -64,19 +73,137 @@ public static function isValid(string $selector): bool
return $numberOfMatches === 1;
}
public function __construct(string $selector)
/**
* @param non-empty-string|non-empty-list<Component> $selector
* Providing a string is deprecated in version 9.2 and will not work from v10.0
*
* @throws UnexpectedTokenException if the selector is not valid
*/
final public function __construct($selector)
{
$this->setSelector($selector);
if (\is_string($selector)) {
$this->setSelector($selector);
} else {
$this->setComponents($selector);
}
}
/**
* @param list<Comment> $comments
*
* @return non-empty-list<Component>
*
* @throws UnexpectedTokenException
*/
private static function parseComponents(ParserState $parserState, array &$comments = []): array
{
// Whitespace is a descendent combinator, not allowed around a compound selector.
// (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
// Gobble any up now to get a clean start.
$parserState->consumeWhiteSpace($comments);
$selectorParts = [];
while (true) {
try {
$selectorParts[] = CompoundSelector::parse($parserState, $comments);
} catch (UnexpectedTokenException $e) {
if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
// The whitespace was not a descendent combinator, and was, in fact, arbitrary,
// after the end of the selector. Discard it.
\array_pop($selectorParts);
break;
} else {
throw $e;
}
}
try {
$selectorParts[] = Combinator::parse($parserState, $comments);
} catch (UnexpectedTokenException $e) {
// End of selector has been reached.
break;
}
}
return $selectorParts;
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$selectorParts = self::parseComponents($parserState, $comments);
// Check that the selector has been fully parsed:
if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
throw new UnexpectedTokenException(
'`,`, `{`, `}` or EOF',
$parserState->peek(5),
'literal',
$parserState->currentLine()
);
}
return new static($selectorParts);
}
/**
* @return non-empty-list<Component>
*/
public function getComponents(): array
{
return $this->components;
}
/**
* @param non-empty-list<Component> $components
* This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a
* `CompoundSelector`, and may be a single `CompoundSelector`.
*/
public function setComponents(array $components): self
{
$this->components = $components;
return $this;
}
/**
* @return non-empty-string
*
* @deprecated in version 9.2, will be removed in v10.0. Use either `getComponents()` or `render()` instead.
*/
public function getSelector(): string
{
return $this->selector;
return $this->render(new OutputFormat());
}
/**
* @param non-empty-string $selector
*
* @throws UnexpectedTokenException if the selector is not valid
*
* @deprecated in version 9.2, will be removed in v10.0. Use `setComponents()` instead.
*/
public function setSelector(string $selector): void
{
$this->selector = \trim($selector);
$parserState = new ParserState($selector, Settings::create());
$components = self::parseComponents($parserState);
// Check that the selector has been fully parsed:
if (!$parserState->isEnd()) {
throw new UnexpectedTokenException(
'EOF',
$parserState->peek(5),
'literal'
);
}
$this->components = $components;
}
/**
@@ -84,11 +211,39 @@ public function setSelector(string $selector): void
*/
public function getSpecificity(): int
{
return SpecificityCalculator::calculate($this->selector);
return \array_sum(\array_map(
static function (Component $component): int {
return $component->getSpecificity();
},
$this->components
));
}
public function render(OutputFormat $outputFormat): string
{
return $this->getSelector();
return \implode('', \array_map(
static function (Component $component) use ($outputFormat): string {
return $component->render($outputFormat);
},
$this->components
));
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'components' => \array_map(
static function (Component $component): array {
return $component->getArrayRepresentation();
},
$this->components
),
];
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* Class representing a CSS selector combinator (space, `>`, `+`, or `~`).
*
* @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~'
*/
class Combinator implements Component
{
use ShortClassNameProvider;
/**
* @var ValidCombinatorValue
*/
private $value;
/**
* @param ValidCombinatorValue $value
*/
public function __construct(string $value)
{
$this->setValue($value);
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$consumedWhitespace = $parserState->consumeWhiteSpace($comments);
$nextToken = $parserState->peek();
if (\in_array($nextToken, ['>', '+', '~'], true)) {
$value = $nextToken;
$parserState->consume(1);
$parserState->consumeWhiteSpace($comments);
} elseif ($consumedWhitespace !== '') {
$value = ' ';
} else {
throw new UnexpectedTokenException(
'combinator',
$nextToken,
'literal',
$parserState->currentLine()
);
}
return new self($value);
}
/**
* @return ValidCombinatorValue
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param non-empty-string $value
*
* @throws \UnexpectedValueException if `$value` is not either space, '>', '+' or '~'
*/
public function setValue(string $value): void
{
if (!\in_array($value, [' ', '>', '+', '~'], true)) {
throw new \UnexpectedValueException('`' . $value . '` is not a valid selector combinator.');
}
$this->value = $value;
}
/**
* @return int<0, max>
*/
public function getSpecificity(): int
{
return 0;
}
public function render(OutputFormat $outputFormat): string
{
$spacing = $outputFormat->getSpaceAroundSelectorCombinator();
if ($this->value === ' ') {
$rendering = $spacing !== '' ? $spacing : ' ';
} else {
$rendering = $spacing . $this->value . $spacing;
}
return $rendering;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'value' => $this->value,
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Renderable;
/**
* This interface is for a class that represents a part of a selector which is either a compound selector (or a simple
* selector, which is effectively a compound selector without any compounding) or a selector combinator.
*
* It allows a selector to be represented as an array of objects that implement this interface.
* This is the formal definition:
* selector = compound-selector [combinator, compound-selector]*
*
* The selector is comprised of an array of alternating types that can't be easily represented in a type-safe manner
* without this.
*
* 'Selector component' is not a known grammar in the spec, but a convenience for the implementation.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure
* @see https://www.w3.org/TR/selectors-4/#structure
*/
interface Component extends Renderable
{
/**
* @return non-empty-string
*/
public function getValue(): string;
/**
* @param non-empty-string $value
*/
public function setValue(string $value): void;
/**
* @return int<0, max>
*/
public function getSpecificity(): int;
}

View File

@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
/**
* Class representing a CSS compound selector.
* Selectors have to be split at combinators (space, `>`, `+`, `~`) before being passed to this class.
*/
class CompoundSelector implements Component
{
use ShortClassNameProvider;
private const PARSER_STOP_CHARACTERS = [
'{',
'}',
'\'',
'"',
'(',
')',
'[',
']',
',',
' ',
"\t",
"\n",
"\r",
'>',
'+',
'~',
ParserState::EOF,
'', // `ParserState::peek()` returns empty string rather than `ParserState::EOF` when end of string is reached
];
private const SELECTOR_VALIDATION_RX = '/
^
# not starting with whitespace
(?!\\s)
(?:
(?:
# any sequence of valid unescaped characters, except quotes
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
|
# one or more escaped characters
(?:\\\\.)++
|
# quoted text, like in `[id="example"]`
(?:
# opening quote
([\'"])
(?:
# sequence of characters except closing quote or backslash
(?:(?!\\g{-1}|\\\\).)++
|
# one or more escaped characters
(?:\\\\.)++
)*+ # zero or more times
# closing quote or end (unmatched quote is currently allowed)
(?:\\g{-1}|$)
)
)++ # one or more times
|
# keyframe animation progress percentage (e.g. 50%)
(?:\\d++%)
)
# not ending with whitespace
(?<!\\s)
$
/ux';
/**
* @var non-empty-string
*/
private $value;
/**
* @param non-empty-string $value
*/
public function __construct(string $value)
{
$this->setValue($value);
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$selectorParts = [];
$stringWrapperCharacter = null;
$functionNestingLevel = 0;
$isWithinAttribute = false;
while (true) {
$selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments);
$nextCharacter = $parserState->peek();
switch ($nextCharacter) {
case '':
// EOF
break 2;
case '\'':
// The fallthrough is intentional.
case '"':
$lastPart = \end($selectorParts);
$backslashCount = \strspn(\strrev($lastPart), '\\');
$quoteIsEscaped = ($backslashCount % 2 === 1);
if (!$quoteIsEscaped) {
if (!\is_string($stringWrapperCharacter)) {
$stringWrapperCharacter = $nextCharacter;
} elseif ($stringWrapperCharacter === $nextCharacter) {
$stringWrapperCharacter = null;
}
}
break;
case '(':
if (!\is_string($stringWrapperCharacter)) {
++$functionNestingLevel;
}
break;
case ')':
if (!\is_string($stringWrapperCharacter)) {
if ($functionNestingLevel <= 0) {
throw new UnexpectedTokenException(
'anything but',
')',
'literal',
$parserState->currentLine()
);
}
--$functionNestingLevel;
}
break;
case '[':
if (!\is_string($stringWrapperCharacter)) {
if ($isWithinAttribute) {
throw new UnexpectedTokenException(
'anything but',
'[',
'literal',
$parserState->currentLine()
);
}
$isWithinAttribute = true;
}
break;
case ']':
if (!\is_string($stringWrapperCharacter)) {
if (!$isWithinAttribute) {
throw new UnexpectedTokenException(
'anything but',
']',
'literal',
$parserState->currentLine()
);
}
$isWithinAttribute = false;
}
break;
case '{':
// The fallthrough is intentional.
case '}':
if (!\is_string($stringWrapperCharacter)) {
break 2;
}
break;
case ',':
// The fallthrough is intentional.
case ' ':
// The fallthrough is intentional.
case "\t":
// The fallthrough is intentional.
case "\n":
// The fallthrough is intentional.
case "\r":
// The fallthrough is intentional.
case '>':
// The fallthrough is intentional.
case '+':
// The fallthrough is intentional.
case '~':
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) {
break 2;
}
break;
}
$selectorParts[] = $parserState->consume(1);
}
if ($functionNestingLevel !== 0) {
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
}
if (\is_string($stringWrapperCharacter)) {
throw new UnexpectedTokenException(
$stringWrapperCharacter,
$nextCharacter,
'literal',
$parserState->currentLine()
);
}
$value = \implode('', $selectorParts);
if ($value === '') {
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
}
if (!self::isValid($value)) {
throw new UnexpectedTokenException(
'Selector component is not valid:',
'`' . $value . '`',
'custom',
$parserState->currentLine()
);
}
return new self($value);
}
/**
* @return non-empty-string
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param non-empty-string $value
*
* @throws \UnexpectedValueException if `$value` contains invalid characters or has surrounding whitespce
*/
public function setValue(string $value): void
{
if (!self::isValid($value)) {
throw new \UnexpectedValueException('`' . $value . '` is not a valid compound selector.');
}
$this->value = $value;
}
/**
* @return int<0, max>
*/
public function getSpecificity(): int
{
return SpecificityCalculator::calculate($this->value);
}
public function render(OutputFormat $outputFormat): string
{
return $this->getValue();
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'value' => $this->value,
];
}
private static function isValid(string $value): bool
{
$numberOfMatches = preg_match(self::SELECTOR_VALIDATION_RX, $value);
return $numberOfMatches === 1;
}
}

View File

@@ -4,6 +4,8 @@
namespace Sabberworm\CSS\Property\Selector;
use function Safe\preg_match_all;
/**
* Utility class to calculate the specificity of a CSS selector.
*
@@ -13,8 +15,6 @@ final class SpecificityCalculator
{
/**
* regexp for specificity calculations
*
* @var non-empty-string
*/
private const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\\.[\\w]+) # classes
@@ -37,8 +37,6 @@ final class SpecificityCalculator
/**
* regexp for specificity calculations
*
* @var non-empty-string
*/
private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\\s\\+\\>\\~]+)[\\w]+ # elements
@@ -67,8 +65,8 @@ public static function calculate(string $selector): int
/// @todo should exclude \# as well as "#"
$matches = null;
$b = \substr_count($selector, '#');
$c = \preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
$d = \preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}