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

MyIntervals / PHP-CSS-Parser / 13628562611

03 Mar 2025 10:21AM UTC coverage: 55.808%. Remained the same
13628562611

Pull #1051

github

web-flow
Merge 86e5460cb into 86aeaa791
Pull Request #1051: [BUGFIX] Fix a type annotation in `RuleSet`

1057 of 1894 relevant lines covered (55.81%)

12.2 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 array<array-key, 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($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 (!$parserState->comes('}')) {
×
69
            $rule = null;
×
70
            if ($parserState->getSettings()->usesLenientParsing()) {
×
71
                try {
72
                    $rule = Rule::parse($parserState);
×
73
                } catch (UnexpectedTokenException $e) {
×
74
                    try {
75
                        $consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
×
76
                        // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
77
                        if ($parserState->streql(\substr($consumedText, -1), '}')) {
×
78
                            $parserState->backtrack(1);
×
79
                        } else {
80
                            while ($parserState->comes(';')) {
×
81
                                $parserState->consume(';');
×
82
                            }
83
                        }
84
                    } catch (UnexpectedTokenException $e) {
×
85
                        // We’ve reached the end of the document. Just close the RuleSet.
86
                        return;
×
87
                    }
88
                }
89
            } else {
90
                $rule = Rule::parse($parserState);
×
91
            }
92
            if ($rule instanceof Rule) {
×
93
                $ruleSet->addRule($rule);
×
94
            }
95
        }
96
        $parserState->consume('}');
×
97
    }
×
98

99
    /**
100
     * @return int<0, max>
101
     */
102
    public function getLineNo(): int
×
103
    {
104
        return $this->lineNumber;
×
105
    }
106

107
    public function addRule(Rule $ruleToAdd, ?Rule $sibling = null): void
×
108
    {
109
        $sRule = $ruleToAdd->getRule();
×
110
        if (!isset($this->rules[$sRule])) {
×
111
            $this->rules[$sRule] = [];
×
112
        }
113

114
        $position = \count($this->rules[$sRule]);
×
115

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

133
        \array_splice($this->rules[$sRule], $position, 0, [$ruleToAdd]);
×
134
    }
×
135

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

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

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

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

261
    /**
262
     * @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
263
     */
264
    public function __toString(): string
×
265
    {
266
        return $this->render(new OutputFormat());
×
267
    }
268

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

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

300
        return $outputFormat->removeLastSemicolon($result);
×
301
    }
302

303
    /**
304
     * @param array<string, Comment> $comments
305
     */
306
    public function addComments(array $comments): void
×
307
    {
308
        $this->comments = \array_merge($this->comments, $comments);
×
309
    }
×
310

311
    /**
312
     * @return array<string, Comment>
313
     */
314
    public function getComments(): array
×
315
    {
316
        return $this->comments;
×
317
    }
318

319
    /**
320
     * @param array<string, Comment> $comments
321
     */
322
    public function setComments(array $comments): void
×
323
    {
324
        $this->comments = $comments;
×
325
    }
×
326
}
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