Merge branch 'MDL-67622_master' of git://github.com/dmonllao/moodle
[moodle.git] / lib / mustache / src / Mustache / Tokenizer.php
1 <?php
3 /*
4  * This file is part of Mustache.php.
5  *
6  * (c) 2010-2017 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
12 /**
13  * Mustache Tokenizer class.
14  *
15  * This class is responsible for turning raw template source into a set of Mustache tokens.
16  */
17 class Mustache_Tokenizer
18 {
19     // Finite state machine states
20     const IN_TEXT     = 0;
21     const IN_TAG_TYPE = 1;
22     const IN_TAG      = 2;
24     // Token types
25     const T_SECTION      = '#';
26     const T_INVERTED     = '^';
27     const T_END_SECTION  = '/';
28     const T_COMMENT      = '!';
29     const T_PARTIAL      = '>';
30     const T_PARENT       = '<';
31     const T_DELIM_CHANGE = '=';
32     const T_ESCAPED      = '_v';
33     const T_UNESCAPED    = '{';
34     const T_UNESCAPED_2  = '&';
35     const T_TEXT         = '_t';
36     const T_PRAGMA       = '%';
37     const T_BLOCK_VAR    = '$';
38     const T_BLOCK_ARG    = '$arg';
40     // Valid token types
41     private static $tagTypes = array(
42         self::T_SECTION      => true,
43         self::T_INVERTED     => true,
44         self::T_END_SECTION  => true,
45         self::T_COMMENT      => true,
46         self::T_PARTIAL      => true,
47         self::T_PARENT       => true,
48         self::T_DELIM_CHANGE => true,
49         self::T_ESCAPED      => true,
50         self::T_UNESCAPED    => true,
51         self::T_UNESCAPED_2  => true,
52         self::T_PRAGMA       => true,
53         self::T_BLOCK_VAR    => true,
54     );
56     // Token properties
57     const TYPE    = 'type';
58     const NAME    = 'name';
59     const OTAG    = 'otag';
60     const CTAG    = 'ctag';
61     const LINE    = 'line';
62     const INDEX   = 'index';
63     const END     = 'end';
64     const INDENT  = 'indent';
65     const NODES   = 'nodes';
66     const VALUE   = 'value';
67     const FILTERS = 'filters';
69     private $state;
70     private $tagType;
71     private $buffer;
72     private $tokens;
73     private $seenTag;
74     private $line;
75     private $otag;
76     private $ctag;
77     private $otagLen;
78     private $ctagLen;
80     /**
81      * Scan and tokenize template source.
82      *
83      * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered
84      *
85      * @param string $text       Mustache template source to tokenize
86      * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
87      *
88      * @return array Set of Mustache tokens
89      */
90     public function scan($text, $delimiters = null)
91     {
92         // Setting mbstring.func_overload makes things *really* slow.
93         // Let's do everyone a favor and scan this string as ASCII instead.
94         //
95         // @codeCoverageIgnoreStart
96         $encoding = null;
97         if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) {
98             $encoding = mb_internal_encoding();
99             mb_internal_encoding('ASCII');
100         }
101         // @codeCoverageIgnoreEnd
103         $this->reset();
105         if ($delimiters = trim($delimiters)) {
106             $this->setDelimiters($delimiters);
107         }
109         $len = strlen($text);
110         for ($i = 0; $i < $len; $i++) {
111             switch ($this->state) {
112                 case self::IN_TEXT:
113                     if ($this->tagChange($this->otag, $this->otagLen, $text, $i)) {
114                         $i--;
115                         $this->flushBuffer();
116                         $this->state = self::IN_TAG_TYPE;
117                     } else {
118                         $char = $text[$i];
119                         $this->buffer .= $char;
120                         if ($char === "\n") {
121                             $this->flushBuffer();
122                             $this->line++;
123                         }
124                     }
125                     break;
127                 case self::IN_TAG_TYPE:
128                     $i += $this->otagLen - 1;
129                     $char = $text[$i + 1];
130                     if (isset(self::$tagTypes[$char])) {
131                         $tag = $char;
132                         $this->tagType = $tag;
133                     } else {
134                         $tag = null;
135                         $this->tagType = self::T_ESCAPED;
136                     }
138                     if ($this->tagType === self::T_DELIM_CHANGE) {
139                         $i = $this->changeDelimiters($text, $i);
140                         $this->state = self::IN_TEXT;
141                     } elseif ($this->tagType === self::T_PRAGMA) {
142                         $i = $this->addPragma($text, $i);
143                         $this->state = self::IN_TEXT;
144                     } else {
145                         if ($tag !== null) {
146                             $i++;
147                         }
148                         $this->state = self::IN_TAG;
149                     }
150                     $this->seenTag = $i;
151                     break;
153                 default:
154                     if ($this->tagChange($this->ctag, $this->ctagLen, $text, $i)) {
155                         $token = array(
156                             self::TYPE  => $this->tagType,
157                             self::NAME  => trim($this->buffer),
158                             self::OTAG  => $this->otag,
159                             self::CTAG  => $this->ctag,
160                             self::LINE  => $this->line,
161                             self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen,
162                         );
164                         if ($this->tagType === self::T_UNESCAPED) {
165                             // Clean up `{{{ tripleStache }}}` style tokens.
166                             if ($this->ctag === '}}') {
167                                 if (($i + 2 < $len) && $text[$i + 2] === '}') {
168                                     $i++;
169                                 } else {
170                                     $msg = sprintf(
171                                         'Mismatched tag delimiters: %s on line %d',
172                                         $token[self::NAME],
173                                         $token[self::LINE]
174                                     );
176                                     throw new Mustache_Exception_SyntaxException($msg, $token);
177                                 }
178                             } else {
179                                 $lastName = $token[self::NAME];
180                                 if (substr($lastName, -1) === '}') {
181                                     $token[self::NAME] = trim(substr($lastName, 0, -1));
182                                 } else {
183                                     $msg = sprintf(
184                                         'Mismatched tag delimiters: %s on line %d',
185                                         $token[self::NAME],
186                                         $token[self::LINE]
187                                     );
189                                     throw new Mustache_Exception_SyntaxException($msg, $token);
190                                 }
191                             }
192                         }
194                         $this->buffer = '';
195                         $i += $this->ctagLen - 1;
196                         $this->state = self::IN_TEXT;
197                         $this->tokens[] = $token;
198                     } else {
199                         $this->buffer .= $text[$i];
200                     }
201                     break;
202             }
203         }
205         $this->flushBuffer();
207         // Restore the user's encoding...
208         // @codeCoverageIgnoreStart
209         if ($encoding) {
210             mb_internal_encoding($encoding);
211         }
212         // @codeCoverageIgnoreEnd
214         return $this->tokens;
215     }
217     /**
218      * Helper function to reset tokenizer internal state.
219      */
220     private function reset()
221     {
222         $this->state   = self::IN_TEXT;
223         $this->tagType = null;
224         $this->buffer  = '';
225         $this->tokens  = array();
226         $this->seenTag = false;
227         $this->line    = 0;
228         $this->otag    = '{{';
229         $this->ctag    = '}}';
230         $this->otagLen = 2;
231         $this->ctagLen = 2;
232     }
234     /**
235      * Flush the current buffer to a token.
236      */
237     private function flushBuffer()
238     {
239         if (strlen($this->buffer) > 0) {
240             $this->tokens[] = array(
241                 self::TYPE  => self::T_TEXT,
242                 self::LINE  => $this->line,
243                 self::VALUE => $this->buffer,
244             );
245             $this->buffer   = '';
246         }
247     }
249     /**
250      * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
251      *
252      * @param string $text  Mustache template source
253      * @param int    $index Current tokenizer index
254      *
255      * @return int New index value
256      */
257     private function changeDelimiters($text, $index)
258     {
259         $startIndex = strpos($text, '=', $index) + 1;
260         $close      = '=' . $this->ctag;
261         $closeIndex = strpos($text, $close, $index);
263         $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex)));
265         $this->tokens[] = array(
266             self::TYPE => self::T_DELIM_CHANGE,
267             self::LINE => $this->line,
268         );
270         return $closeIndex + strlen($close) - 1;
271     }
273     /**
274      * Set the current Mustache `otag` and `ctag` delimiters.
275      *
276      * @param string $delimiters
277      */
278     private function setDelimiters($delimiters)
279     {
280         list($otag, $ctag) = explode(' ', $delimiters);
281         $this->otag = $otag;
282         $this->ctag = $ctag;
283         $this->otagLen = strlen($otag);
284         $this->ctagLen = strlen($ctag);
285     }
287     /**
288      * Add pragma token.
289      *
290      * Pragmas are hoisted to the front of the template, so all pragma tokens
291      * will appear at the front of the token list.
292      *
293      * @param string $text
294      * @param int    $index
295      *
296      * @return int New index value
297      */
298     private function addPragma($text, $index)
299     {
300         $end    = strpos($text, $this->ctag, $index);
301         $pragma = trim(substr($text, $index + 2, $end - $index - 2));
303         // Pragmas are hoisted to the front of the template.
304         array_unshift($this->tokens, array(
305             self::TYPE => self::T_PRAGMA,
306             self::NAME => $pragma,
307             self::LINE => 0,
308         ));
310         return $end + $this->ctagLen - 1;
311     }
313     /**
314      * Test whether it's time to change tags.
315      *
316      * @param string $tag    Current tag name
317      * @param int    $tagLen Current tag name length
318      * @param string $text   Mustache template source
319      * @param int    $index  Current tokenizer index
320      *
321      * @return bool True if this is a closing section tag
322      */
323     private function tagChange($tag, $tagLen, $text, $index)
324     {
325         return substr($text, $index, $tagLen) === $tag;
326     }