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 Compiler class. 14 * 15 * This class is responsible for turning a Mustache token parse tree into normal PHP source code. 16 */ 17class Mustache_Compiler 18{ 19 private $pragmas; 20 private $defaultPragmas = array(); 21 private $sections; 22 private $blocks; 23 private $source; 24 private $indentNextLine; 25 private $customEscape; 26 private $entityFlags; 27 private $charset; 28 private $strictCallables; 29 30 /** 31 * Compile a Mustache token parse tree into PHP source code. 32 * 33 * @param string $source Mustache Template source code 34 * @param string $tree Parse tree of Mustache tokens 35 * @param string $name Mustache Template class name 36 * @param bool $customEscape (default: false) 37 * @param string $charset (default: 'UTF-8') 38 * @param bool $strictCallables (default: false) 39 * @param int $entityFlags (default: ENT_COMPAT) 40 * 41 * @return string Generated PHP source code 42 */ 43 public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT) 44 { 45 $this->pragmas = $this->defaultPragmas; 46 $this->sections = array(); 47 $this->blocks = array(); 48 $this->source = $source; 49 $this->indentNextLine = true; 50 $this->customEscape = $customEscape; 51 $this->entityFlags = $entityFlags; 52 $this->charset = $charset; 53 $this->strictCallables = $strictCallables; 54 55 return $this->writeCode($tree, $name); 56 } 57 58 /** 59 * Enable pragmas across all templates, regardless of the presence of pragma 60 * tags in the individual templates. 61 * 62 * @internal Users should set global pragmas in Mustache_Engine, not here :) 63 * 64 * @param string[] $pragmas 65 */ 66 public function setPragmas(array $pragmas) 67 { 68 $this->pragmas = array(); 69 foreach ($pragmas as $pragma) { 70 $this->pragmas[$pragma] = true; 71 } 72 $this->defaultPragmas = $this->pragmas; 73 } 74 75 /** 76 * Helper function for walking the Mustache token parse tree. 77 * 78 * @throws Mustache_Exception_SyntaxException upon encountering unknown token types 79 * 80 * @param array $tree Parse tree of Mustache tokens 81 * @param int $level (default: 0) 82 * 83 * @return string Generated PHP source code 84 */ 85 private function walk(array $tree, $level = 0) 86 { 87 $code = ''; 88 $level++; 89 foreach ($tree as $node) { 90 switch ($node[Mustache_Tokenizer::TYPE]) { 91 case Mustache_Tokenizer::T_PRAGMA: 92 $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; 93 break; 94 95 case Mustache_Tokenizer::T_SECTION: 96 $code .= $this->section( 97 $node[Mustache_Tokenizer::NODES], 98 $node[Mustache_Tokenizer::NAME], 99 isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), 100 $node[Mustache_Tokenizer::INDEX], 101 $node[Mustache_Tokenizer::END], 102 $node[Mustache_Tokenizer::OTAG], 103 $node[Mustache_Tokenizer::CTAG], 104 $level 105 ); 106 break; 107 108 case Mustache_Tokenizer::T_INVERTED: 109 $code .= $this->invertedSection( 110 $node[Mustache_Tokenizer::NODES], 111 $node[Mustache_Tokenizer::NAME], 112 isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), 113 $level 114 ); 115 break; 116 117 case Mustache_Tokenizer::T_PARTIAL: 118 $code .= $this->partial( 119 $node[Mustache_Tokenizer::NAME], 120 isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', 121 $level 122 ); 123 break; 124 125 case Mustache_Tokenizer::T_PARENT: 126 $code .= $this->parent( 127 $node[Mustache_Tokenizer::NAME], 128 isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', 129 $node[Mustache_Tokenizer::NODES], 130 $level 131 ); 132 break; 133 134 case Mustache_Tokenizer::T_BLOCK_ARG: 135 $code .= $this->blockArg( 136 $node[Mustache_Tokenizer::NODES], 137 $node[Mustache_Tokenizer::NAME], 138 $node[Mustache_Tokenizer::INDEX], 139 $node[Mustache_Tokenizer::END], 140 $node[Mustache_Tokenizer::OTAG], 141 $node[Mustache_Tokenizer::CTAG], 142 $level 143 ); 144 break; 145 146 case Mustache_Tokenizer::T_BLOCK_VAR: 147 $code .= $this->blockVar( 148 $node[Mustache_Tokenizer::NODES], 149 $node[Mustache_Tokenizer::NAME], 150 $node[Mustache_Tokenizer::INDEX], 151 $node[Mustache_Tokenizer::END], 152 $node[Mustache_Tokenizer::OTAG], 153 $node[Mustache_Tokenizer::CTAG], 154 $level 155 ); 156 break; 157 158 case Mustache_Tokenizer::T_COMMENT: 159 break; 160 161 case Mustache_Tokenizer::T_ESCAPED: 162 case Mustache_Tokenizer::T_UNESCAPED: 163 case Mustache_Tokenizer::T_UNESCAPED_2: 164 $code .= $this->variable( 165 $node[Mustache_Tokenizer::NAME], 166 isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), 167 $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED, 168 $level 169 ); 170 break; 171 172 case Mustache_Tokenizer::T_TEXT: 173 $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level); 174 break; 175 176 default: 177 throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node); 178 } 179 } 180 181 return $code; 182 } 183 184 const KLASS = '<?php 185 186 class %s extends Mustache_Template 187 { 188 private $lambdaHelper;%s 189 190 public function renderInternal(Mustache_Context $context, $indent = \'\') 191 { 192 $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); 193 $buffer = \'\'; 194 %s 195 196 return $buffer; 197 } 198 %s 199 %s 200 }'; 201 202 const KLASS_NO_LAMBDAS = '<?php 203 204 class %s extends Mustache_Template 205 {%s 206 public function renderInternal(Mustache_Context $context, $indent = \'\') 207 { 208 $buffer = \'\'; 209 %s 210 211 return $buffer; 212 } 213 }'; 214 215 const STRICT_CALLABLE = 'protected $strictCallables = true;'; 216 217 /** 218 * Generate Mustache Template class PHP source. 219 * 220 * @param array $tree Parse tree of Mustache tokens 221 * @param string $name Mustache Template class name 222 * 223 * @return string Generated PHP source code 224 */ 225 private function writeCode($tree, $name) 226 { 227 $code = $this->walk($tree); 228 $sections = implode("\n", $this->sections); 229 $blocks = implode("\n", $this->blocks); 230 $klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS; 231 232 $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; 233 234 return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks); 235 } 236 237 const BLOCK_VAR = ' 238 $blockFunction = $context->findInBlock(%s); 239 if (is_callable($blockFunction)) { 240 $buffer .= call_user_func($blockFunction, $context); 241 %s} 242 '; 243 244 const BLOCK_VAR_ELSE = '} else {%s'; 245 246 /** 247 * Generate Mustache Template inheritance block variable PHP source. 248 * 249 * @param array $nodes Array of child tokens 250 * @param string $id Section name 251 * @param int $start Section start offset 252 * @param int $end Section end offset 253 * @param string $otag Current Mustache opening tag 254 * @param string $ctag Current Mustache closing tag 255 * @param int $level 256 * 257 * @return string Generated PHP source code 258 */ 259 private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) 260 { 261 $id = var_export($id, true); 262 263 $else = $this->walk($nodes, $level); 264 if ($else !== '') { 265 $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else); 266 } 267 268 return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else); 269 } 270 271 const BLOCK_ARG = '%s => array($this, \'block%s\'),'; 272 273 /** 274 * Generate Mustache Template inheritance block argument PHP source. 275 * 276 * @param array $nodes Array of child tokens 277 * @param string $id Section name 278 * @param int $start Section start offset 279 * @param int $end Section end offset 280 * @param string $otag Current Mustache opening tag 281 * @param string $ctag Current Mustache closing tag 282 * @param int $level 283 * 284 * @return string Generated PHP source code 285 */ 286 private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level) 287 { 288 $key = $this->block($nodes); 289 $keystr = var_export($key, true); 290 $id = var_export($id, true); 291 292 return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key); 293 } 294 295 const BLOCK_FUNCTION = ' 296 public function block%s($context) 297 { 298 $indent = $buffer = \'\';%s 299 300 return $buffer; 301 } 302 '; 303 304 /** 305 * Generate Mustache Template inheritance block function PHP source. 306 * 307 * @param array $nodes Array of child tokens 308 * 309 * @return string key of new block function 310 */ 311 private function block($nodes) 312 { 313 $code = $this->walk($nodes, 0); 314 $key = ucfirst(md5($code)); 315 316 if (!isset($this->blocks[$key])) { 317 $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code); 318 } 319 320 return $key; 321 } 322 323 const SECTION_CALL = ' 324 // %s section 325 $value = $context->%s(%s);%s 326 $buffer .= $this->section%s($context, $indent, $value); 327 '; 328 329 const SECTION = ' 330 private function section%s(Mustache_Context $context, $indent, $value) 331 { 332 $buffer = \'\'; 333 334 if (%s) { 335 $source = %s; 336 $result = call_user_func($value, $source, %s); 337 if (strpos($result, \'{{\') === false) { 338 $buffer .= $result; 339 } else { 340 $buffer .= $this->mustache 341 ->loadLambda((string) $result%s) 342 ->renderInternal($context); 343 } 344 } elseif (!empty($value)) { 345 $values = $this->isIterable($value) ? $value : array($value); 346 foreach ($values as $value) { 347 $context->push($value); 348 %s 349 $context->pop(); 350 } 351 } 352 353 return $buffer; 354 } 355 '; 356 357 /** 358 * Generate Mustache Template section PHP source. 359 * 360 * @param array $nodes Array of child tokens 361 * @param string $id Section name 362 * @param string[] $filters Array of filters 363 * @param int $start Section start offset 364 * @param int $end Section end offset 365 * @param string $otag Current Mustache opening tag 366 * @param string $ctag Current Mustache closing tag 367 * @param int $level 368 * 369 * @return string Generated section PHP source code 370 */ 371 private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level) 372 { 373 $source = var_export(substr($this->source, $start, $end - $start), true); 374 $callable = $this->getCallable(); 375 376 if ($otag !== '{{' || $ctag !== '}}') { 377 $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true); 378 $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag); 379 $delims = ', ' . $delimTag; 380 } else { 381 $helper = '$this->lambdaHelper'; 382 $delims = ''; 383 } 384 385 $key = ucfirst(md5($delims . "\n" . $source)); 386 387 if (!isset($this->sections[$key])) { 388 $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2)); 389 } 390 391 $method = $this->getFindMethod($id); 392 $id = var_export($id, true); 393 $filters = $this->getFilters($filters, $level); 394 395 return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key); 396 } 397 398 const INVERTED_SECTION = ' 399 // %s inverted section 400 $value = $context->%s(%s);%s 401 if (empty($value)) { 402 %s 403 } 404 '; 405 406 /** 407 * Generate Mustache Template inverted section PHP source. 408 * 409 * @param array $nodes Array of child tokens 410 * @param string $id Section name 411 * @param string[] $filters Array of filters 412 * @param int $level 413 * 414 * @return string Generated inverted section PHP source code 415 */ 416 private function invertedSection($nodes, $id, $filters, $level) 417 { 418 $method = $this->getFindMethod($id); 419 $id = var_export($id, true); 420 $filters = $this->getFilters($filters, $level); 421 422 return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $filters, $this->walk($nodes, $level)); 423 } 424 425 const PARTIAL_INDENT = ', $indent . %s'; 426 const PARTIAL = ' 427 if ($partial = $this->mustache->loadPartial(%s)) { 428 $buffer .= $partial->renderInternal($context%s); 429 } 430 '; 431 432 /** 433 * Generate Mustache Template partial call PHP source. 434 * 435 * @param string $id Partial name 436 * @param string $indent Whitespace indent to apply to partial 437 * @param int $level 438 * 439 * @return string Generated partial call PHP source code 440 */ 441 private function partial($id, $indent, $level) 442 { 443 if ($indent !== '') { 444 $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true)); 445 } else { 446 $indentParam = ''; 447 } 448 449 return sprintf( 450 $this->prepare(self::PARTIAL, $level), 451 var_export($id, true), 452 $indentParam 453 ); 454 } 455 456 const PARENT = ' 457 if ($parent = $this->mustache->loadPartial(%s)) { 458 $context->pushBlockContext(array(%s 459 )); 460 $buffer .= $parent->renderInternal($context, $indent); 461 $context->popBlockContext(); 462 } 463 '; 464 465 const PARENT_NO_CONTEXT = ' 466 if ($parent = $this->mustache->loadPartial(%s)) { 467 $buffer .= $parent->renderInternal($context, $indent); 468 } 469 '; 470 471 /** 472 * Generate Mustache Template inheritance parent call PHP source. 473 * 474 * @param string $id Parent tag name 475 * @param string $indent Whitespace indent to apply to parent 476 * @param array $children Child nodes 477 * @param int $level 478 * 479 * @return string Generated PHP source code 480 */ 481 private function parent($id, $indent, array $children, $level) 482 { 483 $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); 484 485 if (empty($realChildren)) { 486 return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true)); 487 } 488 489 return sprintf( 490 $this->prepare(self::PARENT, $level), 491 var_export($id, true), 492 $this->walk($realChildren, $level + 1) 493 ); 494 } 495 496 /** 497 * Helper method for filtering out non-block-arg tokens. 498 * 499 * @param array $node 500 * 501 * @return bool True if $node is a block arg token 502 */ 503 private static function onlyBlockArgs(array $node) 504 { 505 return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG; 506 } 507 508 const VARIABLE = ' 509 $value = $this->resolveValue($context->%s(%s), $context);%s 510 $buffer .= %s%s; 511 '; 512 513 /** 514 * Generate Mustache Template variable interpolation PHP source. 515 * 516 * @param string $id Variable name 517 * @param string[] $filters Array of filters 518 * @param bool $escape Escape the variable value for output? 519 * @param int $level 520 * 521 * @return string Generated variable interpolation PHP source 522 */ 523 private function variable($id, $filters, $escape, $level) 524 { 525 $method = $this->getFindMethod($id); 526 $id = ($method !== 'last') ? var_export($id, true) : ''; 527 $filters = $this->getFilters($filters, $level); 528 $value = $escape ? $this->getEscape() : '$value'; 529 530 return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); 531 } 532 533 const FILTER = ' 534 $filter = $context->%s(%s); 535 if (!(%s)) { 536 throw new Mustache_Exception_UnknownFilterException(%s); 537 } 538 $value = call_user_func($filter, $value);%s 539 '; 540 541 /** 542 * Generate Mustache Template variable filtering PHP source. 543 * 544 * @param string[] $filters Array of filters 545 * @param int $level 546 * 547 * @return string Generated filter PHP source 548 */ 549 private function getFilters(array $filters, $level) 550 { 551 if (empty($filters)) { 552 return ''; 553 } 554 555 $name = array_shift($filters); 556 $method = $this->getFindMethod($name); 557 $filter = ($method !== 'last') ? var_export($name, true) : ''; 558 $callable = $this->getCallable('$filter'); 559 $msg = var_export($name, true); 560 561 return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level)); 562 } 563 564 const LINE = '$buffer .= "\n";'; 565 const TEXT = '$buffer .= %s%s;'; 566 567 /** 568 * Generate Mustache Template output Buffer call PHP source. 569 * 570 * @param string $text 571 * @param int $level 572 * 573 * @return string Generated output Buffer call PHP source 574 */ 575 private function text($text, $level) 576 { 577 $indentNextLine = (substr($text, -1) === "\n"); 578 $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true)); 579 $this->indentNextLine = $indentNextLine; 580 581 return $code; 582 } 583 584 /** 585 * Prepare PHP source code snippet for output. 586 * 587 * @param string $text 588 * @param int $bonus Additional indent level (default: 0) 589 * @param bool $prependNewline Prepend a newline to the snippet? (default: true) 590 * @param bool $appendNewline Append a newline to the snippet? (default: false) 591 * 592 * @return string PHP source code snippet 593 */ 594 private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false) 595 { 596 $text = ($prependNewline ? "\n" : '') . trim($text); 597 if ($prependNewline) { 598 $bonus++; 599 } 600 if ($appendNewline) { 601 $text .= "\n"; 602 } 603 604 return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text); 605 } 606 607 const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)'; 608 const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)'; 609 610 /** 611 * Get the current escaper. 612 * 613 * @param string $value (default: '$value') 614 * 615 * @return string Either a custom callback, or an inline call to `htmlspecialchars` 616 */ 617 private function getEscape($value = '$value') 618 { 619 if ($this->customEscape) { 620 return sprintf(self::CUSTOM_ESCAPE, $value); 621 } 622 623 return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true)); 624 } 625 626 /** 627 * Select the appropriate Context `find` method for a given $id. 628 * 629 * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`. 630 * 631 * @see Mustache_Context::find 632 * @see Mustache_Context::findDot 633 * @see Mustache_Context::last 634 * 635 * @param string $id Variable name 636 * 637 * @return string `find` method name 638 */ 639 private function getFindMethod($id) 640 { 641 if ($id === '.') { 642 return 'last'; 643 } 644 645 if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) { 646 if (substr($id, 0, 1) === '.') { 647 return 'findAnchoredDot'; 648 } 649 } 650 651 if (strpos($id, '.') === false) { 652 return 'find'; 653 } 654 655 return 'findDot'; 656 } 657 658 const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; 659 const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; 660 661 /** 662 * Helper function to compile strict vs lax "is callable" logic. 663 * 664 * @param string $variable (default: '$value') 665 * 666 * @return string "is callable" logic 667 */ 668 private function getCallable($variable = '$value') 669 { 670 $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; 671 672 return sprintf($tpl, $variable, $variable); 673 } 674 675 const LINE_INDENT = '$indent . '; 676 677 /** 678 * Get the current $indent prefix to write to the buffer. 679 * 680 * @return string "$indent . " or "" 681 */ 682 private function flushIndent() 683 { 684 if (!$this->indentNextLine) { 685 return ''; 686 } 687 688 $this->indentNextLine = false; 689 690 return self::LINE_INDENT; 691 } 692} 693