diff options
| author | Michael Foster <savetheinternet@tinyboard.org> | 2013-08-01 15:20:12 -0400 |
|---|---|---|
| committer | Michael Foster <savetheinternet@tinyboard.org> | 2013-08-01 15:20:12 -0400 |
| commit | 0fe552857468aa12097a74e08f51f5aaa983be07 (patch) | |
| tree | 22dbca1be90d7086b20a077752552296abf23325 /inc/lib/Twig/Lexer.php | |
| parent | 22f3a95e0e421de05a11633eac846df96a2a4768 (diff) | |
Upgrade Twig library
Diffstat (limited to 'inc/lib/Twig/Lexer.php')
| -rw-r--r-- | inc/lib/Twig/Lexer.php | 222 |
1 files changed, 151 insertions, 71 deletions
diff --git a/inc/lib/Twig/Lexer.php b/inc/lib/Twig/Lexer.php index 868a814e..000b038e 100644 --- a/inc/lib/Twig/Lexer.php +++ b/inc/lib/Twig/Lexer.php @@ -13,8 +13,7 @@ /** * Lexes a template string. * - * @package twig - * @author Fabien Potencier <fabien@symfony.com> + * @author Fabien Potencier <fabien@symfony.com> */ class Twig_Lexer implements Twig_LexerInterface { @@ -24,21 +23,28 @@ class Twig_Lexer implements Twig_LexerInterface protected $lineno; protected $end; protected $state; + protected $states; protected $brackets; - protected $env; protected $filename; protected $options; - protected $operatorRegex; + protected $regexes; + protected $position; + protected $positions; + protected $currentVarBlockLine; - const STATE_DATA = 0; - const STATE_BLOCK = 1; - const STATE_VAR = 2; + const STATE_DATA = 0; + const STATE_BLOCK = 1; + const STATE_VAR = 2; + const STATE_STRING = 3; + const STATE_INTERPOLATION = 4; - const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; - const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A'; - const REGEX_STRING = '/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; - const PUNCTUATION = '()[]{}?:.,|'; + const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; + const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A'; + const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + const REGEX_DQ_STRING_DELIM = '/"/A'; + const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + const PUNCTUATION = '()[]{}?:.,|'; public function __construct(Twig_Environment $env, array $options = array()) { @@ -49,14 +55,28 @@ class Twig_Lexer implements Twig_LexerInterface 'tag_block' => array('{%', '%}'), 'tag_variable' => array('{{', '}}'), 'whitespace_trim' => '-', + 'interpolation' => array('#{', '}'), ), $options); + + $this->regexes = array( + 'lex_var' => '/\s*'.preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_variable'][1], '/').'/A', + 'lex_block' => '/\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')\n?/A', + 'lex_raw_data' => '/('.preg_quote($this->options['tag_block'][0].$this->options['whitespace_trim'], '/').'|'.preg_quote($this->options['tag_block'][0], '/').')\s*(?:end%s)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/s', + 'operator' => $this->getOperatorRegex(), + 'lex_comment' => '/(?:'.preg_quote($this->options['whitespace_trim'], '/').preg_quote($this->options['tag_comment'][1], '/').'\s*|'.preg_quote($this->options['tag_comment'][1], '/').')\n?/s', + 'lex_block_raw' => '/\s*(raw|verbatim)\s*(?:'.preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/').'\s*|\s*'.preg_quote($this->options['tag_block'][1], '/').')/As', + 'lex_block_line' => '/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', + 'lex_tokens_start' => '/('.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'], '/').')?/s', + 'interpolation_start' => '/'.preg_quote($this->options['interpolation'][0], '/').'\s*/A', + 'interpolation_end' => '/\s*'.preg_quote($this->options['interpolation'][1], '/').'/A', + ); } /** * Tokenizes a source code. * - * @param string $code The source code - * @param string $filename A unique identifier for the source code + * @param string $code The source code + * @param string $filename A unique identifier for the source code * * @return Twig_TokenStream A token stream instance */ @@ -74,7 +94,13 @@ class Twig_Lexer implements Twig_LexerInterface $this->end = strlen($this->code); $this->tokens = array(); $this->state = self::STATE_DATA; + $this->states = array(); $this->brackets = array(); + $this->position = -1; + + // find all token starts in one go + preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, PREG_OFFSET_CAPTURE); + $this->positions = $matches; while ($this->cursor < $this->end) { // dispatch to the lexing functions depending @@ -91,6 +117,14 @@ class Twig_Lexer implements Twig_LexerInterface case self::STATE_VAR: $this->lexVar(); break; + + case self::STATE_STRING: + $this->lexString(); + break; + + case self::STATE_INTERPOLATION: + $this->lexInterpolation(); + break; } } @@ -110,77 +144,66 @@ class Twig_Lexer implements Twig_LexerInterface protected function lexData() { - $pos = $this->end; - $append = ''; - - // Find the first token after the cursor - foreach (array('tag_comment', 'tag_variable', 'tag_block') as $type) { - $tmpPos = strpos($this->code, $this->options[$type][0], $this->cursor); - if (false !== $tmpPos && $tmpPos < $pos) { - $trimBlock = false; - $append = ''; - $pos = $tmpPos; - $token = $this->options[$type][0]; - if (strpos($this->code, $this->options['whitespace_trim'], $pos) === ($pos + strlen($token))) { - $trimBlock = true; - $append = $this->options['whitespace_trim']; - } - } - } - // if no matches are left we return the rest of the template as simple text token - if ($pos === $this->end) { + if ($this->position == count($this->positions[0]) - 1) { $this->pushToken(Twig_Token::TEXT_TYPE, substr($this->code, $this->cursor)); $this->cursor = $this->end; + return; } + // Find the first token after the current cursor + $position = $this->positions[0][++$this->position]; + while ($position[1] < $this->cursor) { + if ($this->position == count($this->positions[0]) - 1) { + return; + } + $position = $this->positions[0][++$this->position]; + } + // push the template text first - $text = $textContent = substr($this->code, $this->cursor, $pos - $this->cursor); - if (true === $trimBlock) { + $text = $textContent = substr($this->code, $this->cursor, $position[1] - $this->cursor); + if (isset($this->positions[2][$this->position][0])) { $text = rtrim($text); } $this->pushToken(Twig_Token::TEXT_TYPE, $text); - $this->moveCursor($textContent.$token.$append); + $this->moveCursor($textContent.$position[0]); - switch ($token) { + switch ($this->positions[1][$this->position][0]) { case $this->options['tag_comment'][0]: $this->lexComment(); break; case $this->options['tag_block'][0]: // raw data? - if (preg_match('/\s*raw\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', $this->code, $match, null, $this->cursor)) { + if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, null, $this->cursor)) { $this->moveCursor($match[0]); - $this->lexRawData(); - $this->state = self::STATE_DATA; + $this->lexRawData($match[1]); // {% line \d+ %} - } else if (preg_match('/\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', $this->code, $match, null, $this->cursor)) { + } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, null, $this->cursor)) { $this->moveCursor($match[0]); $this->lineno = (int) $match[1]; - $this->state = self::STATE_DATA; } else { $this->pushToken(Twig_Token::BLOCK_START_TYPE); - $this->state = self::STATE_BLOCK; + $this->pushState(self::STATE_BLOCK); + $this->currentVarBlockLine = $this->lineno; } break; case $this->options['tag_variable'][0]: $this->pushToken(Twig_Token::VAR_START_TYPE); - $this->state = self::STATE_VAR; + $this->pushState(self::STATE_VAR); + $this->currentVarBlockLine = $this->lineno; break; } } protected function lexBlock() { - $trimTag = preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '/'); - $endTag = preg_quote($this->options['tag_block'][1], '/'); - - if (empty($this->brackets) && preg_match('/\s*(?:'.$trimTag.'\s*|\s*'.$endTag.')\n?/A', $this->code, $match, null, $this->cursor)) { + if (empty($this->brackets) && preg_match($this->regexes['lex_block'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::BLOCK_END_TYPE); $this->moveCursor($match[0]); - $this->state = self::STATE_DATA; + $this->popState(); } else { $this->lexExpression(); } @@ -188,13 +211,10 @@ class Twig_Lexer implements Twig_LexerInterface protected function lexVar() { - $trimTag = preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '/'); - $endTag = preg_quote($this->options['tag_variable'][1], '/'); - - if (empty($this->brackets) && preg_match('/\s*'.$trimTag.'\s*|\s*'.$endTag.'/A', $this->code, $match, null, $this->cursor)) { + if (empty($this->brackets) && preg_match($this->regexes['lex_var'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::VAR_END_TYPE); $this->moveCursor($match[0]); - $this->state = self::STATE_DATA; + $this->popState(); } else { $this->lexExpression(); } @@ -207,12 +227,12 @@ class Twig_Lexer implements Twig_LexerInterface $this->moveCursor($match[0]); if ($this->cursor >= $this->end) { - throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable')); + throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $this->state === self::STATE_BLOCK ? 'block' : 'variable'), $this->currentVarBlockLine, $this->filename); } } // operators - if (preg_match($this->getOperatorRegex(), $this->code, $match, null, $this->cursor)) { + if (preg_match($this->regexes['operator'], $this->code, $match, null, $this->cursor)) { $this->pushToken(Twig_Token::OPERATOR_TYPE, $match[0]); $this->moveCursor($match[0]); } @@ -223,7 +243,11 @@ class Twig_Lexer implements Twig_LexerInterface } // numbers elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor)) { - $this->pushToken(Twig_Token::NUMBER_TYPE, ctype_digit($match[0]) ? (int) $match[0] : (float) $match[0]); + $number = (float) $match[0]; // floats + if (ctype_digit($match[0]) && $number <= PHP_INT_MAX) { + $number = (int) $match[0]; // integers lower than the maximum + } + $this->pushToken(Twig_Token::NUMBER_TYPE, $number); $this->moveCursor($match[0]); } // punctuation @@ -252,35 +276,80 @@ class Twig_Lexer implements Twig_LexerInterface $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1))); $this->moveCursor($match[0]); } + // opening double quoted string + elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array('"', $this->lineno); + $this->pushState(self::STATE_STRING); + $this->moveCursor($match[0]); + } // unlexable else { throw new Twig_Error_Syntax(sprintf('Unexpected character "%s"', $this->code[$this->cursor]), $this->lineno, $this->filename); } } - protected function lexRawData() + protected function lexRawData($tag) { - if (!preg_match('/'.preg_quote($this->options['tag_block'][0], '/').'\s*endraw\s*'.preg_quote($this->options['tag_block'][1], '/').'/s', $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { - throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "block"')); + if (!preg_match(str_replace('%s', $tag, $this->regexes['lex_raw_data']), $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s" block', $tag), $this->lineno, $this->filename); } + $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); - $this->pushToken(Twig_Token::TEXT_TYPE, $text); $this->moveCursor($text.$match[0][0]); + + if (false !== strpos($match[1][0], $this->options['whitespace_trim'])) { + $text = rtrim($text); + } + + $this->pushToken(Twig_Token::TEXT_TYPE, $text); } protected function lexComment() { - $commentEndRegex = '/(?:'.preg_quote($this->options['whitespace_trim'], '/') - .preg_quote($this->options['tag_comment'][1], '/').'\s*|' - .preg_quote($this->options['tag_comment'][1], '/').')\n?/s'; - - if (!preg_match($commentEndRegex, $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { + if (!preg_match($this->regexes['lex_comment'], $this->code, $match, PREG_OFFSET_CAPTURE, $this->cursor)) { throw new Twig_Error_Syntax('Unclosed comment', $this->lineno, $this->filename); } $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); } + protected function lexString() + { + if (preg_match($this->regexes['interpolation_start'], $this->code, $match, null, $this->cursor)) { + $this->brackets[] = array($this->options['interpolation'][0], $this->lineno); + $this->pushToken(Twig_Token::INTERPOLATION_START_TYPE); + $this->moveCursor($match[0]); + $this->pushState(self::STATE_INTERPOLATION); + + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, null, $this->cursor) && strlen($match[0]) > 0) { + $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes($match[0])); + $this->moveCursor($match[0]); + + } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, null, $this->cursor)) { + + list($expect, $lineno) = array_pop($this->brackets); + if ($this->code[$this->cursor] != '"') { + throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename); + } + + $this->popState(); + ++$this->cursor; + } + } + + protected function lexInterpolation() + { + $bracket = end($this->brackets); + if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, null, $this->cursor)) { + array_pop($this->brackets); + $this->pushToken(Twig_Token::INTERPOLATION_END_TYPE); + $this->moveCursor($match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + protected function pushToken($type, $value = '') { // do not push empty text tokens @@ -299,10 +368,6 @@ class Twig_Lexer implements Twig_LexerInterface protected function getOperatorRegex() { - if (null !== $this->operatorRegex) { - return $this->operatorRegex; - } - $operators = array_merge( array('='), array_keys($this->env->getUnaryOperators()), @@ -317,12 +382,27 @@ class Twig_Lexer implements Twig_LexerInterface // an operator that ends with a character must be followed by // a whitespace or a parenthesis if (ctype_alpha($operator[$length - 1])) { - $regex[] = preg_quote($operator, '/').'(?=[ ()])'; + $regex[] = preg_quote($operator, '/').'(?=[\s()])'; } else { $regex[] = preg_quote($operator, '/'); } } - return $this->operatorRegex = '/'.implode('|', $regex).'/A'; + return '/'.implode('|', $regex).'/A'; + } + + protected function pushState($state) + { + $this->states[] = $this->state; + $this->state = $state; + } + + protected function popState() + { + if (0 === count($this->states)) { + throw new Exception('Cannot pop state without a previous state'); + } + + $this->state = array_pop($this->states); } } |
