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

MyIntervals / PHP-CSS-Parser / 13595473812

28 Feb 2025 07:52PM UTC coverage: 55.282%. Remained the same
13595473812

Pull #1038

github

web-flow
Merge 46b15d2b2 into 19ffd076e
Pull Request #1038: [TASK] Drop `getLineNo()` from the `Renderable` interface

1057 of 1912 relevant lines covered (55.28%)

12.2 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->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
77
     */
78
    public function currentLine()
×
79
    {
80
        return $this->lineNumber;
×
81
    }
82

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

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

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

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

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

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

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

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

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

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

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

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

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

335
        return $mComment;
×
336
    }
337

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

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

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

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

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

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

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

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

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

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

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

459
    /**
460
     * @param string $string
461
     *
462
     * @return array<int, string>
463
     *
464
     * @throws SourceException if the charset is UTF-8 and the string contains invalid byte sequences
465
     */
466
    private function strsplit($string)
×
467
    {
468
        if ($this->parserSettings->hasMultibyteSupport()) {
×
469
            if ($this->streql($this->charset, 'utf-8')) {
×
470
                $result = \preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
×
471
                if (!\is_array($result)) {
×
472
                    throw new SourceException('`preg_split` failed with error ' . \preg_last_error());
×
473
                }
474
                return $result;
×
475
            } else {
476
                $length = \mb_strlen($string, $this->charset);
×
477
                $result = [];
×
478
                for ($i = 0; $i < $length; ++$i) {
×
479
                    $result[] = \mb_substr($string, $i, 1, $this->charset);
×
480
                }
481
                return $result;
×
482
            }
483
        } else {
484
            if ($string === '') {
×
485
                return [];
×
486
            } else {
487
                return \str_split($string);
×
488
            }
489
        }
490
    }
491

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