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

MyIntervals / PHP-CSS-Parser / 13794840569

11 Mar 2025 05:53PM UTC coverage: 55.567%. Remained the same
13794840569

Pull #1136

github

web-flow
Merge fd0252084 into 632379dc4
Pull Request #1136: [TASK] Use native type declarations in `ParserState`

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

2 existing lines in 1 file now uncovered.

1038 of 1868 relevant lines covered (55.57%)

12.4 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<0, max>
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<1, max> $lineNumber
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<1, max> $lineNumber
55
     */
NEW
56
    public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1)
×
57
    {
58
        $this->parserSettings = $parserSettings;
×
59
        $this->text = $text;
×
60
        $this->lineNumber = $lineNumber;
×
61
        $this->setCharset($this->parserSettings->getDefaultCharset());
×
62
    }
×
63

64
    /**
65
     * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
66
     *
67
     * @throws SourceException if the charset is UTF-8 and the content has invalid byte sequences
68
     */
69
    public function setCharset(string $charset): void
×
70
    {
71
        $this->charset = $charset;
×
72
        $this->characters = $this->strsplit($this->text);
×
73
    }
×
74

75
    /**
76
     * @return int<1, max>
77
     */
NEW
78
    public function currentLine(): int
×
79
    {
80
        return $this->lineNumber;
×
81
    }
82

83
    /**
84
     * @return int<0, max>
85
     */
NEW
86
    public function currentColumn(): int
×
87
    {
88
        return $this->currentPosition;
×
89
    }
90

NEW
91
    public function getSettings(): Settings
×
92
    {
93
        return $this->parserSettings;
×
94
    }
95

96
    public function anchor(): Anchor
×
97
    {
98
        return new Anchor($this->currentPosition, $this);
×
99
    }
100

101
    /**
102
     * @param int<0, max> $position
103
     */
NEW
104
    public function setPosition(int $position): void
×
105
    {
106
        $this->currentPosition = $position;
×
107
    }
×
108

109
    /**
110
     * @throws UnexpectedTokenException
111
     */
NEW
112
    public function parseIdentifier(bool $ignoreCase = true): string
×
113
    {
114
        if ($this->isEnd()) {
×
115
            throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
×
116
        }
117
        $result = $this->parseCharacter(true);
×
118
        if ($result === null) {
×
119
            throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
×
120
        }
121
        $character = null;
×
122
        while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
×
123
            if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character)) {
×
124
                $result .= $character;
×
125
            } else {
126
                $result .= '\\' . $character;
×
127
            }
128
        }
129
        if ($ignoreCase) {
×
130
            $result = $this->strtolower($result);
×
131
        }
132
        return $result;
×
133
    }
134

135
    /**
136
     * @throws UnexpectedEOFException
137
     * @throws UnexpectedTokenException
138
     */
NEW
139
    public function parseCharacter(bool $isForIdentifier): ?string
×
140
    {
141
        if ($this->peek() === '\\') {
×
142
            $this->consume('\\');
×
143
            if ($this->comes('\\n') || $this->comes('\\r')) {
×
144
                return '';
×
145
            }
146
            if (\preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
×
147
                return $this->consume(1);
×
148
            }
149
            $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
×
150
            if ($this->strlen($hexCodePoint) < 6) {
×
151
                // Consume whitespace after incomplete unicode escape
152
                if (\preg_match('/\\s/isSu', $this->peek())) {
×
153
                    if ($this->comes('\\r\\n')) {
×
154
                        $this->consume(2);
×
155
                    } else {
156
                        $this->consume(1);
×
157
                    }
158
                }
159
            }
160
            $codePoint = \intval($hexCodePoint, 16);
×
161
            $utf32EncodedCharacter = '';
×
162
            for ($i = 0; $i < 4; ++$i) {
×
163
                $utf32EncodedCharacter .= \chr($codePoint & 0xff);
×
164
                $codePoint = $codePoint >> 8;
×
165
            }
166
            return \iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
×
167
        }
168
        if ($isForIdentifier) {
×
169
            $peek = \ord($this->peek());
×
170
            // Ranges: a-z A-Z 0-9 - _
171
            if (
172
                ($peek >= 97 && $peek <= 122)
×
173
                || ($peek >= 65 && $peek <= 90)
×
174
                || ($peek >= 48 && $peek <= 57)
×
175
                || ($peek === 45)
×
176
                || ($peek === 95)
×
177
                || ($peek > 0xa1)
×
178
            ) {
179
                return $this->consume(1);
×
180
            }
181
        } else {
182
            return $this->consume(1);
×
183
        }
184
        return null;
×
185
    }
186

187
    /**
188
     * @return array<int, Comment>|void
189
     *
190
     * @throws UnexpectedEOFException
191
     * @throws UnexpectedTokenException
192
     */
193
    public function consumeWhiteSpace(): array
×
194
    {
195
        $comments = [];
×
196
        do {
197
            while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
×
198
                $this->consume(1);
×
199
            }
200
            if ($this->parserSettings->usesLenientParsing()) {
×
201
                try {
202
                    $comment = $this->consumeComment();
×
203
                } catch (UnexpectedEOFException $e) {
×
204
                    $this->currentPosition = \count($this->characters);
×
205
                    return $comments;
×
206
                }
207
            } else {
208
                $comment = $this->consumeComment();
×
209
            }
210
            if ($comment !== false) {
×
211
                $comments[] = $comment;
×
212
            }
213
        } while ($comment !== false);
×
214
        return $comments;
×
215
    }
216

NEW
217
    public function comes(string $string, bool $caseInsensitive = false): bool
×
218
    {
219
        $peek = $this->peek(\strlen($string));
×
220
        return ($peek == '')
×
221
            ? false
×
222
            : $this->streql($peek, $string, $caseInsensitive);
×
223
    }
224

225
    /**
226
     * @param int<1, max> $length
227
     * @param int<0, max> $offset
228
     */
NEW
229
    public function peek(int $length = 1, int $offset = 0): string
×
230
    {
231
        $offset += $this->currentPosition;
×
232
        if ($offset >= \count($this->characters)) {
×
233
            return '';
×
234
        }
235
        return $this->substr($offset, $length);
×
236
    }
237

238
    /**
239
     * @param string|int<1, max> $value
240
     *
241
     * @throws UnexpectedEOFException
242
     * @throws UnexpectedTokenException
243
     */
244
    public function consume($value = 1): string
×
245
    {
246
        if (\is_string($value)) {
×
247
            $numberOfLines = \substr_count($value, "\n");
×
248
            $length = $this->strlen($value);
×
249
            if (!$this->streql($this->substr($this->currentPosition, $length), $value)) {
×
250
                throw new UnexpectedTokenException(
×
251
                    $value,
×
252
                    $this->peek(\max($length, 5)),
×
253
                    'literal',
×
254
                    $this->lineNumber
×
255
                );
256
            }
257
            $this->lineNumber += $numberOfLines;
×
258
            $this->currentPosition += $this->strlen($value);
×
259
            return $value;
×
260
        } else {
261
            if ($this->currentPosition + $value > \count($this->characters)) {
×
262
                throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber);
×
263
            }
264
            $result = $this->substr($this->currentPosition, $value);
×
265
            $numberOfLines = \substr_count($result, "\n");
×
266
            $this->lineNumber += $numberOfLines;
×
267
            $this->currentPosition += $value;
×
268
            return $result;
×
269
        }
270
    }
271

272
    /**
273
     * @param string $expression
274
     * @param int<1, max>|null $maximumLength
275
     *
276
     * @throws UnexpectedEOFException
277
     * @throws UnexpectedTokenException
278
     */
NEW
279
    public function consumeExpression(string $expression, ?int $maximumLength = null): string
×
280
    {
281
        $matches = null;
×
282
        $input = $maximumLength !== null ? $this->peek($maximumLength) : $this->inputLeft();
×
283
        if (\preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) === 1) {
×
284
            return $this->consume($matches[0][0]);
×
285
        }
286
        throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
×
287
    }
288

289
    /**
290
     * @return Comment|false
291
     */
292
    public function consumeComment()
×
293
    {
294
        $comment = false;
×
295
        if ($this->comes('/*')) {
×
296
            $lineNumber = $this->lineNumber;
×
297
            $this->consume(1);
×
298
            $comment = '';
×
299
            while (($char = $this->consume(1)) !== '') {
×
300
                $comment .= $char;
×
301
                if ($this->comes('*/')) {
×
302
                    $this->consume(2);
×
303
                    break;
×
304
                }
305
            }
306
        }
307

308
        if ($comment !== false) {
×
309
            // We skip the * which was included in the comment.
310
            return new Comment(\substr($comment, 1), $lineNumber);
×
311
        }
312

313
        return $comment;
×
314
    }
315

316
    public function isEnd(): bool
×
317
    {
318
        return $this->currentPosition >= \count($this->characters);
×
319
    }
320

321
    /**
322
     * @param array<array-key, string>|string $stopCharacters
323
     * @param array<int, Comment> $comments
324
     *
325
     * @throws UnexpectedEOFException
326
     * @throws UnexpectedTokenException
327
     */
328
    public function consumeUntil(
×
329
        $stopCharacters,
330
        bool $includeEnd = false,
331
        bool $consumeEnd = false,
332
        array &$comments = []
333
    ): string {
334
        $stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters];
×
335
        $consumedCharacters = '';
×
336
        $start = $this->currentPosition;
×
337

338
        while (!$this->isEnd()) {
×
339
            $character = $this->consume(1);
×
340
            if (\in_array($character, $stopCharacters, true)) {
×
341
                if ($includeEnd) {
×
342
                    $consumedCharacters .= $character;
×
343
                } elseif (!$consumeEnd) {
×
344
                    $this->currentPosition -= $this->strlen($character);
×
345
                }
346
                return $consumedCharacters;
×
347
            }
348
            $consumedCharacters .= $character;
×
349
            if ($comment = $this->consumeComment()) {
×
350
                $comments[] = $comment;
×
351
            }
352
        }
353

354
        if (\in_array(self::EOF, $stopCharacters, true)) {
×
355
            return $consumedCharacters;
×
356
        }
357

358
        $this->currentPosition = $start;
×
359
        throw new UnexpectedEOFException(
×
360
            'One of ("' . \implode('","', $stopCharacters) . '")',
×
361
            $this->peek(5),
×
362
            'search',
×
363
            $this->lineNumber
×
364
        );
365
    }
366

367
    private function inputLeft(): string
×
368
    {
369
        return $this->substr($this->currentPosition, -1);
×
370
    }
371

NEW
372
    public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool
×
373
    {
374
        if ($caseInsensitive) {
×
375
            return $this->strtolower($string1) === $this->strtolower($string2);
×
376
        } else {
377
            return $string1 === $string2;
×
378
        }
379
    }
380

381
    /**
382
     * @param int<1, max> $numberOfCharacters
383
     */
NEW
384
    public function backtrack(int $numberOfCharacters): void
×
385
    {
386
        $this->currentPosition -= $numberOfCharacters;
×
387
    }
×
388

389
    /**
390
     * @return int<0, max>
391
     */
NEW
392
    public function strlen(string $string): int
×
393
    {
394
        if ($this->parserSettings->hasMultibyteSupport()) {
×
395
            return \mb_strlen($string, $this->charset);
×
396
        } else {
397
            return \strlen($string);
×
398
        }
399
    }
400

401
    /**
402
     * @param int<0, max> $offset
403
     */
NEW
404
    private function substr(int $offset, int $length): string
×
405
    {
406
        if ($length < 0) {
×
407
            $length = \count($this->characters) - $offset + $length;
×
408
        }
409
        if ($offset + $length > \count($this->characters)) {
×
410
            $length = \count($this->characters) - $offset;
×
411
        }
412
        $result = '';
×
413
        while ($length > 0) {
×
414
            $result .= $this->characters[$offset];
×
415
            $offset++;
×
416
            $length--;
×
417
        }
418
        return $result;
×
419
    }
420

NEW
421
    private function strtolower(string $string): string
×
422
    {
423
        if ($this->parserSettings->hasMultibyteSupport()) {
×
424
            return \mb_strtolower($string, $this->charset);
×
425
        } else {
426
            return \strtolower($string);
×
427
        }
428
    }
429

430
    /**
431
     * @return array<int, string>
432
     *
433
     * @throws SourceException if the charset is UTF-8 and the string contains invalid byte sequences
434
     */
NEW
435
    private function strsplit(string $string): array
×
436
    {
437
        if ($this->parserSettings->hasMultibyteSupport()) {
×
438
            if ($this->streql($this->charset, 'utf-8')) {
×
439
                $result = \preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
×
440
                if (!\is_array($result)) {
×
441
                    throw new SourceException('`preg_split` failed with error ' . \preg_last_error());
×
442
                }
443
                return $result;
×
444
            } else {
445
                $length = \mb_strlen($string, $this->charset);
×
446
                $result = [];
×
447
                for ($i = 0; $i < $length; ++$i) {
×
448
                    $result[] = \mb_substr($string, $i, 1, $this->charset);
×
449
                }
450
                return $result;
×
451
            }
452
        } else {
453
            if ($string === '') {
×
454
                return [];
×
455
            } else {
456
                return \str_split($string);
×
457
            }
458
        }
459
    }
460

461
    /**
462
     * @param int<0, max> $offset
463
     *
464
     * @return int|false
465
     */
NEW
466
    private function strpos(string $haystack, string $needle, int $offset)
×
467
    {
UNCOV
468
        if ($this->parserSettings->hasMultibyteSupport()) {
×
469
            return \mb_strpos($haystack, $needle, $offset, $this->charset);
×
470
        } else {
UNCOV
471
            return \strpos($haystack, $needle, $offset);
×
472
        }
473
    }
474
}
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