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

MyIntervals / PHP-CSS-Parser / 14449499370

14 Apr 2025 03:25PM UTC coverage: 52.421% (-3.8%) from 56.257%
14449499370

Pull #1194

github

web-flow
Merge 1f908cf10 into 9dbc6a644
Pull Request #1194: [TASK] Use delegation for `DeclarationBlock` -> `RuleSet`

17 of 26 new or added lines in 3 files covered. (65.38%)

70 existing lines in 1 file now uncovered.

942 of 1797 relevant lines covered (52.42%)

7.68 hits per line

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

2.86
/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`, so those interfaces must also be implemented.
28
 */
29
class RuleSet implements CSSElement, CSSListItem, Positionable
30
{
31
    use CommentContainer;
32
    use Position;
33

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

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

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

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

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

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

UNCOV
122
        \array_splice($this->rules[$propertyName], $position, 0, [$ruleToAdd]);
×
UNCOV
123
    }
×
124

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

UNCOV
168
        return $result;
×
169
    }
170

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

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

207
        return $result;
×
208
    }
209

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

253
    /**
254
     * @internal
255
     */
NEW
256
    public function render(OutputFormat $outputFormat): string
×
257
    {
NEW
258
        return $this->renderRules($outputFormat);
×
259
    }
260

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

UNCOV
283
        $formatter = $outputFormat->getFormatter();
×
UNCOV
284
        if (!$isFirst) {
×
285
            // Had some output
UNCOV
286
            $result .= $formatter->spaceAfterRules();
×
287
        }
288

UNCOV
289
        return $formatter->removeLastSemicolon($result);
×
290
    }
291
}
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