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

MyIntervals / PHP-CSS-Parser / 14096236298

27 Mar 2025 12:35AM UTC coverage: 48.844% (-4.0%) from 52.82%
14096236298

Pull #1194

github

web-flow
Merge 3567356de into 11214f05f
Pull Request #1194: [TASK] Use delegation for `DeclarationBlock` -> `RuleSet`

16 of 28 new or added lines in 3 files covered. (57.14%)

73 existing lines in 1 file now uncovered.

887 of 1816 relevant lines covered (48.84%)

6.81 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\CommentContainer;
8
use Sabberworm\CSS\CSSList\CSSListItem;
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
 * Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
26
 */
27
class RuleSet implements CSSListItem
28
{
29
    use CommentContainer;
30

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

39
    /**
40
     * @var int<0, max>
41
     *
42
     * @internal since 8.8.0
43
     */
44
    protected $lineNumber;
45

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

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

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

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

UNCOV
115
        $position = \count($this->rules[$propertyName]);
×
116

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

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

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

UNCOV
180
        return $result;
×
181
    }
182

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

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

219
        return $result;
×
220
    }
221

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

265
    /**
266
     * @internal
267
     */
NEW
268
    public function render(OutputFormat $outputFormat): string
×
269
    {
NEW
270
        return $this->renderRules($outputFormat);
×
271
    }
272

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

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

UNCOV
301
        return $formatter->removeLastSemicolon($result);
×
302
    }
303
}
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