Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Lexer.php

Zuletzt modifiziert: 01.05.2026, 11:26 - Dateigröße: 19.19 KiB


 001  <?php
 002   
 003  /*
 004   * This file is part of Twig.
 005   *
 006   * (c) Fabien Potencier
 007   * (c) Armin Ronacher
 008   *
 009   * For the full copyright and license information, please view the LICENSE
 010   * file that was distributed with this source code.
 011   */
 012   
 013  namespace Twig;
 014   
 015  use Twig\Error\SyntaxError;
 016   
 017  /**
 018   * Lexes a template string.
 019   *
 020   * @author Fabien Potencier <fabien@symfony.com>
 021   */
 022  class Lexer
 023  {
 024      private $isInitialized = false;
 025   
 026      private $tokens;
 027      private $code;
 028      private $cursor;
 029      private $lineno;
 030      private $end;
 031      private $state;
 032      private $states;
 033      private $brackets;
 034      private $env;
 035      private $source;
 036      private $options;
 037      private $regexes;
 038      private $position;
 039      private $positions;
 040      private $currentVarBlockLine;
 041   
 042      public const STATE_DATA = 0;
 043      public const STATE_BLOCK = 1;
 044      public const STATE_VAR = 2;
 045      public const STATE_STRING = 3;
 046      public const STATE_INTERPOLATION = 4;
 047   
 048      public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A';
 049      public const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?([Ee][\+\-][0-9]+)?/A';
 050      public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
 051      public const REGEX_DQ_STRING_DELIM = '/"/A';
 052      public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
 053      public const PUNCTUATION = '()[]{}?:.,|';
 054   
 055      public function __construct(Environment $env, array $options = [])
 056      {
 057          $this->env = $env;
 058   
 059          $this->options = array_merge([
 060              'tag_comment' => ['{#', '#}'],
 061              'tag_block' => ['{%', '%}'],
 062              'tag_variable' => ['{{', '}}'],
 063              'whitespace_trim' => '-',
 064              'whitespace_line_trim' => '~',
 065              'whitespace_line_chars' => ' \t\0\x0B',
 066              'interpolation' => ['#{', '}'],
 067          ], $options);
 068      }
 069   
 070      private function initialize()
 071      {
 072          if ($this->isInitialized) {
 073              return;
 074          }
 075   
 076          $this->isInitialized = true;
 077   
 078          // 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
 079          $this->regexes = [
 080              // }}
 081              'lex_var' => '{
 082                  \s*
 083                  (?:'.
 084                      preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '#').'\s*'. // -}}\s*
 085                      '|'.
 086                      preg_quote($this->options['whitespace_line_trim'].$this->options['tag_variable'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~}}[ \t\0\x0B]*
 087                      '|'.
 088                      preg_quote($this->options['tag_variable'][1], '#'). // }}
 089                  ')
 090              }Ax',
 091   
 092              // %}
 093              'lex_block' => '{
 094                  \s*
 095                  (?:'.
 096                      preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*\n?'. // -%}\s*\n?
 097                      '|'.
 098                      preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
 099                      '|'.
 100                      preg_quote($this->options['tag_block'][1], '#').'\n?'. // %}\n?
 101                  ')
 102              }Ax',
 103   
 104              // {% endverbatim %}
 105              'lex_raw_data' => '{'.
 106                  preg_quote($this->options['tag_block'][0], '#'). // {%
 107                  '('.
 108                      $this->options['whitespace_trim']. // -
 109                      '|'.
 110                      $this->options['whitespace_line_trim']. // ~
 111                  ')?\s*endverbatim\s*'.
 112                  '(?:'.
 113                      preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}
 114                      '|'.
 115                      preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
 116                      '|'.
 117                      preg_quote($this->options['tag_block'][1], '#'). // %}
 118                  ')
 119              }sx',
 120   
 121              'operator' => $this->getOperatorRegex(),
 122   
 123              // #}
 124              'lex_comment' => '{
 125                  (?:'.
 126                      preg_quote($this->options['whitespace_trim'].$this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n?
 127                      '|'.
 128                      preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]*
 129                      '|'.
 130                      preg_quote($this->options['tag_comment'][1], '#').'\n?'. // #}\n?
 131                  ')
 132              }sx',
 133   
 134              // verbatim %}
 135              'lex_block_raw' => '{
 136                  \s*verbatim\s*
 137                  (?:'.
 138                      preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}\s*
 139                      '|'.
 140                      preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]*
 141                      '|'.
 142                      preg_quote($this->options['tag_block'][1], '#'). // %}
 143                  ')
 144              }Asx',
 145   
 146              'lex_block_line' => '{\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '#').'}As',
 147   
 148              // {{ or {% or {#
 149              'lex_tokens_start' => '{
 150                  ('.
 151                      preg_quote($this->options['tag_variable'][0], '#'). // {{
 152                      '|'.
 153                      preg_quote($this->options['tag_block'][0], '#'). // {%
 154                      '|'.
 155                      preg_quote($this->options['tag_comment'][0], '#'). // {#
 156                  ')('.
 157                      preg_quote($this->options['whitespace_trim'], '#'). // -
 158                      '|'.
 159                      preg_quote($this->options['whitespace_line_trim'], '#'). // ~
 160                  ')?
 161              }sx',
 162              'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A',
 163              'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A',
 164          ];
 165      }
 166   
 167      public function tokenize(Source $source)
 168      {
 169          $this->initialize();
 170   
 171          $this->source = $source;
 172          $this->code = str_replace(["\r\n", "\r"], "\n", $source->getCode());
 173          $this->cursor = 0;
 174          $this->lineno = 1;
 175          $this->end = \strlen($this->code);
 176          $this->tokens = [];
 177          $this->state = self::STATE_DATA;
 178          $this->states = [];
 179          $this->brackets = [];
 180          $this->position = -1;
 181   
 182          // find all token starts in one go
 183          preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE);
 184          $this->positions = $matches;
 185   
 186          while ($this->cursor < $this->end) {
 187              // dispatch to the lexing functions depending
 188              // on the current state
 189              switch ($this->state) {
 190                  case self::STATE_DATA:
 191                      $this->lexData();
 192                      break;
 193   
 194                  case self::STATE_BLOCK:
 195                      $this->lexBlock();
 196                      break;
 197   
 198                  case self::STATE_VAR:
 199                      $this->lexVar();
 200                      break;
 201   
 202                  case self::STATE_STRING:
 203                      $this->lexString();
 204                      break;
 205   
 206                  case self::STATE_INTERPOLATION:
 207                      $this->lexInterpolation();
 208                      break;
 209              }
 210          }
 211   
 212          $this->pushToken(/* Token::EOF_TYPE */ -1);
 213   
 214          if (!empty($this->brackets)) {
 215              list($expect, $lineno) = array_pop($this->brackets);
 216              throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
 217          }
 218   
 219          return new TokenStream($this->tokens, $this->source);
 220      }
 221   
 222      private function lexData()
 223      {
 224          // if no matches are left we return the rest of the template as simple text token
 225          if ($this->position == \count($this->positions[0]) - 1) {
 226              $this->pushToken(/* Token::TEXT_TYPE */ 0, substr($this->code, $this->cursor));
 227              $this->cursor = $this->end;
 228   
 229              return;
 230          }
 231   
 232          // Find the first token after the current cursor
 233          $position = $this->positions[0][++$this->position];
 234          while ($position[1] < $this->cursor) {
 235              if ($this->position == \count($this->positions[0]) - 1) {
 236                  return;
 237              }
 238              $position = $this->positions[0][++$this->position];
 239          }
 240   
 241          // push the template text first
 242          $text = $textContent = substr($this->code, $this->cursor, $position[1] - $this->cursor);
 243   
 244          // trim?
 245          if (isset($this->positions[2][$this->position][0])) {
 246              if ($this->options['whitespace_trim'] === $this->positions[2][$this->position][0]) {
 247                  // whitespace_trim detected ({%-, {{- or {#-)
 248                  $text = rtrim($text);
 249              } elseif ($this->options['whitespace_line_trim'] === $this->positions[2][$this->position][0]) {
 250                  // whitespace_line_trim detected ({%~, {{~ or {#~)
 251                  // don't trim \r and \n
 252                  $text = rtrim($text, " \t\0\x0B");
 253              }
 254          }
 255          $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
 256          $this->moveCursor($textContent.$position[0]);
 257   
 258          switch ($this->positions[1][$this->position][0]) {
 259              case $this->options['tag_comment'][0]:
 260                  $this->lexComment();
 261                  break;
 262   
 263              case $this->options['tag_block'][0]:
 264                  // raw data?
 265                  if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) {
 266                      $this->moveCursor($match[0]);
 267                      $this->lexRawData();
 268                  // {% line \d+ %}
 269                  } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) {
 270                      $this->moveCursor($match[0]);
 271                      $this->lineno = (int) $match[1];
 272                  } else {
 273                      $this->pushToken(/* Token::BLOCK_START_TYPE */ 1);
 274                      $this->pushState(self::STATE_BLOCK);
 275                      $this->currentVarBlockLine = $this->lineno;
 276                  }
 277                  break;
 278   
 279              case $this->options['tag_variable'][0]:
 280                  $this->pushToken(/* Token::VAR_START_TYPE */ 2);
 281                  $this->pushState(self::STATE_VAR);
 282                  $this->currentVarBlockLine = $this->lineno;
 283                  break;
 284          }
 285      }
 286   
 287      private function lexBlock()
 288      {
 289          if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) {
 290              $this->pushToken(/* Token::BLOCK_END_TYPE */ 3);
 291              $this->moveCursor($match[0]);
 292              $this->popState();
 293          } else {
 294              $this->lexExpression();
 295          }
 296      }
 297   
 298      private function lexVar()
 299      {
 300          if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) {
 301              $this->pushToken(/* Token::VAR_END_TYPE */ 4);
 302              $this->moveCursor($match[0]);
 303              $this->popState();
 304          } else {
 305              $this->lexExpression();
 306          }
 307      }
 308   
 309      private function lexExpression()
 310      {
 311          // whitespace
 312          if (preg_match('/\s+/A', $this->code, $match, 0, $this->cursor)) {
 313              $this->moveCursor($match[0]);
 314   
 315              if ($this->cursor >= $this->end) {
 316                  throw new SyntaxError(sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source);
 317              }
 318          }
 319   
 320          // arrow function
 321          if ('=' === $this->code[$this->cursor] && '>' === $this->code[$this->cursor + 1]) {
 322              $this->pushToken(Token::ARROW_TYPE, '=>');
 323              $this->moveCursor('=>');
 324          }
 325          // operators
 326          elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) {
 327              $this->pushToken(/* Token::OPERATOR_TYPE */ 8, preg_replace('/\s+/', ' ', $match[0]));
 328              $this->moveCursor($match[0]);
 329          }
 330          // names
 331          elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) {
 332              $this->pushToken(/* Token::NAME_TYPE */ 5, $match[0]);
 333              $this->moveCursor($match[0]);
 334          }
 335          // numbers
 336          elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) {
 337              $number = (float) $match[0];  // floats
 338              if (ctype_digit($match[0]) && $number <= \PHP_INT_MAX) {
 339                  $number = (int) $match[0]; // integers lower than the maximum
 340              }
 341              $this->pushToken(/* Token::NUMBER_TYPE */ 6, $number);
 342              $this->moveCursor($match[0]);
 343          }
 344          // punctuation
 345          elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) {
 346              // opening bracket
 347              if (false !== strpos('([{', $this->code[$this->cursor])) {
 348                  $this->brackets[] = [$this->code[$this->cursor], $this->lineno];
 349              }
 350              // closing bracket
 351              elseif (false !== strpos(')]}', $this->code[$this->cursor])) {
 352                  if (empty($this->brackets)) {
 353                      throw new SyntaxError(sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
 354                  }
 355   
 356                  list($expect, $lineno) = array_pop($this->brackets);
 357                  if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
 358                      throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
 359                  }
 360              }
 361   
 362              $this->pushToken(/* Token::PUNCTUATION_TYPE */ 9, $this->code[$this->cursor]);
 363              ++$this->cursor;
 364          }
 365          // strings
 366          elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) {
 367              $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes(substr($match[0], 1, -1)));
 368              $this->moveCursor($match[0]);
 369          }
 370          // opening double quoted string
 371          elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
 372              $this->brackets[] = ['"', $this->lineno];
 373              $this->pushState(self::STATE_STRING);
 374              $this->moveCursor($match[0]);
 375          }
 376          // unlexable
 377          else {
 378              throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
 379          }
 380      }
 381   
 382      private function lexRawData()
 383      {
 384          if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
 385              throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source);
 386          }
 387   
 388          $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor);
 389          $this->moveCursor($text.$match[0][0]);
 390   
 391          // trim?
 392          if (isset($match[1][0])) {
 393              if ($this->options['whitespace_trim'] === $match[1][0]) {
 394                  // whitespace_trim detected ({%-, {{- or {#-)
 395                  $text = rtrim($text);
 396              } else {
 397                  // whitespace_line_trim detected ({%~, {{~ or {#~)
 398                  // don't trim \r and \n
 399                  $text = rtrim($text, " \t\0\x0B");
 400              }
 401          }
 402   
 403          $this->pushToken(/* Token::TEXT_TYPE */ 0, $text);
 404      }
 405   
 406      private function lexComment()
 407      {
 408          if (!preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) {
 409              throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source);
 410          }
 411   
 412          $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]);
 413      }
 414   
 415      private function lexString()
 416      {
 417          if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) {
 418              $this->brackets[] = [$this->options['interpolation'][0], $this->lineno];
 419              $this->pushToken(/* Token::INTERPOLATION_START_TYPE */ 10);
 420              $this->moveCursor($match[0]);
 421              $this->pushState(self::STATE_INTERPOLATION);
 422          } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && \strlen($match[0]) > 0) {
 423              $this->pushToken(/* Token::STRING_TYPE */ 7, stripcslashes($match[0]));
 424              $this->moveCursor($match[0]);
 425          } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
 426              list($expect, $lineno) = array_pop($this->brackets);
 427              if ('"' != $this->code[$this->cursor]) {
 428                  throw new SyntaxError(sprintf('Unclosed "%s".', $expect), $lineno, $this->source);
 429              }
 430   
 431              $this->popState();
 432              ++$this->cursor;
 433          } else {
 434              // unlexable
 435              throw new SyntaxError(sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
 436          }
 437      }
 438   
 439      private function lexInterpolation()
 440      {
 441          $bracket = end($this->brackets);
 442          if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) {
 443              array_pop($this->brackets);
 444              $this->pushToken(/* Token::INTERPOLATION_END_TYPE */ 11);
 445              $this->moveCursor($match[0]);
 446              $this->popState();
 447          } else {
 448              $this->lexExpression();
 449          }
 450      }
 451   
 452      private function pushToken($type, $value = '')
 453      {
 454          // do not push empty text tokens
 455          if (/* Token::TEXT_TYPE */ 0 === $type && '' === $value) {
 456              return;
 457          }
 458   
 459          $this->tokens[] = new Token($type, $value, $this->lineno);
 460      }
 461   
 462      private function moveCursor($text)
 463      {
 464          $this->cursor += \strlen($text);
 465          $this->lineno += substr_count($text, "\n");
 466      }
 467   
 468      private function getOperatorRegex()
 469      {
 470          $operators = array_merge(
 471              ['='],
 472              array_keys($this->env->getUnaryOperators()),
 473              array_keys($this->env->getBinaryOperators())
 474          );
 475   
 476          $operators = array_combine($operators, array_map('strlen', $operators));
 477          arsort($operators);
 478   
 479          $regex = [];
 480          foreach ($operators as $operator => $length) {
 481              // an operator that ends with a character must be followed by
 482              // a whitespace, a parenthesis, an opening map [ or sequence {
 483              $r = preg_quote($operator, '/');
 484              if (ctype_alpha($operator[$length - 1])) {
 485                  $r .= '(?=[\s()\[{])';
 486              }
 487   
 488              // an operator that begins with a character must not have a dot or pipe before
 489              if (ctype_alpha($operator[0])) {
 490                  $r = '(?<![\.\|])'.$r;
 491              }
 492   
 493              // an operator with a space can be any amount of whitespaces
 494              $r = preg_replace('/\s+/', '\s+', $r);
 495   
 496              $regex[] = $r;
 497          }
 498   
 499          return '/'.implode('|', $regex).'/A';
 500      }
 501   
 502      private function pushState($state)
 503      {
 504          $this->states[] = $this->state;
 505          $this->state = $state;
 506      }
 507   
 508      private function popState()
 509      {
 510          if (0 === \count($this->states)) {
 511              throw new \LogicException('Cannot pop state without a previous state.');
 512          }
 513   
 514          $this->state = array_pop($this->states);
 515      }
 516  }
 517   
 518  class_alias('Twig\Lexer', 'Twig_Lexer');
 519