From dbb2627fc75859d9e48037c2d3893c97fc9ea163 Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Wed, 19 Mar 2025 00:24:59 +0000 Subject: [PATCH 1/2] [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. --- README.md | 5 +- config/phpstan-baseline.neon | 6 ++ src/CSSList/CSSBlockList.php | 2 + src/RuleSet/DeclarationBlock.php | 102 +++++++++++++++++++++++- src/RuleSet/RuleSet.php | 13 ++- tests/ParserTest.php | 22 ++--- tests/RuleSet/DeclarationBlockTest.php | 7 +- tests/Unit/CSSList/CSSBlockListTest.php | 12 +-- 8 files changed, 140 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 9ecdc3e7..4db1f9e7 100644 --- a/README.md +++ b/README.md @@ -726,7 +726,6 @@ classDiagram class Comment { } - RuleSet <|-- DeclarationBlock: inheritance Renderable <|-- CSSElement: inheritance Renderable <|-- CSSListItem: inheritance Commentable <|-- CSSListItem: inheritance @@ -762,6 +761,9 @@ classDiagram AtRule <|.. KeyFrame: realization CSSBlockList <|-- AtRuleBlockList: inheritance AtRule <|.. AtRuleBlockList: realization + Positionable <|.. DeclarationBlock: realization + CSSElement <|.. DeclarationBlock: realization + CSSListItem <|.. DeclarationBlock: realization CSSFunction <|-- Color: inheritance PrimitiveValue <|-- URL: inheritance RuleValueList <|-- CalcRuleValueList: inheritance @@ -791,6 +793,7 @@ classDiagram Charset --> "*" Comment : comments Charset --> "1" CSSString : charset DeclarationBlock --> "*" Selector : selectors + DeclarationBlock --> "*" RuleSet : ruleSet Import --> "*" Comment : comments OutputFormat --> "1" OutputFormat : nextLevelFormat OutputFormat --> "1" OutputFormatter : outputFormatter diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index 9be2ebd7..eb703ccf 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -54,6 +54,12 @@ parameters: count: 1 path: ../src/RuleSet/DeclarationBlock.php + - + message: '#^Parameters should have "string\|null" types as the only types passed to this method$#' + identifier: typePerfect.narrowPublicClassMethodParamType + count: 2 + path: ../src/RuleSet/DeclarationBlock.php + - message: '#^Loose comparison via "\!\=" is not allowed\.$#' identifier: notEqual.notAllowed diff --git a/src/CSSList/CSSBlockList.php b/src/CSSList/CSSBlockList.php index 2dfd284f..122ac5d1 100644 --- a/src/CSSList/CSSBlockList.php +++ b/src/CSSList/CSSBlockList.php @@ -56,6 +56,8 @@ public function getAllRuleSets(): array $result[] = $item; } elseif ($item instanceof CSSBlockList) { $result = \array_merge($result, $item->getAllRuleSets()); + } elseif ($item instanceof DeclarationBlock) { + $result[] = $item->getRuleSet(); } } diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index e4125797..04c7d291 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -4,31 +4,56 @@ namespace Sabberworm\CSS\RuleSet; +use Sabberworm\CSS\Comment\CommentContainer; +use Sabberworm\CSS\CSSElement; use Sabberworm\CSS\CSSList\CSSList; +use Sabberworm\CSS\CSSList\CSSListItem; use Sabberworm\CSS\CSSList\KeyFrame; use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parsing\OutputException; use Sabberworm\CSS\Parsing\ParserState; use Sabberworm\CSS\Parsing\UnexpectedEOFException; use Sabberworm\CSS\Parsing\UnexpectedTokenException; +use Sabberworm\CSS\Position\Position; +use Sabberworm\CSS\Position\Positionable; use Sabberworm\CSS\Property\KeyframeSelector; use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Rule\Rule; /** - * This class represents a `RuleSet` constrained by a `Selector`. + * This class includes a `RuleSet` constrained by a `Selector`. * * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the * matching elements. * * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`). + * + * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ -class DeclarationBlock extends RuleSet +class DeclarationBlock implements CSSElement, CSSListItem, Positionable { + use CommentContainer; + use Position; + /** * @var array */ private $selectors = []; + /** + * @var RuleSet + */ + private $ruleSet; + + /** + * @param int<0, max> $lineNumber + */ + public function __construct(int $lineNumber = 0) + { + $this->setPosition($lineNumber); + $this->ruleSet = new RuleSet($lineNumber); + } + /** * @throws UnexpectedTokenException * @throws UnexpectedEOFException @@ -67,7 +92,9 @@ public static function parse(ParserState $parserState, ?CSSList $list = null): ? } } $result->setComments($comments); - RuleSet::parseRuleSet($parserState, $result); + + RuleSet::parseRuleSet($parserState, $result->ruleSet); + return $result; } @@ -135,6 +162,73 @@ public function getSelectors(): array return $this->selectors; } + public function getRuleSet(): RuleSet + { + return $this->ruleSet; + } + + /** + * @see RuleSet::addRule() + */ + public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void + { + $this->ruleSet->addRule($ruleToAdd, $sibling); + } + + /** + * @see RuleSet::getRules() + * + * @return array, Rule> + */ + public function getRules(?string $searchPattern = null): array + { + return $this->ruleSet->getRules($searchPattern); + } + + /** + * @see RuleSet::setRules() + * + * @param array $rules + */ + public function setRules(array $rules): void + { + $this->ruleSet->setRules($rules); + } + + /** + * @see RuleSet::getRulesAssoc() + * + * @return array + */ + public function getRulesAssoc(?string $searchPattern = null): array + { + return $this->ruleSet->getRulesAssoc($searchPattern); + } + + /** + * @see RuleSet::removeRule() + */ + public function removeRule(Rule $ruleToRemove): void + { + $this->ruleSet->removeRule($ruleToRemove); + } + + /** + * @see RuleSet::removeMatchingRules() + */ + public function removeMatchingRules(string $searchPattern): void + { + $this->ruleSet->removeMatchingRules($searchPattern); + } + + /** + * @see RuleSet::removeAllRules() + */ + public function removeAllRules(): void + { + $this->ruleSet->removeAllRules(); + } + /** * @return non-empty-string * @@ -158,7 +252,7 @@ public function render(OutputFormat $outputFormat): string ); $result .= $outputFormat->getContentAfterDeclarationBlockSelectors(); $result .= $formatter->spaceBeforeOpeningBrace() . '{'; - $result .= $this->renderRules($outputFormat); + $result .= $this->ruleSet->render($outputFormat); $result .= '}'; $result .= $outputFormat->getContentAfterDeclarationBlock(); diff --git a/src/RuleSet/RuleSet.php b/src/RuleSet/RuleSet.php index b24eefab..33290cb4 100644 --- a/src/RuleSet/RuleSet.php +++ b/src/RuleSet/RuleSet.php @@ -24,10 +24,9 @@ * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)` * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules). * - * Note that `CSSListItem` extends both `Commentable` and `Renderable`, - * so those interfaces must also be implemented by concrete subclasses. + * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ -abstract class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer +class RuleSet implements CSSElement, CSSListItem, Positionable, RuleContainer { use CommentContainer; use Position; @@ -264,6 +263,14 @@ public function removeAllRules(): void $this->rules = []; } + /** + * @internal + */ + public function render(OutputFormat $outputFormat): string + { + return $this->renderRules($outputFormat); + } + protected function renderRules(OutputFormat $outputFormat): string { $result = ''; diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 656d943b..8b64c211 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -38,7 +38,7 @@ final class ParserTest extends TestCase /** * @test */ - public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void + public function parseForOneDeclarationBlockReturnsDocumentWithOneDeclarationBlock(): void { $css = '.thing { left: 10px; }'; $parser = new Parser($css); @@ -49,7 +49,7 @@ public function parseForOneRuleSetReturnsDocumentWithOneRuleSet(): void $cssList = $document->getContents(); self::assertCount(1, $cssList); - self::assertInstanceOf(RuleSet::class, $cssList[0]); + self::assertInstanceOf(DeclarationBlock::class, $cssList[0]); } /** @@ -929,9 +929,9 @@ public function missingPropertyValueStrict(): void public function missingPropertyValueLenient(): void { $parsed = self::parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true)); - $rulesets = $parsed->getAllRuleSets(); - self::assertCount(1, $rulesets); - $block = $rulesets[0]; + $declarationBlocks = $parsed->getAllDeclarationBlocks(); + self::assertCount(1, $declarationBlocks); + $block = $declarationBlocks[0]; self::assertInstanceOf(DeclarationBlock::class, $block); self::assertEquals([new Selector('div')], $block->getSelectors()); $rules = $block->getRules(); @@ -1058,7 +1058,7 @@ public function commentExtracting(): void // $this->assertSame("* Number 5 *", $fooBarBlockComments[1]->getComment()); // Declaration rules. - self::assertInstanceOf(RuleSet::class, $fooBarBlock); + self::assertInstanceOf(DeclarationBlock::class, $fooBarBlock); $fooBarRules = $fooBarBlock->getRules(); $fooBarRule = $fooBarRules[0]; $fooBarRuleComments = $fooBarRule->getComments(); @@ -1079,7 +1079,7 @@ public function commentExtracting(): void self::assertSame('* Number 10 *', $fooBarComments[0]->getComment()); // Media -> declaration -> rule. - self::assertInstanceOf(RuleSet::class, $mediaRules[0]); + self::assertInstanceOf(DeclarationBlock::class, $mediaRules[0]); $fooBarRules = $mediaRules[0]->getRules(); $fooBarChildComments = $fooBarRules[0]->getComments(); self::assertCount(1, $fooBarChildComments); @@ -1095,7 +1095,7 @@ public function flatCommentExtractingOneComment(): void $document = $parser->parse(); $contents = $document->getContents(); - self::assertInstanceOf(RuleSet::class, $contents[0]); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); $divRules = $contents[0]->getRules(); $comments = $divRules[0]->getComments(); @@ -1112,7 +1112,7 @@ public function flatCommentExtractingTwoConjoinedCommentsForOneRule(): void $document = $parser->parse(); $contents = $document->getContents(); - self::assertInstanceOf(RuleSet::class, $contents[0]); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); $divRules = $contents[0]->getRules(); $comments = $divRules[0]->getComments(); @@ -1130,7 +1130,7 @@ public function flatCommentExtractingTwoSpaceSeparatedCommentsForOneRule(): void $document = $parser->parse(); $contents = $document->getContents(); - self::assertInstanceOf(RuleSet::class, $contents[0]); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); $divRules = $contents[0]->getRules(); $comments = $divRules[0]->getComments(); @@ -1148,7 +1148,7 @@ public function flatCommentExtractingCommentsForTwoRules(): void $document = $parser->parse(); $contents = $document->getContents(); - self::assertInstanceOf(RuleSet::class, $contents[0]); + self::assertInstanceOf(DeclarationBlock::class, $contents[0]); $divRules = $contents[0]->getRules(); $rule1Comments = $divRules[0]->getComments(); $rule2Comments = $divRules[1]->getComments(); diff --git a/tests/RuleSet/DeclarationBlockTest.php b/tests/RuleSet/DeclarationBlockTest.php index 5aaf0662..63411a64 100644 --- a/tests/RuleSet/DeclarationBlockTest.php +++ b/tests/RuleSet/DeclarationBlockTest.php @@ -8,13 +8,12 @@ use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Parser; use Sabberworm\CSS\Rule\Rule; -use Sabberworm\CSS\RuleSet\RuleSet; +use Sabberworm\CSS\RuleSet\DeclarationBlock; use Sabberworm\CSS\Settings as ParserSettings; use Sabberworm\CSS\Value\Size; /** * @covers \Sabberworm\CSS\RuleSet\DeclarationBlock - * @covers \Sabberworm\CSS\RuleSet\RuleSet */ final class DeclarationBlockTest extends TestCase { @@ -31,7 +30,7 @@ public function overrideRules(): void $contents = $document->getContents(); $wrapper = $contents[0]; - self::assertInstanceOf(RuleSet::class, $wrapper); + self::assertInstanceOf(DeclarationBlock::class, $wrapper); self::assertCount(2, $wrapper->getRules()); $wrapper->setRules([$rule]); @@ -52,7 +51,7 @@ public function ruleInsertion(): void $contents = $document->getContents(); $wrapper = $contents[0]; - self::assertInstanceOf(RuleSet::class, $wrapper); + self::assertInstanceOf(DeclarationBlock::class, $wrapper); $leftRules = $wrapper->getRules('left'); self::assertCount(1, $leftRules); diff --git a/tests/Unit/CSSList/CSSBlockListTest.php b/tests/Unit/CSSList/CSSBlockListTest.php index 41f2b41f..ce7e5447 100644 --- a/tests/Unit/CSSList/CSSBlockListTest.php +++ b/tests/Unit/CSSList/CSSBlockListTest.php @@ -157,7 +157,7 @@ public function getAllRuleSetsWhenNoContentSetReturnsEmptyArray(): void /** * @test */ - public function getAllRuleSetsReturnsOneDeclarationBlockDirectlySetAsContent(): void + public function getAllRuleSetsReturnsRuleSetFromOneDeclarationBlockDirectlySetAsContent(): void { $subject = new ConcreteCSSBlockList(); @@ -166,7 +166,7 @@ public function getAllRuleSetsReturnsOneDeclarationBlockDirectlySetAsContent(): $result = $subject->getAllRuleSets(); - self::assertSame([$declarationBlock], $result); + self::assertSame([$declarationBlock->getRuleSet()], $result); } /** @@ -187,7 +187,7 @@ public function getAllRuleSetsReturnsOneAtRuleSetDirectlySetAsContent(): void /** * @test */ - public function getAllRuleSetsReturnsMultipleDeclarationBlocksDirectlySetAsContents(): void + public function getAllRuleSetsReturnsRuleSetsFromMultipleDeclarationBlocksDirectlySetAsContents(): void { $subject = new ConcreteCSSBlockList(); @@ -197,7 +197,7 @@ public function getAllRuleSetsReturnsMultipleDeclarationBlocksDirectlySetAsConte $result = $subject->getAllRuleSets(); - self::assertSame([$declarationBlock1, $declarationBlock2], $result); + self::assertSame([$declarationBlock1->getRuleSet(), $declarationBlock2->getRuleSet()], $result); } /** @@ -219,7 +219,7 @@ public function getAllRuleSetsReturnsMultipleAtRuleSetsDirectlySetAsContents(): /** * @test */ - public function getAllRuleSetsReturnsDeclarationBlocksWithinAtRuleBlockList(): void + public function getAllRuleSetsReturnsRuleSetsFromDeclarationBlocksWithinAtRuleBlockList(): void { $subject = new ConcreteCSSBlockList(); @@ -230,7 +230,7 @@ public function getAllRuleSetsReturnsDeclarationBlocksWithinAtRuleBlockList(): v $result = $subject->getAllRuleSets(); - self::assertSame([$declarationBlock], $result); + self::assertSame([$declarationBlock->getRuleSet()], $result); } /** From 9db9e2f281a73f6298d833bcb2ee67e04e9fb719 Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Fri, 9 May 2025 19:15:34 +0100 Subject: [PATCH 2/2] Add `RuleContainer` interface for `RuleSet` and `DeclarationBlock` --- config/phpstan-baseline.neon | 6 ------ src/RuleSet/DeclarationBlock.php | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index eb703ccf..9be2ebd7 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -54,12 +54,6 @@ parameters: count: 1 path: ../src/RuleSet/DeclarationBlock.php - - - message: '#^Parameters should have "string\|null" types as the only types passed to this method$#' - identifier: typePerfect.narrowPublicClassMethodParamType - count: 2 - path: ../src/RuleSet/DeclarationBlock.php - - message: '#^Loose comparison via "\!\=" is not allowed\.$#' identifier: notEqual.notAllowed diff --git a/src/RuleSet/DeclarationBlock.php b/src/RuleSet/DeclarationBlock.php index 04c7d291..ef59617c 100644 --- a/src/RuleSet/DeclarationBlock.php +++ b/src/RuleSet/DeclarationBlock.php @@ -30,7 +30,7 @@ * * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented. */ -class DeclarationBlock implements CSSElement, CSSListItem, Positionable +class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer { use CommentContainer; use Position;