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

MyIntervals / PHP-CSS-Parser / 13403825501

19 Feb 2025 01:56AM UTC coverage: 51.253% (-0.08%) from 51.335%
13403825501

Pull #954

github

web-flow
Merge 521415c86 into d71ff2835
Pull Request #954: [CLEANUP] Don't store array length as a property

0 of 14 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

961 of 1875 relevant lines covered (51.25%)

11.54 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 $lineNumber;
51

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

64
    /**
65
     * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
66
     *
67
     * @throws SourceException
68
     */
NEW
69
    public function setCharset(string $charset): void
×
70
    {
71
        // The charset property must be set for `strsplit()`, but may need to be reverted if it fails.
NEW
72
        $oldCharset = $this->charset;
×
73
        $this->charset = $charset;
×
74

NEW
75
        $newCharacters = $this->strsplit($this->text);
×
NEW
76
        if (!\is_array($newCharacters)) {
×
NEW
77
            $this->charset = $oldCharset;
×
NEW
78
            throw new SourceException('Charset `' . $charset . '` cannot be reconciled with the CSS provided.');
×
79
        }
80

NEW
81
        $this->characters = $newCharacters;
×
UNCOV
82
    }
×
83

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

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

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

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

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

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

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

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

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

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

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

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

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

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

344
        return $mComment;
×
345
    }
346

347
    public function isEnd(): bool
×
348
    {
NEW
349
        return $this->currentPosition >= \count($this->characters);
×
350
    }
351

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

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

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

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

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

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

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

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

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

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

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

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