See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
1 <?php 2 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 */ 11 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; 23 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'; 39 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 ); 55 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'; 68 69 private $state; 70 private $tagType; 71 private $buffer; 72 private $tokens; 73 private $seenTag; 74 private $line; 75 76 private $otag; 77 private $otagChar; 78 private $otagLen; 79 80 private $ctag; 81 private $ctagChar; 82 private $ctagLen; 83 84 /** 85 * Scan and tokenize template source. 86 * 87 * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered 88 * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid 89 * 90 * @param string $text Mustache template source to tokenize 91 * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string) 92 * 93 * @return array Set of Mustache tokens 94 */ 95 public function scan($text, $delimiters = '') 96 { 97 // Setting mbstring.func_overload makes things *really* slow. 98 // Let's do everyone a favor and scan this string as ASCII instead. 99 // 100 // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it 101 // when we remove support for older versions of PHP). 102 // 103 // @codeCoverageIgnoreStart 104 $encoding = null; 105 if (version_compare(PHP_VERSION, '8.0.0', '<')) { 106 if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) { 107 $encoding = mb_internal_encoding(); 108 mb_internal_encoding('ASCII'); 109 } 110 } 111 // @codeCoverageIgnoreEnd 112 113 $this->reset(); 114 115 if (is_string($delimiters) && $delimiters = trim($delimiters)) { 116 $this->setDelimiters($delimiters); 117 } 118 119 $len = strlen($text); 120 for ($i = 0; $i < $len; $i++) { 121 switch ($this->state) { 122 case self::IN_TEXT: 123 $char = $text[$i]; 124 // Test whether it's time to change tags. 125 if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) { 126 $i--; 127 $this->flushBuffer(); 128 $this->state = self::IN_TAG_TYPE; 129 } else { 130 $this->buffer .= $char; 131 if ($char === "\n") { 132 $this->flushBuffer(); 133 $this->line++; 134 } 135 } 136 break; 137 138 case self::IN_TAG_TYPE: 139 $i += $this->otagLen - 1; 140 $char = $text[$i + 1]; 141 if (isset(self::$tagTypes[$char])) { 142 $tag = $char; 143 $this->tagType = $tag; 144 } else { 145 $tag = null; 146 $this->tagType = self::T_ESCAPED; 147 } 148 149 if ($this->tagType === self::T_DELIM_CHANGE) { 150 $i = $this->changeDelimiters($text, $i); 151 $this->state = self::IN_TEXT; 152 } elseif ($this->tagType === self::T_PRAGMA) { 153 $i = $this->addPragma($text, $i); 154 $this->state = self::IN_TEXT; 155 } else { 156 if ($tag !== null) { 157 $i++; 158 } 159 $this->state = self::IN_TAG; 160 } 161 $this->seenTag = $i; 162 break; 163 164 default: 165 $char = $text[$i]; 166 // Test whether it's time to change tags. 167 if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) { 168 $token = array( 169 self::TYPE => $this->tagType, 170 self::NAME => trim($this->buffer), 171 self::OTAG => $this->otag, 172 self::CTAG => $this->ctag, 173 self::LINE => $this->line, 174 self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen, 175 ); 176 177 if ($this->tagType === self::T_UNESCAPED) { 178 // Clean up `{{{ tripleStache }}}` style tokens. 179 if ($this->ctag === '}}') { 180 if (($i + 2 < $len) && $text[$i + 2] === '}') { 181 $i++; 182 } else { 183 $msg = sprintf( 184 'Mismatched tag delimiters: %s on line %d', 185 $token[self::NAME], 186 $token[self::LINE] 187 ); 188 189 throw new Mustache_Exception_SyntaxException($msg, $token); 190 } 191 } else { 192 $lastName = $token[self::NAME]; 193 if (substr($lastName, -1) === '}') { 194 $token[self::NAME] = trim(substr($lastName, 0, -1)); 195 } else { 196 $msg = sprintf( 197 'Mismatched tag delimiters: %s on line %d', 198 $token[self::NAME], 199 $token[self::LINE] 200 ); 201 202 throw new Mustache_Exception_SyntaxException($msg, $token); 203 } 204 } 205 } 206 207 $this->buffer = ''; 208 $i += $this->ctagLen - 1; 209 $this->state = self::IN_TEXT; 210 $this->tokens[] = $token; 211 } else { 212 $this->buffer .= $char; 213 } 214 break; 215 } 216 } 217 218 if ($this->state !== self::IN_TEXT) { 219 $this->throwUnclosedTagException(); 220 } 221 222 $this->flushBuffer(); 223 224 // Restore the user's encoding... 225 // @codeCoverageIgnoreStart 226 if ($encoding) { 227 mb_internal_encoding($encoding); 228 } 229 // @codeCoverageIgnoreEnd 230 231 return $this->tokens; 232 } 233 234 /** 235 * Helper function to reset tokenizer internal state. 236 */ 237 private function reset() 238 { 239 $this->state = self::IN_TEXT; 240 $this->tagType = null; 241 $this->buffer = ''; 242 $this->tokens = array(); 243 $this->seenTag = false; 244 $this->line = 0; 245 246 $this->otag = '{{'; 247 $this->otagChar = '{'; 248 $this->otagLen = 2; 249 250 $this->ctag = '}}'; 251 $this->ctagChar = '}'; 252 $this->ctagLen = 2; 253 } 254 255 /** 256 * Flush the current buffer to a token. 257 */ 258 private function flushBuffer() 259 { 260 if (strlen($this->buffer) > 0) { 261 $this->tokens[] = array( 262 self::TYPE => self::T_TEXT, 263 self::LINE => $this->line, 264 self::VALUE => $this->buffer, 265 ); 266 $this->buffer = ''; 267 } 268 } 269 270 /** 271 * Change the current Mustache delimiters. Set new `otag` and `ctag` values. 272 * 273 * @throws Mustache_Exception_SyntaxException when delimiter string is invalid 274 * 275 * @param string $text Mustache template source 276 * @param int $index Current tokenizer index 277 * 278 * @return int New index value 279 */ 280 private function changeDelimiters($text, $index) 281 { 282 $startIndex = strpos($text, '=', $index) + 1; 283 $close = '=' . $this->ctag; 284 $closeIndex = strpos($text, $close, $index); 285 286 if ($closeIndex === false) { 287 $this->throwUnclosedTagException(); 288 } 289 290 $token = array( 291 self::TYPE => self::T_DELIM_CHANGE, 292 self::LINE => $this->line, 293 ); 294 295 try { 296 $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex))); 297 } catch (Mustache_Exception_InvalidArgumentException $e) { 298 throw new Mustache_Exception_SyntaxException($e->getMessage(), $token); 299 } 300 301 $this->tokens[] = $token; 302 303 return $closeIndex + strlen($close) - 1; 304 } 305 306 /** 307 * Set the current Mustache `otag` and `ctag` delimiters. 308 * 309 * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid 310 * 311 * @param string $delimiters 312 */ 313 private function setDelimiters($delimiters) 314 { 315 if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) { 316 throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters)); 317 } 318 319 list($_, $otag, $ctag) = $matches; 320 321 $this->otag = $otag; 322 $this->otagChar = $otag[0]; 323 $this->otagLen = strlen($otag); 324 325 $this->ctag = $ctag; 326 $this->ctagChar = $ctag[0]; 327 $this->ctagLen = strlen($ctag); 328 } 329 330 /** 331 * Add pragma token. 332 * 333 * Pragmas are hoisted to the front of the template, so all pragma tokens 334 * will appear at the front of the token list. 335 * 336 * @param string $text 337 * @param int $index 338 * 339 * @return int New index value 340 */ 341 private function addPragma($text, $index) 342 { 343 $end = strpos($text, $this->ctag, $index); 344 if ($end === false) { 345 $this->throwUnclosedTagException(); 346 } 347 348 $pragma = trim(substr($text, $index + 2, $end - $index - 2)); 349 350 // Pragmas are hoisted to the front of the template. 351 array_unshift($this->tokens, array( 352 self::TYPE => self::T_PRAGMA, 353 self::NAME => $pragma, 354 self::LINE => 0, 355 )); 356 357 return $end + $this->ctagLen - 1; 358 } 359 360 private function throwUnclosedTagException() 361 { 362 $name = trim($this->buffer); 363 if ($name !== '') { 364 $msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line); 365 } else { 366 $msg = sprintf('Unclosed tag on line %d', $this->line); 367 } 368 369 throw new Mustache_Exception_SyntaxException($msg, array( 370 self::TYPE => $this->tagType, 371 self::NAME => $name, 372 self::OTAG => $this->otag, 373 self::CTAG => $this->ctag, 374 self::LINE => $this->line, 375 self::INDEX => $this->seenTag - $this->otagLen, 376 )); 377 } 378 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body