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

MyIntervals / PHP-CSS-Parser / 21233754432

22 Jan 2026 02:28AM UTC coverage: 68.186% (-2.6%) from 70.819%
21233754432

Pull #1470

github

web-flow
Merge b51523770 into bea90126e
Pull Request #1470: [TASK] Add `Selector::parse`

6 of 57 new or added lines in 2 files covered. (10.53%)

3 existing lines in 1 file now uncovered.

1361 of 1996 relevant lines covered (68.19%)

26.26 hits per line

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

81.9
/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
            // Parse as if it's the opening part of a rule.
104
            try {
105
                $parserState = new ParserState($selectors . '{', Settings::create());
166✔
106
                $selectorsToSet = self::parseSelectors($parserState, $list);
166✔
107
                $parserState->consume('{'); // throw exception if this is not next
158✔
108
                if (!$parserState->isEnd()) {
157✔
109
                    throw new UnexpectedTokenException('EOF', 'more');
157✔
110
                }
111
            } catch (UnexpectedTokenException $exception) {
10✔
112
                // The exception message from parsing may refer to the faux `{` block start token,
113
                // which would be confusing.
114
                // Rethrow with a more useful message, that also includes the selector(s) string that was passed.
115
                throw new UnexpectedTokenException(
10✔
116
                    'Selector(s) string is not valid.',
10✔
117
                    $selectors,
118
                    'custom'
10✔
119
                );
120
            }
121
        }
122

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

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

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

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

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

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

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

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

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

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

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

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

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

273
        return $result;
6✔
274
    }
275

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

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

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

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