diff --git a/src/CSSList/CSSList.php b/src/CSSList/CSSList.php index 48e27c68..62f7fe65 100644 --- a/src/CSSList/CSSList.php +++ b/src/CSSList/CSSList.php @@ -68,7 +68,7 @@ public static function parseList(ParserState $parserState, CSSList $list): void $usesLenientParsing = $parserState->getSettings()->usesLenientParsing(); $comments = []; while (!$parserState->isEnd()) { - $comments = \array_merge($comments, $parserState->consumeWhiteSpace()); + $parserState->consumeWhiteSpace($comments); $listItem = null; if ($usesLenientParsing) { try { @@ -93,7 +93,8 @@ public static function parseList(ParserState $parserState, CSSList $list): void $listItem->addComments($comments); $list->append($listItem); } - $comments = $parserState->consumeWhiteSpace(); + $comments = []; + $parserState->consumeWhiteSpace($comments); } $list->addComments($comments); if (!$isRoot && !$usesLenientParsing) { diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 44f96a94..3451fe0c 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -188,7 +188,11 @@ public function parseCharacter(bool $isForIdentifier): ?string } /** - * @return list + * Consumes whitespace and/or comments until the next non-whitespace character that isn't a slash opening a comment. + * + * @param list $comments Any comments consumed will be appended to this array. + * + * @return string the whitespace consumed, without the comments * * @throws UnexpectedEOFException * @throws UnexpectedTokenException @@ -197,12 +201,12 @@ public function parseCharacter(bool $isForIdentifier): ?string * This method may change the state of the object by advancing the internal position; * it does not simply 'get' a value. */ - public function consumeWhiteSpace(): array + public function consumeWhiteSpace(array &$comments = []): string { - $comments = []; + $consumed = ''; do { while (preg_match('/\\s/isSu', $this->peek()) === 1) { - $this->consume(1); + $consumed .= $this->consume(1); } if ($this->parserSettings->usesLenientParsing()) { try { @@ -219,7 +223,7 @@ public function consumeWhiteSpace(): array } } while ($comment instanceof Comment); - return $comments; + return $consumed; } /** diff --git a/src/Rule/Rule.php b/src/Rule/Rule.php index 27b6bd9a..26e68b23 100644 --- a/src/Rule/Rule.php +++ b/src/Rule/Rule.php @@ -65,14 +65,15 @@ public function __construct(string $rule, ?int $lineNumber = null, ?int $columnN */ public static function parse(ParserState $parserState, array $commentsBeforeRule = []): Rule { - $comments = \array_merge($commentsBeforeRule, $parserState->consumeWhiteSpace()); + $comments = $commentsBeforeRule; + $parserState->consumeWhiteSpace($comments); $rule = new Rule( $parserState->parseIdentifier(!$parserState->comes('--')), $parserState->currentLine(), $parserState->currentColumn() ); + $parserState->consumeWhiteSpace($comments); $rule->setComments($comments); - $rule->addComments($parserState->consumeWhiteSpace()); $parserState->consume(':'); $value = Value::parseValue($parserState, self::listDelimiterForRule($rule->getRule())); $rule->setValue($value); diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index 576ae23f..e1086328 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -59,7 +59,8 @@ public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): $parserState->consume(';'); } while (true) { - $commentsBeforeRule = $parserState->consumeWhiteSpace(); + $commentsBeforeRule = []; + $parserState->consumeWhiteSpace($commentsBeforeRule); if ($parserState->comes('}')) { break; } diff --git a/tests/Unit/Parsing/ParserStateTest.php b/tests/Unit/Parsing/ParserStateTest.php index bd22508a..ebc3c7fa 100644 --- a/tests/Unit/Parsing/ParserStateTest.php +++ b/tests/Unit/Parsing/ParserStateTest.php @@ -8,6 +8,7 @@ use Sabberworm\CSS\Comment\Comment; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Settings; +use TRegx\PhpUnit\DataProviders\DataProvider; /** * @covers \Sabberworm\CSS\Parsing\ParserState @@ -157,4 +158,149 @@ public function consumeIfComesUpdatesLineNumber(): void self::assertSame(2, $subject->currentLine()); } + + /** + * @return array + */ + public static function provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption(): array + { + return [ + 'nothing' => ['', ''], + 'space' => [' ', ' '], + 'tab' => ["\t", "\t"], + 'line feed' => ["\n", "\n"], + 'carriage return' => ["\r", "\r"], + 'two spaces' => [' ', ' '], + 'comment' => ['/*hello*/', ''], + 'comment with space to the left' => [' /*hello*/', ' '], + 'comment with space to the right' => ['/*hello*/ ', ' '], + 'two comments' => ['/*hello*//*bye*/', ''], + 'two comments with space between' => ['/*hello*/ /*bye*/', ' '], + 'two comments with line feed between' => ["/*hello*/\n/*bye*/", "\n"], + ]; + } + + /** + * @test + * + * @dataProvider provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption + */ + public function consumeWhiteSpaceReturnsTheConsumed( + string $whitespaceMaybeWithComments, + string $expectedConsumption + ): void { + $subject = new ParserState($whitespaceMaybeWithComments, Settings::create()); + + $result = $subject->consumeWhiteSpace(); + + self::assertSame($expectedConsumption, $result); + } + + /** + * @test + */ + public function consumeWhiteSpaceExtractsComment(): void + { + $commentText = 'Did they get you to trade your heroes for ghosts?'; + $subject = new ParserState('/*' . $commentText . '*/', Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText, $result[0]->getComment()); + } + + /** + * @test + */ + public function consumeWhiteSpaceExtractsTwoComments(): void + { + $commentText1 = 'Hot ashes for trees? Hot air for a cool breeze?'; + $commentText2 = 'Cold comfort for change? Did you exchange'; + $subject = new ParserState('/*' . $commentText1 . '*//*' . $commentText2 . '*/', Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText1, $result[0]->getComment()); + self::assertInstanceOf(Comment::class, $result[1]); + self::assertSame($commentText2, $result[1]->getComment()); + } + + /** + * @return array + */ + public static function provideWhitespace(): array + { + return [ + 'space' => [' '], + 'tab' => ["\t"], + 'line feed' => ["\n"], + 'carriage return' => ["\r"], + 'two spaces' => [' '], + ]; + } + + /** + * @test + * + * @param non-empty-string $whitespace + * + * @dataProvider provideWhitespace + */ + public function consumeWhiteSpaceExtractsCommentWithSurroundingWhitespace(string $whitespace): void + { + $commentText = 'A walk-on part in the war for a lead role in a cage?'; + $subject = new ParserState($whitespace . '/*' . $commentText . '*/' . $whitespace, Settings::create()); + + $result = []; + $subject->consumeWhiteSpace($result); + + self::assertInstanceOf(Comment::class, $result[0]); + self::assertSame($commentText, $result[0]->getComment()); + } + + /** + * @return array + */ + public static function provideNonWhitespace(): array + { + return [ + 'number' => ['7'], + 'uppercase letter' => ['B'], + 'lowercase letter' => ['c'], + 'symbol' => ['@'], + 'sequence of non-whitespace characters' => ['Hello!'], + 'sequence of characters including space which isn\'t first' => ['Oh no!'], + ]; + } + + /** + * @return DataProvider + */ + public function provideNonWhitespaceAndContentWithPossibleWhitespaceOrComments(): DataProvider + { + return DataProvider::cross( + self::provideNonWhitespace(), + self::provideContentWhichMayHaveWhitespaceOrCommentsAndExpectedConsumption() + ); + } + + /** + * @test + * + * @param non-empty-string $nonWhitespace + * + * @dataProvider provideNonWhitespaceAndContentWithPossibleWhitespaceOrComments + */ + public function consumeWhiteSpaceStopsAtNonWhitespace(string $nonWhitespace, string $whitespace): void + { + $subject = new ParserState($whitespace . $nonWhitespace, Settings::create()); + + $subject->consumeWhiteSpace(); + + self::assertTrue($subject->comes($nonWhitespace)); + } }