summaryrefslogtreecommitdiff
path: root/inc/lib/Twig/Lexer.php
diff options
context:
space:
mode:
authorMichael Foster <savetheinternet@tinyboard.org>2013-08-01 15:20:12 -0400
committerMichael Foster <savetheinternet@tinyboard.org>2013-08-01 15:20:12 -0400
commit0fe552857468aa12097a74e08f51f5aaa983be07 (patch)
tree22dbca1be90d7086b20a077752552296abf23325 /inc/lib/Twig/Lexer.php
parent22f3a95e0e421de05a11633eac846df96a2a4768 (diff)
Upgrade Twig library
Diffstat (limited to 'inc/lib/Twig/Lexer.php')
-rw-r--r--inc/lib/Twig/Lexer.php222
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);
}
}