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

MyIntervals / PHP-CSS-Parser / 13395553485

18 Feb 2025 04:36PM UTC coverage: 51.335%. Remained the same
13395553485

push

github

web-flow
[CLEANUP] Drop redundant `@internal` annotations (#951)

If a whole class is `@internal`, there is no need to mark
the individual methods, properties or constants as `@internal`.

961 of 1872 relevant lines covered (51.34%)

11.55 hits per line

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

0.0
/src/Parsing/ParserState.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Sabberworm\CSS\Parsing;
6

7
use Sabberworm\CSS\Comment\Comment;
8
use Sabberworm\CSS\Settings;
9

10
/**
11
 * @internal since 8.7.0
12
 */
13
class ParserState
14
{
15
    /**
16
     * @var null
17
     */
18
    public const EOF = null;
19

20
    /**
21
     * @var Settings
22
     */
23
    private $parserSettings;
24

25
    /**
26
     * @var string
27
     */
28
    private $text;
29

30
    /**
31
     * @var array<int, string>
32
     */
33
    private $characters;
34

35
    /**
36
     * @var int
37
     */
38
    private $currentPosition = 0;
39

40
    /**
41
     * will only be used if the CSS does not contain an `@charset` declaration
42
     *
43
     * @var string
44
     */
45
    private $charset;
46

47
    /**
48
     * @var int
49
     */
50
    private $length;
51

52
    /**
53
     * @var int
54
     */
55
    private $lineNumber;
56

57
    /**
58
     * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
59
     * @param int<0, max> $lineNumber
60
     */
61
    public function __construct($text, Settings $parserSettings, $lineNumber = 1)
×
62
    {
63
        $this->parserSettings = $parserSettings;
×
64
        $this->text = $text;
×
65
        $this->lineNumber = $lineNumber;
×
66
        $this->setCharset($this->parserSettings->sDefaultCharset);
×
67
    }
×
68

69
    /**
70
     * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
71
     *
72
     * @param string $charset
73
     */
74
    public function setCharset($charset): void
×
75
    {
76
        $this->charset = $charset;
×
77
        $this->characters = $this->strsplit($this->text);
×
78
        if (\is_array($this->characters)) {
×
79
            $this->length = \count($this->characters);
×
80
        }
81
    }
×
82

83
    /**
84
     * @return int
85
     */
86
    public function currentLine()
×
87
    {
88
        return $this->lineNumber;
×
89
    }
90

91
    /**
92
     * @return int
93
     */
94
    public function currentColumn()
×
95
    {
96
        return $this->currentPosition;
×
97
    }
98

99
    /**
100
     * @return Settings
101
     */
102
    public function getSettings()
×
103
    {
104
        return $this->parserSettings;
×
105
    }
106

107
    public function anchor(): Anchor
×
108
    {
109
        return new Anchor($this->currentPosition, $this);
×
110
    }
111

112
    /**
113
     * @param int $position
114
     */
115
    public function setPosition($position): void
×
116
    {
117
        $this->currentPosition = $position;
×
118
    }
×
119

120
    /**
121
     * @param bool $bIgnoreCase
122
     *
123
     * @return string
124
     *
125
     * @throws UnexpectedTokenException
126
     */
127
    public function parseIdentifier($bIgnoreCase = true)
×
128
    {
129
        if ($this->isEnd()) {
×
130
            throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
×
131
        }
132
        $result = $this->parseCharacter(true);
×
133
        if ($result === null) {
×
134
            throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
×
135
        }
136
        $sCharacter = null;
×
137
        while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
×
138
            if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $sCharacter)) {
×
139
                $result .= $sCharacter;
×
140
            } else {
141
                $result .= '\\' . $sCharacter;
×
142
            }
143
        }
144
        if ($bIgnoreCase) {
×
145
            $result = $this->strtolower($result);
×
146
        }
147
        return $result;
×
148
    }
149

150
    /**
151
     * @param bool $bIsForIdentifier
152
     *
153
     * @return string|null
154
     *
155
     * @throws UnexpectedEOFException
156
     * @throws UnexpectedTokenException
157
     */
158
    public function parseCharacter($bIsForIdentifier)
×
159
    {
160
        if ($this->peek() === '\\') {
×
161
            if (
162
                $bIsForIdentifier && $this->parserSettings->bLenientParsing
×
163
                && ($this->comes('\\0') || $this->comes('\\9'))
×
164
            ) {
165
                // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
166
                return null;
×
167
            }
168
            $this->consume('\\');
×
169
            if ($this->comes('\\n') || $this->comes('\\r')) {
×
170
                return '';
×
171
            }
172
            if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
×
173
                return $this->consume(1);
×
174
            }
175
            $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
×
176
            if ($this->strlen($sUnicode) < 6) {
×
177
                // Consume whitespace after incomplete unicode escape
178
                if (\preg_match('/\\s/isSu', $this->peek())) {
×
179
                    if ($this->comes('\\r\\n')) {
×
180
                        $this->consume(2);
×
181
                    } else {
182
                        $this->consume(1);
×
183
                    }
184
                }
185
            }
186
            $iUnicode = \intval($sUnicode, 16);
×
187
            $sUtf32 = '';
×
188
            for ($i = 0; $i < 4; ++$i) {
×
189
                $sUtf32 .= \chr($iUnicode & 0xff);
×
190
                $iUnicode = $iUnicode >> 8;
×
191
            }
192
            return \iconv('utf-32le', $this->charset, $sUtf32);
×
193
        }
194
        if ($bIsForIdentifier) {
×
195
            $peek = \ord($this->peek());
×
196
            // Ranges: a-z A-Z 0-9 - _
197
            if (
198
                ($peek >= 97 && $peek <= 122)
×
199
                || ($peek >= 65 && $peek <= 90)
×
200
                || ($peek >= 48 && $peek <= 57)
×
201
                || ($peek === 45)
×
202
                || ($peek === 95)
×
203
                || ($peek > 0xa1)
×
204
            ) {
205
                return $this->consume(1);
×
206
            }
207
        } else {
208
            return $this->consume(1);
×
209
        }
210
        return null;
×
211
    }
212

213
    /**
214
     * @return array<int, Comment>|void
215
     *
216
     * @throws UnexpectedEOFException
217
     * @throws UnexpectedTokenException
218
     */
219
    public function consumeWhiteSpace(): array
×
220
    {
221
        $comments = [];
×
222
        do {
223
            while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
×
224
                $this->consume(1);
×
225
            }
226
            if ($this->parserSettings->bLenientParsing) {
×
227
                try {
228
                    $oComment = $this->consumeComment();
×
229
                } catch (UnexpectedEOFException $e) {
×
230
                    $this->currentPosition = $this->length;
×
231
                    return $comments;
×
232
                }
233
            } else {
234
                $oComment = $this->consumeComment();
×
235
            }
236
            if ($oComment !== false) {
×
237
                $comments[] = $oComment;
×
238
            }
239
        } while ($oComment !== false);
×
240
        return $comments;
×
241
    }
242

243
    /**
244
     * @param string $sString
245
     * @param bool $bCaseInsensitive
246
     */
247
    public function comes($sString, $bCaseInsensitive = false): bool
×
248
    {
249
        $sPeek = $this->peek(\strlen($sString));
×
250
        return ($sPeek == '')
×
251
            ? false
×
252
            : $this->streql($sPeek, $sString, $bCaseInsensitive);
×
253
    }
254

255
    /**
256
     * @param int $length
257
     * @param int $iOffset
258
     */
259
    public function peek($length = 1, $iOffset = 0): string
×
260
    {
261
        $iOffset += $this->currentPosition;
×
262
        if ($iOffset >= $this->length) {
×
263
            return '';
×
264
        }
265
        return $this->substr($iOffset, $length);
×
266
    }
267

268
    /**
269
     * @param int $mValue
270
     *
271
     * @throws UnexpectedEOFException
272
     * @throws UnexpectedTokenException
273
     */
274
    public function consume($mValue = 1): string
×
275
    {
276
        if (\is_string($mValue)) {
×
277
            $iLineCount = \substr_count($mValue, "\n");
×
278
            $length = $this->strlen($mValue);
×
279
            if (!$this->streql($this->substr($this->currentPosition, $length), $mValue)) {
×
280
                throw new UnexpectedTokenException(
×
281
                    $mValue,
×
282
                    $this->peek(\max($length, 5)),
×
283
                    'literal',
×
284
                    $this->lineNumber
×
285
                );
286
            }
287
            $this->lineNumber += $iLineCount;
×
288
            $this->currentPosition += $this->strlen($mValue);
×
289
            return $mValue;
×
290
        } else {
291
            if ($this->currentPosition + $mValue > $this->length) {
×
292
                throw new UnexpectedEOFException((string) $mValue, $this->peek(5), 'count', $this->lineNumber);
×
293
            }
294
            $result = $this->substr($this->currentPosition, $mValue);
×
295
            $iLineCount = \substr_count($result, "\n");
×
296
            $this->lineNumber += $iLineCount;
×
297
            $this->currentPosition += $mValue;
×
298
            return $result;
×
299
        }
300
    }
301

302
    /**
303
     * @param string $mExpression
304
     * @param int|null $iMaxLength
305
     *
306
     * @throws UnexpectedEOFException
307
     * @throws UnexpectedTokenException
308
     */
309
    public function consumeExpression($mExpression, $iMaxLength = null): string
×
310
    {
311
        $aMatches = null;
×
312
        $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
×
313
        if (\preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
×
314
            return $this->consume($aMatches[0][0]);
×
315
        }
316
        throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->lineNumber);
×
317
    }
318

319
    /**
320
     * @return Comment|false
321
     */
322
    public function consumeComment()
×
323
    {
324
        $mComment = false;
×
325
        if ($this->comes('/*')) {
×
326
            $lineNumber = $this->lineNumber;
×
327
            $this->consume(1);
×
328
            $mComment = '';
×
329
            while (($char = $this->consume(1)) !== '') {
×
330
                $mComment .= $char;
×
331
                if ($this->comes('*/')) {
×
332
                    $this->consume(2);
×
333
                    break;
×
334
                }
335
            }
336
        }
337

338
        if ($mComment !== false) {
×
339
            // We skip the * which was included in the comment.
340
            return new Comment(\substr($mComment, 1), $lineNumber);
×
341
        }
342

343
        return $mComment;
×
344
    }
345

346
    public function isEnd(): bool
×
347
    {
348
        return $this->currentPosition >= $this->length;
×
349
    }
350

351
    /**
352
     * @param array<array-key, string>|string $aEnd
353
     * @param string $bIncludeEnd
354
     * @param string $consumeEnd
355
     * @param array<int, Comment> $comments
356
     *
357
     * @throws UnexpectedEOFException
358
     * @throws UnexpectedTokenException
359
     */
360
    public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []): string
×
361
    {
362
        $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
×
363
        $out = '';
×
364
        $start = $this->currentPosition;
×
365

366
        while (!$this->isEnd()) {
×
367
            $char = $this->consume(1);
×
368
            if (\in_array($char, $aEnd, true)) {
×
369
                if ($bIncludeEnd) {
×
370
                    $out .= $char;
×
371
                } elseif (!$consumeEnd) {
×
372
                    $this->currentPosition -= $this->strlen($char);
×
373
                }
374
                return $out;
×
375
            }
376
            $out .= $char;
×
377
            if ($comment = $this->consumeComment()) {
×
378
                $comments[] = $comment;
×
379
            }
380
        }
381

382
        if (\in_array(self::EOF, $aEnd, true)) {
×
383
            return $out;
×
384
        }
385

386
        $this->currentPosition = $start;
×
387
        throw new UnexpectedEOFException(
×
388
            'One of ("' . \implode('","', $aEnd) . '")',
×
389
            $this->peek(5),
×
390
            'search',
×
391
            $this->lineNumber
×
392
        );
393
    }
394

395
    private function inputLeft(): string
×
396
    {
397
        return $this->substr($this->currentPosition, -1);
×
398
    }
399

400
    /**
401
     * @param string $sString1
402
     * @param string $sString2
403
     * @param bool $bCaseInsensitive
404
     */
405
    public function streql($sString1, $sString2, $bCaseInsensitive = true): bool
×
406
    {
407
        if ($bCaseInsensitive) {
×
408
            return $this->strtolower($sString1) === $this->strtolower($sString2);
×
409
        } else {
410
            return $sString1 === $sString2;
×
411
        }
412
    }
413

414
    /**
415
     * @param int $iAmount
416
     */
417
    public function backtrack($iAmount): void
×
418
    {
419
        $this->currentPosition -= $iAmount;
×
420
    }
×
421

422
    /**
423
     * @param string $sString
424
     */
425
    public function strlen($sString): int
×
426
    {
427
        if ($this->parserSettings->bMultibyteSupport) {
×
428
            return \mb_strlen($sString, $this->charset);
×
429
        } else {
430
            return \strlen($sString);
×
431
        }
432
    }
433

434
    /**
435
     * @param int $iStart
436
     * @param int $length
437
     */
438
    private function substr($iStart, $length): string
×
439
    {
440
        if ($length < 0) {
×
441
            $length = $this->length - $iStart + $length;
×
442
        }
443
        if ($iStart + $length > $this->length) {
×
444
            $length = $this->length - $iStart;
×
445
        }
446
        $result = '';
×
447
        while ($length > 0) {
×
448
            $result .= $this->characters[$iStart];
×
449
            $iStart++;
×
450
            $length--;
×
451
        }
452
        return $result;
×
453
    }
454

455
    /**
456
     * @param string $sString
457
     */
458
    private function strtolower($sString): string
×
459
    {
460
        if ($this->parserSettings->bMultibyteSupport) {
×
461
            return \mb_strtolower($sString, $this->charset);
×
462
        } else {
463
            return \strtolower($sString);
×
464
        }
465
    }
466

467
    /**
468
     * @param string $sString
469
     *
470
     * @return array<int, string>
471
     */
472
    private function strsplit($sString)
×
473
    {
474
        if ($this->parserSettings->bMultibyteSupport) {
×
475
            if ($this->streql($this->charset, 'utf-8')) {
×
476
                return \preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
×
477
            } else {
478
                $length = \mb_strlen($sString, $this->charset);
×
479
                $result = [];
×
480
                for ($i = 0; $i < $length; ++$i) {
×
481
                    $result[] = \mb_substr($sString, $i, 1, $this->charset);
×
482
                }
483
                return $result;
×
484
            }
485
        } else {
486
            if ($sString === '') {
×
487
                return [];
×
488
            } else {
489
                return \str_split($sString);
×
490
            }
491
        }
492
    }
493

494
    /**
495
     * @param string $sString
496
     * @param string $sNeedle
497
     * @param int $iOffset
498
     *
499
     * @return int|false
500
     */
501
    private function strpos($sString, $sNeedle, $iOffset)
×
502
    {
503
        if ($this->parserSettings->bMultibyteSupport) {
×
504
            return \mb_strpos($sString, $sNeedle, $iOffset, $this->charset);
×
505
        } else {
506
            return \strpos($sString, $sNeedle, $iOffset);
×
507
        }
508
    }
509
}
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