Skip to content

Commit c935f2c

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 03ed64a commit c935f2c

File tree

8 files changed

+139
-36
lines changed

8 files changed

+139
-36
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,6 @@ classDiagram
723723
class Comment {
724724
}
725725
726-
RuleSet <|-- DeclarationBlock: inheritance
727726
Renderable <|-- CSSElement: inheritance
728727
Renderable <|-- CSSListItem: inheritance
729728
Commentable <|-- CSSListItem: inheritance
@@ -758,6 +757,9 @@ classDiagram
758757
AtRule <|.. KeyFrame: realization
759758
CSSBlockList <|-- AtRuleBlockList: inheritance
760759
AtRule <|.. AtRuleBlockList: realization
760+
Positionable <|.. DeclarationBlock: realization
761+
CSSElement <|.. DeclarationBlock: realization
762+
CSSListItem <|.. DeclarationBlock: realization
761763
CSSFunction <|-- Color: inheritance
762764
PrimitiveValue <|-- URL: inheritance
763765
RuleValueList <|-- CalcRuleValueList: inheritance
@@ -787,6 +789,7 @@ classDiagram
787789
Charset --> "*" Comment : comments
788790
Charset --> "1" CSSString : charset
789791
DeclarationBlock --> "*" Selector : selectors
792+
DeclarationBlock --> "*" RuleSet : ruleSet
790793
Import --> "*" Comment : comments
791794
OutputFormat --> "1" OutputFormat : nextLevelFormat
792795
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

+3-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public function getAllRuleSets(): array
5555
$result[] = $item;
5656
} elseif ($item instanceof CSSBlockList) {
5757
$result = \array_merge($result, $item->getAllRuleSets());
58+
} elseif ($item instanceof DeclarationBlock) {
59+
$result[] = $item->getRuleSet();
5860
}
5961
}
6062

@@ -95,7 +97,7 @@ public function getAllValues(
9597
);
9698
}
9799
}
98-
} elseif ($element instanceof RuleSet) {
100+
} elseif ($element instanceof RuleSet || $element instanceof DeclarationBlock) {
99101
foreach ($element->getRules($ruleSearchPattern) as $rule) {
100102
$result = \array_merge(
101103
$result,

src/RuleSet/DeclarationBlock.php

+88-4
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,56 @@
44

55
namespace Sabberworm\CSS\RuleSet;
66

7+
use Sabberworm\CSS\Comment\CommentContainer;
8+
use Sabberworm\CSS\CSSElement;
79
use Sabberworm\CSS\CSSList\CSSList;
10+
use Sabberworm\CSS\CSSList\CSSListItem;
811
use Sabberworm\CSS\CSSList\KeyFrame;
912
use Sabberworm\CSS\OutputFormat;
1013
use Sabberworm\CSS\Parsing\OutputException;
1114
use Sabberworm\CSS\Parsing\ParserState;
1215
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
1316
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
17+
use Sabberworm\CSS\Position\Position;
18+
use Sabberworm\CSS\Position\Positionable;
1419
use Sabberworm\CSS\Property\KeyframeSelector;
1520
use Sabberworm\CSS\Property\Selector;
21+
use Sabberworm\CSS\Rule\Rule;
1622

1723
/**
18-
* This class represents a `RuleSet` constrained by a `Selector`.
24+
* This class includes a `RuleSet` constrained by a `Selector`.
1925
*
2026
* It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
2127
* matching elements.
2228
*
2329
* Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
30+
*
31+
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
2432
*/
25-
class DeclarationBlock extends RuleSet
33+
class DeclarationBlock implements CSSElement, CSSListItem, Positionable
2634
{
35+
use CommentContainer;
36+
use Position;
37+
2738
/**
2839
* @var array<Selector|string>
2940
*/
3041
private $selectors = [];
3142

43+
/**
44+
* @var RuleSet
45+
*/
46+
private $ruleSet;
47+
48+
/**
49+
* @param int<0, max> $lineNumber
50+
*/
51+
public function __construct(int $lineNumber = 0)
52+
{
53+
$this->setPosition($lineNumber);
54+
$this->ruleSet = new RuleSet($lineNumber);
55+
}
56+
3257
/**
3358
* @throws UnexpectedTokenException
3459
* @throws UnexpectedEOFException
@@ -67,7 +92,9 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ?
6792
}
6893
}
6994
$result->setComments($comments);
70-
RuleSet::parseRuleSet($parserState, $result);
95+
96+
RuleSet::parseRuleSet($parserState, $result->ruleSet);
97+
7198
return $result;
7299
}
73100

@@ -135,6 +162,63 @@ public function getSelectors(): array
135162
return $this->selectors;
136163
}
137164

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

src/RuleSet/RuleSet.php

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

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