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

MyIntervals / PHP-CSS-Parser / 21298683815

23 Jan 2026 07:34PM UTC coverage: 70.738% (-0.8%) from 71.554%
21298683815

Pull #1470

github

web-flow
Merge cbf6bbd92 into ae80d04ec
Pull Request #1470: [TASK] Use `Selector::parse()`

6 of 6 new or added lines in 1 file covered. (100.0%)

3 existing lines in 1 file now uncovered.

1419 of 2006 relevant lines covered (70.74%)

27.61 hits per line

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

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

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\RuleSet;
6

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

25
/**
26
 * This class represents a `RuleSet` constrained by a `Selector`.
27
 *
28
 * It contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the
29
 * matching elements.
30
 *
31
 * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
32
 *
33
 * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
34
 */
35
class DeclarationBlock implements CSSElement, CSSListItem, Positionable, RuleContainer
36
{
37
    use CommentContainer;
38
    use Position;
39

40
    /**
41
     * @var list<Selector>
42
     */
43
    private $selectors = [];
44

45
    /**
46
     * @var RuleSet
47
     */
48
    private $ruleSet;
49

50
    /**
51
     * @param int<1, max>|null $lineNumber
52
     */
53
    public function __construct(?int $lineNumber = null)
767✔
54
    {
55
        $this->ruleSet = new RuleSet($lineNumber);
767✔
56
        $this->setPosition($lineNumber);
767✔
57
    }
767✔
58

59
    /**
60
     * @throws UnexpectedTokenException
61
     * @throws UnexpectedEOFException
62
     *
63
     * @internal since V8.8.0
64
     */
65
    public static function parse(ParserState $parserState, ?CSSList $list = null): ?DeclarationBlock
239✔
66
    {
67
        $comments = [];
239✔
68
        $result = new DeclarationBlock($parserState->currentLine());
239✔
69
        try {
70
            $selectors = self::parseSelectors($parserState, $list, $comments);
239✔
71
            $result->setSelectors($selectors, $list);
172✔
72
            if ($parserState->comes('{')) {
172✔
73
                $parserState->consume(1);
172✔
74
            }
75
        } catch (UnexpectedTokenException $e) {
67✔
76
            if ($parserState->getSettings()->usesLenientParsing()) {
67✔
77
                if (!$parserState->consumeIfComes('}')) {
59✔
78
                    $parserState->consumeUntil(['}', ParserState::EOF], false, true);
46✔
79
                }
80
                return null;
59✔
81
            } else {
82
                throw $e;
8✔
83
            }
84
        }
85
        $result->setComments($comments);
172✔
86

87
        RuleSet::parseRuleSet($parserState, $result->getRuleSet());
172✔
88

89
        return $result;
172✔
90
    }
91

92
    /**
93
     * @param array<Selector|string>|string $selectors
94
     *
95
     * @throws UnexpectedTokenException
96
     */
97
    public function setSelectors($selectors, ?CSSList $list = null): void
340✔
98
    {
99
        if (\is_array($selectors)) {
340✔
100
            $selectorsToSet = $selectors;
174✔
101
        } else {
102
            // A string of comma-separated selectors requires parsing.
103
            try {
104
                $parserState = new ParserState($selectors, Settings::create());
166✔
105
                $selectorsToSet = self::parseSelectors($parserState, $list);
166✔
106
                if (!$parserState->isEnd()) {
158✔
107
                    throw new UnexpectedTokenException('EOF', 'more');
158✔
108
                }
109
            } catch (UnexpectedTokenException $exception) {
10✔
110
                // The exception message from parsing may refer to the faux `{` block start token,
111
                // which would be confusing.
112
                // Rethrow with a more useful message, that also includes the selector(s) string that was passed.
113
                throw new UnexpectedTokenException(
10✔
114
                    'Selector(s) string is not valid.',
10✔
115
                    $selectors,
116
                    'custom'
10✔
117
                );
118
            }
119
        }
120

121
        // Convert all items to a `Selector` if not already
122
        foreach ($selectorsToSet as $key => $selector) {
330✔
123
            if (!($selector instanceof Selector)) {
330✔
124
                if ($list === null || !($list instanceof KeyFrame)) {
1✔
125
                    if (!Selector::isValid($selector)) {
1✔
UNCOV
126
                        throw new UnexpectedTokenException(
×
UNCOV
127
                            "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
×
128
                            $selector,
UNCOV
129
                            'custom'
×
130
                        );
131
                    }
132
                    $selectorsToSet[$key] = new Selector($selector);
1✔
133
                } else {
134
                    if (!KeyframeSelector::isValid($selector)) {
×
135
                        throw new UnexpectedTokenException(
×
136
                            "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
×
137
                            $selector,
138
                            'custom'
×
139
                        );
140
                    }
141
                    $selectorsToSet[$key] = new KeyframeSelector($selector);
×
142
                }
143
            }
144
        }
145

146
        // Discard the keys and reindex the array
147
        $this->selectors = \array_values($selectorsToSet);
330✔
148
    }
330✔
149

150
    /**
151
     * Remove one of the selectors of the block.
152
     *
153
     * @param Selector|string $selectorToRemove
154
     */
155
    public function removeSelector($selectorToRemove): bool
×
156
    {
157
        if ($selectorToRemove instanceof Selector) {
×
158
            $selectorToRemove = $selectorToRemove->getSelector();
×
159
        }
160
        foreach ($this->selectors as $key => $selector) {
×
161
            if ($selector->getSelector() === $selectorToRemove) {
×
162
                unset($this->selectors[$key]);
×
163
                return true;
×
164
            }
165
        }
166
        return false;
×
167
    }
168

169
    /**
170
     * @return list<Selector>
171
     */
172
    public function getSelectors(): array
318✔
173
    {
174
        return $this->selectors;
318✔
175
    }
176

177
    public function getRuleSet(): RuleSet
183✔
178
    {
179
        return $this->ruleSet;
183✔
180
    }
181

182
    /**
183
     * @see RuleSet::addRule()
184
     */
185
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
215✔
186
    {
187
        $this->ruleSet->addRule($ruleToAdd, $sibling);
215✔
188
    }
215✔
189

190
    /**
191
     * @return array<int<0, max>, Rule>
192
     *
193
     * @see RuleSet::getRules()
194
     */
195
    public function getRules(?string $searchPattern = null): array
177✔
196
    {
197
        return $this->ruleSet->getRules($searchPattern);
177✔
198
    }
199

200
    /**
201
     * @param array<Rule> $rules
202
     *
203
     * @see RuleSet::setRules()
204
     */
205
    public function setRules(array $rules): void
346✔
206
    {
207
        $this->ruleSet->setRules($rules);
346✔
208
    }
346✔
209

210
    /**
211
     * @return array<string, Rule>
212
     *
213
     * @see RuleSet::getRulesAssoc()
214
     */
215
    public function getRulesAssoc(?string $searchPattern = null): array
50✔
216
    {
217
        return $this->ruleSet->getRulesAssoc($searchPattern);
50✔
218
    }
219

220
    /**
221
     * @see RuleSet::removeRule()
222
     */
223
    public function removeRule(Rule $ruleToRemove): void
22✔
224
    {
225
        $this->ruleSet->removeRule($ruleToRemove);
22✔
226
    }
22✔
227

228
    /**
229
     * @see RuleSet::removeMatchingRules()
230
     */
231
    public function removeMatchingRules(string $searchPattern): void
28✔
232
    {
233
        $this->ruleSet->removeMatchingRules($searchPattern);
28✔
234
    }
28✔
235

236
    /**
237
     * @see RuleSet::removeAllRules()
238
     */
239
    public function removeAllRules(): void
4✔
240
    {
241
        $this->ruleSet->removeAllRules();
4✔
242
    }
4✔
243

244
    /**
245
     * @return non-empty-string
246
     *
247
     * @throws OutputException
248
     */
249
    public function render(OutputFormat $outputFormat): string
6✔
250
    {
251
        $formatter = $outputFormat->getFormatter();
6✔
252
        $result = $formatter->comments($this);
6✔
253
        if (\count($this->selectors) === 0) {
6✔
254
            // If all the selectors have been removed, this declaration block becomes invalid
255
            throw new OutputException(
×
256
                'Attempt to print declaration block with missing selector',
×
257
                $this->getLineNumber()
×
258
            );
259
        }
260
        $result .= $outputFormat->getContentBeforeDeclarationBlock();
6✔
261
        $result .= $formatter->implode(
6✔
262
            $formatter->spaceBeforeSelectorSeparator() . ',' . $formatter->spaceAfterSelectorSeparator(),
6✔
263
            $this->selectors
6✔
264
        );
265
        $result .= $outputFormat->getContentAfterDeclarationBlockSelectors();
6✔
266
        $result .= $formatter->spaceBeforeOpeningBrace() . '{';
6✔
267
        $result .= $this->ruleSet->render($outputFormat);
6✔
268
        $result .= '}';
6✔
269
        $result .= $outputFormat->getContentAfterDeclarationBlock();
6✔
270

271
        return $result;
6✔
272
    }
273

274
    /**
275
     * @return array<string, bool|int|float|string|array<mixed>|null>
276
     *
277
     * @internal
278
     */
279
    public function getArrayRepresentation(): array
1✔
280
    {
281
        throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
1✔
282
    }
283

284
    /**
285
     * @param list<Comment> $comments
286
     *
287
     * @return list<Selector>
288
     *
289
     * @throws UnexpectedTokenException
290
     */
291
    private static function parseSelectors(ParserState $parserState, ?CSSList $list, array &$comments = []): array
405✔
292
    {
293
        $selectorClass = $list instanceof KeyFrame ? KeyFrameSelector::class : Selector::class;
405✔
294
        $selectors = [];
405✔
295

296
        while (true) {
405✔
297
            $selectors[] = $selectorClass::parse($parserState, $comments);
405✔
298
            if (!$parserState->consumeIfComes(',')) {
339✔
299
                break;
330✔
300
            }
301
        }
302

303
        return $selectors;
330✔
304
    }
305
}
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