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

MyIntervals / PHP-CSS-Parser / 13078651759

31 Jan 2025 06:31PM UTC coverage: 45.068%. Remained the same
13078651759

push

github

web-flow
[CLEANUP] Avoid Hungarian notation for `iKey` (#855)

Part of #756

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

1 existing line in 1 file now uncovered.

795 of 1764 relevant lines covered (45.07%)

10.79 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 $comments;
40

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

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

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

61
    /**
62
     * @throws UnexpectedTokenException
63
     * @throws SourceException
64
     */
65
    public static function parseList(ParserState $parserState, CSSList $oList): void
×
66
    {
67
        $bIsRoot = $oList instanceof Document;
×
68
        if (\is_string($parserState)) {
×
69
            $parserState = new ParserState($parserState, Settings::create());
×
70
        }
71
        $bLenientParsing = $parserState->getSettings()->bLenientParsing;
×
72
        $comments = [];
×
73
        while (!$parserState->isEnd()) {
×
74
            $comments = \array_merge($comments, $parserState->consumeWhiteSpace());
×
75
            $oListItem = null;
×
76
            if ($bLenientParsing) {
×
77
                try {
78
                    $oListItem = self::parseListItem($parserState, $oList);
×
79
                } catch (UnexpectedTokenException $e) {
×
80
                    $oListItem = false;
×
81
                }
82
            } else {
83
                $oListItem = self::parseListItem($parserState, $oList);
×
84
            }
85
            if ($oListItem === null) {
×
86
                // List parsing finished
87
                return;
×
88
            }
89
            if ($oListItem) {
×
90
                $oListItem->addComments($comments);
×
91
                $oList->append($oListItem);
×
92
            }
93
            $comments = $parserState->consumeWhiteSpace();
×
94
        }
95
        $oList->addComments($comments);
×
96
        if (!$bIsRoot && !$bLenientParsing) {
×
97
            throw new SourceException('Unexpected end of document', $parserState->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 $parserState, CSSList $oList)
×
109
    {
110
        $bIsRoot = $oList instanceof Document;
×
111
        if ($parserState->comes('@')) {
×
112
            $oAtRule = self::parseAtRule($parserState);
×
113
            if ($oAtRule instanceof Charset) {
×
114
                if (!$bIsRoot) {
×
115
                    throw new UnexpectedTokenException(
×
116
                        '@charset may only occur in root document',
×
117
                        '',
×
118
                        'custom',
×
119
                        $parserState->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
                        $parserState->currentLine()
×
128
                    );
129
                }
130
                $parserState->setCharset($oAtRule->getCharset());
×
131
            }
132
            return $oAtRule;
×
133
        } elseif ($parserState->comes('}')) {
×
134
            if ($bIsRoot) {
×
135
                if ($parserState->getSettings()->bLenientParsing) {
×
136
                    return DeclarationBlock::parse($parserState);
×
137
                } else {
138
                    throw new SourceException('Unopened {', $parserState->currentLine());
×
139
                }
140
            } else {
141
                // End of list
142
                return null;
×
143
            }
144
        } else {
145
            return DeclarationBlock::parse($parserState, $oList);
×
146
        }
147
    }
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

436
        return $sResult;
×
437
    }
438

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

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

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

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

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