Skip to content

Commit 3a278e0

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 9b52615 commit 3a278e0

File tree

8 files changed

+138
-37
lines changed

8 files changed

+138
-37
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,6 @@ classDiagram
720720
class Comment {
721721
}
722722
723-
RuleSet <|-- DeclarationBlock: inheritance
724723
Renderable <|-- CSSListItem: inheritance
725724
Commentable <|-- CSSListItem: inheritance
726725
Positionable <|.. RuleSet: realization
@@ -752,6 +751,8 @@ classDiagram
752751
AtRule <|.. KeyFrame: realization
753752
CSSBlockList <|-- AtRuleBlockList: inheritance
754753
AtRule <|.. AtRuleBlockList: realization
754+
Positionable <|.. DeclarationBlock: realization
755+
CSSListItem <|.. DeclarationBlock: realization
755756
CSSFunction <|-- Color: inheritance
756757
PrimitiveValue <|-- URL: inheritance
757758
RuleValueList <|-- CalcRuleValueList: inheritance
@@ -781,6 +782,7 @@ classDiagram
781782
Charset --> "*" Comment : comments
782783
Charset --> "1" CSSString : charset
783784
DeclarationBlock --> "*" Selector : selectors
785+
DeclarationBlock --> "*" RuleSet : ruleSet
784786
Import --> "*" Comment : comments
785787
OutputFormat --> "1" OutputFormat : nextLevelFormat
786788
OutputFormat --> "1" OutputFormatter : outputFormatter

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

+87-4
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,55 @@
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;
1113
use Sabberworm\CSS\Parsing\ParserState;
1214
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
1315
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
16+
use Sabberworm\CSS\Position\Position;
17+
use Sabberworm\CSS\Position\Positionable;
1418
use Sabberworm\CSS\Property\KeyframeSelector;
1519
use Sabberworm\CSS\Property\Selector;
20+
use Sabberworm\CSS\Rule\Rule;
1621

1722
/**
18-
* This class represents a `RuleSet` constrained by a `Selector`.
23+
* This class includes a `RuleSet` constrained by a `Selector`.
1924
*
2025
* It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
2126
* matching elements.
2227
*
2328
* Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
29+
*
30+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
2431
*/
25-
class DeclarationBlock extends RuleSet
32+
class DeclarationBlock implements CSSListItem, Positionable
2633
{
34+
use CommentContainer;
35+
use Position;
36+
2737
/**
2838
* @var array<Selector|string>
2939
*/
3040
private $selectors = [];
3141

42+
/**
43+
* @var RuleSet
44+
*/
45+
private $ruleSet;
46+
47+
/**
48+
* @param int<0, max> $lineNumber
49+
*/
50+
public function __construct(int $lineNumber = 0)
51+
{
52+
$this->setPosition($lineNumber);
53+
$this->ruleSet = new RuleSet($lineNumber);
54+
}
55+
3256
/**
3357
* @throws UnexpectedTokenException
3458
* @throws UnexpectedEOFException
@@ -67,7 +91,9 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6791
}
6892
}
6993
$result->setComments($comments);
70-
RuleSet::parseRuleSet($parserState, $result);
94+
95+
RuleSet::parseRuleSet($parserState, $result->ruleSet);
96+
7197
return $result;
7298
}
7399

@@ -135,6 +161,63 @@ public function getSelectors(): array
135161
return $this->selectors;
136162
}
137163

164+
public function getRuleSet(): RuleSet
165+
{
166+
return $this->ruleSet;
167+
}
168+
169+
/**
170+
* @see RuleSet::addRule()
171+
*/
172+
public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
173+
{
174+
$this->ruleSet->addRule($ruleToAdd, $sibling);
175+
}
176+
177+
/**
178+
* @see RuleSet::getRules()
179+
*
180+
* @param Rule|string|null $searchPattern
181+
*
182+
* @return array<int<0, max>, Rule>
183+
*/
184+
public function getRules($searchPattern = null): array
185+
{
186+
return $this->ruleSet->getRules($searchPattern);
187+
}
188+
189+
/**
190+
* @see RuleSet::setRules()
191+
*
192+
* @param array<Rule> $rules
193+
*/
194+
public function setRules(array $rules): void
195+
{
196+
$this->ruleSet->setRules($rules);
197+
}
198+
199+
/**
200+
* @see RuleSet::getRulesAssoc()
201+
*
202+
* @param Rule|string|null $searchPattern
203+
*
204+
* @return array<string, Rule>
205+
*/
206+
public function getRulesAssoc($searchPattern = null): array
207+
{
208+
return $this->ruleSet->getRulesAssoc($searchPattern);
209+
}
210+
211+
/**
212+
* @see RuleSet::removeRule()
213+
*
214+
* @param Rule|string|null $searchPattern
215+
*/
216+
public function removeRule($searchPattern): void
217+
{
218+
$this->ruleSet->removeRule($searchPattern);
219+
}
220+
138221
/**
139222
* @return non-empty-string
140223
*
@@ -158,7 +241,7 @@ public function render(OutputFormat $outputFormat): string
158241
);
159242
$result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
160243
$result .= $formatter->spaceBeforeOpeningBrace() . '{';
161-
$result .= $this->renderRules($outputFormat);
244+
$result .= $this->ruleSet->render($outputFormat);
162245
$result .= '}';
163246
$result .= $outputFormat->getContentAfterDeclarationBlock();
164247

src/RuleSet/RuleSet.php

+10-3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@
2323
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
2424
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
2525
*
26-
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
27-
* so those interfaces must also be implemented by concrete subclasses.
26+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
2827
*/
29-
abstract class RuleSet implements CSSListItem, Positionable
28+
class RuleSet implements CSSListItem, Positionable
3029
{
3130
use CommentContainer;
3231
use Position;
@@ -250,6 +249,14 @@ public function removeRule($searchPattern): void
250249
}
251250
}
252251

252+
/**
253+
* @internal
254+
*/
255+
public function render(OutputFormat $outputFormat): string
256+
{
257+
return $this->renderRules($outputFormat);
258+
}
259+
253260
protected function renderRules(OutputFormat $outputFormat): string
254261
{
255262
$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)