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

MyIntervals / PHP-CSS-Parser / 14804946311

02 May 2025 11:21PM UTC coverage: 56.004% (-0.3%) from 56.257%
14804946311

Pull #1249

github

web-flow
Merge b15a3ab9e into f6b5cd340
Pull Request #1249: [TASK] Add separate methods for `RuleSet::removeRule()`, with deprecation

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

998 of 1782 relevant lines covered (56.0%)

7.87 hits per line

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

65.77
/src/RuleSet/RuleSet.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\CSSElement;
9
use Sabberworm\CSS\CSSList\CSSListItem;
10
use Sabberworm\CSS\OutputFormat;
11
use Sabberworm\CSS\Parsing\ParserState;
12
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
13
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
14
use Sabberworm\CSS\Position\Position;
15
use Sabberworm\CSS\Position\Positionable;
16
use Sabberworm\CSS\Rule\Rule;
17

18
/**
19
 * This class is a container for individual 'Rule's.
20
 *
21
 * The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`.
22
 * However, unknown `AtRule`s (like `@font-face`) are rule sets as well.
23
 *
24
 * If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
25
 * (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
26
 *
27
 * Note that `CSSListItem` extends both `Commentable` and `Renderable`,
28
 * so those interfaces must also be implemented by concrete subclasses.
29
 */
30
abstract class RuleSet implements CSSElement, CSSListItem, Positionable
31
{
32
    use CommentContainer;
33
    use Position;
34

35
    /**
36
     * the rules in this rule set, using the property name as the key,
37
     * with potentially multiple rules per property name.
38
     *
39
     * @var array<string, array<int<0, max>, Rule>>
40
     */
41
    private $rules = [];
42

43
    /**
44
     * @param int<0, max> $lineNumber
45
     */
46
    public function __construct(int $lineNumber = 0)
8✔
47
    {
48
        $this->setPosition($lineNumber);
8✔
49
    }
8✔
50

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

97
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
6✔
98
    {
99
        $propertyName = $ruleToAdd->getRule();
6✔
100
        if (!isset($this->rules[$propertyName])) {
6✔
101
            $this->rules[$propertyName] = [];
6✔
102
        }
103

104
        $position = \count($this->rules[$propertyName]);
6✔
105

106
        if ($sibling !== null) {
6✔
107
            $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true);
1✔
108
            if ($siblingPosition !== false) {
1✔
109
                $position = $siblingPosition;
1✔
110
                $ruleToAdd->setPosition($sibling->getLineNo(), $sibling->getColNo() - 1);
1✔
111
            }
112
        }
113
        if ($ruleToAdd->getLineNo() === 0 && $ruleToAdd->getColNo() === 0) {
6✔
114
            //this node is added manually, give it the next best line
115
            $rules = $this->getRules();
2✔
116
            $rulesCount = \count($rules);
2✔
117
            if ($rulesCount > 0) {
2✔
118
                $last = $rules[$rulesCount - 1];
1✔
119
                $ruleToAdd->setPosition($last->getLineNo() + 1, 0);
1✔
120
            }
121
        }
122

123
        \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]);
6✔
124
    }
6✔
125

126
    /**
127
     * Returns all rules matching the given rule name
128
     *
129
     * @example $ruleSet->getRules('font') // returns array(0 => $rule, …) or array().
130
     *
131
     * @example $ruleSet->getRules('font-')
132
     *          //returns an array of all rules either beginning with font- or matching font.
133
     *
134
     * @param Rule|string|null $searchPattern
135
     *        Pattern to search for. If null, returns all rules.
136
     *        If the pattern ends with a dash, all rules starting with the pattern are returned
137
     *        as well as one matching the pattern with the dash excluded.
138
     *        Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
139
     *        Call `getRules($rule->getRule())` instead.
140
     *
141
     * @return array<int<0, max>, Rule>
142
     */
143
    public function getRules($searchPattern = null): array
6✔
144
    {
145
        if ($searchPattern instanceof Rule) {
6✔
146
            $searchPattern = $searchPattern->getRule();
×
147
        }
148
        $result = [];
6✔
149
        foreach ($this->rules as $propertyName => $rules) {
6✔
150
            // Either no search rule is given or the search rule matches the found rule exactly
151
            // or the search rule ends in “-” and the found rule starts with the search rule.
152
            if (
153
                $searchPattern === null || $propertyName === $searchPattern
6✔
154
                || (
155
                    \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
1✔
156
                    && (\strpos($propertyName, $searchPattern) === 0
1✔
157
                        || $propertyName === \substr($searchPattern, 0, -1))
6✔
158
                )
159
            ) {
160
                $result = \array_merge($result, $rules);
6✔
161
            }
162
        }
163
        \usort($result, static function (Rule $first, Rule $second): int {
164
            if ($first->getLineNo() === $second->getLineNo()) {
2✔
165
                return $first->getColNo() - $second->getColNo();
2✔
166
            }
167
            return $first->getLineNo() - $second->getLineNo();
1✔
168
        });
6✔
169

170
        return $result;
6✔
171
    }
172

173
    /**
174
     * Overrides all the rules of this set.
175
     *
176
     * @param array<Rule> $rules The rules to override with.
177
     */
178
    public function setRules(array $rules): void
1✔
179
    {
180
        $this->rules = [];
1✔
181
        foreach ($rules as $rule) {
1✔
182
            $this->addRule($rule);
1✔
183
        }
184
    }
1✔
185

186
    /**
187
     * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name
188
     * as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
189
     *
190
     * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
191
     * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
192
     * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
193
     *
194
     * @param Rule|string|null $searchPattern
195
     *        Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
196
     *        all rules starting with the pattern are returned as well as one matching the pattern with the dash
197
     *        excluded.
198
     *        Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
199
     *        Call `getRulesAssoc($rule->getRule())` instead.
200
     *
201
     * @return array<string, Rule>
202
     */
203
    public function getRulesAssoc($searchPattern = null): array
×
204
    {
205
        /** @var array<string, Rule> $result */
206
        $result = [];
×
207
        foreach ($this->getRules($searchPattern) as $rule) {
×
208
            $result[$rule->getRule()] = $rule;
×
209
        }
210

211
        return $result;
×
212
    }
213

214
    /**
215
     * Removes a `Rule` from this `RuleSet` by identity.
216
     *
217
     * @param Rule|string|null $searchPattern
218
     *        `Rule` to remove.
219
     *        Passing a `string` or `null` is deprecated in version 8.9.0, and will no longer work from v9.0.
220
     *        Use `removeMatchingRules()` or `removeAllRules()` instead.
221
     */
222
    public function removeRule($searchPattern): void
×
223
    {
224
        if ($searchPattern instanceof Rule) {
×
225
            $nameOfPropertyToRemove = $searchPattern->getRule();
×
226
            if (!isset($this->rules[$nameOfPropertyToRemove])) {
×
227
                return;
×
228
            }
229
            foreach ($this->rules[$nameOfPropertyToRemove] as $key => $rule) {
×
230
                if ($rule === $searchPattern) {
×
231
                    unset($this->rules[$nameOfPropertyToRemove][$key]);
×
232
                }
233
            }
NEW
234
        } elseif ($searchPattern !== null) {
×
NEW
235
            $this->removeMatchingRules($searchPattern);
×
236
        } else {
NEW
237
            $this->removeAllRules();
×
238
        }
NEW
239
    }
×
240

241
    /**
242
     * Removes rules by property name or search pattern.
243
     *
244
     * @param string $searchPattern
245
     *        pattern to remove.
246
     *        If the pattern ends in a dash,
247
     *        all rules starting with the pattern are removed as well as one matching the pattern with the dash
248
     *        excluded.
249
     */
NEW
250
    public function removeMatchingRules(string $searchPattern): void
×
251
    {
NEW
252
        foreach ($this->rules as $propertyName => $rules) {
×
253
            // Either 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 (
NEW
257
                $propertyName === $searchPattern
×
NEW
258
                || (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
×
NEW
259
                    && (\strpos($propertyName, $searchPattern) === 0
×
NEW
260
                        || $propertyName === \substr($searchPattern, 0, -1)))
×
261
            ) {
NEW
262
                unset($this->rules[$propertyName]);
×
263
            }
264
        }
265
    }
×
266

NEW
267
    public function removeAllRules(): void
×
268
    {
NEW
269
        $this->rules = [];
×
NEW
270
    }
×
271

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

294
        $formatter = $outputFormat->getFormatter();
5✔
295
        if (!$isFirst) {
5✔
296
            // Had some output
297
            $result .= $formatter->spaceAfterRules();
5✔
298
        }
299

300
        return $formatter->removeLastSemicolon($result);
5✔
301
    }
302
}
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