Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace Sabberworm\CSS\Parsing;
4
 
5
use Sabberworm\CSS\Comment\Comment;
6
use Sabberworm\CSS\Settings;
7
 
8
/**
9
 * @internal since 8.7.0
10
 */
11
class ParserState
12
{
13
    /**
14
     * @var null
15
     *
16
     * @internal since 8.5.2
17
     */
18
    const EOF = null;
19
 
20
    /**
21
     * @var Settings
22
     */
23
    private $oParserSettings;
24
 
25
    /**
26
     * @var string
27
     */
28
    private $sText;
29
 
30
    /**
31
     * @var array<int, string>
32
     */
33
    private $aText;
34
 
35
    /**
36
     * @var int
37
     */
38
    private $iCurrentPosition;
39
 
40
    /**
41
     * will only be used if the CSS does not contain an `@charset` declaration
42
     *
43
     * @var string
44
     */
45
    private $sCharset;
46
 
47
    /**
48
     * @var int
49
     */
50
    private $iLength;
51
 
52
    /**
53
     * @var int
54
     */
55
    private $iLineNo;
56
 
57
    /**
58
     * @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
59
     * @param int $iLineNo
60
     */
61
    public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
62
    {
63
        $this->oParserSettings = $oParserSettings;
64
        $this->sText = $sText;
65
        $this->iCurrentPosition = 0;
66
        $this->iLineNo = $iLineNo;
67
        $this->setCharset($this->oParserSettings->sDefaultCharset);
68
    }
69
 
70
    /**
71
     * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
72
     *
73
     * @param string $sCharset
74
     *
75
     * @return void
76
     */
77
    public function setCharset($sCharset)
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
     * @return \Sabberworm\CSS\Parsing\Anchor
122
     */
123
    public function anchor()
124
    {
125
        return new Anchor($this->iCurrentPosition, $this);
126
    }
127
 
128
    /**
129
     * @param int $iPosition
130
     *
131
     * @return void
132
     */
133
    public function setPosition($iPosition)
134
    {
135
        $this->iCurrentPosition = $iPosition;
136
    }
137
 
138
    /**
139
     * @param bool $bIgnoreCase
140
     *
141
     * @return string
142
     *
143
     * @throws UnexpectedTokenException
144
     */
145
    public function parseIdentifier($bIgnoreCase = true)
146
    {
147
        if ($this->isEnd()) {
148
            throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
149
        }
150
        $sResult = $this->parseCharacter(true);
151
        if ($sResult === null) {
152
            throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
153
        }
154
        $sCharacter = null;
155
        while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
156
            if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
157
                $sResult .= $sCharacter;
158
            } else {
159
                $sResult .= '\\' . $sCharacter;
160
            }
161
        }
162
        if ($bIgnoreCase) {
163
            $sResult = $this->strtolower($sResult);
164
        }
165
        return $sResult;
166
    }
167
 
168
    /**
169
     * @param bool $bIsForIdentifier
170
     *
171
     * @return string|null
172
     *
173
     * @throws UnexpectedEOFException
174
     * @throws UnexpectedTokenException
175
     */
176
    public function parseCharacter($bIsForIdentifier)
177
    {
178
        if ($this->peek() === '\\') {
179
            if (
180
                $bIsForIdentifier && $this->oParserSettings->bLenientParsing
181
                && ($this->comes('\0') || $this->comes('\9'))
182
            ) {
183
                // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
184
                return null;
185
            }
186
            $this->consume('\\');
187
            if ($this->comes('\n') || $this->comes('\r')) {
188
                return '';
189
            }
190
            if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
191
                return $this->consume(1);
192
            }
193
            $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
194
            if ($this->strlen($sUnicode) < 6) {
195
                // Consume whitespace after incomplete unicode escape
196
                if (preg_match('/\\s/isSu', $this->peek())) {
197
                    if ($this->comes('\r\n')) {
198
                        $this->consume(2);
199
                    } else {
200
                        $this->consume(1);
201
                    }
202
                }
203
            }
204
            $iUnicode = intval($sUnicode, 16);
205
            $sUtf32 = "";
206
            for ($i = 0; $i < 4; ++$i) {
207
                $sUtf32 .= chr($iUnicode & 0xff);
208
                $iUnicode = $iUnicode >> 8;
209
            }
210
            return iconv('utf-32le', $this->sCharset, $sUtf32);
211
        }
212
        if ($bIsForIdentifier) {
213
            $peek = ord($this->peek());
214
            // Ranges: a-z A-Z 0-9 - _
215
            if (
216
                ($peek >= 97 && $peek <= 122)
217
                || ($peek >= 65 && $peek <= 90)
218
                || ($peek >= 48 && $peek <= 57)
219
                || ($peek === 45)
220
                || ($peek === 95)
221
                || ($peek > 0xa1)
222
            ) {
223
                return $this->consume(1);
224
            }
225
        } else {
226
            return $this->consume(1);
227
        }
228
        return null;
229
    }
230
 
231
    /**
232
     * @return array<int, Comment>|void
233
     *
234
     * @throws UnexpectedEOFException
235
     * @throws UnexpectedTokenException
236
     */
237
    public function consumeWhiteSpace()
238
    {
239
        $aComments = [];
240
        do {
241
            while (preg_match('/\\s/isSu', $this->peek()) === 1) {
242
                $this->consume(1);
243
            }
244
            if ($this->oParserSettings->bLenientParsing) {
245
                try {
246
                    $oComment = $this->consumeComment();
247
                } catch (UnexpectedEOFException $e) {
248
                    $this->iCurrentPosition = $this->iLength;
249
                    return $aComments;
250
                }
251
            } else {
252
                $oComment = $this->consumeComment();
253
            }
254
            if ($oComment !== false) {
255
                $aComments[] = $oComment;
256
            }
257
        } while ($oComment !== false);
258
        return $aComments;
259
    }
260
 
261
    /**
262
     * @param string $sString
263
     * @param bool $bCaseInsensitive
264
     *
265
     * @return bool
266
     */
267
    public function comes($sString, $bCaseInsensitive = false)
268
    {
269
        $sPeek = $this->peek(strlen($sString));
270
        return ($sPeek == '')
271
            ? false
272
            : $this->streql($sPeek, $sString, $bCaseInsensitive);
273
    }
274
 
275
    /**
276
     * @param int $iLength
277
     * @param int $iOffset
278
     *
279
     * @return string
280
     */
281
    public function peek($iLength = 1, $iOffset = 0)
282
    {
283
        $iOffset += $this->iCurrentPosition;
284
        if ($iOffset >= $this->iLength) {
285
            return '';
286
        }
287
        return $this->substr($iOffset, $iLength);
288
    }
289
 
290
    /**
291
     * @param int $mValue
292
     *
293
     * @return string
294
     *
295
     * @throws UnexpectedEOFException
296
     * @throws UnexpectedTokenException
297
     */
298
    public function consume($mValue = 1)
299
    {
300
        if (is_string($mValue)) {
301
            $iLineCount = substr_count($mValue, "\n");
302
            $iLength = $this->strlen($mValue);
303
            if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
304
                throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
305
            }
306
            $this->iLineNo += $iLineCount;
307
            $this->iCurrentPosition += $this->strlen($mValue);
308
            return $mValue;
309
        } else {
310
            if ($this->iCurrentPosition + $mValue > $this->iLength) {
311
                throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
312
            }
313
            $sResult = $this->substr($this->iCurrentPosition, $mValue);
314
            $iLineCount = substr_count($sResult, "\n");
315
            $this->iLineNo += $iLineCount;
316
            $this->iCurrentPosition += $mValue;
317
            return $sResult;
318
        }
319
    }
320
 
321
    /**
322
     * @param string $mExpression
323
     * @param int|null $iMaxLength
324
     *
325
     * @return string
326
     *
327
     * @throws UnexpectedEOFException
328
     * @throws UnexpectedTokenException
329
     */
330
    public function consumeExpression($mExpression, $iMaxLength = null)
331
    {
332
        $aMatches = null;
333
        $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
334
        if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
335
            return $this->consume($aMatches[0][0]);
336
        }
337
        throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
338
    }
339
 
340
    /**
341
     * @return Comment|false
342
     */
343
    public function consumeComment()
344
    {
345
        $mComment = false;
346
        if ($this->comes('/*')) {
347
            $iLineNo = $this->iLineNo;
348
            $this->consume(1);
349
            $mComment = '';
350
            while (($char = $this->consume(1)) !== '') {
351
                $mComment .= $char;
352
                if ($this->comes('*/')) {
353
                    $this->consume(2);
354
                    break;
355
                }
356
            }
357
        }
358
 
359
        if ($mComment !== false) {
360
            // We skip the * which was included in the comment.
361
            return new Comment(substr($mComment, 1), $iLineNo);
362
        }
363
 
364
        return $mComment;
365
    }
366
 
367
    /**
368
     * @return bool
369
     */
370
    public function isEnd()
371
    {
372
        return $this->iCurrentPosition >= $this->iLength;
373
    }
374
 
375
    /**
376
     * @param array<array-key, string>|string $aEnd
377
     * @param string $bIncludeEnd
378
     * @param string $consumeEnd
379
     * @param array<int, Comment> $comments
380
     *
381
     * @return string
382
     *
383
     * @throws UnexpectedEOFException
384
     * @throws UnexpectedTokenException
385
     */
386
    public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
387
    {
388
        $aEnd = is_array($aEnd) ? $aEnd : [$aEnd];
389
        $out = '';
390
        $start = $this->iCurrentPosition;
391
 
392
        while (!$this->isEnd()) {
393
            $char = $this->consume(1);
394
            if (in_array($char, $aEnd)) {
395
                if ($bIncludeEnd) {
396
                    $out .= $char;
397
                } elseif (!$consumeEnd) {
398
                    $this->iCurrentPosition -= $this->strlen($char);
399
                }
400
                return $out;
401
            }
402
            $out .= $char;
403
            if ($comment = $this->consumeComment()) {
404
                $comments[] = $comment;
405
            }
406
        }
407
 
408
        if (in_array(self::EOF, $aEnd)) {
409
            return $out;
410
        }
411
 
412
        $this->iCurrentPosition = $start;
413
        throw new UnexpectedEOFException(
414
            'One of ("' . implode('","', $aEnd) . '")',
415
            $this->peek(5),
416
            'search',
417
            $this->iLineNo
418
        );
419
    }
420
 
421
    /**
422
     * @return string
423
     */
424
    private function inputLeft()
425
    {
426
        return $this->substr($this->iCurrentPosition, -1);
427
    }
428
 
429
    /**
430
     * @param string $sString1
431
     * @param string $sString2
432
     * @param bool $bCaseInsensitive
433
     *
434
     * @return bool
435
     */
436
    public function streql($sString1, $sString2, $bCaseInsensitive = true)
437
    {
438
        if ($bCaseInsensitive) {
439
            return $this->strtolower($sString1) === $this->strtolower($sString2);
440
        } else {
441
            return $sString1 === $sString2;
442
        }
443
    }
444
 
445
    /**
446
     * @param int $iAmount
447
     *
448
     * @return void
449
     */
450
    public function backtrack($iAmount)
451
    {
452
        $this->iCurrentPosition -= $iAmount;
453
    }
454
 
455
    /**
456
     * @param string $sString
457
     *
458
     * @return int
459
     */
460
    public function strlen($sString)
461
    {
462
        if ($this->oParserSettings->bMultibyteSupport) {
463
            return mb_strlen($sString, $this->sCharset);
464
        } else {
465
            return strlen($sString);
466
        }
467
    }
468
 
469
    /**
470
     * @param int $iStart
471
     * @param int $iLength
472
     *
473
     * @return string
474
     */
475
    private function substr($iStart, $iLength)
476
    {
477
        if ($iLength < 0) {
478
            $iLength = $this->iLength - $iStart + $iLength;
479
        }
480
        if ($iStart + $iLength > $this->iLength) {
481
            $iLength = $this->iLength - $iStart;
482
        }
483
        $sResult = '';
484
        while ($iLength > 0) {
485
            $sResult .= $this->aText[$iStart];
486
            $iStart++;
487
            $iLength--;
488
        }
489
        return $sResult;
490
    }
491
 
492
    /**
493
     * @param string $sString
494
     *
495
     * @return string
496
     */
497
    private function strtolower($sString)
498
    {
499
        if ($this->oParserSettings->bMultibyteSupport) {
500
            return mb_strtolower($sString, $this->sCharset);
501
        } else {
502
            return strtolower($sString);
503
        }
504
    }
505
 
506
    /**
507
     * @param string $sString
508
     *
509
     * @return array<int, string>
510
     */
511
    private function strsplit($sString)
512
    {
513
        if ($this->oParserSettings->bMultibyteSupport) {
514
            if ($this->streql($this->sCharset, 'utf-8')) {
515
                return preg_split('//u', $sString, -1, PREG_SPLIT_NO_EMPTY);
516
            } else {
517
                $iLength = mb_strlen($sString, $this->sCharset);
518
                $aResult = [];
519
                for ($i = 0; $i < $iLength; ++$i) {
520
                    $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
521
                }
522
                return $aResult;
523
            }
524
        } else {
525
            if ($sString === '') {
526
                return [];
527
            } else {
528
                return str_split($sString);
529
            }
530
        }
531
    }
532
 
533
    /**
534
     * @param string $sString
535
     * @param string $sNeedle
536
     * @param int $iOffset
537
     *
538
     * @return int|false
539
     */
540
    private function strpos($sString, $sNeedle, $iOffset)
541
    {
542
        if ($this->oParserSettings->bMultibyteSupport) {
543
            return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
544
        } else {
545
            return strpos($sString, $sNeedle, $iOffset);
546
        }
547
    }
548
}