PluginProbe
Elementor Website Builder – more than just a page builder / 3.28.0-dev1
Elementor Website Builder – more than just a page builder v3.28.0-dev1
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / vendor_prefixed / twig / src / Lexer.php

Lexer.php in Elementor Website Builder – more than just a page builder 3.28.0-dev1, at vendor_prefixed/twig/src/Lexer.php

437 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of Twig.
5 *
6 * (c) Fabien Potencier
7 * (c) Armin Ronacher
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 */
12 namespace ElementorDeps\Twig;
13
14 use ElementorDeps\Twig\Error\SyntaxError;
15 /**
16 * @author Fabien Potencier <fabien@symfony.com>
17 */
18 class Lexer
19 {
20 private $isInitialized = \false;
21 private $tokens;
22 private $code;
23 private $cursor;
24 private $lineno;
25 private $end;
26 private $state;
27 private $states;
28 private $brackets;
29 private $env;
30 private $source;
31 private $options;
32 private $regexes;
33 private $position;
34 private $positions;
35 private $currentVarBlockLine;
36 public const STATE_DATA = 0;
37 public const STATE_BLOCK = 1;
38 public const STATE_VAR = 2;
39 public const STATE_STRING = 3;
40 public const STATE_INTERPOLATION = 4;
41 public const REGEX_NAME = '/[a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*/A';
42 public const REGEX_NUMBER = '/[0-9]+(?:\\.[0-9]+)?([Ee][\\+\\-][0-9]+)?/A';
43 public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
44 public const REGEX_DQ_STRING_DELIM = '/"/A';
45 public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\\{))[^#"\\\\]*)*/As';
46 public const PUNCTUATION = '()[]{}?:.,|';
47 public function __construct(Environment $env, array $options = [])
48 {
49 $this->env = $env;
50 $this->options = \array_merge(['tag_comment' => ['{#', '#}'], 'tag_block' => ['{%', '%}'], 'tag_variable' => ['{{', '}}'], 'whitespace_trim' => '-', 'whitespace_line_trim' => '~', 'whitespace_line_chars' => ' \\t\\0\\x0B', 'interpolation' => ['#{', '}']], $options);
51 }
52 private function initialize()
53 {
54 if ($this->isInitialized) {
55 return;
56 }
57 // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default
58 $this->regexes = [
59 // }}
60 'lex_var' => '{
61 \\s*
62 (?:' . \preg_quote($this->options['whitespace_trim'] . $this->options['tag_variable'][1], '#') . '\\s*' . '|' . \preg_quote($this->options['whitespace_line_trim'] . $this->options['tag_variable'][1], '#') . '[' . $this->options['whitespace_line_chars'] . ']*' . '|' . \preg_quote($this->options['tag_variable'][1], '#') . ')
63 }Ax',
64 // %}
65 'lex_block' => '{
66 \\s*
67 (?:' . \preg_quote($this->options['whitespace_trim'] . $this->options['tag_block'][1], '#') . '\\s*\\n?' . '|' . \preg_quote($this->options['whitespace_line_trim'] . $this->options['tag_block'][1], '#') . '[' . $this->options['whitespace_line_chars'] . ']*' . '|' . \preg_quote($this->options['tag_block'][1], '#') . '\\n?' . ')
68 }Ax',
69 // {% endverbatim %}
70 'lex_raw_data' => '{' . \preg_quote($this->options['tag_block'][0], '#') . '(' . $this->options['whitespace_trim'] . '|' . $this->options['whitespace_line_trim'] . ')?\\s*endverbatim\\s*' . '(?:' . \preg_quote($this->options['whitespace_trim'] . $this->options['tag_block'][1], '#') . '\\s*' . '|' . \preg_quote($this->options['whitespace_line_trim'] . $this->options['tag_block'][1], '#') . '[' . $this->options['whitespace_line_chars'] . ']*' . '|' . \preg_quote($this->options['tag_block'][1], '#') . ')
71 }sx',
72 'operator' => $this->getOperatorRegex(),
73 // #}
74 'lex_comment' => '{
75 (?:' . \preg_quote($this->options['whitespace_trim'] . $this->options['tag_comment'][1], '#') . '\\s*\\n?' . '|' . \preg_quote($this->options['whitespace_line_trim'] . $this->options['tag_comment'][1], '#') . '[' . $this->options['whitespace_line_chars'] . ']*' . '|' . \preg_quote($this->options['tag_comment'][1], '#') . '\\n?' . ')
76 }sx',
77 // verbatim %}
78 'lex_block_raw' => '{
79 \\s*verbatim\\s*
80 (?:' . \preg_quote($this->options['whitespace_trim'] . $this->options['tag_block'][1], '#') . '\\s*' . '|' . \preg_quote($this->options['whitespace_line_trim'] . $this->options['tag_block'][1], '#') . '[' . $this->options['whitespace_line_chars'] . ']*' . '|' . \preg_quote($this->options['tag_block'][1], '#') . ')
81 }Asx',
82 'lex_block_line' => '{\\s*line\\s+(\\d+)\\s*' . \preg_quote($this->options['tag_block'][1], '#') . '}As',
83 // {{ or {% or {#
84 'lex_tokens_start' => '{
85 (' . \preg_quote($this->options['tag_variable'][0], '#') . '|' . \preg_quote($this->options['tag_block'][0], '#') . '|' . \preg_quote($this->options['tag_comment'][0], '#') . ')(' . \preg_quote($this->options['whitespace_trim'], '#') . '|' . \preg_quote($this->options['whitespace_line_trim'], '#') . ')?
86 }sx',
87 'interpolation_start' => '{' . \preg_quote($this->options['interpolation'][0], '#') . '\\s*}A',
88 'interpolation_end' => '{\\s*' . \preg_quote($this->options['interpolation'][1], '#') . '}A',
89 ];
90 $this->isInitialized = \true;
91 }
92 public function tokenize(Source $source) : TokenStream
93 {
94 $this->initialize();
95 $this->source = $source;
96 $this->code = \str_replace(["\r\n", "\r"], "\n", $source->getCode());
97 $this->cursor = 0;
98 $this->lineno = 1;
99 $this->end = \strlen($this->code);
100 $this->tokens = [];
101 $this->state = self::STATE_DATA;
102 $this->states = [];
103 $this->brackets = [];
104 $this->position = -1;
105 // find all token starts in one go
106 \preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE);
107 $this->positions = $matches;
108 while ($this->cursor < $this->end) {
109 // dispatch to the lexing functions depending
110 // on the current state
111 switch ($this->state) {
112 case self::STATE_DATA:
113 $this->lexData();
114 break;
115 case self::STATE_BLOCK:
116 $this->lexBlock();
117 break;
118 case self::STATE_VAR:
119 $this->lexVar();
120 break;
121 case self::STATE_STRING:
122 $this->lexString();
123 break;
124 case self::STATE_INTERPOLATION:
125 $this->lexInterpolation();
126 break;
127 }
128 }
129 $this->pushToken(
130 /* Token::EOF_TYPE */
131 -1
132 );
133 if (!empty($this->brackets)) {
134 [$expect, $lineno] = \array_pop($this->brackets);
135 throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
136 }
137 return new TokenStream($this->tokens, $this->source);
138 }
139 private function lexData() : void
140 {
141 // if no matches are left we return the rest of the template as simple text token
142 if ($this->position == \count($this->positions[0]) - 1) {
143 $this->pushToken(
144 /* Token::TEXT_TYPE */
145 0,
146 \substr($this->code, $this->cursor)
147 );
148 $this->cursor = $this->end;
149 return;
150 }
151 // Find the first token after the current cursor
152 $position = $this->positions[0][++$this->position];
153 while ($position[1] < $this->cursor) {
154 if ($this->position == \count($this->positions[0]) - 1) {
155 return;
156 }
157 $position = $this->positions[0][++$this->position];
158 }
159 // push the template text first
160 $text = $textContent = \substr($this->code, $this->cursor, $position[1] - $this->cursor);
161 // trim?
162 if (isset($this->positions[2][$this->position][0])) {
163 if ($this->options['whitespace_trim'] === $this->positions[2][$this->position][0]) {
164 // whitespace_trim detected ({%-, {{- or {#-)
165 $text = \rtrim($text);
166 } elseif ($this->options['whitespace_line_trim'] === $this->positions[2][$this->position][0]) {
167 // whitespace_line_trim detected ({%~, {{~ or {#~)
168 // don't trim \r and \n
169 $text = \rtrim($text, " \t\x00\v");
170 }
171 }
172 $this->pushToken(
173 /* Token::TEXT_TYPE */
174 0,
175 $text
176 );
177 $this->moveCursor($textContent . $position[0]);
178 switch ($this->positions[1][$this->position][0]) {
179 case $this->options['tag_comment'][0]:
180 $this->lexComment();
181 break;
182 case $this->options['tag_block'][0]:
183 // raw data?
184 if (\preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
185 $this->moveCursor($match[0]);
186 $this->lexRawData();
187 // {% line \d+ %}
188 } elseif (\preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
189 $this->moveCursor($match[0]);
190 $this->lineno = (int) $match[1];
191 } else {
192 $this->pushToken(
193 /* Token::BLOCK_START_TYPE */
194 1
195 );
196 $this->pushState(self::STATE_BLOCK);
197 $this->currentVarBlockLine = $this->lineno;
198 }
199 break;
200 case $this->options['tag_variable'][0]:
201 $this->pushToken(
202 /* Token::VAR_START_TYPE */
203 2
204 );
205 $this->pushState(self::STATE_VAR);
206 $this->currentVarBlockLine = $this->lineno;
207 break;
208 }
209 }
210 private function lexBlock() : void
211 {
212 if (empty($this->brackets) && \preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
213 $this->pushToken(
214 /* Token::BLOCK_END_TYPE */
215 3
216 );
217 $this->moveCursor($match[0]);
218 $this->popState();
219 } else {
220 $this->lexExpression();
221 }
222 }
223 private function lexVar() : void
224 {
225 if (empty($this->brackets) && \preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
226 $this->pushToken(
227 /* Token::VAR_END_TYPE */
228 4
229 );
230 $this->moveCursor($match[0]);
231 $this->popState();
232 } else {
233 $this->lexExpression();
234 }
235 }
236 private function lexExpression() : void
237 {
238 // whitespace
239 if (\preg_match('/\\s+/A', $this->code, $match, 0, $this->cursor)) {
240 $this->moveCursor($match[0]);
241 if ($this->cursor >= $this->end) {
242 throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
243 }
244 }
245 // spread operator
246 if ('.' === $this->code[$this->cursor] && $this->cursor + 2 < $this->end && '.' === $this->code[$this->cursor + 1] && '.' === $this->code[$this->cursor + 2]) {
247 $this->pushToken(Token::SPREAD_TYPE, '...');
248 $this->moveCursor('...');
249 } elseif ('=' === $this->code[$this->cursor] && $this->cursor + 1 < $this->end && '>' === $this->code[$this->cursor + 1]) {
250 $this->pushToken(Token::ARROW_TYPE, '=>');
251 $this->moveCursor('=>');
252 } elseif (\preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
253 $this->pushToken(
254 /* Token::OPERATOR_TYPE */
255 8,
256 \preg_replace('/\\s+/', ' ', $match[0])
257 );
258 $this->moveCursor($match[0]);
259 } elseif (\preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
260 $this->pushToken(
261 /* Token::NAME_TYPE */
262 5,
263 $match[0]
264 );
265 $this->moveCursor($match[0]);
266 } elseif (\preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
267 $number = (float) $match[0];
268 // floats
269 if (\ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
270 $number = (int) $match[0];
271 // integers lower than the maximum
272 }
273 $this->pushToken(
274 /* Token::NUMBER_TYPE */
275 6,
276 $number
277 );
278 $this->moveCursor($match[0]);
279 } elseif (\str_contains(self::PUNCTUATION, $this->code[$this->cursor])) {
280 // opening bracket
281 if (\str_contains('([{', $this->code[$this->cursor])) {
282 $this->brackets[] = [$this->code[$this->cursor], $this->lineno];
283 } elseif (\str_contains(')]}', $this->code[$this->cursor])) {
284 if (empty($this->brackets)) {
285 throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
286 }
287 [$expect, $lineno] = \array_pop($this->brackets);
288 if ($this->code[$this->cursor] != \strtr($expect, '([{', ')]}')) {
289 throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
290 }
291 }
292 $this->pushToken(
293 /* Token::PUNCTUATION_TYPE */
294 9,
295 $this->code[$this->cursor]
296 );
297 ++$this->cursor;
298 } elseif (\preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
299 $this->pushToken(
300 /* Token::STRING_TYPE */
301 7,
302 \stripcslashes(\substr($match[0], 1, -1))
303 );
304 $this->moveCursor($match[0]);
305 } elseif (\preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
306 $this->brackets[] = ['"', $this->lineno];
307 $this->pushState(self::STATE_STRING);
308 $this->moveCursor($match[0]);
309 } else {
310 throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
311 }
312 }
313 private function lexRawData() : void
314 {
315 if (!\preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
316 throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source);
317 }
318 $text = \substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
319 $this->moveCursor($text . $match[0][0]);
320 // trim?
321 if (isset($match[1][0])) {
322 if ($this->options['whitespace_trim'] === $match[1][0]) {
323 // whitespace_trim detected ({%-, {{- or {#-)
324 $text = \rtrim($text);
325 } else {
326 // whitespace_line_trim detected ({%~, {{~ or {#~)
327 // don't trim \r and \n
328 $text = \rtrim($text, " \t\x00\v");
329 }
330 }
331 $this->pushToken(
332 /* Token::TEXT_TYPE */
333 0,
334 $text
335 );
336 }
337 private function lexComment() : void
338 {
339 if (!\preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
340 throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source);
341 }
342 $this->moveCursor(\substr($this->code, $this->cursor, $match[0][1] - $this->cursor) . $match[0][0]);
343 }
344 private function lexString() : void
345 {
346 if (\preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
347 $this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
348 $this->pushToken(
349 /* Token::INTERPOLATION_START_TYPE */
350 10
351 );
352 $this->moveCursor($match[0]);
353 $this->pushState(self::STATE_INTERPOLATION);
354 } elseif (\preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) {
355 $this->pushToken(
356 /* Token::STRING_TYPE */
357 7,
358 \stripcslashes($match[0])
359 );
360 $this->moveCursor($match[0]);
361 } elseif (\preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
362 [$expect, $lineno] = \array_pop($this->brackets);
363 if ('"' != $this->code[$this->cursor]) {
364 throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
365 }
366 $this->popState();
367 ++$this->cursor;
368 } else {
369 // unlexable
370 throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
371 }
372 }
373 private function lexInterpolation() : void
374 {
375 $bracket = \end($this->brackets);
376 if ($this->options['interpolation'][0] === $bracket[0] && \preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
377 \array_pop($this->brackets);
378 $this->pushToken(
379 /* Token::INTERPOLATION_END_TYPE */
380 11
381 );
382 $this->moveCursor($match[0]);
383 $this->popState();
384 } else {
385 $this->lexExpression();
386 }
387 }
388 private function pushToken($type, $value = '') : void
389 {
390 // do not push empty text tokens
391 if (0 === $type && '' === $value) {
392 return;
393 }
394 $this->tokens[] = new Token($type, $value, $this->lineno);
395 }
396 private function moveCursor($text) : void
397 {
398 $this->cursor += \strlen($text);
399 $this->lineno += \substr_count($text, "\n");
400 }
401 private function getOperatorRegex() : string
402 {
403 $operators = \array_merge(['='], \array_keys($this->env->getUnaryOperators()), \array_keys($this->env->getBinaryOperators()));
404 $operators = \array_combine($operators, \array_map('strlen', $operators));
405 \arsort($operators);
406 $regex = [];
407 foreach ($operators as $operator => $length) {
408 // an operator that ends with a character must be followed by
409 // a whitespace, a parenthesis, an opening map [ or sequence {
410 $r = \preg_quote($operator, '/');
411 if (\ctype_alpha($operator[$length - 1])) {
412 $r .= '(?=[\\s()\\[{])';
413 }
414 // an operator that begins with a character must not have a dot or pipe before
415 if (\ctype_alpha($operator[0])) {
416 $r = '(?<![\\.\\|])' . $r;
417 }
418 // an operator with a space can be any amount of whitespaces
419 $r = \preg_replace('/\\s+/', '\\s+', $r);
420 $regex[] = $r;
421 }
422 return '/' . \implode('|', $regex) . '/A';
423 }
424 private function pushState($state) : void
425 {
426 $this->states[] = $this->state;
427 $this->state = $state;
428 }
429 private function popState() : void
430 {
431 if (0 === \count($this->states)) {
432 throw new \LogicException('Cannot pop state without a previous state.');
433 }
434 $this->state = \array_pop($this->states);
435 }
436 }
437