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

MyIntervals / PHP-CSS-Parser / 13816645941

12 Mar 2025 04:33PM UTC coverage: 55.573% (-0.1%) from 55.68%
13816645941

Pull #898

github

web-flow
Merge 18801d18b into b0238f0a4
Pull Request #898: [TASK] Avoid magic method forwarding in `OutputFormat`

5 of 17 new or added lines in 4 files covered. (29.41%)

1 existing line in 1 file now uncovered.

1042 of 1875 relevant lines covered (55.57%)

12.37 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(int $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
        $propertyName = $ruleToAdd->getRule();
×
110
        if (!isset($this->rules[$propertyName])) {
×
111
            $this->rules[$propertyName] = [];
×
112
        }
113

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

116
        if ($sibling !== null) {
×
117
            $siblingPosition = \array_search($sibling, $this->rules[$propertyName], true);
×
118
            if ($siblingPosition !== false) {
×
119
                $position = $siblingPosition;
×
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
            $rulesCount = \count($rules);
×
127
            if ($rulesCount > 0) {
×
128
                $last = $rules[$rulesCount - 1];
×
129
                $ruleToAdd->setPosition($last->getLineNo() + 1, 0);
×
130
            }
131
        }
132

133
        \array_splice($this->rules[$propertyName], $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($rule->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 $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 (
163
                !$searchPattern || $propertyName === $searchPattern
×
164
                || (
165
                    \strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
×
166
                    && (\strpos($propertyName, $searchPattern) === 0
×
167
                        || $propertyName === \substr($searchPattern, 0, -1))
×
168
                )
169
            ) {
170
                $result = \array_merge($result, $rules);
×
171
            }
172
        }
173
        \usort($result, static function (Rule $first, Rule $second): int {
174
            if ($first->getLineNo() === $second->getLineNo()) {
×
175
                return $first->getColNo() - $second->getColNo();
×
176
            }
177
            return $first->getLineNo() - $second->getLineNo();
×
178
        });
×
179

180
        return $result;
×
181
    }
182

183
    /**
184
     * Overrides all the rules of this set.
185
     *
186
     * @param array<array-key, Rule> $rules The rules to override with.
187
     */
188
    public function setRules(array $rules): void
×
189
    {
190
        $this->rules = [];
×
191
        foreach ($rules as $rule) {
×
192
            $this->addRule($rule);
×
193
        }
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)
×
212
    {
213
        /** @var array<string, Rule> $result */
214
        $result = [];
×
215
        foreach ($this->getRules($searchPattern) as $rule) {
×
216
            $result[$rule->getRule()] = $rule;
×
217
        }
218
        return $result;
×
219
    }
220

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

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

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

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

NEW
303
        return $formatter->removeLastSemicolon($result);
×
304
    }
305

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

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

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