diff --git a/CHANGELOG.md b/CHANGELOG.md index 224916a7..97c6683a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Please also have a look at our `getPropertyName()` (#1506) - `Selector` is now represented as a sequence of `Selector\Component` objects which can be accessed via `getComponents()`, manipulated individually, or set - via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496) + via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536) - `Selector::setSelector()` and `Selector` constructor will now throw exception upon provision of an invalid selectior (#1498, #1502) - Clean up extra whitespace in CSS selector (#1398) diff --git a/src/Property/Selector/CompoundSelector.php b/src/Property/Selector/CompoundSelector.php index 4e0b8521..1e6fa541 100644 --- a/src/Property/Selector/CompoundSelector.php +++ b/src/Property/Selector/CompoundSelector.php @@ -27,6 +27,8 @@ class CompoundSelector implements Component '"', '(', ')', + '[', + ']', ',', ' ', "\t", @@ -46,7 +48,7 @@ class CompoundSelector implements Component (?: (?: # any sequence of valid unescaped characters, except quotes - [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=\\[\\]()\\-\\.:#,\\s]++ + [a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*~=\\[\\]()\\-\\.:#,\\s]++ | # one or more escaped characters (?:\\\\.)++ @@ -100,6 +102,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s $selectorParts = []; $stringWrapperCharacter = null; $functionNestingLevel = 0; + $isWithinAttribute = false; while (true) { $selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments); @@ -140,6 +143,32 @@ public static function parse(ParserState $parserState, array &$comments = []): s --$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 '}': @@ -162,7 +191,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s case '+': // The fallthrough is intentional. case '~': - if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) { + if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) { break 2; } break; diff --git a/tests/Unit/Property/Selector/CompoundSelectorTest.php b/tests/Unit/Property/Selector/CompoundSelectorTest.php index 1e3f6bd6..e8e659e8 100644 --- a/tests/Unit/Property/Selector/CompoundSelectorTest.php +++ b/tests/Unit/Property/Selector/CompoundSelectorTest.php @@ -57,6 +57,17 @@ public static function provideCompoundSelectorAndSpecificity(): array '`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110], 'attribute with `"`' => ['[alt="{}()[]\\"\',"]', 10], 'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']', 10], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `#` is in a string. + 'attribute with `^=`' => ['a[href^="#"]', 111], + 'attribute with `*=`' => ['a[href*="example"]', 11], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `.` is in a string. + 'attribute with `$=`' => ['a[href$=".org"]', 21], + 'attribute with `~=`' => ['span[title~="bonjour"]', 11], + 'attribute with `|=`' => ['[lang|="en"]', 10], + // TODO, broken: specificity should be 11, but the calculator doesn't realize the `i` is in an attribute. + 'attribute with case insensitive modifier' => ['a[href*="insensitive" i]', 12], + // TODO, broken: specificity should be 21, but the calculator doesn't realize the `.` is in a string. + 'multiple attributes' => ['a[href^="https://"][href$=".org"]', 31], ]; } @@ -173,6 +184,8 @@ public static function provideInvalidCompoundSelectorForParse(): array 'attribute value missing closing double quote' => ['a[href="#top]'], 'attribute value with mismatched quotes, single quote opening' => ['a[href=\'#top"]'], 'attribute value with mismatched quotes, double quote opening' => ['a[href="#top\']'], + 'attribute value with extra `[`' => ['a[[href="#top"]'], + 'attribute value with extra `]`' => ['a[href="#top"]]'], ]; }