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

MyIntervals / PHP-CSS-Parser / 13267478804

11 Feb 2025 04:17PM UTC coverage: 49.156%. Remained the same
13267478804

Pull #910

github

web-flow
Merge ed5dea74d into b3e83b867
Pull Request #910: [CLEANUP] Avoid Hungarian notation for `outputFormat`

22 of 58 new or added lines in 18 files covered. (37.93%)

1 existing line in 1 file now uncovered.

932 of 1896 relevant lines covered (49.16%)

11.42 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
     * @internal since 8.8.0
40
     */
41
    protected $comments;
42

43
    /**
44
     * @var array<int, RuleSet|CSSList|Import|Charset>
45
     *
46
     * @internal since 8.8.0
47
     */
48
    protected $contents;
49

50
    /**
51
     * @var int<0, max>
52
     *
53
     * @internal since 8.8.0
54
     */
55
    protected $lineNumber;
56

57
    /**
58
     * @param int<0, max> $lineNumber
59
     */
60
    public function __construct($lineNumber = 0)
×
61
    {
62
        $this->comments = [];
×
63
        $this->contents = [];
×
64
        $this->lineNumber = $lineNumber;
×
65
    }
×
66

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

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

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

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

257
    /**
258
     * @return int<0, max>
259
     */
260
    public function getLineNo(): int
×
261
    {
262
        return $this->lineNumber;
×
263
    }
264

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

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

285
    /**
286
     * Splices the list of contents.
287
     *
288
     * @param int $offset
289
     * @param int $length
290
     * @param array<int, RuleSet|CSSList|Import|Charset> $replacement
291
     */
292
    public function splice($offset, $length = null, $replacement = null): void
×
293
    {
294
        \array_splice($this->contents, $offset, $length, $replacement);
×
295
    }
×
296

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

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

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

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

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

405
    public function __toString(): string
×
406
    {
407
        return $this->render(new OutputFormat());
×
408
    }
409

410
    /**
411
     * @return string
412
     */
NEW
413
    protected function renderListContents(OutputFormat $outputFormat)
×
414
    {
415
        $result = '';
×
416
        $isFirst = true;
×
NEW
417
        $nextLevelFormat = $outputFormat;
×
418
        if (!$this->isRootList()) {
×
NEW
419
            $nextLevelFormat = $outputFormat->nextLevel();
×
420
        }
421
        foreach ($this->contents as $listItem) {
×
422
            $renderedCss = $outputFormat->safely(static function () use ($nextLevelFormat, $listItem): string {
423
                return $listItem->render($nextLevelFormat);
×
424
            });
×
425
            if ($renderedCss === null) {
×
426
                continue;
×
427
            }
428
            if ($isFirst) {
×
429
                $isFirst = false;
×
430
                $result .= $nextLevelFormat->spaceBeforeBlocks();
×
431
            } else {
432
                $result .= $nextLevelFormat->spaceBetweenBlocks();
×
433
            }
434
            $result .= $renderedCss;
×
435
        }
436

437
        if (!$isFirst) {
×
438
            // Had some output
NEW
439
            $result .= $outputFormat->spaceAfterBlocks();
×
440
        }
441

442
        return $result;
×
443
    }
444

445
    /**
446
     * Return true if the list can not be further outdented. Only important when rendering.
447
     *
448
     * @return bool
449
     */
450
    abstract public function isRootList();
451

452
    /**
453
     * Returns the stored items.
454
     *
455
     * @return array<int, RuleSet|Import|Charset|CSSList>
456
     */
457
    public function getContents()
×
458
    {
459
        return $this->contents;
×
460
    }
461

462
    /**
463
     * @param array<array-key, Comment> $comments
464
     */
465
    public function addComments(array $comments): void
×
466
    {
467
        $this->comments = \array_merge($this->comments, $comments);
×
468
    }
×
469

470
    /**
471
     * @return array<array-key, Comment>
472
     */
473
    public function getComments()
×
474
    {
475
        return $this->comments;
×
476
    }
477

478
    /**
479
     * @param array<array-key, Comment> $comments
480
     */
481
    public function setComments(array $comments): void
×
482
    {
483
        $this->comments = $comments;
×
484
    }
×
485
}
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