Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 69 additions & 52 deletions src/RuleSet/DeclarationBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Sabberworm\CSS\RuleSet;

use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\CSSList\CSSList;
Expand Down Expand Up @@ -65,58 +66,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
$comments = [];
$result = new DeclarationBlock($parserState->currentLine());
try {
$selectors = [];
$selectorParts = [];
$stringWrapperCharacter = null;
$functionNestingLevel = 0;
$consumedNextCharacter = false;
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
do {
if (!$consumedNextCharacter) {
$selectorParts[] = $parserState->consume(1);
}
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
$nextCharacter = $parserState->peek();
$consumedNextCharacter = false;
switch ($nextCharacter) {
case '\'':
// The fallthrough is intentional.
case '"':
if (!\is_string($stringWrapperCharacter)) {
$stringWrapperCharacter = $nextCharacter;
} elseif ($stringWrapperCharacter === $nextCharacter) {
if (\substr(\end($selectorParts), -1) !== '\\') {
$stringWrapperCharacter = null;
}
}
break;
case '(':
if (!\is_string($stringWrapperCharacter)) {
++$functionNestingLevel;
}
break;
case ')':
if (!\is_string($stringWrapperCharacter)) {
if ($functionNestingLevel <= 0) {
throw new UnexpectedTokenException('anything but', ')');
}
--$functionNestingLevel;
}
break;
case ',':
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
$selectors[] = \implode('', $selectorParts);
$selectorParts = [];
$parserState->consume(1);
$consumedNextCharacter = true;
}
break;
}
} while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));
if ($functionNestingLevel !== 0) {
throw new UnexpectedTokenException(')', $nextCharacter);
}
$selectors[] = \implode('', $selectorParts); // add final or only selector
$selectors = self::parseSelectors($parserState, $comments);
$result->setSelectors($selectors, $list);
if ($parserState->comes('{')) {
$parserState->consume(1);
Expand Down Expand Up @@ -303,4 +253,71 @@ public function render(OutputFormat $outputFormat): string

return $result;
}

/**
* @param array<int, Comment> $comments
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't get Stan to be happy with list<Comment>...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My best guess is that that's because ParserState::consumeUntil has $comments annotated as @param array<int, Comment> $comments as well, and it's passed by reference.

We probably can change both type annotations to list (in a separate PR).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That is indeed the case.

We probably can change both type annotations to list (in a separate PR).

#1418.

*
* @return list<string>
*
* @throws UnexpectedTokenException
*/
private static function parseSelectors(ParserState $parserState, array &$comments): array
{
$selectors = [];
$selectorParts = [];
$stringWrapperCharacter = null;
$functionNestingLevel = 0;
$consumedNextCharacter = false;
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];

do {
if (!$consumedNextCharacter) {
$selectorParts[] = $parserState->consume(1);
}
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
$nextCharacter = $parserState->peek();
$consumedNextCharacter = false;
switch ($nextCharacter) {
case '\'':
// The fallthrough is intentional.
case '"':
if (!\is_string($stringWrapperCharacter)) {
$stringWrapperCharacter = $nextCharacter;
} elseif ($stringWrapperCharacter === $nextCharacter) {
if (\substr(\end($selectorParts), -1) !== '\\') {
$stringWrapperCharacter = null;
}
}
break;
case '(':
if (!\is_string($stringWrapperCharacter)) {
++$functionNestingLevel;
}
break;
case ')':
if (!\is_string($stringWrapperCharacter)) {
if ($functionNestingLevel <= 0) {
throw new UnexpectedTokenException('anything but', ')');
}
--$functionNestingLevel;
}
break;
case ',':
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
$selectors[] = \implode('', $selectorParts);
$selectorParts = [];
$parserState->consume(1);
$consumedNextCharacter = true;
}
break;
}
} while (!\in_array($nextCharacter, ['{', '}'], true) || \is_string($stringWrapperCharacter));

if ($functionNestingLevel !== 0) {
throw new UnexpectedTokenException(')', $nextCharacter);
}
$selectors[] = \implode('', $selectorParts); // add final or only selector

return $selectors;
}
}