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

MyIntervals / PHP-CSS-Parser / 13911032827

17 Mar 2025 10:23PM UTC coverage: 56.839%. Remained the same
13911032827

push

github

web-flow
[TASK] Add native type declarations for `RuleSet` (#1186)

Part of #811

0 of 2 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

1043 of 1835 relevant lines covered (56.84%)

12.88 hits per line

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

0.0
/src/RuleSet/RuleSet.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\Commentable;
9
use Sabberworm\CSS\OutputFormat;
10
use Sabberworm\CSS\Parsing\ParserState;
11
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
12
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
13
use Sabberworm\CSS\Renderable;
14
use Sabberworm\CSS\Rule\Rule;
15

16
/**
17
 * This class is a container for individual 'Rule's.
18
 *
19
 * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`.
20
 * However, unknown `AtRule`s (like `@font-face`) are rule sets as well.
21
 *
22
 * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
23
 * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
24
 */
25
abstract class RuleSet implements Renderable, Commentable
26
{
27
    /**
28
     * the rules in this rule set, using the property name as the key,
29
     * with potentially multiple rules per property name.
30
     *
31
     * @var array<string, array<int<0, max>, Rule>>
32
     */
33
    private $rules = [];
34

35
    /**
36
     * @var int<0, max>
37
     *
38
     * @internal since 8.8.0
39
     */
40
    protected $lineNumber;
41

42
    /**
43
     * @var list<Comment>
44
     *
45
     * @internal since 8.8.0
46
     */
47
    protected $comments = [];
48

49
    /**
50
     * @param int<0, max> $lineNumber
51
     */
52
    public function __construct(int $lineNumber = 0)
×
53
    {
54
        $this->lineNumber = $lineNumber;
×
55
    }
×
56

57
    /**
58
     * @throws UnexpectedTokenException
59
     * @throws UnexpectedEOFException
60
     *
61
     * @internal since V8.8.0
62
     */
63
    public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void
×
64
    {
65
        while ($parserState->comes(';')) {
×
66
            $parserState->consume(';');
×
67
        }
68
        while (true) {
×
69
            $commentsBeforeRule = $parserState->consumeWhiteSpace();
×
70
            if ($parserState->comes('}')) {
×
71
                break;
×
72
            }
73
            $rule = null;
×
74
            if ($parserState->getSettings()->usesLenientParsing()) {
×
75
                try {
76
                    $rule = Rule::parse($parserState, $commentsBeforeRule);
×
77
                } catch (UnexpectedTokenException $e) {
×
78
                    try {
79
                        $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
×
80
                        // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
81
                        if ($parserState->streql(\substr($consumedText, -1), '}')) {
×
82
                            $parserState->backtrack(1);
×
83
                        } else {
84
                            while ($parserState->comes(';')) {
×
85
                                $parserState->consume(';');
×
86
                            }
87
                        }
88
                    } catch (UnexpectedTokenException $e) {
×
89
                        // We’ve reached the end of the document. Just close the RuleSet.
90
                        return;
×
91
                    }
92
                }
93
            } else {
94
                $rule = Rule::parse($parserState, $commentsBeforeRule);
×
95
            }
96
            if ($rule instanceof Rule) {
×
97
                $ruleSet->addRule($rule);
×
98
            }
99
        }
100
        $parserState->consume('}');
×
101
    }
×
102

103
    /**
104
     * @return int<0, max>
105
     */
106
    public function getLineNo(): int
×
107
    {
108
        return $this->lineNumber;
×
109
    }
110

111
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
×
112
    {
113
        $propertyName = $ruleToAdd->getRule();
×
114
        if (!isset($this->rules[$propertyName])) {
×
115
            $this->rules[$propertyName] = [];
×
116
        }
117

118
        $position = \count($this->rules[$propertyName]);
×
119

120
        if ($sibling !== null) {
×
121
            $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true);
×
122
            if ($siblingPosition !== false) {
×
123
                $position = $siblingPosition;
×
124
                $ruleToAdd->setPosition($sibling->getLineNo(), $sibling->getColNo() - 1);
×
125
            }
126
        }
127
        if ($ruleToAdd->getLineNo() === 0 && $ruleToAdd->getColNo() === 0) {
×
128
            //this node is added manually, give it the next best line
129
            $rules = $this->getRules();
×
130
            $rulesCount = \count($rules);
×
131
            if ($rulesCount > 0) {
×
132
                $last = $rules[$rulesCount - 1];
×
133
                $ruleToAdd->setPosition($last->getLineNo() + 1, 0);
×
134
            }
135
        }
136

137
        \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]);
×
138
    }
×
139

140
    /**
141
     * Returns all rules matching the given rule name
142
     *
143
     * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array().
144
     *
145
     * @example $ruleSet->getRules('font-')
146
     *          //returns an array of all rules either beginning with font- or matching font.
147
     *
148
     * @param Rule|string|null $searchPattern
149
     *        Pattern to search for. If null, returns all rules.
150
     *        If the pattern ends with a dash, all rules starting with the pattern are returned
151
     *        as well as one matching the pattern with the dash excluded.
152
     *        Passing a `Rule` behaves like calling `getRules($rule->getRule())`.
153
     *
154
     * @return array<int<0, max>, Rule>
155
     */
NEW
156
    public function getRules($searchPattern = null): array
×
157
    {
158
        if ($searchPattern instanceof Rule) {
×
159
            $searchPattern = $searchPattern->getRule();
×
160
        }
UNCOV
161
        $result = [];
×
162
        foreach ($this->rules as $propertyName => $rules) {
×
163
            // Either no search rule is given or the search rule matches the found rule exactly
164
            // or the search rule ends in “-” and the found rule starts with the search rule.
165
            if (
166
                !$searchPattern || $propertyName === $searchPattern
×
167
                || (
168
                    \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
×
169
                    && (\strpos($propertyName, $searchPattern) === 0
×
170
                        || $propertyName === \substr($searchPattern, 0, -1))
×
171
                )
172
            ) {
173
                $result = \array_merge($result, $rules);
×
174
            }
175
        }
176
        \usort($result, static function (Rule $first, Rule $second): int {
177
            if ($first->getLineNo() === $second->getLineNo()) {
×
178
                return $first->getColNo() - $second->getColNo();
×
179
            }
180
            return $first->getLineNo() - $second->getLineNo();
×
181
        });
×
182

183
        return $result;
×
184
    }
185

186
    /**
187
     * Overrides all the rules of this set.
188
     *
189
     * @param array<Rule> $rules The rules to override with.
190
     */
191
    public function setRules(array $rules): void
×
192
    {
193
        $this->rules = [];
×
194
        foreach ($rules as $rule) {
×
195
            $this->addRule($rule);
×
196
        }
197
    }
×
198

199
    /**
200
     * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name
201
     * as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
202
     *
203
     * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
204
     * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
205
     * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
206
     *
207
     * @param Rule|string|null $searchPattern
208
     *        Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
209
     *        all rules starting with the pattern are returned as well as one matching the pattern with the dash
210
     *        excluded. Passing a `Rule` behaves like calling `getRules($rule->getRule())`.
211
     *
212
     * @return array<string, Rule>
213
     */
NEW
214
    public function getRulesAssoc($searchPattern = null): array
×
215
    {
216
        /** @var array<string, Rule> $result */
217
        $result = [];
×
218
        foreach ($this->getRules($searchPattern) as $rule) {
×
219
            $result[$rule->getRule()] = $rule;
×
220
        }
221

UNCOV
222
        return $result;
×
223
    }
224

225
    /**
226
     * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
227
     *
228
     * If given a Rule, it will only remove this particular rule (by identity).
229
     * If given a name, it will remove all rules by that name.
230
     *
231
     * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
232
     * remove all rules with the same name. To get the old behaviour, use `removeRule($rule->getRule())`.
233
     *
234
     * @param Rule|string|null $searchPattern
235
     *        pattern to remove. If null, all rules are removed. If the pattern ends in a dash,
236
     *        all rules starting with the pattern are removed as well as one matching the pattern with the dash
237
     *        excluded. Passing a Rule behaves matches by identity.
238
     */
239
    public function removeRule($searchPattern): void
×
240
    {
241
        if ($searchPattern instanceof Rule) {
×
242
            $nameOfPropertyToRemove = $searchPattern->getRule();
×
243
            if (!isset($this->rules[$nameOfPropertyToRemove])) {
×
244
                return;
×
245
            }
246
            foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) {
×
247
                if ($rule === $searchPattern) {
×
248
                    unset($this->rules[$nameOfPropertyToRemove][$key]);
×
249
                }
250
            }
251
        } else {
252
            foreach ($this->rules as $propertyName => $rules) {
×
253
                // Either no search rule is given or the search rule matches the found rule exactly
254
                // or the search rule ends in “-” and the found rule starts with the search rule or equals it
255
                // (without the trailing dash).
256
                if (
257
                    !$searchPattern || $propertyName === $searchPattern
×
258
                    || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
×
259
                        && (\strpos($propertyName, $searchPattern) === 0
×
260
                            || $propertyName === \substr($searchPattern, 0, -1)))
×
261
                ) {
262
                    unset($this->rules[$propertyName]);
×
263
                }
264
            }
265
        }
266
    }
×
267

268
    protected function renderRules(OutputFormat $outputFormat): string
×
269
    {
270
        $result = '';
×
271
        $isFirst = true;
×
272
        $nextLevelFormat = $outputFormat->nextLevel();
×
273
        foreach ($this->getRules() as $rule) {
×
274
            $nextLevelFormatter = $nextLevelFormat->getFormatter();
×
275
            $renderedRule = $nextLevelFormatter->safely(static function () use ($rule, $nextLevelFormat): string {
276
                return $rule->render($nextLevelFormat);
×
277
            });
×
278
            if ($renderedRule === null) {
×
279
                continue;
×
280
            }
281
            if ($isFirst) {
×
282
                $isFirst = false;
×
283
                $result .= $nextLevelFormatter->spaceBeforeRules();
×
284
            } else {
285
                $result .= $nextLevelFormatter->spaceBetweenRules();
×
286
            }
287
            $result .= $renderedRule;
×
288
        }
289

290
        $formatter = $outputFormat->getFormatter();
×
291
        if (!$isFirst) {
×
292
            // Had some output
293
            $result .= $formatter->spaceAfterRules();
×
294
        }
295

296
        return $formatter->removeLastSemicolon($result);
×
297
    }
298

299
    /**
300
     * @param list<Comment> $comments
301
     */
302
    public function addComments(array $comments): void
×
303
    {
304
        $this->comments = \array_merge($this->comments, $comments);
×
305
    }
×
306

307
    /**
308
     * @return list<Comment>
309
     */
310
    public function getComments(): array
×
311
    {
312
        return $this->comments;
×
313
    }
314

315
    /**
316
     * @param list<Comment> $comments
317
     */
318
    public function setComments(array $comments): void
×
319
    {
320
        $this->comments = $comments;
×
321
    }
×
322
}
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