Skip to content

Commit a415794

Browse files
committed
[FEATURE] Represent Selector as Component objects
Part of #1325.
1 parent b67aac0 commit a415794

3 files changed

Lines changed: 133 additions & 35 deletions

File tree

src/Property/Selector.php

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@
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;
16+
use Sabberworm\CSS\ShortClassNameProvider;
1617

1718
use function Safe\preg_match;
18-
use function Safe\preg_replace;
1919

2020
/**
2121
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
2222
* class.
2323
*/
2424
class Selector implements Renderable
2525
{
26+
use ShortClassNameProvider;
27+
2628
/**
2729
* @internal since 8.5.2
2830
*/
@@ -54,9 +56,9 @@ class Selector implements Renderable
5456
/ux';
5557

5658
/**
57-
* @var string
59+
* @var list<Component>
5860
*/
59-
private $selector;
61+
private $components = [];
6062

6163
/**
6264
* @internal since V8.8.0
@@ -72,9 +74,13 @@ public static function isValid(string $selector): bool
7274
/**
7375
* @throws \UnexpectedValueException if the selector is not valid
7476
*/
75-
final public function __construct(string $selector)
77+
final public function __construct(string $selector = '')
7678
{
77-
$this->setSelector($selector);
79+
// Allow construction of empty object for content to be set via `setComponents()`.
80+
// (`setSelector()` will throw an exception when provided with an empty string.)
81+
if ($selector !== '') {
82+
$this->setSelector($selector);
83+
}
7884
}
7985

8086
/**
@@ -137,52 +143,76 @@ public static function parse(ParserState $parserState, array &$comments = []): s
137143
);
138144
}
139145

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

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

153169
public function getSelector(): string
154170
{
155-
return $this->selector;
171+
return $this->render(new OutputFormat());
156172
}
157173

158174
/**
159175
* @throws \UnexpectedValueException if the selector is not valid
160176
*/
161177
public function setSelector(string $selector): void
162178
{
163-
if (!self::isValid($selector)) {
164-
throw new \UnexpectedValueException("Selector `$selector` is not valid.");
165-
}
179+
$parserState = new ParserState($selector, Settings::create());
166180

167-
$selector = \trim($selector);
181+
$components = self::parseComponents($parserState);
168182

169-
$hasAttribute = \strpos($selector, '[') !== false;
183+
// Check that the selector has been fully parsed:
184+
if (!$parserState->isEnd()) {
185+
throw new UnexpectedTokenException(
186+
'EOF',
187+
$parserState->peek(5),
188+
'literal'
189+
);
190+
}
170191

171-
// Whitespace can't be adjusted within an attribute selector, as it would change its meaning
172-
$this->selector = !$hasAttribute ? preg_replace('/\\s++/', ' ', $selector) : $selector;
192+
$this->components = $components;
173193
}
174194

175195
/**
176196
* @return int<0, max>
177197
*/
178198
public function getSpecificity(): int
179199
{
180-
return SpecificityCalculator::calculate($this->selector);
200+
return \array_sum(\array_map(
201+
static function (Component $component): int {
202+
return $component->getSpecificity();
203+
},
204+
$this->components
205+
));
181206
}
182207

183208
public function render(OutputFormat $outputFormat): string
184209
{
185-
return $this->getSelector();
210+
return \implode('', \array_map(
211+
static function (Component $component) use ($outputFormat): string {
212+
return $component->render($outputFormat);
213+
},
214+
$this->components
215+
));
186216
}
187217

188218
/**
@@ -192,6 +222,14 @@ public function render(OutputFormat $outputFormat): string
192222
*/
193223
public function getArrayRepresentation(): array
194224
{
195-
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
225+
return [
226+
'class' => $this->getShortClassName(),
227+
'components' => \array_map(
228+
static function (Component $component): array {
229+
return $component->getArrayRepresentation();
230+
},
231+
$this->components
232+
),
233+
];
196234
}
197235
}

tests/Unit/Property/KeyframeSelectorTest.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use PHPUnit\Framework\TestCase;
88
use Sabberworm\CSS\Property\KeyframeSelector;
9+
use Sabberworm\CSS\Property\Selector\CompoundSelector;
910

1011
/**
1112
* @covers \Sabberworm\CSS\Property\KeyframeSelector
@@ -16,12 +17,24 @@ final class KeyframeSelectorTest extends TestCase
1617
/**
1718
* @test
1819
*/
19-
public function getArrayRepresentationThrowsException(): void
20+
public function getArrayRepresentationIncludesClassName(): void
2021
{
21-
$this->expectException(\BadMethodCallException::class);
22+
$subject = new KeyframeSelector('');
2223

23-
$subject = new KeyframeSelector('a');
24+
$result = $subject->getArrayRepresentation();
2425

25-
$subject->getArrayRepresentation();
26+
self::assertSame('KeyframeSelector', $result['class']);
27+
}
28+
29+
/**
30+
* @test
31+
*/
32+
public function getArrayRepresentationIncludesComponent(): void
33+
{
34+
$subject = (new KeyframeSelector(''))->setComponents([new CompoundSelector('50%')]);
35+
36+
$result = $subject->getArrayRepresentation();
37+
38+
self::assertSame('50%', $result['components'][0]['value']);
2639
}
2740
}

tests/Unit/Property/SelectorTest.php

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Sabberworm\CSS\Parsing\ParserState;
1010
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
1111
use Sabberworm\CSS\Property\Selector;
12+
use Sabberworm\CSS\Property\Selector\CompoundSelector;
1213
use Sabberworm\CSS\Renderable;
1314
use Sabberworm\CSS\Settings;
1415
use TRegx\PhpUnit\DataProviders\DataProvider;
@@ -351,6 +352,40 @@ public function setSelectorThrowsExceptionWithInvalidSelector(string $selector):
351352
$subject->setSelector($selector);
352353
}
353354

355+
/**
356+
* @test
357+
*
358+
* @dataProvider provideInvalidSelectors
359+
* @dataProvider provideInvalidSelectorsForParse
360+
*/
361+
public function constructorThrowsExceptionWithInvalidSelector(string $selector): void
362+
{
363+
// An empty string is allowed to construct an empty object
364+
if ($selector === '') {
365+
self::expectNotToPerformAssertions();
366+
return;
367+
}
368+
369+
$this->expectException(UnexpectedTokenException::class);
370+
371+
new Selector($selector);
372+
}
373+
374+
/**
375+
* @test
376+
*
377+
* @dataProvider provideInvalidSelectors
378+
* @dataProvider provideInvalidSelectorsForParse
379+
*/
380+
public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void
381+
{
382+
$this->expectException(UnexpectedTokenException::class);
383+
384+
$subject = new Selector('a');
385+
386+
$subject->setSelector($selector);
387+
}
388+
354389
/**
355390
* @test
356391
*
@@ -458,12 +493,24 @@ public function doesNotCleanupSpacesWithinAttributeSelector(): void
458493
/**
459494
* @test
460495
*/
461-
public function getArrayRepresentationThrowsException(): void
496+
public function getArrayRepresentationIncludesClassName(): void
462497
{
463-
$this->expectException(\BadMethodCallException::class);
498+
$subject = new Selector('');
464499

465-
$subject = new Selector('a');
500+
$result = $subject->getArrayRepresentation();
501+
502+
self::assertSame('Selector', $result['class']);
503+
}
504+
505+
/**
506+
* @test
507+
*/
508+
public function getArrayRepresentationIncludesComponent(): void
509+
{
510+
$subject = (new Selector(''))->setComponents([new CompoundSelector('p.test')]);
511+
512+
$result = $subject->getArrayRepresentation();
466513

467-
$subject->getArrayRepresentation();
514+
self::assertSame('p.test', $result['components'][0]['value']);
468515
}
469516
}

0 commit comments

Comments
 (0)