]>
git.immae.eu Git - github/wallabag/wallabag.git/blob - vendor/twig/twig/lib/Twig/Lexer.php
000b038e60e825a235cfa91c50157505e6dbf4cc
4 * This file is part of Twig.
6 * (c) 2009 Fabien Potencier
7 * (c) 2009 Armin Ronacher
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
14 * Lexes a template string.
16 * @author Fabien Potencier <fabien@symfony.com>
18 class Twig_Lexer
implements Twig_LexerInterface
34 protected $currentVarBlockLine;
37 const STATE_BLOCK
= 1;
39 const STATE_STRING
= 3;
40 const STATE_INTERPOLATION
= 4;
42 const REGEX_NAME
= '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A';
43 const REGEX_NUMBER
= '/[0-9]+(?:\.[0-9]+)?/A';
44 const REGEX_STRING
= '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
45 const REGEX_DQ_STRING_DELIM
= '/"/A';
46 const REGEX_DQ_STRING_PART
= '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As';
47 const PUNCTUATION
= '()[]{}?:.,|';
49 public function __construct(Twig_Environment
$env, array $options = array())
53 $this->options
= array_merge(array(
54 'tag_comment' => array('{#', '#}'),
55 'tag_block' => array('{%', '%}'),
56 'tag_variable' => array('{{', '}}'),
57 'whitespace_trim' => '-',
58 'interpolation' => array('#{', '}'),
61 $this->regexes
= array(
62 'lex_var' => '/\s*'.preg_quote($this->options
['whitespace_trim'].$this->options
['tag_variable'][1], '/').'\s*|\s*'.preg_quote($this->options
['tag_variable'][1], '/').'/A',
63 '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',
64 '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',
65 'operator' => $this->getOperatorRegex(),
66 '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',
67 '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',
68 'lex_block_line' => '/\s*line\s+(\d+)\s*'.preg_quote($this->options
['tag_block'][1], '/').'/As',
69 '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',
70 'interpolation_start' => '/'.preg_quote($this->options
['interpolation'][0], '/').'\s*/A',
71 'interpolation_end' => '/\s*'.preg_quote($this->options
['interpolation'][1], '/').'/A',
76 * Tokenizes a source code.
78 * @param string $code The source code
79 * @param string $filename A unique identifier for the source code
81 * @return Twig_TokenStream A token stream instance
83 public function tokenize($code, $filename = null)
85 if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
86 $mbEncoding = mb_internal_encoding();
87 mb_internal_encoding('ASCII');
90 $this->code
= str_replace(array("\r\n", "\r"), "\n", $code);
91 $this->filename
= $filename;
94 $this->end
= strlen($this->code
);
95 $this->tokens
= array();
96 $this->state
= self
::STATE_DATA
;
97 $this->states
= array();
98 $this->brackets
= array();
101 // find all token starts in one go
102 preg_match_all($this->regexes
['lex_tokens_start'], $this->code
, $matches, PREG_OFFSET_CAPTURE
);
103 $this->positions
= $matches;
105 while ($this->cursor
< $this->end
) {
106 // dispatch to the lexing functions depending
107 // on the current state
108 switch ($this->state
) {
109 case self
::STATE_DATA
:
113 case self
::STATE_BLOCK
:
117 case self
::STATE_VAR
:
121 case self
::STATE_STRING
:
125 case self
::STATE_INTERPOLATION
:
126 $this->lexInterpolation();
131 $this->pushToken(Twig_Token
::EOF_TYPE
);
133 if (!empty($this->brackets
)) {
134 list($expect, $lineno) = array_pop($this->brackets
);
135 throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename
);
138 if (isset($mbEncoding)) {
139 mb_internal_encoding($mbEncoding);
142 return new Twig_TokenStream($this->tokens
, $this->filename
);
145 protected function lexData()
147 // if no matches are left we return the rest of the template as simple text token
148 if ($this->position
== count($this->positions
[0]) - 1) {
149 $this->pushToken(Twig_Token
::TEXT_TYPE
, substr($this->code
, $this->cursor
));
150 $this->cursor
= $this->end
;
155 // Find the first token after the current cursor
156 $position = $this->positions
[0][++
$this->position
];
157 while ($position[1] < $this->cursor
) {
158 if ($this->position
== count($this->positions
[0]) - 1) {
161 $position = $this->positions
[0][++
$this->position
];
164 // push the template text first
165 $text = $textContent = substr($this->code
, $this->cursor
, $position[1] - $this->cursor
);
166 if (isset($this->positions
[2][$this->position
][0])) {
167 $text = rtrim($text);
169 $this->pushToken(Twig_Token
::TEXT_TYPE
, $text);
170 $this->moveCursor($textContent.$position[0]);
172 switch ($this->positions
[1][$this->position
][0]) {
173 case $this->options
['tag_comment'][0]:
177 case $this->options
['tag_block'][0]:
179 if (preg_match($this->regexes
['lex_block_raw'], $this->code
, $match, null, $this->cursor
)) {
180 $this->moveCursor($match[0]);
181 $this->lexRawData($match[1]);
183 } elseif (preg_match($this->regexes
['lex_block_line'], $this->code
, $match, null, $this->cursor
)) {
184 $this->moveCursor($match[0]);
185 $this->lineno
= (int) $match[1];
187 $this->pushToken(Twig_Token
::BLOCK_START_TYPE
);
188 $this->pushState(self
::STATE_BLOCK
);
189 $this->currentVarBlockLine
= $this->lineno
;
193 case $this->options
['tag_variable'][0]:
194 $this->pushToken(Twig_Token
::VAR_START_TYPE
);
195 $this->pushState(self
::STATE_VAR
);
196 $this->currentVarBlockLine
= $this->lineno
;
201 protected function lexBlock()
203 if (empty($this->brackets
) && preg_match($this->regexes
['lex_block'], $this->code
, $match, null, $this->cursor
)) {
204 $this->pushToken(Twig_Token
::BLOCK_END_TYPE
);
205 $this->moveCursor($match[0]);
208 $this->lexExpression();
212 protected function lexVar()
214 if (empty($this->brackets
) && preg_match($this->regexes
['lex_var'], $this->code
, $match, null, $this->cursor
)) {
215 $this->pushToken(Twig_Token
::VAR_END_TYPE
);
216 $this->moveCursor($match[0]);
219 $this->lexExpression();
223 protected function lexExpression()
226 if (preg_match('/\s+/A', $this->code
, $match, null, $this->cursor
)) {
227 $this->moveCursor($match[0]);
229 if ($this->cursor
>= $this->end
) {
230 throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $this->state
=== self
::STATE_BLOCK
? 'block' : 'variable'), $this->currentVarBlockLine
, $this->filename
);
235 if (preg_match($this->regexes
['operator'], $this->code
, $match, null, $this->cursor
)) {
236 $this->pushToken(Twig_Token
::OPERATOR_TYPE
, $match[0]);
237 $this->moveCursor($match[0]);
240 elseif (preg_match(self
::REGEX_NAME
, $this->code
, $match, null, $this->cursor
)) {
241 $this->pushToken(Twig_Token
::NAME_TYPE
, $match[0]);
242 $this->moveCursor($match[0]);
245 elseif (preg_match(self
::REGEX_NUMBER
, $this->code
, $match, null, $this->cursor
)) {
246 $number = (float) $match[0]; // floats
247 if (ctype_digit($match[0]) && $number <= PHP_INT_MAX
) {
248 $number = (int) $match[0]; // integers lower than the maximum
250 $this->pushToken(Twig_Token
::NUMBER_TYPE
, $number);
251 $this->moveCursor($match[0]);
254 elseif (false !== strpos(self
::PUNCTUATION
, $this->code
[$this->cursor
])) {
256 if (false !== strpos('([{', $this->code
[$this->cursor
])) {
257 $this->brackets
[] = array($this->code
[$this->cursor
], $this->lineno
);
260 elseif (false !== strpos(')]}', $this->code
[$this->cursor
])) {
261 if (empty($this->brackets
)) {
262 throw new Twig_Error_Syntax(sprintf('Unexpected "%s"', $this->code
[$this->cursor
]), $this->lineno
, $this->filename
);
265 list($expect, $lineno) = array_pop($this->brackets
);
266 if ($this->code
[$this->cursor
] != strtr($expect, '([{', ')]}')) {
267 throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename
);
271 $this->pushToken(Twig_Token
::PUNCTUATION_TYPE
, $this->code
[$this->cursor
]);
275 elseif (preg_match(self
::REGEX_STRING
, $this->code
, $match, null, $this->cursor
)) {
276 $this->pushToken(Twig_Token
::STRING_TYPE
, stripcslashes(substr($match[0], 1, -1)));
277 $this->moveCursor($match[0]);
279 // opening double quoted string
280 elseif (preg_match(self
::REGEX_DQ_STRING_DELIM
, $this->code
, $match, null, $this->cursor
)) {
281 $this->brackets
[] = array('"', $this->lineno
);
282 $this->pushState(self
::STATE_STRING
);
283 $this->moveCursor($match[0]);
287 throw new Twig_Error_Syntax(sprintf('Unexpected character "%s"', $this->code
[$this->cursor
]), $this->lineno
, $this->filename
);
291 protected function lexRawData($tag)
293 if (!preg_match(str_replace('%s', $tag, $this->regexes
['lex_raw_data']), $this->code
, $match, PREG_OFFSET_CAPTURE
, $this->cursor
)) {
294 throw new Twig_Error_Syntax(sprintf('Unexpected end of file: Unclosed "%s" block', $tag), $this->lineno
, $this->filename
);
297 $text = substr($this->code
, $this->cursor
, $match[0][1] - $this->cursor
);
298 $this->moveCursor($text.$match[0][0]);
300 if (false !== strpos($match[1][0], $this->options
['whitespace_trim'])) {
301 $text = rtrim($text);
304 $this->pushToken(Twig_Token
::TEXT_TYPE
, $text);
307 protected function lexComment()
309 if (!preg_match($this->regexes
['lex_comment'], $this->code
, $match, PREG_OFFSET_CAPTURE
, $this->cursor
)) {
310 throw new Twig_Error_Syntax('Unclosed comment', $this->lineno
, $this->filename
);
313 $this->moveCursor(substr($this->code
, $this->cursor
, $match[0][1] - $this->cursor
).$match[0][0]);
316 protected function lexString()
318 if (preg_match($this->regexes
['interpolation_start'], $this->code
, $match, null, $this->cursor
)) {
319 $this->brackets
[] = array($this->options
['interpolation'][0], $this->lineno
);
320 $this->pushToken(Twig_Token
::INTERPOLATION_START_TYPE
);
321 $this->moveCursor($match[0]);
322 $this->pushState(self
::STATE_INTERPOLATION
);
324 } elseif (preg_match(self
::REGEX_DQ_STRING_PART
, $this->code
, $match, null, $this->cursor
) && strlen($match[0]) > 0) {
325 $this->pushToken(Twig_Token
::STRING_TYPE
, stripcslashes($match[0]));
326 $this->moveCursor($match[0]);
328 } elseif (preg_match(self
::REGEX_DQ_STRING_DELIM
, $this->code
, $match, null, $this->cursor
)) {
330 list($expect, $lineno) = array_pop($this->brackets
);
331 if ($this->code
[$this->cursor
] != '"') {
332 throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename
);
340 protected function lexInterpolation()
342 $bracket = end($this->brackets
);
343 if ($this->options
['interpolation'][0] === $bracket[0] && preg_match($this->regexes
['interpolation_end'], $this->code
, $match, null, $this->cursor
)) {
344 array_pop($this->brackets
);
345 $this->pushToken(Twig_Token
::INTERPOLATION_END_TYPE
);
346 $this->moveCursor($match[0]);
349 $this->lexExpression();
353 protected function pushToken($type, $value = '')
355 // do not push empty text tokens
356 if (Twig_Token
::TEXT_TYPE
=== $type && '' === $value) {
360 $this->tokens
[] = new Twig_Token($type, $value, $this->lineno
);
363 protected function moveCursor($text)
365 $this->cursor +
= strlen($text);
366 $this->lineno +
= substr_count($text, "\n");
369 protected function getOperatorRegex()
371 $operators = array_merge(
373 array_keys($this->env
->getUnaryOperators()),
374 array_keys($this->env
->getBinaryOperators())
377 $operators = array_combine($operators, array_map('strlen', $operators));
381 foreach ($operators as $operator => $length) {
382 // an operator that ends with a character must be followed by
383 // a whitespace or a parenthesis
384 if (ctype_alpha($operator[$length - 1])) {
385 $regex[] = preg_quote($operator, '/').'(?=[\s()])';
387 $regex[] = preg_quote($operator, '/');
391 return '/'.implode('|', $regex).'/A';
394 protected function pushState($state)
396 $this->states
[] = $this->state
;
397 $this->state
= $state;
400 protected function popState()
402 if (0 === count($this->states
)) {
403 throw new Exception('Cannot pop state without a previous state');
406 $this->state
= array_pop($this->states
);