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

MyIntervals / PHP-CSS-Parser / 10719941194

05 Sep 2024 12:03PM UTC coverage: 38.652%. Remained the same
10719941194

Pull #708

github

web-flow
Merge d0f47c8ee into e6632ae5c
Pull Request #708: Update phpstan/extension-installer requirement from ^1.4.2 to ^1.4.3

780 of 2018 relevant lines covered (38.65%)

5.3 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
class ParserState
11
{
12
    /**
13
     * @var null
14
     *
15
     * @internal
16
     */
17
    public const EOF = null;
18

19
    /**
20
     * @var Settings
21
     */
22
    private $oParserSettings;
23

24
    /**
25
     * @var string
26
     */
27
    private $sText;
28

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

34
    /**
35
     * @var int
36
     */
37
    private $iCurrentPosition;
38

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

46
    /**
47
     * @var int
48
     */
49
    private $iLength;
50

51
    /**
52
     * @var int
53
     */
54
    private $iLineNo;
55

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

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

83
    /**
84
     * Returns the charset that is used if the CSS does not contain an `@charset` declaration.
85
     *
86
     * @return string
87
     */
88
    public function getCharset()
×
89
    {
90
        return $this->sCharset;
×
91
    }
92

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

101
    /**
102
     * @return int
103
     */
104
    public function currentColumn()
×
105
    {
106
        return $this->iCurrentPosition;
×
107
    }
108

109
    /**
110
     * @return Settings
111
     */
112
    public function getSettings()
×
113
    {
114
        return $this->oParserSettings;
×
115
    }
116

117

118
    public function anchor(): Anchor
×
119
    {
120
        return new Anchor($this->iCurrentPosition, $this);
×
121
    }
122

123
    /**
124
     * @param int $iPosition
125
     */
126
    public function setPosition($iPosition): void
×
127
    {
128
        $this->iCurrentPosition = $iPosition;
×
129
    }
×
130

131
    /**
132
     * @param bool $bIgnoreCase
133
     *
134
     * @return string
135
     *
136
     * @throws UnexpectedTokenException
137
     */
138
    public function parseIdentifier($bIgnoreCase = true)
×
139
    {
140
        if ($this->isEnd()) {
×
141
            throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
×
142
        }
143
        $sResult = $this->parseCharacter(true);
×
144
        if ($sResult === null) {
×
145
            throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
×
146
        }
147
        $sCharacter = null;
×
148
        while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
×
149
            if (\preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $sCharacter)) {
×
150
                $sResult .= $sCharacter;
×
151
            } else {
152
                $sResult .= '\\' . $sCharacter;
×
153
            }
154
        }
155
        if ($bIgnoreCase) {
×
156
            $sResult = $this->strtolower($sResult);
×
157
        }
158
        return $sResult;
×
159
    }
160

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

224
    /**
225
     * @return array<int, Comment>|void
226
     *
227
     * @throws UnexpectedEOFException
228
     * @throws UnexpectedTokenException
229
     */
230
    public function consumeWhiteSpace(): array
×
231
    {
232
        $aComments = [];
×
233
        do {
234
            while (\preg_match('/\\s/isSu', $this->peek()) === 1) {
×
235
                $this->consume(1);
×
236
            }
237
            if ($this->oParserSettings->bLenientParsing) {
×
238
                try {
239
                    $oComment = $this->consumeComment();
×
240
                } catch (UnexpectedEOFException $e) {
×
241
                    $this->iCurrentPosition = $this->iLength;
×
242
                    return $aComments;
×
243
                }
244
            } else {
245
                $oComment = $this->consumeComment();
×
246
            }
247
            if ($oComment !== false) {
×
248
                $aComments[] = $oComment;
×
249
            }
250
        } while ($oComment !== false);
×
251
        return $aComments;
×
252
    }
253

254
    /**
255
     * @param string $sString
256
     * @param bool $bCaseInsensitive
257
     */
258
    public function comes($sString, $bCaseInsensitive = false): bool
×
259
    {
260
        $sPeek = $this->peek(\strlen($sString));
×
261
        return ($sPeek == '')
×
262
            ? false
×
263
            : $this->streql($sPeek, $sString, $bCaseInsensitive);
×
264
    }
265

266
    /**
267
     * @param int $iLength
268
     * @param int $iOffset
269
     */
270
    public function peek($iLength = 1, $iOffset = 0): string
×
271
    {
272
        $iOffset += $this->iCurrentPosition;
×
273
        if ($iOffset >= $this->iLength) {
×
274
            return '';
×
275
        }
276
        return $this->substr($iOffset, $iLength);
×
277
    }
278

279
    /**
280
     * @param int $mValue
281
     *
282
     * @throws UnexpectedEOFException
283
     * @throws UnexpectedTokenException
284
     */
285
    public function consume($mValue = 1): string
×
286
    {
287
        if (\is_string($mValue)) {
×
288
            $iLineCount = \substr_count($mValue, "\n");
×
289
            $iLength = $this->strlen($mValue);
×
290
            if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
×
291
                throw new UnexpectedTokenException($mValue, $this->peek(\max($iLength, 5)), $this->iLineNo);
×
292
            }
293
            $this->iLineNo += $iLineCount;
×
294
            $this->iCurrentPosition += $this->strlen($mValue);
×
295
            return $mValue;
×
296
        } else {
297
            if ($this->iCurrentPosition + $mValue > $this->iLength) {
×
298
                throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
×
299
            }
300
            $sResult = $this->substr($this->iCurrentPosition, $mValue);
×
301
            $iLineCount = \substr_count($sResult, "\n");
×
302
            $this->iLineNo += $iLineCount;
×
303
            $this->iCurrentPosition += $mValue;
×
304
            return $sResult;
×
305
        }
306
    }
307

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

325
    /**
326
     * @return Comment|false
327
     */
328
    public function consumeComment()
×
329
    {
330
        $mComment = false;
×
331
        if ($this->comes('/*')) {
×
332
            $iLineNo = $this->iLineNo;
×
333
            $this->consume(1);
×
334
            $mComment = '';
×
335
            while (($char = $this->consume(1)) !== '') {
×
336
                $mComment .= $char;
×
337
                if ($this->comes('*/')) {
×
338
                    $this->consume(2);
×
339
                    break;
×
340
                }
341
            }
342
        }
343

344
        if ($mComment !== false) {
×
345
            // We skip the * which was included in the comment.
346
            return new Comment(\substr($mComment, 1), $iLineNo);
×
347
        }
348

349
        return $mComment;
×
350
    }
351

352
    public function isEnd(): bool
×
353
    {
354
        return $this->iCurrentPosition >= $this->iLength;
×
355
    }
356

357
    /**
358
     * @param array<array-key, string>|string $aEnd
359
     * @param string $bIncludeEnd
360
     * @param string $consumeEnd
361
     * @param array<int, Comment> $comments
362
     *
363
     * @return string
364
     *
365
     * @throws UnexpectedEOFException
366
     * @throws UnexpectedTokenException
367
     */
368
    public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
×
369
    {
370
        $aEnd = \is_array($aEnd) ? $aEnd : [$aEnd];
×
371
        $out = '';
×
372
        $start = $this->iCurrentPosition;
×
373

374
        while (!$this->isEnd()) {
×
375
            $char = $this->consume(1);
×
376
            if (\in_array($char, $aEnd, true)) {
×
377
                if ($bIncludeEnd) {
×
378
                    $out .= $char;
×
379
                } elseif (!$consumeEnd) {
×
380
                    $this->iCurrentPosition -= $this->strlen($char);
×
381
                }
382
                return $out;
×
383
            }
384
            $out .= $char;
×
385
            if ($comment = $this->consumeComment()) {
×
386
                $comments[] = $comment;
×
387
            }
388
        }
389

390
        if (\in_array(self::EOF, $aEnd, true)) {
×
391
            return $out;
×
392
        }
393

394
        $this->iCurrentPosition = $start;
×
395
        throw new UnexpectedEOFException(
×
396
            'One of ("' . \implode('","', $aEnd) . '")',
×
397
            $this->peek(5),
×
398
            'search',
×
399
            $this->iLineNo
×
400
        );
401
    }
402

403
    private function inputLeft(): string
×
404
    {
405
        return $this->substr($this->iCurrentPosition, -1);
×
406
    }
407

408
    /**
409
     * @param string $sString1
410
     * @param string $sString2
411
     * @param bool $bCaseInsensitive
412
     */
413
    public function streql($sString1, $sString2, $bCaseInsensitive = true): bool
×
414
    {
415
        if ($bCaseInsensitive) {
×
416
            return $this->strtolower($sString1) === $this->strtolower($sString2);
×
417
        } else {
418
            return $sString1 === $sString2;
×
419
        }
420
    }
421

422
    /**
423
     * @param int $iAmount
424
     */
425
    public function backtrack($iAmount): void
×
426
    {
427
        $this->iCurrentPosition -= $iAmount;
×
428
    }
×
429

430
    /**
431
     * @param string $sString
432
     */
433
    public function strlen($sString): int
×
434
    {
435
        if ($this->oParserSettings->bMultibyteSupport) {
×
436
            return \mb_strlen($sString, $this->sCharset);
×
437
        } else {
438
            return \strlen($sString);
×
439
        }
440
    }
441

442
    /**
443
     * @param int $iStart
444
     * @param int $iLength
445
     */
446
    private function substr($iStart, $iLength): string
×
447
    {
448
        if ($iLength < 0) {
×
449
            $iLength = $this->iLength - $iStart + $iLength;
×
450
        }
451
        if ($iStart + $iLength > $this->iLength) {
×
452
            $iLength = $this->iLength - $iStart;
×
453
        }
454
        $sResult = '';
×
455
        while ($iLength > 0) {
×
456
            $sResult .= $this->aText[$iStart];
×
457
            $iStart++;
×
458
            $iLength--;
×
459
        }
460
        return $sResult;
×
461
    }
462

463
    /**
464
     * @param string $sString
465
     */
466
    private function strtolower($sString): string
×
467
    {
468
        if ($this->oParserSettings->bMultibyteSupport) {
×
469
            return \mb_strtolower($sString, $this->sCharset);
×
470
        } else {
471
            return \strtolower($sString);
×
472
        }
473
    }
474

475
    /**
476
     * @param string $sString
477
     *
478
     * @return array<int, string>
479
     */
480
    private function strsplit($sString)
×
481
    {
482
        if ($this->oParserSettings->bMultibyteSupport) {
×
483
            if ($this->streql($this->sCharset, 'utf-8')) {
×
484
                return \preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
×
485
            } else {
486
                $iLength = \mb_strlen($sString, $this->sCharset);
×
487
                $aResult = [];
×
488
                for ($i = 0; $i < $iLength; ++$i) {
×
489
                    $aResult[] = \mb_substr($sString, $i, 1, $this->sCharset);
×
490
                }
491
                return $aResult;
×
492
            }
493
        } else {
494
            if ($sString === '') {
×
495
                return [];
×
496
            } else {
497
                return \str_split($sString);
×
498
            }
499
        }
500
    }
501

502
    /**
503
     * @param string $sString
504
     * @param string $sNeedle
505
     * @param int $iOffset
506
     *
507
     * @return int|false
508
     */
509
    private function strpos($sString, $sNeedle, $iOffset)
×
510
    {
511
        if ($this->oParserSettings->bMultibyteSupport) {
×
512
            return \mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
×
513
        } else {
514
            return \strpos($sString, $sNeedle, $iOffset);
×
515
        }
516
    }
517
}
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