Skip to content

Commit 4d7831a

Browse files
committed
[TASK] Use delegation for DeclarationBlock -> RuleSet
... rather than inheritance. This will allow `DeclarationBlock` to instead extend `CSSBlockList` in order to support [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting). This is a slightly-breaking change, since now `CSSBlockList::getAllRuleSets()` will include the `RuleSet` property of the `DeclarationBlock` instead of the `DeclarationBlock` itself. Part of #1170.
1 parent 36ed5cd commit 4d7831a

File tree

7 files changed

+146
-36
lines changed

7 files changed

+146
-36
lines changed

config/phpstan-baseline.neon

+12-6
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,28 @@ parameters:
4848
count: 1
4949
path: ../src/RuleSet/DeclarationBlock.php
5050

51+
-
52+
message: '#^Parameters should have "Sabberworm\\CSS\\Rule\\Rule" types as the only types passed to this method$#'
53+
identifier: typePerfect.narrowPublicClassMethodParamType
54+
count: 1
55+
path: ../src/RuleSet/DeclarationBlock.php
56+
5157
-
5258
message: '#^Parameters should have "string" types as the only types passed to this method$#'
5359
identifier: typePerfect.narrowPublicClassMethodParamType
5460
count: 1
5561
path: ../src/RuleSet/DeclarationBlock.php
5662

5763
-
58-
message: '#^Only booleans are allowed in a negated boolean, string\|null given\.$#'
59-
identifier: booleanNot.exprNotBoolean
64+
message: '#^Parameters should have "string\|null" types as the only types passed to this method$#'
65+
identifier: typePerfect.narrowPublicClassMethodParamType
6066
count: 2
61-
path: ../src/RuleSet/RuleSet.php
67+
path: ../src/RuleSet/DeclarationBlock.php
6268

6369
-
64-
message: '#^Parameters should have "string" types as the only types passed to this method$#'
65-
identifier: typePerfect.narrowPublicClassMethodParamType
66-
count: 1
70+
message: '#^Only booleans are allowed in a negated boolean, string\|null given\.$#'
71+
identifier: booleanNot.exprNotBoolean
72+
count: 2
6773
path: ../src/RuleSet/RuleSet.php
6874

6975
-

src/CSSList/CSSBlockList.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,16 @@ public function getAllRuleSets(): array
6868
$result[] = $item;
6969
} elseif ($item instanceof CSSBlockList) {
7070
$result = \array_merge($result, $item->getAllRuleSets());
71+
} elseif ($item instanceof DeclarationBlock) {
72+
$result[] = $item->getRuleSet();
7173
}
7274
}
7375

7476
return $result;
7577
}
7678

7779
/**
78-
* @param CSSList|Rule|RuleSet|Value $element
80+
* @param CSSList|DeclarationBlock|Rule|RuleSet|Value $element
7981
* @param list<Value> $result
8082
*/
8183
protected function allValues(
@@ -88,7 +90,7 @@ protected function allValues(
8890
foreach ($element->getContents() as $content) {
8991
$this->allValues($content, $result, $searchString, $searchInFunctionArguments);
9092
}
91-
} elseif ($element instanceof RuleSet) {
93+
} elseif ($element instanceof RuleSet || $element instanceof DeclarationBlock) {
9294
foreach ($element->getRules($searchString) as $rule) {
9395
$this->allValues($rule, $result, $searchString, $searchInFunctionArguments);
9496
}

src/RuleSet/DeclarationBlock.php

+98-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
namespace Sabberworm\CSS\RuleSet;
66

7+
use Sabberworm\CSS\Comment\CommentContainer;
78
use Sabberworm\CSS\CSSList\CSSList;
9+
use Sabberworm\CSS\CSSList\CSSListItem;
810
use Sabberworm\CSS\CSSList\KeyFrame;
911
use Sabberworm\CSS\OutputFormat;
1012
use Sabberworm\CSS\Parsing\OutputException;
@@ -13,22 +15,47 @@
1315
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
1416
use Sabberworm\CSS\Property\KeyframeSelector;
1517
use Sabberworm\CSS\Property\Selector;
18+
use Sabberworm\CSS\Renderable;
19+
use Sabberworm\CSS\Rule\Rule;
1620

1721
/**
18-
* This class represents a `RuleSet` constrained by a `Selector`.
22+
* This class includes a `RuleSet` constrained by a `Selector`.
1923
*
2024
* It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
2125
* matching elements.
2226
*
2327
* Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
28+
*
29+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
2430
*/
25-
class DeclarationBlock extends RuleSet
31+
class DeclarationBlock implements CSSListItem
2632
{
33+
use CommentContainer;
34+
2735
/**
2836
* @var array<Selector|string>
2937
*/
3038
private $selectors = [];
3139

40+
/**
41+
* @var RuleSet
42+
*/
43+
private $ruleSet;
44+
45+
/**
46+
* @var int<0, max>
47+
*/
48+
private $lineNumber;
49+
50+
/**
51+
* @param int<0, max> $lineNumber
52+
*/
53+
public function __construct(int $lineNumber = 0)
54+
{
55+
$this->lineNumber = $lineNumber;
56+
$this->ruleSet = new RuleSet($lineNumber);
57+
}
58+
3259
/**
3360
* @throws UnexpectedTokenException
3461
* @throws UnexpectedEOFException
@@ -67,10 +94,20 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6794
}
6895
}
6996
$result->setComments($comments);
70-
RuleSet::parseRuleSet($parserState, $result);
97+
98+
RuleSet::parseRuleSet($parserState, $result->ruleSet);
99+
71100
return $result;
72101
}
73102

103+
/**
104+
* @return int<0, max>
105+
*/
106+
public function getLineNo(): int
107+
{
108+
return $this->lineNumber;
109+
}
110+
74111
/**
75112
* @param array<Selector|string>|string $selectors
76113
*
@@ -135,6 +172,63 @@ public function getSelectors(): array
135172
return $this->selectors;
136173
}
137174

175+
public function getRuleSet(): RuleSet
176+
{
177+
return $this->ruleSet;
178+
}
179+
180+
/**
181+
* @see RuleSet::addRule()
182+
*/
183+
public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
184+
{
185+
$this->ruleSet->addRule($ruleToAdd, $sibling);
186+
}
187+
188+
/**
189+
* @see RuleSet::getRules()
190+
*
191+
* @param Rule|string|null $searchPattern
192+
*
193+
* @return array<int<0, max>, Rule>
194+
*/
195+
public function getRules($searchPattern = null): array
196+
{
197+
return $this->ruleSet->getRules($searchPattern);
198+
}
199+
200+
/**
201+
* @see RuleSet::setRules()
202+
*
203+
* @param array<Rule> $rules
204+
*/
205+
public function setRules(array $rules): void
206+
{
207+
$this->ruleSet->setRules($rules);
208+
}
209+
210+
/**
211+
* @see RuleSet::getRulesAssoc()
212+
*
213+
* @param Rule|string|null $searchPattern
214+
*
215+
* @return array<string, Rule>
216+
*/
217+
public function getRulesAssoc($searchPattern = null): array
218+
{
219+
return $this->ruleSet->getRulesAssoc($searchPattern);
220+
}
221+
222+
/**
223+
* @see RuleSet::removeRule()
224+
*
225+
* @param Rule|string|null $searchPattern
226+
*/
227+
public function removeRule($searchPattern): void
228+
{
229+
$this->ruleSet->removeRule($searchPattern);
230+
}
231+
138232
/**
139233
* @return non-empty-string
140234
*
@@ -155,7 +249,7 @@ public function render(OutputFormat $outputFormat): string
155249
);
156250
$result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
157251
$result .= $formatter->spaceBeforeOpeningBrace() . '{';
158-
$result .= $this->renderRules($outputFormat);
252+
$result .= $this->ruleSet->render($outputFormat);
159253
$result .= '}';
160254
$result .= $outputFormat->getContentAfterDeclarationBlock();
161255

src/RuleSet/RuleSet.php

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@
2121
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
2222
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
2323
*
24-
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
25-
* so those interfaces must also be implemented by concrete subclasses.
24+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
2625
*/
27-
abstract class RuleSet implements CSSListItem
26+
class RuleSet implements CSSListItem
2827
{
2928
use CommentContainer;
3029

@@ -262,6 +261,14 @@ public function removeRule($searchPattern): void
262261
}
263262
}
264263

264+
/**
265+
* @internal
266+
*/
267+
public function render(OutputFormat $outputFormat): string
268+
{
269+
return $this->renderRules($outputFormat);
270+
}
271+
265272
protected function renderRules(OutputFormat $outputFormat): string
266273
{
267274
$result = '';

tests/ParserTest.php

+13-11
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class ParserTest extends TestCase
3737
/**
3838
* @test
3939
*/
40-
public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void
40+
public function parseForOneDeclarationBlockReturnsDocumentWithOneDeclarationBlock(): void
4141
{
4242
$css = '.thing { left: 10px; }';
4343
$parser = new Parser($css);
@@ -48,7 +48,7 @@ public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void
4848

4949
$cssList = $document->getContents();
5050
self::assertCount(1, $cssList);
51-
self::assertInstanceOf(RuleSet::class, $cssList[0]);
51+
self::assertInstanceOf(DeclarationBlock::class, $cssList[0]);
5252
}
5353

5454
/**
@@ -928,9 +928,9 @@ public function missingPropertyValueStrict(): void
928928
public function missingPropertyValueLenient(): void
929929
{
930930
$parsed = self::parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true));
931-
$rulesets = $parsed->getAllRuleSets();
932-
self::assertCount(1, $rulesets);
933-
$block = $rulesets[0];
931+
$declarationBlocks = $parsed->getAllDeclarationBlocks();
932+
self::assertCount(1, $declarationBlocks);
933+
$block = $declarationBlocks[0];
934934
self::assertInstanceOf(DeclarationBlock::class, $block);
935935
self::assertEquals([new Selector('div')], $block->getSelectors());
936936
$rules = $block->getRules();
@@ -985,6 +985,7 @@ public function lineNumbersParsing(): void
985985
&& !$contentItem instanceof CSSNamespace
986986
&& !$contentItem instanceof Import
987987
&& !$contentItem instanceof RuleSet
988+
&& !$contentItem instanceof DeclarationBlock
988989
) {
989990
self::fail('Content item is not of an expected type. It\'s a `' . \get_class($contentItem) . '`.');
990991
}
@@ -994,6 +995,7 @@ public function lineNumbersParsing(): void
994995
if (
995996
!$block instanceof CSSList
996997
&& !$block instanceof RuleSet
998+
&& !$block instanceof DeclarationBlock
997999
) {
9981000
self::fail(
9991001
'KeyFrame content item is not of an expected type. It\'s a `' . \get_class($block) . '`.'
@@ -1076,7 +1078,7 @@ public function commentExtracting(): void
10761078
// $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment());
10771079

10781080
// Declaration rules.
1079-
self::assertInstanceOf(RuleSet::class, $fooBarBlock);
1081+
self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock);
10801082
$fooBarRules = $fooBarBlock->getRules();
10811083
$fooBarRule = $fooBarRules[0];
10821084
$fooBarRuleComments = $fooBarRule->getComments();
@@ -1097,7 +1099,7 @@ public function commentExtracting(): void
10971099
self::assertSame('* Number 10 *', $fooBarComments[0]->getComment());
10981100

10991101
// Media -> declaration -> rule.
1100-
self::assertInstanceOf(RuleSet::class, $mediaRules[0]);
1102+
self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]);
11011103
$fooBarRules = $mediaRules[0]->getRules();
11021104
$fooBarChildComments = $fooBarRules[0]->getComments();
11031105
self::assertCount(1, $fooBarChildComments);
@@ -1113,7 +1115,7 @@ public function flatCommentExtractingOneComment(): void
11131115
$document = $parser->parse();
11141116

11151117
$contents = $document->getContents();
1116-
self::assertInstanceOf(RuleSet::class, $contents[0]);
1118+
self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
11171119
$divRules = $contents[0]->getRules();
11181120
$comments = $divRules[0]->getComments();
11191121

@@ -1130,7 +1132,7 @@ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void
11301132
$document = $parser->parse();
11311133

11321134
$contents = $document->getContents();
1133-
self::assertInstanceOf(RuleSet::class, $contents[0]);
1135+
self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
11341136
$divRules = $contents[0]->getRules();
11351137
$comments = $divRules[0]->getComments();
11361138

@@ -1148,7 +1150,7 @@ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void
11481150
$document = $parser->parse();
11491151

11501152
$contents = $document->getContents();
1151-
self::assertInstanceOf(RuleSet::class, $contents[0]);
1153+
self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
11521154
$divRules = $contents[0]->getRules();
11531155
$comments = $divRules[0]->getComments();
11541156

@@ -1166,7 +1168,7 @@ public function flatCommentExtractingCommentsForTwoRules(): void
11661168
$document = $parser->parse();
11671169

11681170
$contents = $document->getContents();
1169-
self::assertInstanceOf(RuleSet::class, $contents[0]);
1171+
self::assertInstanceOf(DeclarationBlock::class, $contents[0]);
11701172
$divRules = $contents[0]->getRules();
11711173
$rule1Comments = $divRules[0]->getComments();
11721174
$rule2Comments = $divRules[1]->getComments();

tests/RuleSet/DeclarationBlockTest.php

+3-4
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
use Sabberworm\CSS\OutputFormat;
99
use Sabberworm\CSS\Parser;
1010
use Sabberworm\CSS\Rule\Rule;
11-
use Sabberworm\CSS\RuleSet\RuleSet;
11+
use Sabberworm\CSS\RuleSet\DeclarationBlock;
1212
use Sabberworm\CSS\Settings as ParserSettings;
1313
use Sabberworm\CSS\Value\Size;
1414

1515
/**
1616
* @covers \Sabberworm\CSS\RuleSet\DeclarationBlock
17-
* @covers \Sabberworm\CSS\RuleSet\RuleSet
1817
*/
1918
final class DeclarationBlockTest extends TestCase
2019
{
@@ -31,7 +30,7 @@ public function overrideRules(): void
3130
$contents = $document->getContents();
3231
$wrapper = $contents[0];
3332

34-
self::assertInstanceOf(RuleSet::class, $wrapper);
33+
self::assertInstanceOf(DeclarationBlock::class, $wrapper);
3534
self::assertCount(2, $wrapper->getRules());
3635
$wrapper->setRules([$rule]);
3736

@@ -52,7 +51,7 @@ public function ruleInsertion(): void
5251
$contents = $document->getContents();
5352
$wrapper = $contents[0];
5453

55-
self::assertInstanceOf(RuleSet::class, $wrapper);
54+
self::assertInstanceOf(DeclarationBlock::class, $wrapper);
5655

5756
$leftRules = $wrapper->getRules('left');
5857
self::assertCount(1, $leftRules);

0 commit comments

Comments
 (0)