diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc114fa..f60db312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ Please also have a look at our - Reject selector comprising only whitespace (#1433) - Improve recovery parsing when a rogue `}` is encountered (#1425, #1426) -- Parse comment(s) immediately preceding a selector (#1421) +- Parse comment(s) immediately preceding a selector (#1421, #1424) - Parse consecutive comments (#1421) - Support attribute selectors with values containing commas in `DeclarationBlock::setSelectors()` (#1419) diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 7d669ce8..c8cdb6d4 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -307,7 +307,6 @@ private static function parseSelector(ParserState $parserState, array &$comments static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ',']; while (true) { - $selectorParts[] = $parserState->consume(1); $selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments); $nextCharacter = $parserState->peek(); switch ($nextCharacter) { @@ -348,6 +347,7 @@ private static function parseSelector(ParserState $parserState, array &$comments } break; } + $selectorParts[] = $parserState->consume(1); } if ($functionNestingLevel !== 0) { diff --git a/tests/Unit/RuleSet/DeclarationBlockTest.php b/tests/Unit/RuleSet/DeclarationBlockTest.php index 97dcb221..9eaab956 100644 --- a/tests/Unit/RuleSet/DeclarationBlockTest.php +++ b/tests/Unit/RuleSet/DeclarationBlockTest.php @@ -5,6 +5,7 @@ namespace Sabberworm\CSS\Tests\Unit\RuleSet; use PHPUnit\Framework\TestCase; +use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\CSSList\CSSListItem; use Sabberworm\CSS\Parsing\ParserState; @@ -158,21 +159,85 @@ public function parsesTwoCommaSeparatedSelectors(string $firstSelector, string $ self::assertSame([$firstSelector, $secondSelector], self::getSelectorsAsStrings($subject)); } + /** + * @return array + */ + public static function provideSelectorWithAndWithoutComment(): array + { + return [ + 'comment before' => ['/*comment*/body', 'body'], + 'comment after' => ['body/*comment*/', 'body'], + 'comment within' => ['./*comment*/teapot', '.teapot'], + 'comment within function' => [':not(#your-mug,/*comment*/.their-mug)', ':not(#your-mug,.their-mug)'], + ]; + } + + /** + * @test + * + * @param non-empty-string $selectorWith + * @param non-empty-string $selectorWithout + * + * @dataProvider provideSelectorWithAndWithoutComment + */ + public function parsesSelectorWithComment(string $selectorWith, string $selectorWithout): void + { + $subject = DeclarationBlock::parse(new ParserState($selectorWith . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame([$selectorWithout], self::getSelectorsAsStrings($subject)); + } + + /** + * @test + * + * @param non-empty-string $selector + * + * @dataProvider provideSelectorWithAndWithoutComment + */ + public function parseExtractsCommentFromSelector(string $selector): void + { + $subject = DeclarationBlock::parse(new ParserState($selector . ' {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['comment'], self::getCommentsAsStrings($subject)); + } + + /** + * @test + */ + public function parsesSelectorWithTwoComments(): void + { + $subject = DeclarationBlock::parse(new ParserState('/*comment1*/a/*comment2*/ {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['a'], self::getSelectorsAsStrings($subject)); + } + + /** + * @test + */ + public function parseExtractsTwoCommentsFromSelector(): void + { + $subject = DeclarationBlock::parse(new ParserState('/*comment1*/a/*comment2*/ {}', Settings::create())); + + self::assertInstanceOf(DeclarationBlock::class, $subject); + self::assertSame(['comment1', 'comment2'], self::getCommentsAsStrings($subject)); + } + /** * @return array */ public static function provideInvalidSelectorAndExpectedExceptionMessage(): array { - // TODO: the `parse` method consumes the first character without inspection, - // so some of the test strings are prefixed with a space. return [ - 'no selector' => [' ', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], - 'lone `(`' => [' (', 'Token “)” (literal) not found. Got “{”.'], - 'lone `)`' => [' )', 'Token “anything but” (literal) not found. Got “)”.'], - 'lone `,`' => [' ,', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + 'no selector' => ['', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], + 'lone `(`' => ['(', 'Token “)” (literal) not found. Got “{”.'], + 'lone `)`' => [')', 'Token “anything but” (literal) not found. Got “)”.'], + 'lone `,`' => [',', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], 'unclosed `(`' => [':not(#your-mug', 'Token “)” (literal) not found. Got “{”.'], 'extra `)`' => [':not(#your-mug))', 'Token “anything but” (literal) not found. Got “)”.'], - '`,` missing left operand' => [' , a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], + '`,` missing left operand' => [', a', 'Token “selector” (literal) not found. Got “,”. [line no: 1]'], '`,` missing right operand' => ['a,', 'Token “selector” (literal) not found. Got “{”. [line no: 1]'], ]; } @@ -199,8 +264,6 @@ static function (array $testData): array { /** * @test * - * @param non-empty-string $selector - * * @dataProvider provideInvalidSelector */ public function parseSkipsBlockWithInvalidSelector(string $selector): void @@ -326,6 +389,19 @@ static function (Selector $selectorObject): string { ); } + /** + * @return list + */ + private static function getCommentsAsStrings(DeclarationBlock $declarationBlock): array + { + return \array_map( + static function (Comment $comment): string { + return $comment->getComment(); + }, + $declarationBlock->getComments() + ); + } + /** * @test */