Skip to content

Commit 4a4d0f0

Browse files
committed
[FEATURE] Represent Selector as Component objects
Part of #1325.
1 parent 3282587 commit 4a4d0f0

2 files changed

Lines changed: 99 additions & 26 deletions

File tree

src/Property/Selector.php

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,19 @@
1111
use Sabberworm\CSS\Property\Selector\Combinator;
1212
use Sabberworm\CSS\Property\Selector\Component;
1313
use Sabberworm\CSS\Property\Selector\CompoundSelector;
14-
use Sabberworm\CSS\Property\Selector\SpecificityCalculator;
1514
use Sabberworm\CSS\Renderable;
15+
use Sabberworm\CSS\Settings;
1616

1717
use function Safe\preg_match;
18-
use function Safe\preg_replace;
1918

2019
/**
2120
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
2221
* class.
2322
*/
2423
class Selector implements Renderable
2524
{
25+
use ShortClassNameProvider;
26+
2627
/**
2728
* @internal since 8.5.2
2829
*/
@@ -54,9 +55,9 @@ class Selector implements Renderable
5455
/ux';
5556

5657
/**
57-
* @var string
58+
* @var list<Component>
5859
*/
59-
private $selector;
60+
private $components = [];
6061

6162
/**
6263
* @internal since V8.8.0
@@ -69,9 +70,13 @@ public static function isValid(string $selector): bool
6970
return $numberOfMatches === 1;
7071
}
7172

72-
final public function __construct(string $selector)
73+
final public function __construct(string $selector = '')
7374
{
74-
$this->setSelector($selector);
75+
// Allow construction of empty object for content to be set via `setComponents()`.
76+
// (`setSelector()` will throw an exception when provided with an empty string.)
77+
if ($selector !== '') {
78+
$this->setSelector($selector);
79+
}
7580
}
7681

7782
/**
@@ -134,45 +139,73 @@ public static function parse(ParserState $parserState, array &$comments = []): s
134139
);
135140
}
136141

137-
$selectorString = '';
138-
foreach ($selectorParts as $selectorPart) {
139-
$selectorPartValue = $selectorPart->getValue();
140-
if (\in_array($selectorPartValue, ['>', '+', '~'], true)) {
141-
$selectorString .= ' ' . $selectorPartValue . ' ';
142-
} else {
143-
$selectorString .= $selectorPartValue;
144-
}
145-
}
142+
return (new static())->setComponents($selectorParts);
143+
}
146144

147-
return new static($selectorString);
145+
/**
146+
* @return list<Component>
147+
*/
148+
public function getComponents(): array
149+
{
150+
return $this->components;
151+
}
152+
153+
/**
154+
* @param list<Component> $components
155+
* This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a
156+
* `CompoundSelector`, and may be a single `CompoundSelector`.
157+
*/
158+
public function setComponents(array $components): self
159+
{
160+
$this->components = $components;
161+
162+
return $this;
148163
}
149164

150165
public function getSelector(): string
151166
{
152-
return $this->selector;
167+
return $this->render(new OutputFormat());
153168
}
154169

155170
public function setSelector(string $selector): void
156171
{
157-
$selector = \trim($selector);
172+
$parserState = new ParserState($selector, Settings::create());
173+
174+
$components = self::parseComponents($parserState);
158175

159-
$hasAttribute = \strpos($selector, '[') !== false;
176+
// Check that the selector has been fully parsed:
177+
if (!$parserState->isEnd()) {
178+
throw new UnexpectedTokenException(
179+
'EOF',
180+
$parserState->peek(5),
181+
'literal'
182+
);
183+
}
160184

161-
// Whitespace can't be adjusted within an attribute selector, as it would change its meaning
162-
$this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
185+
$this->components = $components;
163186
}
164187

165188
/**
166189
* @return int<0, max>
167190
*/
168191
public function getSpecificity(): int
169192
{
170-
return SpecificityCalculator::calculate($this->selector);
193+
return \array_sum(\array_map(
194+
static function (Component $component): int {
195+
return $component->getSpecificity();
196+
},
197+
$this->components
198+
));
171199
}
172200

173201
public function render(OutputFormat $outputFormat): string
174202
{
175-
return $this->getSelector();
203+
return \implode('', \array_map(
204+
static function (Component $component) use ($outputFormat): string {
205+
return $component->render($outputFormat);
206+
},
207+
$this->components
208+
));
176209
}
177210

178211
/**
@@ -182,6 +215,14 @@ public function render(OutputFormat $outputFormat): string
182215
*/
183216
public function getArrayRepresentation(): array
184217
{
185-
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
218+
return [
219+
'class' => $this->getShortClassName(),
220+
'components' => \array_map(
221+
static function (Component $component): array {
222+
return $component->getArrayRepresentation();
223+
},
224+
$this->components
225+
),
226+
];
186227
}
187228
}

tests/Unit/Property/SelectorTest.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,6 @@ public static function provideInvalidSelectorsForParse(): array
198198
/**
199199
* @test
200200
*
201-
* @param non-empty-string $selector
202-
*
203201
* @dataProvider provideInvalidSelectors
204202
* @dataProvider provideInvalidSelectorsForParse
205203
*/
@@ -317,6 +315,40 @@ public function parseExtractsTwoCommentsFromSelector(): void
317315
self::assertSame('comment2', $result[1]->getComment());
318316
}
319317

318+
/**
319+
* @test
320+
*
321+
* @dataProvider provideInvalidSelectors
322+
* @dataProvider provideInvalidSelectorsForParse
323+
*/
324+
public function constructorThrowsExceptionWithInvalidSelector(string $selector): void
325+
{
326+
// An empty string is allowed to construct an empty object
327+
if ($selector === '') {
328+
self::expectNotToPerformAssertions();
329+
return;
330+
}
331+
332+
$this->expectException(UnexpectedTokenException::class);
333+
334+
new Selector($selector);
335+
}
336+
337+
/**
338+
* @test
339+
*
340+
* @dataProvider provideInvalidSelectors
341+
* @dataProvider provideInvalidSelectorsForParse
342+
*/
343+
public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void
344+
{
345+
$this->expectException(UnexpectedTokenException::class);
346+
347+
$subject = new Selector('a');
348+
349+
$subject->setSelector($selector);
350+
}
351+
320352
/**
321353
* @test
322354
*

0 commit comments

Comments
 (0)