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

MyIntervals / PHP-CSS-Parser / 11785175003

11 Nov 2024 07:46PM UTC coverage: 38.583% (-0.04%) from 38.622%
11785175003

push

github

web-flow
[TASK] Clean up the code with Rector (#772)

Just the Rector changes, and some redundancy removal related to
those, but nothing more.

10 of 19 new or added lines in 9 files covered. (52.63%)

1 existing line in 1 file now uncovered.

779 of 2019 relevant lines covered (38.58%)

5.33 hits per line

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

0.0
/src/CSSList/CSSList.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\CSSList;
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\SourceException;
12
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
13
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
14
use Sabberworm\CSS\Property\AtRule;
15
use Sabberworm\CSS\Property\Charset;
16
use Sabberworm\CSS\Property\CSSNamespace;
17
use Sabberworm\CSS\Property\Import;
18
use Sabberworm\CSS\Property\Selector;
19
use Sabberworm\CSS\Renderable;
20
use Sabberworm\CSS\RuleSet\AtRuleSet;
21
use Sabberworm\CSS\RuleSet\DeclarationBlock;
22
use Sabberworm\CSS\RuleSet\RuleSet;
23
use Sabberworm\CSS\Settings;
24
use Sabberworm\CSS\Value\CSSString;
25
use Sabberworm\CSS\Value\URL;
26
use Sabberworm\CSS\Value\Value;
27

28
/**
29
 * This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),
30
 * `RuleSet`s as well as other `CSSList` objects.
31
 *
32
 * It can also contain `Import` and `Charset` objects stemming from at-rules.
33
 */
34
abstract class CSSList implements Renderable, Commentable
35
{
36
    /**
37
     * @var array<array-key, Comment>
38
     */
39
    protected $aComments;
40

41
    /**
42
     * @var array<int, RuleSet|CSSList|Import|Charset>
43
     */
44
    protected $aContents;
45

46
    /**
47
     * @var int
48
     */
49
    protected $iLineNo;
50

51
    /**
52
     * @param int $iLineNo
53
     */
54
    public function __construct($iLineNo = 0)
×
55
    {
56
        $this->aComments = [];
×
57
        $this->aContents = [];
×
58
        $this->iLineNo = $iLineNo;
×
59
    }
×
60

61
    /**
62
     * @throws UnexpectedTokenException
63
     * @throws SourceException
64
     */
65
    public static function parseList(ParserState $oParserState, CSSList $oList): void
×
66
    {
67
        $bIsRoot = $oList instanceof Document;
×
68
        if (\is_string($oParserState)) {
×
69
            $oParserState = new ParserState($oParserState, Settings::create());
×
70
        }
71
        $bLenientParsing = $oParserState->getSettings()->bLenientParsing;
×
72
        $aComments = [];
×
73
        while (!$oParserState->isEnd()) {
×
74
            $aComments = \array_merge($aComments, $oParserState->consumeWhiteSpace());
×
75
            $oListItem = null;
×
76
            if ($bLenientParsing) {
×
77
                try {
78
                    $oListItem = self::parseListItem($oParserState, $oList);
×
79
                } catch (UnexpectedTokenException $e) {
×
80
                    $oListItem = false;
×
81
                }
82
            } else {
83
                $oListItem = self::parseListItem($oParserState, $oList);
×
84
            }
85
            if ($oListItem === null) {
×
86
                // List parsing finished
87
                return;
×
88
            }
89
            if ($oListItem) {
×
90
                $oListItem->addComments($aComments);
×
91
                $oList->append($oListItem);
×
92
            }
93
            $aComments = $oParserState->consumeWhiteSpace();
×
94
        }
95
        $oList->addComments($aComments);
×
96
        if (!$bIsRoot && !$bLenientParsing) {
×
97
            throw new SourceException('Unexpected end of document', $oParserState->currentLine());
×
98
        }
99
    }
×
100

101
    /**
102
     * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|false|null
103
     *
104
     * @throws SourceException
105
     * @throws UnexpectedEOFException
106
     * @throws UnexpectedTokenException
107
     */
108
    private static function parseListItem(ParserState $oParserState, CSSList $oList)
×
109
    {
110
        $bIsRoot = $oList instanceof Document;
×
111
        if ($oParserState->comes('@')) {
×
112
            $oAtRule = self::parseAtRule($oParserState);
×
113
            if ($oAtRule instanceof Charset) {
×
114
                if (!$bIsRoot) {
×
115
                    throw new UnexpectedTokenException(
×
116
                        '@charset may only occur in root document',
×
117
                        '',
×
118
                        'custom',
×
119
                        $oParserState->currentLine()
×
120
                    );
121
                }
122
                if (\count($oList->getContents()) > 0) {
×
123
                    throw new UnexpectedTokenException(
×
124
                        '@charset must be the first parseable token in a document',
×
125
                        '',
×
126
                        'custom',
×
127
                        $oParserState->currentLine()
×
128
                    );
129
                }
130
                $oParserState->setCharset($oAtRule->getCharset());
×
131
            }
132
            return $oAtRule;
×
133
        } elseif ($oParserState->comes('}')) {
×
134
            if ($bIsRoot) {
×
135
                if ($oParserState->getSettings()->bLenientParsing) {
×
136
                    return DeclarationBlock::parse($oParserState);
×
137
                } else {
138
                    throw new SourceException('Unopened {', $oParserState->currentLine());
×
139
                }
140
            } else {
141
                // End of list
142
                return null;
×
143
            }
144
        } else {
145
            return DeclarationBlock::parse($oParserState, $oList);
×
146
        }
147
    }
148

149
    /**
150
     * @param ParserState $oParserState
151
     *
152
     * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
153
     *
154
     * @throws SourceException
155
     * @throws UnexpectedTokenException
156
     * @throws UnexpectedEOFException
157
     */
158
    private static function parseAtRule(ParserState $oParserState)
×
159
    {
160
        $oParserState->consume('@');
×
161
        $sIdentifier = $oParserState->parseIdentifier();
×
162
        $iIdentifierLineNum = $oParserState->currentLine();
×
163
        $oParserState->consumeWhiteSpace();
×
164
        if ($sIdentifier === 'import') {
×
165
            $oLocation = URL::parse($oParserState);
×
166
            $oParserState->consumeWhiteSpace();
×
167
            $sMediaQuery = null;
×
168
            if (!$oParserState->comes(';')) {
×
169
                $sMediaQuery = \trim($oParserState->consumeUntil([';', ParserState::EOF]));
×
NEW
170
                if ($sMediaQuery === '') {
×
NEW
171
                    $sMediaQuery = null;
×
172
                }
173
            }
174
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
×
NEW
175
            return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
×
176
        } elseif ($sIdentifier === 'charset') {
×
177
            $oCharsetString = CSSString::parse($oParserState);
×
178
            $oParserState->consumeWhiteSpace();
×
179
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
×
180
            return new Charset($oCharsetString, $iIdentifierLineNum);
×
181
        } elseif (self::identifierIs($sIdentifier, 'keyframes')) {
×
182
            $oResult = new KeyFrame($iIdentifierLineNum);
×
183
            $oResult->setVendorKeyFrame($sIdentifier);
×
184
            $oResult->setAnimationName(\trim($oParserState->consumeUntil('{', false, true)));
×
185
            CSSList::parseList($oParserState, $oResult);
×
186
            if ($oParserState->comes('}')) {
×
187
                $oParserState->consume('}');
×
188
            }
189
            return $oResult;
×
190
        } elseif ($sIdentifier === 'namespace') {
×
191
            $sPrefix = null;
×
192
            $mUrl = Value::parsePrimitiveValue($oParserState);
×
193
            if (!$oParserState->comes(';')) {
×
194
                $sPrefix = $mUrl;
×
195
                $mUrl = Value::parsePrimitiveValue($oParserState);
×
196
            }
197
            $oParserState->consumeUntil([';', ParserState::EOF], true, true);
×
198
            if ($sPrefix !== null && !\is_string($sPrefix)) {
×
199
                throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
×
200
            }
201
            if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
×
202
                throw new UnexpectedTokenException(
×
203
                    'Wrong namespace url of invalid type',
×
204
                    $mUrl,
205
                    'custom',
×
206
                    $iIdentifierLineNum
207
                );
208
            }
209
            return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
×
210
        } else {
211
            // Unknown other at rule (font-face or such)
212
            $sArgs = \trim($oParserState->consumeUntil('{', false, true));
×
213
            if (\substr_count($sArgs, '(') != \substr_count($sArgs, ')')) {
×
214
                if ($oParserState->getSettings()->bLenientParsing) {
×
215
                    return null;
×
216
                } else {
217
                    throw new SourceException('Unmatched brace count in media query', $oParserState->currentLine());
×
218
                }
219
            }
220
            $bUseRuleSet = true;
×
221
            foreach (\explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
×
222
                if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
×
223
                    $bUseRuleSet = false;
×
224
                    break;
×
225
                }
226
            }
227
            if ($bUseRuleSet) {
×
228
                $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
×
229
                RuleSet::parseRuleSet($oParserState, $oAtRule);
×
230
            } else {
231
                $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
×
232
                CSSList::parseList($oParserState, $oAtRule);
×
233
                if ($oParserState->comes('}')) {
×
234
                    $oParserState->consume('}');
×
235
                }
236
            }
237
            return $oAtRule;
×
238
        }
239
    }
240

241
    /**
242
     * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
243
     * We need to check for these versions too.
244
     *
245
     * @param string $sIdentifier
246
     */
NEW
247
    private static function identifierIs($sIdentifier, string $sMatch): bool
×
248
    {
249
        return (\strcasecmp($sIdentifier, $sMatch) === 0)
×
250
            ?: \preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
×
251
    }
252

253
    /**
254
     * @return int
255
     */
256
    public function getLineNo()
×
257
    {
258
        return $this->iLineNo;
×
259
    }
260

261
    /**
262
     * Prepends an item to the list of contents.
263
     *
264
     * @param RuleSet|CSSList|Import|Charset $oItem
265
     */
266
    public function prepend($oItem): void
×
267
    {
268
        \array_unshift($this->aContents, $oItem);
×
269
    }
×
270

271
    /**
272
     * Appends an item to the list of contents.
273
     *
274
     * @param RuleSet|CSSList|Import|Charset $oItem
275
     */
276
    public function append($oItem): void
×
277
    {
278
        $this->aContents[] = $oItem;
×
279
    }
×
280

281
    /**
282
     * Splices the list of contents.
283
     *
284
     * @param int $iOffset
285
     * @param int $iLength
286
     * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
287
     */
288
    public function splice($iOffset, $iLength = null, $mReplacement = null): void
×
289
    {
290
        \array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
×
291
    }
×
292

293
    /**
294
     * Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
295
     * the item is appended at the end.
296
     *
297
     * @param RuleSet|CSSList|Import|Charset $item
298
     * @param RuleSet|CSSList|Import|Charset $sibling
299
     */
300
    public function insertBefore($item, $sibling): void
×
301
    {
302
        if (\in_array($sibling, $this->aContents, true)) {
×
303
            $this->replace($sibling, [$item, $sibling]);
×
304
        } else {
305
            $this->append($item);
×
306
        }
307
    }
×
308

309
    /**
310
     * Removes an item from the CSS list.
311
     *
312
     * @param RuleSet|Import|Charset|CSSList $oItemToRemove
313
     *        May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`,
314
     *        a `Charset` or another `CSSList` (most likely a `MediaQuery`)
315
     *
316
     * @return bool whether the item was removed
317
     */
318
    public function remove($oItemToRemove)
×
319
    {
320
        $iKey = \array_search($oItemToRemove, $this->aContents, true);
×
321
        if ($iKey !== false) {
×
322
            unset($this->aContents[$iKey]);
×
323
            return true;
×
324
        }
325
        return false;
×
326
    }
327

328
    /**
329
     * Replaces an item from the CSS list.
330
     *
331
     * @param RuleSet|Import|Charset|CSSList $oOldItem
332
     *        May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
333
     *        or another `CSSList` (most likely a `MediaQuery`)
334
     *
335
     * @return bool
336
     */
337
    public function replace($oOldItem, $mNewItem)
×
338
    {
339
        $iKey = \array_search($oOldItem, $this->aContents, true);
×
340
        if ($iKey !== false) {
×
341
            if (\is_array($mNewItem)) {
×
342
                \array_splice($this->aContents, $iKey, 1, $mNewItem);
×
343
            } else {
344
                \array_splice($this->aContents, $iKey, 1, [$mNewItem]);
×
345
            }
346
            return true;
×
347
        }
348
        return false;
×
349
    }
350

351
    /**
352
     * @param array<int, RuleSet|Import|Charset|CSSList> $aContents
353
     */
354
    public function setContents(array $aContents): void
×
355
    {
356
        $this->aContents = [];
×
357
        foreach ($aContents as $content) {
×
358
            $this->append($content);
×
359
        }
360
    }
×
361

362
    /**
363
     * Removes a declaration block from the CSS list if it matches all given selectors.
364
     *
365
     * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
366
     * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
367
     */
368
    public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false): void
×
369
    {
370
        if ($mSelector instanceof DeclarationBlock) {
×
371
            $mSelector = $mSelector->getSelectors();
×
372
        }
373
        if (!\is_array($mSelector)) {
×
374
            $mSelector = \explode(',', $mSelector);
×
375
        }
376
        foreach ($mSelector as $iKey => &$mSel) {
×
377
            if (!($mSel instanceof Selector)) {
×
378
                if (!Selector::isValid($mSel)) {
×
379
                    throw new UnexpectedTokenException(
×
380
                        "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
×
381
                        $mSel,
382
                        'custom'
×
383
                    );
384
                }
385
                $mSel = new Selector($mSel);
×
386
            }
387
        }
388
        foreach ($this->aContents as $iKey => $mItem) {
×
389
            if (!($mItem instanceof DeclarationBlock)) {
×
390
                continue;
×
391
            }
392
            if ($mItem->getSelectors() == $mSelector) {
×
393
                unset($this->aContents[$iKey]);
×
394
                if (!$bRemoveAll) {
×
395
                    return;
×
396
                }
397
            }
398
        }
399
    }
×
400

401
    public function __toString(): string
×
402
    {
403
        return $this->render(new OutputFormat());
×
404
    }
405

406
    /**
407
     * @return string
408
     */
409
    protected function renderListContents(OutputFormat $oOutputFormat)
×
410
    {
411
        $sResult = '';
×
412
        $bIsFirst = true;
×
413
        $oNextLevel = $oOutputFormat;
×
414
        if (!$this->isRootList()) {
×
415
            $oNextLevel = $oOutputFormat->nextLevel();
×
416
        }
417
        foreach ($this->aContents as $oContent) {
×
418
            $sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
419
                return $oContent->render($oNextLevel);
×
420
            });
×
421
            if ($sRendered === null) {
×
422
                continue;
×
423
            }
424
            if ($bIsFirst) {
×
425
                $bIsFirst = false;
×
426
                $sResult .= $oNextLevel->spaceBeforeBlocks();
×
427
            } else {
428
                $sResult .= $oNextLevel->spaceBetweenBlocks();
×
429
            }
430
            $sResult .= $sRendered;
×
431
        }
432

433
        if (!$bIsFirst) {
×
434
            // Had some output
435
            $sResult .= $oOutputFormat->spaceAfterBlocks();
×
436
        }
437

438
        return $sResult;
×
439
    }
440

441
    /**
442
     * Return true if the list can not be further outdented. Only important when rendering.
443
     *
444
     * @return bool
445
     */
446
    abstract public function isRootList();
447

448
    /**
449
     * Returns the stored items.
450
     *
451
     * @return array<int, RuleSet|Import|Charset|CSSList>
452
     */
453
    public function getContents()
×
454
    {
455
        return $this->aContents;
×
456
    }
457

458
    /**
459
     * @param array<array-key, Comment> $aComments
460
     */
461
    public function addComments(array $aComments): void
×
462
    {
463
        $this->aComments = \array_merge($this->aComments, $aComments);
×
464
    }
×
465

466
    /**
467
     * @return array<array-key, Comment>
468
     */
469
    public function getComments()
×
470
    {
471
        return $this->aComments;
×
472
    }
473

474
    /**
475
     * @param array<array-key, Comment> $aComments
476
     */
477
    public function setComments(array $aComments): void
×
478
    {
479
        $this->aComments = $aComments;
×
480
    }
×
481
}
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