Skip to content

Commit 6a81ef5

Browse files
committed
[BUGFIX] Parse attribute selectors
Don't treat spaces or characters like `~` within them as a stop character representing a combinator. The bug was introduced by #1496 and related changes that split selector representation and parsing into compound selectors and combinators. Addresses some of the issues in #1533.
1 parent 34ba5d4 commit 6a81ef5

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Please also have a look at our
2727
`getPropertyName()` (#1506)
2828
- `Selector` is now represented as a sequence of `Selector\Component` objects
2929
which can be accessed via `getComponents()`, manipulated individually, or set
30-
via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496)
30+
via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536)
3131
- `Selector::setSelector()` and `Selector` constructor will now throw exception
3232
upon provision of an invalid selectior (#1498, #1502)
3333
- Clean up extra whitespace in CSS selector (#1398)

src/Property/Selector/CompoundSelector.php

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class CompoundSelector implements Component
2727
'"',
2828
'(',
2929
')',
30+
'[',
31+
']',
3032
',',
3133
' ',
3234
"\t",
@@ -46,7 +48,7 @@ class CompoundSelector implements Component
4648
(?:
4749
(?:
4850
# any sequence of valid unescaped characters, except quotes
49-
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=\\[\\]()\\-\\.:#,\\s]++
51+
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*~=\\[\\]()\\-\\.:#,\\s]++
5052
|
5153
# one or more escaped characters
5254
(?:\\\\.)++
@@ -100,6 +102,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s
100102
$selectorParts = [];
101103
$stringWrapperCharacter = null;
102104
$functionNestingLevel = 0;
105+
$isWithinAttribute = false;
103106

104107
while (true) {
105108
$selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments);
@@ -140,6 +143,32 @@ public static function parse(ParserState $parserState, array &$comments = []): s
140143
--$functionNestingLevel;
141144
}
142145
break;
146+
case '[':
147+
if (!\is_string($stringWrapperCharacter)) {
148+
if ($isWithinAttribute) {
149+
throw new UnexpectedTokenException(
150+
'anything but',
151+
'[',
152+
'literal',
153+
$parserState->currentLine()
154+
);
155+
}
156+
$isWithinAttribute = true;
157+
}
158+
break;
159+
case ']':
160+
if (!\is_string($stringWrapperCharacter)) {
161+
if (!$isWithinAttribute) {
162+
throw new UnexpectedTokenException(
163+
'anything but',
164+
']',
165+
'literal',
166+
$parserState->currentLine()
167+
);
168+
}
169+
$isWithinAttribute = false;
170+
}
171+
break;
143172
case '{':
144173
// The fallthrough is intentional.
145174
case '}':
@@ -162,7 +191,7 @@ public static function parse(ParserState $parserState, array &$comments = []): s
162191
case '+':
163192
// The fallthrough is intentional.
164193
case '~':
165-
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
194+
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) {
166195
break 2;
167196
}
168197
break;

tests/Unit/Property/Selector/CompoundSelectorTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ public static function provideCompoundSelectorAndSpecificity(): array
5757
'`not` with multiple arguments' => [':not(#your-mug, .their-mug)', 110],
5858
'attribute with `"`' => ['[alt="{}()[]\\"\',"]', 10],
5959
'attribute with `\'`' => ['[alt=\'{}()[]"\\\',\']', 10],
60+
// TODO, broken: specificity should be 11, but the calculator doesn't realize the `#` is in a string.
61+
'attribute with `^=`' => ['a[href^="#"]', 111],
62+
'attribute with `*=`' => ['a[href*="example"]', 11],
63+
// TODO, broken: specificity should be 11, but the calculator doesn't realize the `.` is in a string.
64+
'attribute with `$=`' => ['a[href$=".org"]', 21],
65+
'attribute with `~=`' => ['span[title~="bonjour"]', 11],
66+
'attribute with `|=`' => ['[lang|="en"]', 10],
67+
// TODO, broken: specificity should be 11, but the calculator doesn't realize the `i` is in an attribute.
68+
'attribute with case insensitive modifier' => ['a[href*="insensitive" i]', 12],
69+
// TODO, broken: specificity should be 21, but the calculator doesn't realize the `.` is in a string.
70+
'multiple attributes' => ['a[href^="https://"][href$=".org"]', 31],
6071
];
6172
}
6273

@@ -173,6 +184,8 @@ public static function provideInvalidCompoundSelectorForParse(): array
173184
'attribute value missing closing double quote' => ['a[href="#top]'],
174185
'attribute value with mismatched quotes, single quote opening' => ['a[href=\'#top"]'],
175186
'attribute value with mismatched quotes, double quote opening' => ['a[href="#top\']'],
187+
'attribute value with extra `[`' => ['a[[href="#top"]'],
188+
'attribute value with extra `]`' => ['a[href="#top"]]'],
176189
];
177190
}
178191

0 commit comments

Comments
 (0)