Skip to content

Commit b515237

Browse files
committed
[TASK] Add Selector::parse
This replaces `DeclarationBlock::parseSelector`.
1 parent bea9012 commit b515237

2 files changed

Lines changed: 92 additions & 79 deletions

File tree

src/Property/Selector.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Sabberworm\CSS\Property;
66

7+
use Sabberworm\CSS\Comment\Comment;
78
use Sabberworm\CSS\OutputFormat;
9+
use Sabberworm\CSS\Parsing\ParserState;
10+
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
811
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
912
use Sabberworm\CSS\Renderable;
1013

@@ -63,11 +66,93 @@ public static function isValid(string $selector): bool
6366
return $numberOfMatches === 1;
6467
}
6568

66-
public function __construct(string $selector)
69+
final public function __construct(string $selector)
6770
{
6871
$this->setSelector($selector);
6972
}
7073

74+
/**
75+
* @param list<Comment> $comments
76+
*
77+
* @throws UnexpectedTokenException
78+
*
79+
* @internal
80+
*/
81+
public static function parse(ParserState $parserState, array &$comments = []): self
82+
{
83+
$selectorParts = [];
84+
$stringWrapperCharacter = null;
85+
$functionNestingLevel = 0;
86+
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
87+
88+
while (true) {
89+
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
90+
$nextCharacter = $parserState->peek();
91+
switch ($nextCharacter) {
92+
case '\'':
93+
// The fallthrough is intentional.
94+
case '"':
95+
if (!\is_string($stringWrapperCharacter)) {
96+
$stringWrapperCharacter = $nextCharacter;
97+
} elseif ($stringWrapperCharacter === $nextCharacter) {
98+
if (\substr(\end($selectorParts), -1) !== '\\') {
99+
$stringWrapperCharacter = null;
100+
}
101+
}
102+
break;
103+
case '(':
104+
if (!\is_string($stringWrapperCharacter)) {
105+
++$functionNestingLevel;
106+
}
107+
break;
108+
case ')':
109+
if (!\is_string($stringWrapperCharacter)) {
110+
if ($functionNestingLevel <= 0) {
111+
throw new UnexpectedTokenException(
112+
'anything but',
113+
')',
114+
'literal',
115+
$parserState->currentLine()
116+
);
117+
}
118+
--$functionNestingLevel;
119+
}
120+
break;
121+
case '{':
122+
// The fallthrough is intentional.
123+
case '}':
124+
if (!\is_string($stringWrapperCharacter)) {
125+
break 2;
126+
}
127+
break;
128+
case ',':
129+
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
130+
break 2;
131+
}
132+
break;
133+
}
134+
$selectorParts[] = $parserState->consume(1);
135+
}
136+
137+
if ($functionNestingLevel !== 0) {
138+
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
139+
}
140+
141+
$selector = \trim(\implode('', $selectorParts));
142+
if ($selector === '') {
143+
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
144+
}
145+
if (!self::isValid($selector)) {
146+
throw new UnexpectedTokenException(
147+
"Selector did not match '" . static::SELECTOR_VALIDATION_RX . "'.",
148+
$selector,
149+
'custom'
150+
);
151+
}
152+
153+
return new static($selector);
154+
}
155+
71156
public function getSelector(): string
72157
{
73158
return $this->selector;

src/RuleSet/DeclarationBlock.php

Lines changed: 6 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6767
$comments = [];
6868
$result = new DeclarationBlock($parserState->currentLine());
6969
try {
70-
$selectors = self::parseSelectors($parserState, $comments);
70+
$selectors = self::parseSelectors($parserState, $list, $comments);
7171
$result->setSelectors($selectors, $list);
7272
if ($parserState->comes('{')) {
7373
$parserState->consume(1);
@@ -103,7 +103,7 @@ public function setSelectors($selectors, ?CSSList $list = null): void
103103
// Parse as if it's the opening part of a rule.
104104
try {
105105
$parserState = new ParserState($selectors . '{', Settings::create());
106-
$selectorsToSet = self::parseSelectors($parserState);
106+
$selectorsToSet = self::parseSelectors($parserState, $list);
107107
$parserState->consume('{'); // throw exception if this is not next
108108
if (!$parserState->isEnd()) {
109109
throw new UnexpectedTokenException('EOF', 'more');
@@ -286,94 +286,22 @@ public function getArrayRepresentation(): array
286286
/**
287287
* @param list<Comment> $comments
288288
*
289-
* @return list<string>
289+
* @return list<Selector>
290290
*
291291
* @throws UnexpectedTokenException
292292
*/
293-
private static function parseSelectors(ParserState $parserState, array &$comments = []): array
293+
private static function parseSelectors(ParserState $parserState, ?CSSList $list, array &$comments = []): array
294294
{
295+
$selectorClass = $list instanceof KeyFrame ? KeyFrameSelector::class : Selector::class;
295296
$selectors = [];
296297

297298
while (true) {
298-
$selectors[] = self::parseSelector($parserState, $comments);
299+
$selectors[] = $selectorClass::parse($parserState, $comments);
299300
if (!$parserState->consumeIfComes(',')) {
300301
break;
301302
}
302303
}
303304

304305
return $selectors;
305306
}
306-
307-
/**
308-
* @param list<Comment> $comments
309-
*
310-
* @throws UnexpectedTokenException
311-
*/
312-
private static function parseSelector(ParserState $parserState, array &$comments = []): string
313-
{
314-
$selectorParts = [];
315-
$stringWrapperCharacter = null;
316-
$functionNestingLevel = 0;
317-
static $stopCharacters = ['{', '}', '\'', '"', '(', ')', ','];
318-
319-
while (true) {
320-
$selectorParts[] = $parserState->consumeUntil($stopCharacters, false, false, $comments);
321-
$nextCharacter = $parserState->peek();
322-
switch ($nextCharacter) {
323-
case '\'':
324-
// The fallthrough is intentional.
325-
case '"':
326-
if (!\is_string($stringWrapperCharacter)) {
327-
$stringWrapperCharacter = $nextCharacter;
328-
} elseif ($stringWrapperCharacter === $nextCharacter) {
329-
if (\substr(\end($selectorParts), -1) !== '\\') {
330-
$stringWrapperCharacter = null;
331-
}
332-
}
333-
break;
334-
case '(':
335-
if (!\is_string($stringWrapperCharacter)) {
336-
++$functionNestingLevel;
337-
}
338-
break;
339-
case ')':
340-
if (!\is_string($stringWrapperCharacter)) {
341-
if ($functionNestingLevel <= 0) {
342-
throw new UnexpectedTokenException(
343-
'anything but',
344-
')',
345-
'literal',
346-
$parserState->currentLine()
347-
);
348-
}
349-
--$functionNestingLevel;
350-
}
351-
break;
352-
case '{':
353-
// The fallthrough is intentional.
354-
case '}':
355-
if (!\is_string($stringWrapperCharacter)) {
356-
break 2;
357-
}
358-
break;
359-
case ',':
360-
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0) {
361-
break 2;
362-
}
363-
break;
364-
}
365-
$selectorParts[] = $parserState->consume(1);
366-
}
367-
368-
if ($functionNestingLevel !== 0) {
369-
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
370-
}
371-
372-
$selector = \trim(\implode('', $selectorParts));
373-
if ($selector === '') {
374-
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
375-
}
376-
377-
return $selector;
378-
}
379307
}

0 commit comments

Comments
 (0)