1*04fd306cSNickeau<?php 2*04fd306cSNickeau/** 3*04fd306cSNickeau * Handlebars tokenizer (based on mustache) 4*04fd306cSNickeau * 5*04fd306cSNickeau * @category Xamin 6*04fd306cSNickeau * @package Handlebars 7*04fd306cSNickeau * @author Justin Hileman <dontknow@example.org> 8*04fd306cSNickeau * @author fzerorubigd <fzerorubigd@gmail.com> 9*04fd306cSNickeau * @author Behrooz Shabani <everplays@gmail.com> 10*04fd306cSNickeau * @author Mardix <https://github.com/mardix> 11*04fd306cSNickeau * @copyright 2012 (c) ParsPooyesh Co 12*04fd306cSNickeau * @copyright 2013 (c) Behrooz Shabani 13*04fd306cSNickeau * @copyright 2013 (c) Mardix 14*04fd306cSNickeau * @license MIT 15*04fd306cSNickeau * @link http://voodoophp.org/docs/handlebars 16*04fd306cSNickeau */ 17*04fd306cSNickeau 18*04fd306cSNickeaunamespace Handlebars; 19*04fd306cSNickeau 20*04fd306cSNickeauclass Tokenizer 21*04fd306cSNickeau{ 22*04fd306cSNickeau 23*04fd306cSNickeau // Finite state machine states 24*04fd306cSNickeau const IN_TEXT = 0; 25*04fd306cSNickeau const IN_TAG_TYPE = 1; 26*04fd306cSNickeau const IN_TAG = 2; 27*04fd306cSNickeau 28*04fd306cSNickeau // Token types 29*04fd306cSNickeau const T_SECTION = '#'; 30*04fd306cSNickeau const T_INVERTED = '^'; 31*04fd306cSNickeau const T_END_SECTION = '/'; 32*04fd306cSNickeau const T_COMMENT = '!'; 33*04fd306cSNickeau // XXX: remove partials support from tokenizer and make it a helper? 34*04fd306cSNickeau const T_PARTIAL = '>'; 35*04fd306cSNickeau const T_PARTIAL_2 = '<'; 36*04fd306cSNickeau const T_DELIM_CHANGE = '='; 37*04fd306cSNickeau const T_ESCAPED = '_v'; 38*04fd306cSNickeau const T_UNESCAPED = '{'; 39*04fd306cSNickeau const T_UNESCAPED_2 = '&'; 40*04fd306cSNickeau const T_TEXT = '_t'; 41*04fd306cSNickeau 42*04fd306cSNickeau // Valid token types 43*04fd306cSNickeau private $tagTypes = [ 44*04fd306cSNickeau self::T_SECTION => true, 45*04fd306cSNickeau self::T_INVERTED => true, 46*04fd306cSNickeau self::T_END_SECTION => true, 47*04fd306cSNickeau self::T_COMMENT => true, 48*04fd306cSNickeau self::T_PARTIAL => true, 49*04fd306cSNickeau self::T_PARTIAL_2 => true, 50*04fd306cSNickeau self::T_DELIM_CHANGE => true, 51*04fd306cSNickeau self::T_ESCAPED => true, 52*04fd306cSNickeau self::T_UNESCAPED => true, 53*04fd306cSNickeau self::T_UNESCAPED_2 => true, 54*04fd306cSNickeau ]; 55*04fd306cSNickeau 56*04fd306cSNickeau // Interpolated tags 57*04fd306cSNickeau private $interpolatedTags = [ 58*04fd306cSNickeau self::T_ESCAPED => true, 59*04fd306cSNickeau self::T_UNESCAPED => true, 60*04fd306cSNickeau self::T_UNESCAPED_2 => true, 61*04fd306cSNickeau ]; 62*04fd306cSNickeau 63*04fd306cSNickeau // Token properties 64*04fd306cSNickeau const TYPE = 'type'; 65*04fd306cSNickeau const NAME = 'name'; 66*04fd306cSNickeau const OTAG = 'otag'; 67*04fd306cSNickeau const CTAG = 'ctag'; 68*04fd306cSNickeau const INDEX = 'index'; 69*04fd306cSNickeau const END = 'end'; 70*04fd306cSNickeau const INDENT = 'indent'; 71*04fd306cSNickeau const NODES = 'nodes'; 72*04fd306cSNickeau const VALUE = 'value'; 73*04fd306cSNickeau const ARGS = 'args'; 74*04fd306cSNickeau 75*04fd306cSNickeau protected $state; 76*04fd306cSNickeau protected $tagType; 77*04fd306cSNickeau protected $tag; 78*04fd306cSNickeau protected $buffer; 79*04fd306cSNickeau protected $tokens; 80*04fd306cSNickeau protected $seenTag; 81*04fd306cSNickeau protected $lineStart; 82*04fd306cSNickeau protected $otag; 83*04fd306cSNickeau protected $ctag; 84*04fd306cSNickeau 85*04fd306cSNickeau /** 86*04fd306cSNickeau * Scan and tokenize template source. 87*04fd306cSNickeau * 88*04fd306cSNickeau * @param string $text Mustache template source to tokenize 89*04fd306cSNickeau * @param string $delimiters Optional, pass opening and closing delimiters 90*04fd306cSNickeau * 91*04fd306cSNickeau * @return array Set of Mustache tokens 92*04fd306cSNickeau */ 93*04fd306cSNickeau public function scan($text, $delimiters = null) 94*04fd306cSNickeau { 95*04fd306cSNickeau if ($text instanceof HandlebarsString) { 96*04fd306cSNickeau $text = $text->getString(); 97*04fd306cSNickeau } 98*04fd306cSNickeau 99*04fd306cSNickeau $this->reset(); 100*04fd306cSNickeau 101*04fd306cSNickeau if ($delimiters !== null && $delimiters = trim($delimiters)) { 102*04fd306cSNickeau list($otag, $ctag) = explode(' ', $delimiters); 103*04fd306cSNickeau $this->otag = $otag; 104*04fd306cSNickeau $this->ctag = $ctag; 105*04fd306cSNickeau } 106*04fd306cSNickeau 107*04fd306cSNickeau $openingTagLength = strlen($this->otag); 108*04fd306cSNickeau $closingTagLength = strlen($this->ctag); 109*04fd306cSNickeau $firstOpeningTagCharacter = $this->otag[0]; 110*04fd306cSNickeau $firstClosingTagCharacter = $this->ctag[0]; 111*04fd306cSNickeau 112*04fd306cSNickeau $len = strlen($text); 113*04fd306cSNickeau 114*04fd306cSNickeau for ($i = 0; $i < $len; $i++) { 115*04fd306cSNickeau 116*04fd306cSNickeau $character = $text[$i]; 117*04fd306cSNickeau 118*04fd306cSNickeau switch ($this->state) { 119*04fd306cSNickeau 120*04fd306cSNickeau case self::IN_TEXT: 121*04fd306cSNickeau if ($character === $firstOpeningTagCharacter && $this->tagChange($this->otag, $text, $i, $openingTagLength) 122*04fd306cSNickeau ) { 123*04fd306cSNickeau $i--; 124*04fd306cSNickeau $this->flushBuffer(); 125*04fd306cSNickeau $this->state = self::IN_TAG_TYPE; 126*04fd306cSNickeau } else { 127*04fd306cSNickeau if ($character == "\n") { 128*04fd306cSNickeau $this->filterLine(); 129*04fd306cSNickeau } else { 130*04fd306cSNickeau $this->buffer .= $character; 131*04fd306cSNickeau } 132*04fd306cSNickeau } 133*04fd306cSNickeau break; 134*04fd306cSNickeau 135*04fd306cSNickeau case self::IN_TAG_TYPE: 136*04fd306cSNickeau 137*04fd306cSNickeau $i += $openingTagLength - 1; 138*04fd306cSNickeau if (isset($this->tagTypes[$text[$i + 1]])) { 139*04fd306cSNickeau $tag = $text[$i + 1]; 140*04fd306cSNickeau $this->tagType = $tag; 141*04fd306cSNickeau } else { 142*04fd306cSNickeau $tag = null; 143*04fd306cSNickeau $this->tagType = self::T_ESCAPED; 144*04fd306cSNickeau } 145*04fd306cSNickeau 146*04fd306cSNickeau if ($this->tagType === self::T_DELIM_CHANGE) { 147*04fd306cSNickeau $i = $this->changeDelimiters($text, $i); 148*04fd306cSNickeau $openingTagLength = strlen($this->otag); 149*04fd306cSNickeau $closingTagLength = strlen($this->ctag); 150*04fd306cSNickeau $firstOpeningTagCharacter = $this->otag[0]; 151*04fd306cSNickeau $firstClosingTagCharacter = $this->ctag[0]; 152*04fd306cSNickeau 153*04fd306cSNickeau $this->state = self::IN_TEXT; 154*04fd306cSNickeau } else { 155*04fd306cSNickeau if ($tag !== null) { 156*04fd306cSNickeau $i++; 157*04fd306cSNickeau } 158*04fd306cSNickeau $this->state = self::IN_TAG; 159*04fd306cSNickeau } 160*04fd306cSNickeau $this->seenTag = $i; 161*04fd306cSNickeau break; 162*04fd306cSNickeau 163*04fd306cSNickeau default: 164*04fd306cSNickeau if ($character === $firstClosingTagCharacter && $this->tagChange($this->ctag, $text, $i, $closingTagLength)) { 165*04fd306cSNickeau // Sections (Helpers) can accept parameters 166*04fd306cSNickeau // Same thing for Partials (little known fact) 167*04fd306cSNickeau if (in_array($this->tagType, [ 168*04fd306cSNickeau self::T_SECTION, 169*04fd306cSNickeau self::T_PARTIAL, 170*04fd306cSNickeau self::T_PARTIAL_2] 171*04fd306cSNickeau )) { 172*04fd306cSNickeau $newBuffer = explode(' ', trim($this->buffer), 2); 173*04fd306cSNickeau $args = ''; 174*04fd306cSNickeau if (count($newBuffer) == 2) { 175*04fd306cSNickeau $args = $newBuffer[1]; 176*04fd306cSNickeau } 177*04fd306cSNickeau $this->buffer = $newBuffer[0]; 178*04fd306cSNickeau } 179*04fd306cSNickeau $t = [ 180*04fd306cSNickeau self::TYPE => $this->tagType, 181*04fd306cSNickeau self::NAME => trim($this->buffer), 182*04fd306cSNickeau self::OTAG => $this->otag, 183*04fd306cSNickeau self::CTAG => $this->ctag, 184*04fd306cSNickeau self::INDEX => ($this->tagType == self::T_END_SECTION) ? 185*04fd306cSNickeau $this->seenTag - $openingTagLength : 186*04fd306cSNickeau $i + strlen($this->ctag), 187*04fd306cSNickeau ]; 188*04fd306cSNickeau if (isset($args)) { 189*04fd306cSNickeau $t[self::ARGS] = $args; 190*04fd306cSNickeau } 191*04fd306cSNickeau $this->tokens[] = $t; 192*04fd306cSNickeau unset($t); 193*04fd306cSNickeau unset($args); 194*04fd306cSNickeau $this->buffer = ''; 195*04fd306cSNickeau $i += strlen($this->ctag) - 1; 196*04fd306cSNickeau $this->state = self::IN_TEXT; 197*04fd306cSNickeau if ($this->tagType == self::T_UNESCAPED) { 198*04fd306cSNickeau if ($this->ctag == '}}') { 199*04fd306cSNickeau $i++; 200*04fd306cSNickeau } else { 201*04fd306cSNickeau // Clean up `{{{ tripleStache }}}` style tokens. 202*04fd306cSNickeau $lastIndex = count($this->tokens) - 1; 203*04fd306cSNickeau $lastName = $this->tokens[$lastIndex][self::NAME]; 204*04fd306cSNickeau if (substr($lastName, -1) === '}') { 205*04fd306cSNickeau $this->tokens[$lastIndex][self::NAME] = trim( 206*04fd306cSNickeau substr($lastName, 0, -1) 207*04fd306cSNickeau ); 208*04fd306cSNickeau } 209*04fd306cSNickeau } 210*04fd306cSNickeau } 211*04fd306cSNickeau } else { 212*04fd306cSNickeau $this->buffer .= $character; 213*04fd306cSNickeau } 214*04fd306cSNickeau break; 215*04fd306cSNickeau } 216*04fd306cSNickeau 217*04fd306cSNickeau } 218*04fd306cSNickeau 219*04fd306cSNickeau $this->filterLine(true); 220*04fd306cSNickeau 221*04fd306cSNickeau return $this->tokens; 222*04fd306cSNickeau } 223*04fd306cSNickeau 224*04fd306cSNickeau /** 225*04fd306cSNickeau * Helper function to reset tokenizer internal state. 226*04fd306cSNickeau * 227*04fd306cSNickeau * @return void 228*04fd306cSNickeau */ 229*04fd306cSNickeau protected function reset() 230*04fd306cSNickeau { 231*04fd306cSNickeau $this->state = self::IN_TEXT; 232*04fd306cSNickeau $this->tagType = null; 233*04fd306cSNickeau $this->tag = null; 234*04fd306cSNickeau $this->buffer = ''; 235*04fd306cSNickeau $this->tokens = []; 236*04fd306cSNickeau $this->seenTag = false; 237*04fd306cSNickeau $this->lineStart = 0; 238*04fd306cSNickeau $this->otag = '{{'; 239*04fd306cSNickeau $this->ctag = '}}'; 240*04fd306cSNickeau } 241*04fd306cSNickeau 242*04fd306cSNickeau /** 243*04fd306cSNickeau * Flush the current buffer to a token. 244*04fd306cSNickeau * 245*04fd306cSNickeau * @return void 246*04fd306cSNickeau */ 247*04fd306cSNickeau protected function flushBuffer() 248*04fd306cSNickeau { 249*04fd306cSNickeau if (!empty($this->buffer)) { 250*04fd306cSNickeau $this->tokens[] = [ 251*04fd306cSNickeau self::TYPE => self::T_TEXT, 252*04fd306cSNickeau self::VALUE => $this->buffer 253*04fd306cSNickeau ]; 254*04fd306cSNickeau $this->buffer = ''; 255*04fd306cSNickeau } 256*04fd306cSNickeau } 257*04fd306cSNickeau 258*04fd306cSNickeau /** 259*04fd306cSNickeau * Test whether the current line is entirely made up of whitespace. 260*04fd306cSNickeau * 261*04fd306cSNickeau * @return boolean True if the current line is all whitespace 262*04fd306cSNickeau */ 263*04fd306cSNickeau protected function lineIsWhitespace() 264*04fd306cSNickeau { 265*04fd306cSNickeau $tokensCount = count($this->tokens); 266*04fd306cSNickeau for ($j = $this->lineStart; $j < $tokensCount; $j++) { 267*04fd306cSNickeau $token = $this->tokens[$j]; 268*04fd306cSNickeau if (isset($this->tagTypes[$token[self::TYPE]])) { 269*04fd306cSNickeau if (isset($this->interpolatedTags[$token[self::TYPE]])) { 270*04fd306cSNickeau return false; 271*04fd306cSNickeau } 272*04fd306cSNickeau } elseif ($token[self::TYPE] == self::T_TEXT) { 273*04fd306cSNickeau if (preg_match('/\S/', $token[self::VALUE])) { 274*04fd306cSNickeau return false; 275*04fd306cSNickeau } 276*04fd306cSNickeau } 277*04fd306cSNickeau } 278*04fd306cSNickeau 279*04fd306cSNickeau return true; 280*04fd306cSNickeau } 281*04fd306cSNickeau 282*04fd306cSNickeau /** 283*04fd306cSNickeau * Filter out whitespace-only lines and store indent levels for partials. 284*04fd306cSNickeau * 285*04fd306cSNickeau * @param bool $noNewLine Suppress the newline? (default: false) 286*04fd306cSNickeau * 287*04fd306cSNickeau * @return void 288*04fd306cSNickeau */ 289*04fd306cSNickeau protected function filterLine($noNewLine = false) 290*04fd306cSNickeau { 291*04fd306cSNickeau $this->flushBuffer(); 292*04fd306cSNickeau if ($this->seenTag && $this->lineIsWhitespace()) { 293*04fd306cSNickeau $tokensCount = count($this->tokens); 294*04fd306cSNickeau for ($j = $this->lineStart; $j < $tokensCount; $j++) { 295*04fd306cSNickeau if ($this->tokens[$j][self::TYPE] == self::T_TEXT) { 296*04fd306cSNickeau if (isset($this->tokens[$j + 1]) 297*04fd306cSNickeau && $this->tokens[$j + 1][self::TYPE] == self::T_PARTIAL 298*04fd306cSNickeau ) { 299*04fd306cSNickeau $this->tokens[$j + 1][self::INDENT] 300*04fd306cSNickeau = $this->tokens[$j][self::VALUE]; 301*04fd306cSNickeau } 302*04fd306cSNickeau 303*04fd306cSNickeau $this->tokens[$j] = null; 304*04fd306cSNickeau } 305*04fd306cSNickeau } 306*04fd306cSNickeau } elseif (!$noNewLine) { 307*04fd306cSNickeau $this->tokens[] = [self::TYPE => self::T_TEXT, self::VALUE => "\n"]; 308*04fd306cSNickeau } 309*04fd306cSNickeau 310*04fd306cSNickeau $this->seenTag = false; 311*04fd306cSNickeau $this->lineStart = count($this->tokens); 312*04fd306cSNickeau } 313*04fd306cSNickeau 314*04fd306cSNickeau /** 315*04fd306cSNickeau * Change the current Mustache delimiters. Set new `otag` and `ctag` values. 316*04fd306cSNickeau * 317*04fd306cSNickeau * @param string $text Mustache template source 318*04fd306cSNickeau * @param int $index Current tokenizer index 319*04fd306cSNickeau * 320*04fd306cSNickeau * @return int New index value 321*04fd306cSNickeau */ 322*04fd306cSNickeau protected function changeDelimiters($text, $index) 323*04fd306cSNickeau { 324*04fd306cSNickeau $startIndex = strpos($text, '=', $index) + 1; 325*04fd306cSNickeau $close = '=' . $this->ctag; 326*04fd306cSNickeau $closeIndex = strpos($text, $close, $index); 327*04fd306cSNickeau 328*04fd306cSNickeau list($otag, $ctag) = explode( 329*04fd306cSNickeau ' ', 330*04fd306cSNickeau trim(substr($text, $startIndex, $closeIndex - $startIndex)) 331*04fd306cSNickeau ); 332*04fd306cSNickeau $this->otag = $otag; 333*04fd306cSNickeau $this->ctag = $ctag; 334*04fd306cSNickeau 335*04fd306cSNickeau return $closeIndex + strlen($close) - 1; 336*04fd306cSNickeau } 337*04fd306cSNickeau 338*04fd306cSNickeau /** 339*04fd306cSNickeau * Test whether it's time to change tags. 340*04fd306cSNickeau * 341*04fd306cSNickeau * @param string $tag Current tag name 342*04fd306cSNickeau * @param string $text Mustache template source 343*04fd306cSNickeau * @param int $index Current tokenizer index 344*04fd306cSNickeau * @param int $tagLength Length of the opening/closing tag string 345*04fd306cSNickeau * 346*04fd306cSNickeau * @return boolean True if this is a closing section tag 347*04fd306cSNickeau */ 348*04fd306cSNickeau protected function tagChange($tag, $text, $index, $tagLength) 349*04fd306cSNickeau { 350*04fd306cSNickeau return substr($text, $index, $tagLength) === $tag; 351*04fd306cSNickeau } 352*04fd306cSNickeau 353*04fd306cSNickeau} 354