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

MyIntervals / PHP-CSS-Parser / 13403586897

19 Feb 2025 01:33AM UTC coverage: 51.335%. Remained the same
13403586897

Pull #954

github

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

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

94 existing lines in 1 file now uncovered.

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 $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
     */
NEW
UNCOV
67
    public function setCharset(string $charset): void
×
68
    {
69
        $this->charset = $charset;
×
UNCOV
70
        $this->characters = $this->strsplit($this->text);
×
NEW
71
        if (!\is_array($this->characters)) {
×
NEW
72
            throw new SourceException('Charset `' . $charset . '` cannot be reconciled with the CSS provided.');
×
73
        }
74
    }
×
75

76
    /**
77
     * @return int
78
     */
UNCOV
79
    public function currentLine()
×
80
    {
UNCOV
81
        return $this->lineNumber;
×
82
    }
83

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

92
    /**
93
     * @return Settings
94
     */
UNCOV
95
    public function getSettings()
×
96
    {
UNCOV
97
        return $this->parserSettings;
×
98
    }
99

UNCOV
100
    public function anchor(): Anchor
×
101
    {
UNCOV
102
        return new Anchor($this->currentPosition, $this);
×
103
    }
104

105
    /**
106
     * @param int $position
107
     */
UNCOV
108
    public function setPosition($position): void
×
109
    {
UNCOV
110
        $this->currentPosition = $position;
×
111
    }
×
112

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

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

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

236
    /**
237
     * @param string $sString
238
     * @param bool $bCaseInsensitive
239
     */
UNCOV
240
    public function comes($sString, $bCaseInsensitive = false): bool
×
241
    {
UNCOV
242
        $sPeek = $this->peek(\strlen($sString));
×
243
        return ($sPeek == '')
×
244
            ? false
×
245
            : $this->streql($sPeek, $sString, $bCaseInsensitive);
×
246
    }
247

248
    /**
249
     * @param int $length
250
     * @param int $iOffset
251
     */
UNCOV
252
    public function peek($length = 1, $iOffset = 0): string
×
253
    {
254
        $iOffset += $this->currentPosition;
×
NEW
255
        if ($iOffset >= \count($this->characters)) {
×
256
            return '';
×
257
        }
UNCOV
258
        return $this->substr($iOffset, $length);
×
259
    }
260

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

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

312
    /**
313
     * @return Comment|false
314
     */
UNCOV
315
    public function consumeComment()
×
316
    {
UNCOV
317
        $mComment = false;
×
318
        if ($this->comes('/*')) {
×
319
            $lineNumber = $this->lineNumber;
×
320
            $this->consume(1);
×
321
            $mComment = '';
×
322
            while (($char = $this->consume(1)) !== '') {
×
323
                $mComment .= $char;
×
324
                if ($this->comes('*/')) {
×
325
                    $this->consume(2);
×
326
                    break;
×
327
                }
328
            }
329
        }
330

UNCOV
331
        if ($mComment !== false) {
×
332
            // We skip the * which was included in the comment.
UNCOV
333
            return new Comment(\substr($mComment, 1), $lineNumber);
×
334
        }
335

UNCOV
336
        return $mComment;
×
337
    }
338

UNCOV
339
    public function isEnd(): bool
×
340
    {
NEW
UNCOV
341
        return $this->currentPosition >= \count($this->characters);
×
342
    }
343

344
    /**
345
     * @param array<array-key, string>|string $aEnd
346
     * @param string $bIncludeEnd
347
     * @param string $consumeEnd
348
     * @param array<int, Comment> $comments
349
     *
350
     * @throws UnexpectedEOFException
351
     * @throws UnexpectedTokenException
352
     */
UNCOV
353
    public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = []): string
×
354
    {
UNCOV
355
        $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
×
356
        $out = '';
×
357
        $start = $this->currentPosition;
×
358

UNCOV
359
        while (!$this->isEnd()) {
×
360
            $char = $this->consume(1);
×
361
            if (\in_array($char, $aEnd, true)) {
×
362
                if ($bIncludeEnd) {
×
363
                    $out .= $char;
×
364
                } elseif (!$consumeEnd) {
×
365
                    $this->currentPosition -= $this->strlen($char);
×
366
                }
UNCOV
367
                return $out;
×
368
            }
UNCOV
369
            $out .= $char;
×
370
            if ($comment = $this->consumeComment()) {
×
371
                $comments[] = $comment;
×
372
            }
373
        }
374

UNCOV
375
        if (\in_array(self::EOF, $aEnd, true)) {
×
376
            return $out;
×
377
        }
378

UNCOV
379
        $this->currentPosition = $start;
×
380
        throw new UnexpectedEOFException(
×
381
            'One of ("' . \implode('","', $aEnd) . '")',
×
382
            $this->peek(5),
×
383
            'search',
×
384
            $this->lineNumber
×
385
        );
386
    }
387

UNCOV
388
    private function inputLeft(): string
×
389
    {
UNCOV
390
        return $this->substr($this->currentPosition, -1);
×
391
    }
392

393
    /**
394
     * @param string $sString1
395
     * @param string $sString2
396
     * @param bool $bCaseInsensitive
397
     */
UNCOV
398
    public function streql($sString1, $sString2, $bCaseInsensitive = true): bool
×
399
    {
UNCOV
400
        if ($bCaseInsensitive) {
×
401
            return $this->strtolower($sString1) === $this->strtolower($sString2);
×
402
        } else {
UNCOV
403
            return $sString1 === $sString2;
×
404
        }
405
    }
406

407
    /**
408
     * @param int $iAmount
409
     */
UNCOV
410
    public function backtrack($iAmount): void
×
411
    {
UNCOV
412
        $this->currentPosition -= $iAmount;
×
413
    }
×
414

415
    /**
416
     * @param string $sString
417
     */
UNCOV
418
    public function strlen($sString): int
×
419
    {
UNCOV
420
        if ($this->parserSettings->bMultibyteSupport) {
×
421
            return \mb_strlen($sString, $this->charset);
×
422
        } else {
UNCOV
423
            return \strlen($sString);
×
424
        }
425
    }
426

427
    /**
428
     * @param int $iStart
429
     * @param int $length
430
     */
UNCOV
431
    private function substr($iStart, $length): string
×
432
    {
433
        if ($length < 0) {
×
NEW
434
            $length = \count($this->characters) - $iStart + $length;
×
435
        }
NEW
436
        if ($iStart + $length > \count($this->characters)) {
×
NEW
437
            $length = \count($this->characters) - $iStart;
×
438
        }
UNCOV
439
        $result = '';
×
440
        while ($length > 0) {
×
441
            $result .= $this->characters[$iStart];
×
442
            $iStart++;
×
443
            $length--;
×
444
        }
UNCOV
445
        return $result;
×
446
    }
447

448
    /**
449
     * @param string $sString
450
     */
UNCOV
451
    private function strtolower($sString): string
×
452
    {
UNCOV
453
        if ($this->parserSettings->bMultibyteSupport) {
×
454
            return \mb_strtolower($sString, $this->charset);
×
455
        } else {
UNCOV
456
            return \strtolower($sString);
×
457
        }
458
    }
459

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

487
    /**
488
     * @param string $sString
489
     * @param string $sNeedle
490
     * @param int $iOffset
491
     *
492
     * @return int|false
493
     */
UNCOV
494
    private function strpos($sString, $sNeedle, $iOffset)
×
495
    {
UNCOV
496
        if ($this->parserSettings->bMultibyteSupport) {
×
497
            return \mb_strpos($sString, $sNeedle, $iOffset, $this->charset);
×
498
        } else {
UNCOV
499
            return \strpos($sString, $sNeedle, $iOffset);
×
500
        }
501
    }
502
}
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