Skip to content

Commit 0effcdb

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

3 files changed

Lines changed: 134 additions & 34 deletions

File tree

src/Property/Selector.php

Lines changed: 66 additions & 24 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
@@ -69,9 +71,13 @@ public static function isValid(string $selector): bool
6971
return $numberOfMatches === 1;
7072
}
7173

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

7783
/**
@@ -134,45 +140,73 @@ public static function parse(ParserState $parserState, array &$comments = []): s
134140
);
135141
}
136142

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-
}
143+
return (new static())->setComponents($selectorParts);
144+
}
146145

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

150166
public function getSelector(): string
151167
{
152-
return $this->selector;
168+
return $this->render(new OutputFormat());
153169
}
154170

155171
public function setSelector(string $selector): void
156172
{
157-
$selector = \trim($selector);
173+
$parserState = new ParserState($selector, Settings::create());
174+
175+
$components = self::parseComponents($parserState);
158176

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

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;
186+
$this->components = $components;
163187
}
164188

165189
/**
166190
* @return int<0, max>
167191
*/
168192
public function getSpecificity(): int
169193
{
170-
return SpecificityCalculator::calculate($this->selector);
194+
return \array_sum(\array_map(
195+
static function (Component $component): int {
196+
return $component->getSpecificity();
197+
},
198+
$this->components
199+
));
171200
}
172201

173202
public function render(OutputFormat $outputFormat): string
174203
{
175-
return $this->getSelector();
204+
return \implode('', \array_map(
205+
static function (Component $component) use ($outputFormat): string {
206+
return $component->render($outputFormat);
207+
},
208+
$this->components
209+
));
176210
}
177211

178212
/**
@@ -182,6 +216,14 @@ public function render(OutputFormat $outputFormat): string
182216
*/
183217
public function getArrayRepresentation(): array
184218
{
185-
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
219+
return [
220+
'class' => $this->getShortClassName(),
221+
'components' => \array_map(
222+
static function (Component $component): array {
223+
return $component->getArrayRepresentation();
224+
},
225+
$this->components
226+
),
227+
];
186228
}
187229
}

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 & 6 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;
@@ -198,8 +199,6 @@ public static function provideInvalidSelectorsForParse(): array
198199
/**
199200
* @test
200201
*
201-
* @param non-empty-string $selector
202-
*
203202
* @dataProvider provideInvalidSelectors
204203
* @dataProvider provideInvalidSelectorsForParse
205204
*/
@@ -317,6 +316,40 @@ public function parseExtractsTwoCommentsFromSelector(): void
317316
self::assertSame('comment2', $result[1]->getComment());
318317
}
319318

319+
/**
320+
* @test
321+
*
322+
* @dataProvider provideInvalidSelectors
323+
* @dataProvider provideInvalidSelectorsForParse
324+
*/
325+
public function constructorThrowsExceptionWithInvalidSelector(string $selector): void
326+
{
327+
// An empty string is allowed to construct an empty object
328+
if ($selector === '') {
329+
self::expectNotToPerformAssertions();
330+
return;
331+
}
332+
333+
$this->expectException(UnexpectedTokenException::class);
334+
335+
new Selector($selector);
336+
}
337+
338+
/**
339+
* @test
340+
*
341+
* @dataProvider provideInvalidSelectors
342+
* @dataProvider provideInvalidSelectorsForParse
343+
*/
344+
public function setSelectorThrowsExceptionWithInvalidSelector(string $selector): void
345+
{
346+
$this->expectException(UnexpectedTokenException::class);
347+
348+
$subject = new Selector('a');
349+
350+
$subject->setSelector($selector);
351+
}
352+
320353
/**
321354
* @test
322355
*
@@ -424,12 +457,24 @@ public function doesNotCleanupSpacesWithinAttributeSelector(): void
424457
/**
425458
* @test
426459
*/
427-
public function getArrayRepresentationThrowsException(): void
460+
public function getArrayRepresentationIncludesClassName(): void
428461
{
429-
$this->expectException(\BadMethodCallException::class);
462+
$subject = new Selector('');
430463

431-
$subject = new Selector('a');
464+
$result = $subject->getArrayRepresentation();
465+
466+
self::assertSame('Selector', $result['class']);
467+
}
468+
469+
/**
470+
* @test
471+
*/
472+
public function getArrayRepresentationIncludesComponent(): void
473+
{
474+
$subject = (new Selector(''))->setComponents([new CompoundSelector('p.test')]);
475+
476+
$result = $subject->getArrayRepresentation();
432477

433-
$subject->getArrayRepresentation();
478+
self::assertSame('p.test', $result['components'][0]['value']);
434479
}
435480
}

0 commit comments

Comments
 (0)