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

MyIntervals / PHP-CSS-Parser / 11602808631

30 Oct 2024 10:43PM UTC coverage: 38.622%. Remained the same
11602808631

push

github

web-flow
[TASK] Create directory structure for making fine-grained tests (#754)

We'll rework and sort the tests into these folders:

- `Unit` and `Functional`: for tests that do not call deprecated methods,
  and where PHPUnit should warn if deprecated methods are called
- `UnitDeprecated` and `FunctionalDeprecated`: for tests that call
  deprecated methods, and where PHPUnit should not warn about those

For the time being, we still call all tests with the same configuration.

779 of 2017 relevant lines covered (38.62%)

5.33 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
     * @internal since 8.5.2
19
     */
20
    public const EOF = null;
21

22
    /**
23
     * @var Settings
24
     */
25
    private $oParserSettings;
26

27
    /**
28
     * @var string
29
     */
30
    private $sText;
31

32
    /**
33
     * @var array<int, string>
34
     */
35
    private $aText;
36

37
    /**
38
     * @var int
39
     */
40
    private $iCurrentPosition;
41

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

49
    /**
50
     * @var int
51
     */
52
    private $iLength;
53

54
    /**
55
     * @var int
56
     */
57
    private $iLineNo;
58

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

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

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

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

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

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

120

121
    public function anchor(): Anchor
×
122
    {
123
        return new Anchor($this->iCurrentPosition, $this);
×
124
    }
125

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

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

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

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

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

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

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

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

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

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

352
        return $mComment;
×
353
    }
354

355
    public function isEnd(): bool
×
356
    {
357
        return $this->iCurrentPosition >= $this->iLength;
×
358
    }
359

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

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

393
        if (\in_array(self::EOF, $aEnd, true)) {
×
394
            return $out;
×
395
        }
396

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

406
    private function inputLeft(): string
×
407
    {
408
        return $this->substr($this->iCurrentPosition, -1);
×
409
    }
410

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

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

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

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

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

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

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