source: pro-violet-viettel/www/deploy/20150304/application/third_party/Twig/Lexer.php @ 780

Last change on this file since 780 was 780, checked in by dungnv, 10 years ago
File size: 11.0 KB
Line 
1<?php
2
3/*
4 * This file is part of Twig.
5 *
6 * (c) 2009 Fabien Potencier
7 * (c) 2009 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
13/**
14 * Lexes a template string.
15 *
16 * @package    twig
17 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
18 */
19class Twig_Lexer implements Twig_LexerInterface
20{
21    protected $tokens;
22    protected $code;
23    protected $cursor;
24    protected $lineno;
25    protected $end;
26    protected $state;
27    protected $brackets;
28
29    protected $env;
30    protected $filename;
31    protected $options;
32    protected $operatorRegex;
33
34    const STATE_DATA  = 0;
35    const STATE_BLOCK = 1;
36    const STATE_VAR   = 2;
37
38    const REGEX_NAME   = '/[A-Za-z_][A-Za-z0-9_]*/A';
39    const REGEX_NUMBER = '/[0-9]+(?:\.[0-9]+)?/A';
40    const REGEX_STRING = '/"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As';
41    const PUNCTUATION  = '()[]{}?:.,|';
42
43    public function __construct(Twig_Environment $env, array $options = array())
44    {
45        $this->env = $env;
46
47        $this->options = array_merge(array(
48            'tag_comment'  => array('{#', '#}'),
49            'tag_block'    => array('{%', '%}'),
50            'tag_variable' => array('{{', '}}'),
51            'whitespace_trim' => '-'
52        ), $options);
53    }
54
55    /**
56     * Tokenizes a source code.
57     *
58     * @param  string $code     The source code
59     * @param  string $filename A unique identifier for the source code
60     *
61     * @return Twig_TokenStream A token stream instance
62     */
63    public function tokenize($code, $filename = null)
64    {
65        if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
66            $mbEncoding = mb_internal_encoding();
67            mb_internal_encoding('ASCII');
68        }
69
70        $this->code = str_replace(array("\r\n", "\r"), "\n", $code);
71        $this->filename = $filename;
72        $this->cursor = 0;
73        $this->lineno = 1;
74        $this->end = strlen($this->code);
75        $this->tokens = array();
76        $this->state = self::STATE_DATA;
77        $this->brackets = array();
78
79        while ($this->cursor < $this->end) {
80            // dispatch to the lexing functions depending
81            // on the current state
82            switch ($this->state) {
83                case self::STATE_DATA:
84                    $this->lexData();
85                    break;
86
87                case self::STATE_BLOCK:
88                    $this->lexBlock();
89                    break;
90
91                case self::STATE_VAR:
92                    $this->lexVar();
93                    break;
94            }
95        }
96
97        $this->pushToken(Twig_Token::EOF_TYPE);
98
99        if (!empty($this->brackets)) {
100            list($expect, $lineno) = array_pop($this->brackets);
101            throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename);
102        }
103
104        if (isset($mbEncoding)) {
105            mb_internal_encoding($mbEncoding);
106        }
107
108        return new Twig_TokenStream($this->tokens, $this->filename);
109    }
110
111    protected function lexData()
112    {
113        $pos = $this->end;
114        $append = '';
115
116        // Find the first token after the cursor.
117        foreach (array('tag_comment', 'tag_variable', 'tag_block') as $type) {
118            $tmpPos = strpos($this->code, $this->options[$type][0], $this->cursor);
119            if (false !== $tmpPos && $tmpPos < $pos) {
120                $trimBlock = false;
121                $append = '';
122                $pos = $tmpPos;
123                $token = $this->options[$type][0];
124                if (strpos($this->code, $this->options['whitespace_trim'], $pos) === ($pos + strlen($token))) {
125                    $trimBlock = true;
126                    $append = $this->options['whitespace_trim'];
127                }
128            }
129        }
130
131        // if no matches are left we return the rest of the template as simple text token
132        if ($pos === $this->end) {
133            $this->pushToken(Twig_Token::TEXT_TYPE, substr($this->code, $this->cursor));
134            $this->cursor = $this->end;
135            return;
136        }
137       
138        // push the template text first
139        $text = $textContent = substr($this->code, $this->cursor, $pos - $this->cursor);
140        if (true === $trimBlock) {
141            $text = rtrim($text);
142        }
143        $this->pushToken(Twig_Token::TEXT_TYPE, $text);
144        $this->moveCursor($textContent . $token . $append);
145
146        switch ($token) {
147            case $this->options['tag_comment'][0]:
148                $commentEndRegex = '/.*?(?:' . preg_quote($this->options['whitespace_trim'], '/')
149                                   . preg_quote($this->options['tag_comment'][1], '/') . '\s*|'
150                                   . preg_quote($this->options['tag_comment'][1], '/') . ')\n?/As';
151
152                if (!preg_match($commentEndRegex, $this->code, $match, null, $this->cursor)) {
153                    throw new Twig_Error_Syntax('unclosed comment', $this->lineno, $this->filename);
154                }
155
156                $this->moveCursor($match[0]);
157                break;
158
159            case $this->options['tag_block'][0]:
160                // raw data?
161                if (preg_match('/\s*raw\s*'.preg_quote($this->options['tag_block'][1], '/').'(.*?)'.preg_quote($this->options['tag_block'][0], '/').'\s*endraw\s*'.preg_quote($this->options['tag_block'][1], '/').'/As', $this->code, $match, null, $this->cursor)) {
162                    $this->pushToken(Twig_Token::TEXT_TYPE, $match[1]);
163                    $this->moveCursor($match[0]);
164                    $this->state = self::STATE_DATA;
165                } else {
166                    $this->pushToken(Twig_Token::BLOCK_START_TYPE);
167                    $this->state = self::STATE_BLOCK;
168                }
169                break;
170
171            case $this->options['tag_variable'][0]:
172                $this->pushToken(Twig_Token::VAR_START_TYPE);
173                $this->state = self::STATE_VAR;
174                break;
175        }
176    }
177
178    protected function lexBlock()
179    {
180        $trimTag = preg_quote($this->options['whitespace_trim'] . $this->options['tag_block'][1], '/');
181        $endTag = preg_quote($this->options['tag_block'][1], '/');
182
183        if (empty($this->brackets) && preg_match('/\s*(?:' . $trimTag . '\s*|\s*' . $endTag . ')\n?/A', $this->code, $match, null, $this->cursor)) {
184            $this->pushToken(Twig_Token::BLOCK_END_TYPE);
185            $this->moveCursor($match[0]);
186            $this->state = self::STATE_DATA;
187        }
188        else {
189            $this->lexExpression();
190        }
191    }
192
193    protected function lexVar()
194    {
195        $trimTag = preg_quote($this->options['whitespace_trim'] . $this->options['tag_variable'][1], '/');
196        $endTag = preg_quote($this->options['tag_variable'][1], '/');
197       
198        if (empty($this->brackets) && preg_match('/\s*' . $trimTag . '\s*|\s*' . $endTag . '/A', $this->code, $match, null, $this->cursor)) {
199            $this->pushToken(Twig_Token::VAR_END_TYPE);
200            $this->moveCursor($match[0]);
201            $this->state = self::STATE_DATA;
202        }
203        else {
204            $this->lexExpression();
205        }
206    }
207
208    protected function lexExpression()
209    {
210        // whitespace
211        if (preg_match('/\s+/A', $this->code, $match, null, $this->cursor)) {
212            $this->moveCursor($match[0]);
213
214            if ($this->cursor >= $this->end) {
215                throw new Twig_Error_Syntax('Unexpected end of file: Unclosed ' . ($this->state === self::STATE_BLOCK ? 'block' : 'variable'));
216            }
217        }
218
219        // operators
220        if (preg_match($this->getOperatorRegex(), $this->code, $match, null, $this->cursor)) {
221            $this->pushToken(Twig_Token::OPERATOR_TYPE, $match[0]);
222            $this->moveCursor($match[0]);
223        }
224        // names
225        elseif (preg_match(self::REGEX_NAME, $this->code, $match, null, $this->cursor)) {
226            $this->pushToken(Twig_Token::NAME_TYPE, $match[0]);
227            $this->moveCursor($match[0]);
228        }
229        // numbers
230        elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, null, $this->cursor)) {
231            $this->pushToken(Twig_Token::NUMBER_TYPE, ctype_digit($match[0]) ? (int) $match[0] : (float) $match[0]);
232            $this->moveCursor($match[0]);
233        }
234        // punctuation
235        elseif (false !== strpos(self::PUNCTUATION, $this->code[$this->cursor])) {
236            // opening bracket
237            if (false !== strpos('([{', $this->code[$this->cursor])) {
238                $this->brackets[] = array($this->code[$this->cursor], $this->lineno);
239            }
240            // closing bracket
241            elseif (false !== strpos(')]}', $this->code[$this->cursor])) {
242                if (empty($this->brackets)) {
243                    throw new Twig_Error_Syntax(sprintf('Unexpected "%s"', $this->code[$this->cursor]), $this->lineno, $this->filename);
244                }
245
246                list($expect, $lineno) = array_pop($this->brackets);
247                if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) {
248                    throw new Twig_Error_Syntax(sprintf('Unclosed "%s"', $expect), $lineno, $this->filename);
249                }
250            }
251
252            $this->pushToken(Twig_Token::PUNCTUATION_TYPE, $this->code[$this->cursor]);
253            ++$this->cursor;
254        }
255        // strings
256        elseif (preg_match(self::REGEX_STRING, $this->code, $match, null, $this->cursor)) {
257            $this->pushToken(Twig_Token::STRING_TYPE, stripcslashes(substr($match[0], 1, -1)));
258            $this->moveCursor($match[0]);
259        }
260        // unlexable
261        else {
262            throw new Twig_Error_Syntax(sprintf("Unexpected character '%s'", $this->code[$this->cursor]), $this->lineno, $this->filename);
263        }
264    }
265
266    protected function pushToken($type, $value = '')
267    {
268        // do not push empty text tokens
269        if (Twig_Token::TEXT_TYPE === $type && '' === $value) {
270            return;
271        }
272
273        $this->tokens[] = new Twig_Token($type, $value, $this->lineno);
274    }
275
276    protected function moveCursor($text)
277    {
278        $this->cursor += strlen($text);
279        $this->lineno += substr_count($text, "\n");
280    }
281
282    protected function getOperatorRegex()
283    {
284        if (null !== $this->operatorRegex) {
285            return $this->operatorRegex;
286        }
287
288        $operators = array_merge(
289            array('='),
290            array_keys($this->env->getUnaryOperators()),
291            array_keys($this->env->getBinaryOperators())
292        );
293
294        $operators = array_combine($operators, array_map('strlen', $operators));
295        arsort($operators);
296
297        $regex = array();
298        foreach ($operators as $operator => $length) {
299            // an operator that ends with a character must be followed by
300            // a whitespace or a parenthesis
301            if (ctype_alpha($operator[$length - 1])) {
302                $regex[] = preg_quote($operator, '/').'(?=[ ()])';
303            } else {
304                $regex[] = preg_quote($operator, '/');
305            }
306        }
307
308        return $this->operatorRegex = '/'.implode('|', $regex).'/A';
309    }
310}
Note: See TracBrowser for help on using the repository browser.