1<?php 2/** 3 * Handlebars base template 4 * contain some utility method to get context and helpers 5 * 6 * @category Xamin 7 * @package Handlebars 8 * @author fzerorubigd <fzerorubigd@gmail.com> 9 * @author Behrooz Shabani <everplays@gmail.com> 10 * @author Mardix <https://github.com/mardix> 11 * @copyright 2012 (c) ParsPooyesh Co 12 * @copyright 2013 (c) Behrooz Shabani 13 * @copyright 2013 (c) Mardix 14 * @license MIT 15 * @link http://voodoophp.org/docs/handlebars 16 */ 17 18namespace Handlebars; 19 20use InvalidArgumentException; 21use RuntimeException; 22 23class Template 24{ 25 /** 26 * @var Handlebars 27 */ 28 protected $handlebars; 29 30 protected $tree = []; 31 32 protected $source = ''; 33 34 /** 35 * @var array Run stack 36 */ 37 private $stack = []; 38 39 /** 40 * Handlebars template constructor 41 * 42 * @param Handlebars $engine handlebar engine 43 * @param array $tree Parsed tree 44 * @param string $source Handlebars source 45 */ 46 public function __construct(Handlebars $engine, $tree, $source) 47 { 48 $this->handlebars = $engine; 49 $this->tree = $tree; 50 $this->source = $source; 51 array_push($this->stack, [0, $this->getTree(), false]); 52 } 53 54 /** 55 * Get current tree 56 * 57 * @return array 58 */ 59 public function getTree() 60 { 61 return $this->tree; 62 } 63 64 /** 65 * Get current source 66 * 67 * @return string 68 */ 69 public function getSource() 70 { 71 return $this->source; 72 } 73 74 /** 75 * Get current engine associated with this object 76 * 77 * @return Handlebars 78 */ 79 public function getEngine() 80 { 81 return $this->handlebars; 82 } 83 84 /** 85 * set stop token for render and discard method 86 * 87 * @param string|false $token token to set as stop token or false to remove 88 * 89 * @return void 90 */ 91 public function setStopToken($token) 92 { 93 $topStack = array_pop($this->stack); 94 $topStack[2] = $token; 95 array_push($this->stack, $topStack); 96 } 97 98 /** 99 * get current stop token 100 * 101 * @return string|false 102 */ 103 public function getStopToken() 104 { 105 return end($this->stack)[2]; 106 } 107 108 /** 109 * Render top tree 110 * 111 * @param mixed $context current context 112 * 113 * @throws \RuntimeException 114 * @return string 115 */ 116 public function render($context) 117 { 118 if (!$context instanceof Context) { 119 $context = new Context($context, [ 120 'enableDataVariables' => $this->handlebars->isDataVariablesEnabled(), 121 ]); 122 } 123 $topTree = end($this->stack); // never pop a value from stack 124 list($index, $tree, $stop) = $topTree; 125 126 $buffer = ''; 127 while (array_key_exists($index, $tree)) { 128 $current = $tree[$index]; 129 $index++; 130 //if the section is exactly like waitFor 131 if (is_string($stop) 132 && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED 133 && $current[Tokenizer::NAME] === $stop 134 ) { 135 break; 136 } 137 switch ($current[Tokenizer::TYPE]) { 138 case Tokenizer::T_SECTION : 139 $newStack = isset($current[Tokenizer::NODES]) 140 ? $current[Tokenizer::NODES] : []; 141 array_push($this->stack, [0, $newStack, false]); 142 $buffer .= $this->section($context, $current); 143 array_pop($this->stack); 144 break; 145 case Tokenizer::T_INVERTED : 146 $newStack = isset($current[Tokenizer::NODES]) ? 147 $current[Tokenizer::NODES] : []; 148 array_push($this->stack, [0, $newStack, false]); 149 $buffer .= $this->inverted($context, $current); 150 array_pop($this->stack); 151 break; 152 case Tokenizer::T_COMMENT : 153 $buffer .= ''; 154 break; 155 case Tokenizer::T_PARTIAL: 156 case Tokenizer::T_PARTIAL_2: 157 $buffer .= $this->partial($context, $current); 158 break; 159 case Tokenizer::T_UNESCAPED: 160 case Tokenizer::T_UNESCAPED_2: 161 $buffer .= $this->variables($context, $current, false); 162 break; 163 case Tokenizer::T_ESCAPED: 164 $buffer .= $this->variables($context, $current, true); 165 break; 166 case Tokenizer::T_TEXT: 167 $buffer .= $current[Tokenizer::VALUE]; 168 break; 169 default: 170 throw new RuntimeException( 171 'Invalid node type : ' . json_encode($current) 172 ); 173 } 174 } 175 if ($stop) { 176 //Ok break here, the helper should be aware of this. 177 $newStack = array_pop($this->stack); 178 $newStack[0] = $index; 179 $newStack[2] = false; //No stop token from now on 180 array_push($this->stack, $newStack); 181 } 182 183 return $buffer; 184 } 185 186 /** 187 * Discard top tree 188 * 189 * @return string 190 */ 191 public function discard() 192 { 193 $topTree = end($this->stack); //This method never pop a value from stack 194 list($index, $tree, $stop) = $topTree; 195 while (array_key_exists($index, $tree)) { 196 $current = $tree[$index]; 197 $index++; 198 //if the section is exactly like waitFor 199 if (is_string($stop) 200 && $current[Tokenizer::TYPE] == Tokenizer::T_ESCAPED 201 && $current[Tokenizer::NAME] === $stop 202 ) { 203 break; 204 } 205 } 206 if ($stop) { 207 //Ok break here, the helper should be aware of this. 208 $newStack = array_pop($this->stack); 209 $newStack[0] = $index; 210 $newStack[2] = false; 211 array_push($this->stack, $newStack); 212 } 213 214 return ''; 215 } 216 217 /** 218 * Process section nodes 219 * 220 * @param Context $context current context 221 * @param array $current section node data 222 * 223 * @throws \RuntimeException 224 * @return string the result 225 */ 226 private function section(Context $context, $current) 227 { 228 $helpers = $this->handlebars->getHelpers(); 229 $sectionName = $current[Tokenizer::NAME]; 230 if ($helpers->has($sectionName)) { 231 if (isset($current[Tokenizer::END])) { 232 $source = substr( 233 $this->getSource(), 234 $current[Tokenizer::INDEX], 235 $current[Tokenizer::END] - $current[Tokenizer::INDEX] 236 ); 237 } else { 238 $source = ''; 239 } 240 $params = [ 241 $this, //First argument is this template 242 $context, //Second is current context 243 $current[Tokenizer::ARGS], //Arguments 244 $source 245 ]; 246 247 $return = call_user_func_array($helpers->$sectionName, $params); 248 if ($return instanceof String) { 249 return $this->handlebars->loadString($return)->render($context); 250 } else { 251 return $return; 252 } 253 } elseif (trim($current[Tokenizer::ARGS]) == '') { 254 // fallback to mustache style each/with/for just if there is 255 // no argument at all. 256 try { 257 $sectionVar = $context->get($sectionName, true); 258 } catch (InvalidArgumentException $e) { 259 throw new RuntimeException( 260 $sectionName . ' is not registered as a helper' 261 ); 262 } 263 $buffer = ''; 264 if (is_array($sectionVar) || $sectionVar instanceof \Traversable) { 265 foreach ($sectionVar as $index => $d) { 266 $context->pushIndex($index); 267 $context->push($d); 268 $buffer .= $this->render($context); 269 $context->pop(); 270 $context->popIndex(); 271 } 272 } elseif (is_object($sectionVar)) { 273 //Act like with 274 $context->push($sectionVar); 275 $buffer = $this->render($context); 276 $context->pop(); 277 } elseif ($sectionVar) { 278 $buffer = $this->render($context); 279 } 280 281 return $buffer; 282 } else { 283 throw new RuntimeException( 284 $sectionName . ' is not registered as a helper' 285 ); 286 } 287 } 288 289 /** 290 * Process inverted section 291 * 292 * @param Context $context current context 293 * @param array $current section node data 294 * 295 * @return string the result 296 */ 297 private function inverted(Context $context, $current) 298 { 299 $sectionName = $current[Tokenizer::NAME]; 300 $data = $context->get($sectionName); 301 if (!$data) { 302 return $this->render($context); 303 } else { 304 //No need to discard here, since it has no else 305 return ''; 306 } 307 } 308 309 /** 310 * Process partial section 311 * 312 * @param Context $context current context 313 * @param array $current section node data 314 * 315 * @return string the result 316 */ 317 private function partial(Context $context, $current) 318 { 319 $partial = $this->handlebars->loadPartial($current[Tokenizer::NAME]); 320 321 if ($current[Tokenizer::ARGS]) { 322 $context = $context->get($current[Tokenizer::ARGS]); 323 } 324 325 return $partial->render($context); 326 } 327 328 /** 329 * Process partial section 330 * 331 * @param Context $context current context 332 * @param array $current section node data 333 * @param boolean $escaped escape result or not 334 * 335 * @return string the result 336 */ 337 private function variables(Context $context, $current, $escaped) 338 { 339 $name = $current[Tokenizer::NAME]; 340 $value = $context->get($name); 341 342 // If @data variables are enabled, use the more complex algorithm for handling the the variables otherwise 343 // use the previous version. 344 if ($this->handlebars->isDataVariablesEnabled()) { 345 if (substr(trim($name), 0, 1) == '@') { 346 $variable = $context->getDataVariable($name); 347 if (is_bool($variable)) { 348 return $variable ? 'true' : 'false'; 349 } 350 return $variable; 351 } 352 } else { 353 // If @data variables are not enabled, then revert back to legacy behavior 354 if ($name == '@index') { 355 return $context->lastIndex(); 356 } 357 if ($name == '@key') { 358 return $context->lastKey(); 359 } 360 } 361 362 if ($escaped) { 363 $args = $this->handlebars->getEscapeArgs(); 364 array_unshift($args, $value); 365 $value = call_user_func_array( 366 $this->handlebars->getEscape(), 367 array_values($args) 368 ); 369 } 370 371 return $value; 372 } 373 374 public function __clone() 375 { 376 return $this; 377 } 378} 379