• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MyIntervals / PHP-CSS-Parser / 14363870590

09 Apr 2025 05:59PM UTC coverage: 50.582% (-3.8%) from 54.382%
14363870590

Pull #1194

github

web-flow
Merge 6d9620462 into f0360052d
Pull Request #1194: [TASK] Use delegation for `DeclarationBlock` -> `RuleSet`

16 of 26 new or added lines in 3 files covered. (61.54%)

70 existing lines in 1 file now uncovered.

912 of 1803 relevant lines covered (50.58%)

7.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.91
/src/RuleSet/DeclarationBlock.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\RuleSet;
6

7
use Sabberworm\CSS\Comment\CommentContainer;
8
use Sabberworm\CSS\CSSList\CSSList;
9
use Sabberworm\CSS\CSSList\CSSListItem;
10
use Sabberworm\CSS\CSSList\KeyFrame;
11
use Sabberworm\CSS\OutputFormat;
12
use Sabberworm\CSS\Parsing\OutputException;
13
use Sabberworm\CSS\Parsing\ParserState;
14
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
15
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
16
use Sabberworm\CSS\Position\Position;
17
use Sabberworm\CSS\Position\Positionable;
18
use Sabberworm\CSS\Property\KeyframeSelector;
19
use Sabberworm\CSS\Property\Selector;
20
use Sabberworm\CSS\Rule\Rule;
21

22
/**
23
 * This class includes a `RuleSet` constrained by a `Selector`.
24
 *
25
 * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
26
 * matching elements.
27
 *
28
 * 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.
31
 */
32
class DeclarationBlock implements CSSListItem, Positionable
33
{
34
    use CommentContainer;
35
    use Position;
36

37
    /**
38
     * @var array<Selector|string>
39
     */
40
    private $selectors = [];
41

42
    /**
43
     * @var RuleSet
44
     */
45
    private $ruleSet;
46

47
    /**
48
     * @param int<0, max> $lineNumber
49
     */
50
    public function __construct(int $lineNumber = 0)
11✔
51
    {
52
        $this->setPosition($lineNumber);
11✔
53
        $this->ruleSet = new RuleSet($lineNumber);
11✔
54
    }
11✔
55

56
    /**
57
     * @throws UnexpectedTokenException
58
     * @throws UnexpectedEOFException
59
     *
60
     * @internal since V8.8.0
61
     */
62
    public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock
9✔
63
    {
64
        $comments = [];
9✔
65
        $result = new DeclarationBlock($parserState->currentLine());
9✔
66
        try {
67
            $selectorParts = [];
9✔
68
            do {
69
                $selectorParts[] = $parserState->consume(1)
9✔
70
                    . $parserState->consumeUntil(['{', '}', '\'', '"'], false, false, $comments);
9✔
71
                if (\in_array($parserState->peek(), ['\'', '"'], true) && \substr(\end($selectorParts), -1) != '\\') {
9✔
72
                    if (!isset($stringWrapperCharacter)) {
×
73
                        $stringWrapperCharacter = $parserState->peek();
×
74
                    } elseif ($stringWrapperCharacter === $parserState->peek()) {
×
75
                        unset($stringWrapperCharacter);
×
76
                    }
77
                }
78
            } while (!\in_array($parserState->peek(), ['{', '}'], true) || isset($stringWrapperCharacter));
9✔
79
            $result->setSelectors(\implode('', $selectorParts), $list);
9✔
80
            if ($parserState->comes('{')) {
6✔
81
                $parserState->consume(1);
6✔
82
            }
83
        } catch (UnexpectedTokenException $e) {
3✔
84
            if ($parserState->getSettings()->usesLenientParsing()) {
3✔
85
                if (!$parserState->comes('}')) {
3✔
86
                    $parserState->consumeUntil('}', false, true);
1✔
87
                }
88
                return null;
3✔
89
            } else {
90
                throw $e;
×
91
            }
92
        }
93
        $result->setComments($comments);
6✔
94

95
        RuleSet::parseRuleSet($parserState, $result->ruleSet);
6✔
96

97
        return $result;
6✔
98
    }
99

100
    /**
101
     * @param array<Selector|string>|string $selectors
102
     *
103
     * @throws UnexpectedTokenException
104
     */
105
    public function setSelectors($selectors, ?CSSList $list = null): void
10✔
106
    {
107
        if (\is_array($selectors)) {
10✔
108
            $this->selectors = $selectors;
1✔
109
        } else {
110
            $this->selectors = \explode(',', $selectors);
9✔
111
        }
112
        foreach ($this->selectors as $key => $selector) {
10✔
113
            if (!($selector instanceof Selector)) {
10✔
114
                if ($list === null || !($list instanceof KeyFrame)) {
9✔
115
                    if (!Selector::isValid($selector)) {
9✔
116
                        throw new UnexpectedTokenException(
3✔
117
                            "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
3✔
118
                            $selectors,
119
                            'custom'
3✔
120
                        );
121
                    }
122
                    $this->selectors[$key] = new Selector($selector);
6✔
123
                } else {
124
                    if (!KeyframeSelector::isValid($selector)) {
×
125
                        throw new UnexpectedTokenException(
×
126
                            "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
×
127
                            $selector,
128
                            'custom'
×
129
                        );
130
                    }
131
                    $this->selectors[$key] = new KeyframeSelector($selector);
×
132
                }
133
            }
134
        }
135
    }
7✔
136

137
    /**
138
     * Remove one of the selectors of the block.
139
     *
140
     * @param Selector|string $selectorToRemove
141
     */
142
    public function removeSelector($selectorToRemove): bool
×
143
    {
144
        if ($selectorToRemove instanceof Selector) {
×
145
            $selectorToRemove = $selectorToRemove->getSelector();
×
146
        }
147
        foreach ($this->selectors as $key => $selector) {
×
148
            if ($selector->getSelector() === $selectorToRemove) {
×
149
                unset($this->selectors[$key]);
×
150
                return true;
×
151
            }
152
        }
153
        return false;
×
154
    }
155

156
    /**
157
     * @return array<Selector>
158
     */
159
    public function getSelectors(): array
×
160
    {
161
        return $this->selectors;
×
162
    }
163

NEW
164
    public function getRuleSet(): RuleSet
×
165
    {
NEW
166
        return $this->ruleSet;
×
167
    }
168

169
    /**
170
     * @see RuleSet::addRule()
171
     */
172
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
2✔
173
    {
174
        $this->ruleSet->addRule($ruleToAdd, $sibling);
2✔
175
    }
2✔
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
2✔
185
    {
186
        return $this->ruleSet->getRules($searchPattern);
2✔
187
    }
188

189
    /**
190
     * @see RuleSet::setRules()
191
     *
192
     * @param array<Rule> $rules
193
     */
194
    public function setRules(array $rules): void
1✔
195
    {
196
        $this->ruleSet->setRules($rules);
1✔
197
    }
1✔
198

199
    /**
200
     * @see RuleSet::getRulesAssoc()
201
     *
202
     * @param Rule|string|null $searchPattern
203
     *
204
     * @return array<string, Rule>
205
     */
NEW
206
    public function getRulesAssoc($searchPattern = null): array
×
207
    {
NEW
208
        return $this->ruleSet->getRulesAssoc($searchPattern);
×
209
    }
210

211
    /**
212
     * @see RuleSet::removeRule()
213
     *
214
     * @param Rule|string|null $searchPattern
215
     */
NEW
216
    public function removeRule($searchPattern): void
×
217
    {
NEW
218
        $this->ruleSet->removeRule($searchPattern);
×
NEW
219
    }
×
220

221
    /**
222
     * @return non-empty-string
223
     *
224
     * @throws OutputException
225
     */
226
    public function render(OutputFormat $outputFormat): string
6✔
227
    {
228
        $formatter = $outputFormat->getFormatter();
6✔
229
        $result = $formatter->comments($this);
6✔
230
        if (\count($this->selectors) === 0) {
6✔
231
            // If all the selectors have been removed, this declaration block becomes invalid
232
            throw new OutputException(
×
233
                'Attempt to print declaration block with missing selector',
×
234
                $this->getLineNumber()
×
235
            );
236
        }
237
        $result .= $outputFormat->getContentBeforeDeclarationBlock();
6✔
238
        $result .= $formatter->implode(
6✔
239
            $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
6✔
240
            $this->selectors
6✔
241
        );
242
        $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
6✔
243
        $result .= $formatter->spaceBeforeOpeningBrace() . '{';
6✔
244
        $result .= $this->ruleSet->render($outputFormat);
6✔
245
        $result .= '}';
6✔
246
        $result .= $outputFormat->getContentAfterDeclarationBlock();
6✔
247

248
        return $result;
6✔
249
    }
250
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc