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

MyIntervals / PHP-CSS-Parser / 14025852631

24 Mar 2025 02:23AM UTC coverage: 51.611%. Remained the same
14025852631

Pull #1212

github

web-flow
Merge 090d8afa4 into 639366092
Pull Request #1212: [TASK] Add (and use) a `CSSListItem` type

6 of 10 new or added lines in 2 files covered. (60.0%)

41 existing lines in 3 files now uncovered.

945 of 1831 relevant lines covered (51.61%)

6.72 hits per line

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

69.91
/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\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\Renderable;
15
use Sabberworm\CSS\Rule\Rule;
16

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

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

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

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

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

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

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

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

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

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

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

184
        return $result;
6✔
185
    }
186

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

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

223
        return $result;
×
224
    }
225

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

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

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

297
        return $formatter->removeLastSemicolon($result);
5✔
298
    }
299

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

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

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

© 2025 Coveralls, Inc