Skip to content

Commit 9fd06ec

Browse files
[BUGFIX] Support escaped quotes selector in selectors (#1485)
Tailwind allows selectors that contain quotes like this one: .before\:content-\[\'\'\]:before
1 parent ba1dd90 commit 9fd06ec

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Please also have a look at our
1010

1111
### Added
1212

13+
- Add support for escaped quotes in the selectors (#1485)
1314
- Provide line number in exception message for mismatched parentheses in
1415
selector (#1435)
1516
- Add support for CSS container queries (#1400)

src/Property/Selector.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,13 @@ public static function parse(ParserState $parserState, array &$comments = []): s
9595
case '\'':
9696
// The fallthrough is intentional.
9797
case '"':
98-
if (!\is_string($stringWrapperCharacter)) {
99-
$stringWrapperCharacter = $nextCharacter;
100-
} elseif ($stringWrapperCharacter === $nextCharacter) {
101-
if (\substr(\end($selectorParts), -1) !== '\\') {
98+
$lastPart = \end($selectorParts);
99+
$backslashCount = \strspn(\strrev($lastPart), '\\');
100+
$quoteIsEscaped = ($backslashCount % 2 === 1);
101+
if (!$quoteIsEscaped) {
102+
if (!\is_string($stringWrapperCharacter)) {
103+
$stringWrapperCharacter = $nextCharacter;
104+
} elseif ($stringWrapperCharacter === $nextCharacter) {
102105
$stringWrapperCharacter = null;
103106
}
104107
}

tests/Unit/Property/SelectorTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,4 +369,88 @@ public function getArrayRepresentationThrowsException(): void
369369

370370
$subject->getArrayRepresentation();
371371
}
372+
373+
/**
374+
* @return array<non-empty-string, array{0: non-empty-string}>
375+
*/
376+
public static function provideSelectorsWithEscapedQuotes(): array
377+
{
378+
return [
379+
'escaped double quote in double-quoted attribute' => ['a[href="test\\"value"]'],
380+
'escaped single quote in single-quoted attribute' => ['a[href=\'test\\\'value\']'],
381+
'multiple escaped double quotes in double-quoted attribute' => ['a[title="say \\"hello\\" world"]'],
382+
'multiple escaped single quotes in single-quoted attribute' => ['a[title=\'say \\\'hello\\\' world\']'],
383+
'escaped quote at start of attribute value' => ['a[data-test="\\"start"]'],
384+
'escaped quote at end of attribute value' => ['a[data-test="end\\""]'],
385+
'escaped backslash followed by quote' => ['a[data-test="test\\\\"]'],
386+
'escaped backslash before escaped quote' => ['a[data-test="test\\\\\\"value"]'],
387+
'triple backslash before quote' => ['a[data-test="test\\\\\\""]'],
388+
];
389+
}
390+
391+
/**
392+
* @test
393+
*
394+
* @param non-empty-string $selector
395+
*
396+
* @dataProvider provideSelectorsWithEscapedQuotes
397+
*/
398+
public function parsesSelectorsWithEscapedQuotes(string $selector): void
399+
{
400+
$result = Selector::parse(new ParserState($selector, Settings::create()));
401+
402+
self::assertInstanceOf(Selector::class, $result);
403+
self::assertSame($selector, $result->getSelector());
404+
}
405+
406+
/**
407+
* @test
408+
*
409+
* @param non-empty-string $selector
410+
*
411+
* @dataProvider provideSelectorsWithEscapedQuotes
412+
*/
413+
public function isValidForSelectorsWithEscapedQuotesReturnsTrue(string $selector): void
414+
{
415+
self::assertTrue(Selector::isValid($selector));
416+
}
417+
418+
/**
419+
* @test
420+
*/
421+
public function parsingAttributeWithEscapedQuoteDoesNotPrematurelyCloseString(): void
422+
{
423+
$selector = 'input[placeholder="Enter \\"quoted\\" text here"]';
424+
425+
$result = Selector::parse(new ParserState($selector, Settings::create()));
426+
427+
self::assertInstanceOf(Selector::class, $result);
428+
self::assertSame($selector, $result->getSelector());
429+
}
430+
431+
/**
432+
* @test
433+
*/
434+
public function parseDistinguishesEscapedFromUnescapedQuotes(): void
435+
{
436+
// One backslash = escaped quote (should not close string)
437+
$selector = 'a[data-value="test\\"more"]';
438+
439+
$result = Selector::parse(new ParserState($selector, Settings::create()));
440+
441+
self::assertSame($selector, $result->getSelector());
442+
}
443+
444+
/**
445+
* @test
446+
*/
447+
public function parseHandlesEvenNumberOfBackslashesBeforeQuote(): void
448+
{
449+
// Two backslashes = escaped backslash + unescaped quote (should close string)
450+
$selector = 'a[data-value="test\\\\"]';
451+
452+
$result = Selector::parse(new ParserState($selector, Settings::create()));
453+
454+
self::assertSame($selector, $result->getSelector());
455+
}
372456
}

0 commit comments

Comments
 (0)