Dotclear

source: inc/libs/twig/Lexer.php @ 0:54703be25dd6

Revision 0:54703be25dd6, 10.8 KB checked in by Dsls <dsls@…>, 14 years ago (diff)

2.3 branch (trunk) first checkin

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

Sites map