PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.10.2
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.10.2
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
acymailing / back / Libraries / Sabberworm / CSS / src / Parsing / ParserState.php

ParserState.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 10.10.2, at back/Libraries/Sabberworm/CSS/src/Parsing/ParserState.php

459 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
459