PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 11.1.0
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v11.1.0
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
← All changes | back/Libraries/Sabberworm/CSS/src/Parsing/ParserState.php +458 -0 10.10.1 → 11.1.0 View file →
@@ -1,0 +1,458 @@
1 +<?php
2 +
3 +declare(strict_types=1);
4 +
5 +namespace AcyMailing\Libraries\Sabberworm\CSS\Parsing;
6 +
7 +use AcyMailing\Libraries\Sabberworm\CSS\Comment\Comment;
8 +use AcyMailing\Libraries\Sabberworm\CSS\CSSList\CSSList;
9 +use AcyMailing\Libraries\Sabberworm\CSS\Settings;
10 +
11 +/**
12 + * @internal since 8.7.0
13 + */
14 +class ParserState
15 +{
16 + /**
17 + * @var null
18 + */
19 + public const EOF = null;
20 +
21 + /**
22 + * @var Settings
23 + */
24 + private $parserSettings;
25 +
26 + /**
27 + * @var string
28 + */
29 + private $text;
30 +
31 + /**
32 + * @var array<int, string>
33 + */
34 + private $characters;
35 +
36 + /**
37 + * @var int<0, max>
38 + */
39 + private $currentPosition = 0;
40 +
41 + /**
42 + * will only be used if the CSS does not contain an `@charset` declaration
43 + *
44 + * @var string
45 + */
46 + private $charset;
47 +
48 + /**
49 + * @var int<1, max> $lineNumber
50 + */
51 + private $lineNumber;
52 +
53 + /**
54 + * @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
55 + * @param int<1, max> $lineNumber
56 + */
57 + public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1)
58 + {
59 + $this->parserSettings = $parserSettings;
60 + $this->text = $text;
61 + $this->lineNumber = $lineNumber;
62 + $this->setCharset($this->parserSettings->getDefaultCharset());
63 + }
64 +
65 + /**
66 + * Sets the charset to be used if the CSS does not contain an `@charset` declaration.
67 + */
68 + public function setCharset(string $charset): void
69 + {
70 + $this->charset = $charset;
71 + $this->characters = $this->strsplit($this->text);
72 + }
73 +
74 + /**
75 + * @return int<1, max>
76 + */
77 + public function currentLine(): int
78 + {
79 + return $this->lineNumber;
80 + }
81 +
82 + /**
83 + * @return int<0, max>
84 + */
85 + public function currentColumn(): int
86 + {
87 + return $this->currentPosition;
88 + }
89 +
90 + public function getSettings(): Settings
91 + {
92 + return $this->parserSettings;
93 + }
94 +
95 + public function anchor(): Anchor
96 + {
97 + return new Anchor($this->currentPosition, $this);
98 + }
99 +
100 + /**
101 + * @param int<0, max> $position
102 + */
103 + public function setPosition(int $position): void
104 + {
105 + $this->currentPosition = $position;
106 + }
107 +
108 + /**
109 + * @return non-empty-string
110 + *
111 + * @throws UnexpectedTokenException
112 + */
113 + public function parseIdentifier(bool $ignoreCase = true): string
114 + {
115 + if ($this->isEnd()) {
116 + throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
117 + }
118 + $result = $this->parseCharacter(true);
119 + if ($result === null) {
120 + throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
121 + }
122 + $character = null;
123 + while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
124 + if (CSSList::preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
125 + $result .= $character;
126 + } else {
127 + $result .= '\\' . $character;
128 + }
129 + }
130 + if ($ignoreCase) {
131 + $result = $this->strtolower($result);
132 + }
133 +
134 + return $result;
135 + }
136 +
137 + /**
138 + * @throws UnexpectedEOFException
139 + * @throws UnexpectedTokenException
140 + */
141 + public function parseCharacter(bool $isForIdentifier): ?string
142 + {
143 + if ($this->peek() === '\\') {
144 + $this->consume('\\');
145 + if ($this->comes('\\n') || $this->comes('\\r')) {
146 + return '';
147 + }
148 + if (CSSList::preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
149 + return $this->consume(1);
150 + }
151 + $hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
152 + if ($this->strlen($hexCodePoint) < 6) {
153 + // Consume whitespace after incomplete unicode escape
154 + if (CSSList::preg_match('/\\s/isSu', $this->peek()) !== 0) {
155 + if ($this->comes('\\r\\n')) {
156 + $this->consume(2);
157 + } else {
158 + $this->consume(1);
159 + }
160 + }
161 + }
162 + $codePoint = \intval($hexCodePoint, 16);
163 + $utf32EncodedCharacter = '';
164 + for ($i = 0; $i < 4; ++$i) {
165 + $utf32EncodedCharacter .= \chr($codePoint & 0xff);
166 + $codePoint = $codePoint >> 8;
167 + }
168 + return CSSList::iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
169 + }
170 + if ($isForIdentifier) {
171 + $peek = \ord($this->peek());
172 + // Ranges: a-z A-Z 0-9 - _
173 + if (
174 + ($peek >= 97 && $peek <= 122)
175 + || ($peek >= 65 && $peek <= 90)
176 + || ($peek >= 48 && $peek <= 57)
177 + || ($peek === 45)
178 + || ($peek === 95)
179 + || ($peek > 0xa1)
180 + ) {
181 + return $this->consume(1);
182 + }
183 + } else {
184 + return $this->consume(1);
185 + }
186 +
187 + return null;
188 + }
189 +
190 + /**
191 + * @return list<Comment>
192 + *
193 + * @throws UnexpectedEOFException
194 + * @throws UnexpectedTokenException
195 + */
196 + public function consumeWhiteSpace(): array
197 + {
198 + $comments = [];
199 + do {
200 + while (CSSList::preg_match('/\\s/isSu', $this->peek()) === 1) {
201 + $this->consume(1);
202 + }
203 + if ($this->parserSettings->usesLenientParsing()) {
204 + try {
205 + $comment = $this->consumeComment();
206 + } catch (UnexpectedEOFException $e) {
207 + $this->currentPosition = \count($this->characters);
208 + break;
209 + }
210 + } else {
211 + $comment = $this->consumeComment();
212 + }
213 + if ($comment instanceof Comment) {
214 + $comments[] = $comment;
215 + }
216 + } while ($comment instanceof Comment);
217 +
218 + return $comments;
219 + }
220 +
221 + /**
222 + * @param non-empty-string $string
223 + */
224 + public function comes(string $string, bool $caseInsensitive = false): bool
225 + {
226 + $peek = $this->peek(\strlen($string));
227 +
228 + return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive);
229 + }
230 +
231 + /**
232 + * @param int<1, max> $length
233 + * @param int<0, max> $offset
234 + */
235 + public function peek(int $length = 1, int $offset = 0): string
236 + {
237 + $offset += $this->currentPosition;
238 + if ($offset >= \count($this->characters)) {
239 + return '';
240 + }
241 +
242 + return $this->substr($offset, $length);
243 + }
244 +
245 + /**
246 + * @param string|int<1, max> $value
247 + *
248 + * @throws UnexpectedEOFException
249 + * @throws UnexpectedTokenException
250 + */
251 + public function consume($value = 1): string
252 + {
253 + if (\is_string($value)) {
254 + $numberOfLines = \substr_count($value, "\n");
255 + $length = $this->strlen($value);
256 + if (!$this->streql($this->substr($this->currentPosition, $length), $value)) {
257 + throw new UnexpectedTokenException(
258 + $value,
259 + $this->peek(\max($length, 5)),
260 + 'literal',
261 + $this->lineNumber
262 + );
263 + }
264 +
265 + $this->lineNumber += $numberOfLines;
266 + $this->currentPosition += $this->strlen($value);
267 + $result = $value;
268 + } else {
269 + if ($this->currentPosition + $value > \count($this->characters)) {
270 + throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber);
271 + }
272 +
273 + $result = $this->substr($this->currentPosition, $value);
274 + $numberOfLines = \substr_count($result, "\n");
275 + $this->lineNumber += $numberOfLines;
276 + $this->currentPosition += $value;
277 + }
278 +
279 + return $result;
280 + }
281 +
282 + /**
283 + * @param string $expression
284 + * @param int<1, max>|null $maximumLength
285 + *
286 + * @throws UnexpectedEOFException
287 + * @throws UnexpectedTokenException
288 + */
289 + public function consumeExpression(string $expression, ?int $maximumLength = null): string
290 + {
291 + $matches = null;
292 + $input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
293 + if (CSSList::preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
294 + throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
295 + }
296 +
297 + return $this->consume($matches[0][0]);
298 + }
299 +
300 + /**
301 + * @return Comment|false
302 + */
303 + public function consumeComment()
304 + {
305 + $lineNumber = $this->lineNumber;
306 + $comment = null;
307 +
308 + if ($this->comes('/*')) {
309 + $this->consume(1);
310 + $comment = '';
311 + while (($char = $this->consume(1)) !== '') {
312 + $comment .= $char;
313 + if ($this->comes('*/')) {
314 + $this->consume(2);
315 + break;
316 + }
317 + }
318 + }
319 +
320 + // We skip the * which was included in the comment.
321 + return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false;
322 + }
323 +
324 + public function isEnd(): bool
325 + {
326 + return $this->currentPosition >= \count($this->characters);
327 + }
328 +
329 + /**
330 + * @param list<string|self::EOF>|string|self::EOF $stopCharacters
331 + * @param array<int, Comment> $comments
332 + *
333 + * @throws UnexpectedEOFException
334 + * @throws UnexpectedTokenException
335 + */
336 + public function consumeUntil(
337 + $stopCharacters,
338 + bool $includeEnd = false,
339 + bool $consumeEnd = false,
340 + array &$comments = []
341 + ): string {
342 + $stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters];
343 + $consumedCharacters = '';
344 + $start = $this->currentPosition;
345 +
346 + while (!$this->isEnd()) {
347 + $character = $this->consume(1);
348 + if (\in_array($character, $stopCharacters, true)) {
349 + if ($includeEnd) {
350 + $consumedCharacters .= $character;
351 + } elseif (!$consumeEnd) {
352 + $this->currentPosition -= $this->strlen($character);
353 + }
354 + return $consumedCharacters;
355 + }
356 + $consumedCharacters .= $character;
357 + $comment = $this->consumeComment();
358 + if ($comment instanceof Comment) {
359 + $comments[] = $comment;
360 + }
361 + }
362 +
363 + if (\in_array(self::EOF, $stopCharacters, true)) {
364 + return $consumedCharacters;
365 + }
366 +
367 + $this->currentPosition = $start;
368 + throw new UnexpectedEOFException(
369 + 'One of ("' . \implode('","', $stopCharacters) . '")',
370 + $this->peek(5),
371 + 'search',
372 + $this->lineNumber
373 + );
374 + }
375 +
376 + private function inputLeft(): string
377 + {
378 + return $this->substr($this->currentPosition, -1);
379 + }
380 +
381 + public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool
382 + {
383 + return $caseInsensitive
384 + ? ($this->strtolower($string1) === $this->strtolower($string2))
385 + : ($string1 === $string2);
386 + }
387 +
388 + /**
389 + * @param int<1, max> $numberOfCharacters
390 + */
391 + public function backtrack(int $numberOfCharacters): void
392 + {
393 + $this->currentPosition -= $numberOfCharacters;
394 + }
395 +
396 + /**
397 + * @return int<0, max>
398 + */
399 + public function strlen(string $string): int
400 + {
401 + return $this->parserSettings->hasMultibyteSupport()
402 + ? \mb_strlen($string, $this->charset)
403 + : \strlen($string);
404 + }
405 +
406 + /**
407 + * @param int<0, max> $offset
408 + */
409 + private function substr(int $offset, int $length): string
410 + {
411 + if ($length < 0) {
412 + $length = \count($this->characters) - $offset + $length;
413 + }
414 + if ($offset + $length > \count($this->characters)) {
415 + $length = \count($this->characters) - $offset;
416 + }
417 + $result = '';
418 + while ($length > 0) {
419 + $result .= $this->characters[$offset];
420 + $offset++;
421 + $length--;
422 + }
423 +
424 + return $result;
425 + }
426 +
427 + /**
428 + * @return ($string is non-empty-string ? non-empty-string : string)
429 + */
430 + private function strtolower(string $string): string
431 + {
432 + return $this->parserSettings->hasMultibyteSupport()
433 + ? \mb_strtolower($string, $this->charset)
434 + : \strtolower($string);
435 + }
436 +
437 + /**
438 + * @return list<string>
439 + */
440 + private function strsplit(string $string): array
441 + {
442 + if ($this->parserSettings->hasMultibyteSupport()) {
443 + if ($this->streql($this->charset, 'utf-8')) {
444 + $result = CSSList::preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
445 + } else {
446 + $length = \mb_strlen($string, $this->charset);
447 + $result = [];
448 + for ($i = 0; $i < $length; ++$i) {
449 + $result[] = \mb_substr($string, $i, 1, $this->charset);
450 + }
451 + }
452 + } else {
453 + $result = ($string !== '') ? \str_split($string) : [];
454 + }
455 +
456 + return $result;
457 + }
458 +}