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 * A Mustache implementation in PHP. 14 * 15 * {@link http://defunkt.github.com/mustache} 16 * 17 * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view 18 * logic from template files. In fact, it is not even possible to embed logic in the template. 19 * 20 * This is very, very rad. 21 * 22 * @author Justin Hileman {@link http://justinhileman.com} 23 */ 24class Mustache_Engine 25{ 26 const VERSION = '2.13.0'; 27 const SPEC_VERSION = '1.1.2'; 28 29 const PRAGMA_FILTERS = 'FILTERS'; 30 const PRAGMA_BLOCKS = 'BLOCKS'; 31 const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT'; 32 33 // Known pragmas 34 private static $knownPragmas = array( 35 self::PRAGMA_FILTERS => true, 36 self::PRAGMA_BLOCKS => true, 37 self::PRAGMA_ANCHORED_DOT => true, 38 ); 39 40 // Template cache 41 private $templates = array(); 42 43 // Environment 44 private $templateClassPrefix = '__Mustache_'; 45 private $cache; 46 private $lambdaCache; 47 private $cacheLambdaTemplates = false; 48 private $loader; 49 private $partialsLoader; 50 private $helpers; 51 private $escape; 52 private $entityFlags = ENT_COMPAT; 53 private $charset = 'UTF-8'; 54 private $logger; 55 private $strictCallables = false; 56 private $pragmas = array(); 57 private $delimiters; 58 59 // Services 60 private $tokenizer; 61 private $parser; 62 private $compiler; 63 64 /** 65 * Mustache class constructor. 66 * 67 * Passing an $options array allows overriding certain Mustache options during instantiation: 68 * 69 * $options = array( 70 * // The class prefix for compiled templates. Defaults to '__Mustache_'. 71 * 'template_class_prefix' => '__MyTemplates_', 72 * 73 * // A Mustache cache instance or a cache directory string for compiled templates. 74 * // Mustache will not cache templates unless this is set. 75 * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', 76 * 77 * // Override default permissions for cache files. Defaults to using the system-defined umask. It is 78 * // *strongly* recommended that you configure your umask properly rather than overriding permissions here. 79 * 'cache_file_mode' => 0666, 80 * 81 * // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda 82 * // sections are often too dynamic to benefit from caching. 83 * 'cache_lambda_templates' => true, 84 * 85 * // Customize the tag delimiters used by this engine instance. Note that overriding here changes the 86 * // delimiters used to parse all templates and partials loaded by this instance. To override just for a 87 * // single template, use an inline "change delimiters" tag at the start of the template file: 88 * // 89 * // {{=<% %>=}} 90 * // 91 * 'delimiters' => '<% %>', 92 * 93 * // A Mustache template loader instance. Uses a StringLoader if not specified. 94 * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), 95 * 96 * // A Mustache loader instance for partials. 97 * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), 98 * 99 * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as 100 * // efficient or lazy as a Filesystem (or database) loader. 101 * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')), 102 * 103 * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order 104 * // sections), or any other valid Mustache context value. They will be prepended to the context stack, 105 * // so they will be available in any template loaded by this Mustache instance. 106 * 'helpers' => array('i18n' => function ($text) { 107 * // do something translatey here... 108 * }), 109 * 110 * // An 'escape' callback, responsible for escaping double-mustache variables. 111 * 'escape' => function ($value) { 112 * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); 113 * }, 114 * 115 * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES. 116 * 'entity_flags' => ENT_QUOTES, 117 * 118 * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. 119 * 'charset' => 'ISO-8859-1', 120 * 121 * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible 122 * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is 123 * // available as well: 124 * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'), 125 * 126 * // Only treat Closure instances and invokable classes as callable. If true, values like 127 * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally 128 * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This 129 * // helps protect against arbitrary code execution when user input is passed directly into the template. 130 * // This currently defaults to false, but will default to true in v3.0. 131 * 'strict_callables' => true, 132 * 133 * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual 134 * // templates. 135 * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], 136 * ); 137 * 138 * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable 139 * 140 * @param array $options (default: array()) 141 */ 142 public function __construct(array $options = array()) 143 { 144 if (isset($options['template_class_prefix'])) { 145 if ((string) $options['template_class_prefix'] === '') { 146 throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty'); 147 } 148 149 $this->templateClassPrefix = $options['template_class_prefix']; 150 } 151 152 if (isset($options['cache'])) { 153 $cache = $options['cache']; 154 155 if (is_string($cache)) { 156 $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null; 157 $cache = new Mustache_Cache_FilesystemCache($cache, $mode); 158 } 159 160 $this->setCache($cache); 161 } 162 163 if (isset($options['cache_lambda_templates'])) { 164 $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates']; 165 } 166 167 if (isset($options['loader'])) { 168 $this->setLoader($options['loader']); 169 } 170 171 if (isset($options['partials_loader'])) { 172 $this->setPartialsLoader($options['partials_loader']); 173 } 174 175 if (isset($options['partials'])) { 176 $this->setPartials($options['partials']); 177 } 178 179 if (isset($options['helpers'])) { 180 $this->setHelpers($options['helpers']); 181 } 182 183 if (isset($options['escape'])) { 184 if (!is_callable($options['escape'])) { 185 throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable'); 186 } 187 188 $this->escape = $options['escape']; 189 } 190 191 if (isset($options['entity_flags'])) { 192 $this->entityFlags = $options['entity_flags']; 193 } 194 195 if (isset($options['charset'])) { 196 $this->charset = $options['charset']; 197 } 198 199 if (isset($options['logger'])) { 200 $this->setLogger($options['logger']); 201 } 202 203 if (isset($options['strict_callables'])) { 204 $this->strictCallables = $options['strict_callables']; 205 } 206 207 if (isset($options['delimiters'])) { 208 $this->delimiters = $options['delimiters']; 209 } 210 211 if (isset($options['pragmas'])) { 212 foreach ($options['pragmas'] as $pragma) { 213 if (!isset(self::$knownPragmas[$pragma])) { 214 throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma)); 215 } 216 $this->pragmas[$pragma] = true; 217 } 218 } 219 } 220 221 /** 222 * Shortcut 'render' invocation. 223 * 224 * Equivalent to calling `$mustache->loadTemplate($template)->render($context);` 225 * 226 * @see Mustache_Engine::loadTemplate 227 * @see Mustache_Template::render 228 * 229 * @param string $template 230 * @param mixed $context (default: array()) 231 * 232 * @return string Rendered template 233 */ 234 public function render($template, $context = array()) 235 { 236 return $this->loadTemplate($template)->render($context); 237 } 238 239 /** 240 * Get the current Mustache escape callback. 241 * 242 * @return callable|null 243 */ 244 public function getEscape() 245 { 246 return $this->escape; 247 } 248 249 /** 250 * Get the current Mustache entitity type to escape. 251 * 252 * @return int 253 */ 254 public function getEntityFlags() 255 { 256 return $this->entityFlags; 257 } 258 259 /** 260 * Get the current Mustache character set. 261 * 262 * @return string 263 */ 264 public function getCharset() 265 { 266 return $this->charset; 267 } 268 269 /** 270 * Get the current globally enabled pragmas. 271 * 272 * @return array 273 */ 274 public function getPragmas() 275 { 276 return array_keys($this->pragmas); 277 } 278 279 /** 280 * Set the Mustache template Loader instance. 281 * 282 * @param Mustache_Loader $loader 283 */ 284 public function setLoader(Mustache_Loader $loader) 285 { 286 $this->loader = $loader; 287 } 288 289 /** 290 * Get the current Mustache template Loader instance. 291 * 292 * If no Loader instance has been explicitly specified, this method will instantiate and return 293 * a StringLoader instance. 294 * 295 * @return Mustache_Loader 296 */ 297 public function getLoader() 298 { 299 if (!isset($this->loader)) { 300 $this->loader = new Mustache_Loader_StringLoader(); 301 } 302 303 return $this->loader; 304 } 305 306 /** 307 * Set the Mustache partials Loader instance. 308 * 309 * @param Mustache_Loader $partialsLoader 310 */ 311 public function setPartialsLoader(Mustache_Loader $partialsLoader) 312 { 313 $this->partialsLoader = $partialsLoader; 314 } 315 316 /** 317 * Get the current Mustache partials Loader instance. 318 * 319 * If no Loader instance has been explicitly specified, this method will instantiate and return 320 * an ArrayLoader instance. 321 * 322 * @return Mustache_Loader 323 */ 324 public function getPartialsLoader() 325 { 326 if (!isset($this->partialsLoader)) { 327 $this->partialsLoader = new Mustache_Loader_ArrayLoader(); 328 } 329 330 return $this->partialsLoader; 331 } 332 333 /** 334 * Set partials for the current partials Loader instance. 335 * 336 * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable 337 * 338 * @param array $partials (default: array()) 339 */ 340 public function setPartials(array $partials = array()) 341 { 342 if (!isset($this->partialsLoader)) { 343 $this->partialsLoader = new Mustache_Loader_ArrayLoader(); 344 } 345 346 if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) { 347 throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance'); 348 } 349 350 $this->partialsLoader->setTemplates($partials); 351 } 352 353 /** 354 * Set an array of Mustache helpers. 355 * 356 * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or 357 * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in 358 * any template loaded by this Mustache instance. 359 * 360 * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable 361 * 362 * @param array|Traversable $helpers 363 */ 364 public function setHelpers($helpers) 365 { 366 if (!is_array($helpers) && !$helpers instanceof Traversable) { 367 throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers'); 368 } 369 370 $this->getHelpers()->clear(); 371 372 foreach ($helpers as $name => $helper) { 373 $this->addHelper($name, $helper); 374 } 375 } 376 377 /** 378 * Get the current set of Mustache helpers. 379 * 380 * @see Mustache_Engine::setHelpers 381 * 382 * @return Mustache_HelperCollection 383 */ 384 public function getHelpers() 385 { 386 if (!isset($this->helpers)) { 387 $this->helpers = new Mustache_HelperCollection(); 388 } 389 390 return $this->helpers; 391 } 392 393 /** 394 * Add a new Mustache helper. 395 * 396 * @see Mustache_Engine::setHelpers 397 * 398 * @param string $name 399 * @param mixed $helper 400 */ 401 public function addHelper($name, $helper) 402 { 403 $this->getHelpers()->add($name, $helper); 404 } 405 406 /** 407 * Get a Mustache helper by name. 408 * 409 * @see Mustache_Engine::setHelpers 410 * 411 * @param string $name 412 * 413 * @return mixed Helper 414 */ 415 public function getHelper($name) 416 { 417 return $this->getHelpers()->get($name); 418 } 419 420 /** 421 * Check whether this Mustache instance has a helper. 422 * 423 * @see Mustache_Engine::setHelpers 424 * 425 * @param string $name 426 * 427 * @return bool True if the helper is present 428 */ 429 public function hasHelper($name) 430 { 431 return $this->getHelpers()->has($name); 432 } 433 434 /** 435 * Remove a helper by name. 436 * 437 * @see Mustache_Engine::setHelpers 438 * 439 * @param string $name 440 */ 441 public function removeHelper($name) 442 { 443 $this->getHelpers()->remove($name); 444 } 445 446 /** 447 * Set the Mustache Logger instance. 448 * 449 * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface 450 * 451 * @param Mustache_Logger|Psr\Log\LoggerInterface $logger 452 */ 453 public function setLogger($logger = null) 454 { 455 if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { 456 throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); 457 } 458 459 if ($this->getCache()->getLogger() === null) { 460 $this->getCache()->setLogger($logger); 461 } 462 463 $this->logger = $logger; 464 } 465 466 /** 467 * Get the current Mustache Logger instance. 468 * 469 * @return Mustache_Logger|Psr\Log\LoggerInterface 470 */ 471 public function getLogger() 472 { 473 return $this->logger; 474 } 475 476 /** 477 * Set the Mustache Tokenizer instance. 478 * 479 * @param Mustache_Tokenizer $tokenizer 480 */ 481 public function setTokenizer(Mustache_Tokenizer $tokenizer) 482 { 483 $this->tokenizer = $tokenizer; 484 } 485 486 /** 487 * Get the current Mustache Tokenizer instance. 488 * 489 * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one. 490 * 491 * @return Mustache_Tokenizer 492 */ 493 public function getTokenizer() 494 { 495 if (!isset($this->tokenizer)) { 496 $this->tokenizer = new Mustache_Tokenizer(); 497 } 498 499 return $this->tokenizer; 500 } 501 502 /** 503 * Set the Mustache Parser instance. 504 * 505 * @param Mustache_Parser $parser 506 */ 507 public function setParser(Mustache_Parser $parser) 508 { 509 $this->parser = $parser; 510 } 511 512 /** 513 * Get the current Mustache Parser instance. 514 * 515 * If no Parser instance has been explicitly specified, this method will instantiate and return a new one. 516 * 517 * @return Mustache_Parser 518 */ 519 public function getParser() 520 { 521 if (!isset($this->parser)) { 522 $this->parser = new Mustache_Parser(); 523 } 524 525 return $this->parser; 526 } 527 528 /** 529 * Set the Mustache Compiler instance. 530 * 531 * @param Mustache_Compiler $compiler 532 */ 533 public function setCompiler(Mustache_Compiler $compiler) 534 { 535 $this->compiler = $compiler; 536 } 537 538 /** 539 * Get the current Mustache Compiler instance. 540 * 541 * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one. 542 * 543 * @return Mustache_Compiler 544 */ 545 public function getCompiler() 546 { 547 if (!isset($this->compiler)) { 548 $this->compiler = new Mustache_Compiler(); 549 } 550 551 return $this->compiler; 552 } 553 554 /** 555 * Set the Mustache Cache instance. 556 * 557 * @param Mustache_Cache $cache 558 */ 559 public function setCache(Mustache_Cache $cache) 560 { 561 if (isset($this->logger) && $cache->getLogger() === null) { 562 $cache->setLogger($this->getLogger()); 563 } 564 565 $this->cache = $cache; 566 } 567 568 /** 569 * Get the current Mustache Cache instance. 570 * 571 * If no Cache instance has been explicitly specified, this method will instantiate and return a new one. 572 * 573 * @return Mustache_Cache 574 */ 575 public function getCache() 576 { 577 if (!isset($this->cache)) { 578 $this->setCache(new Mustache_Cache_NoopCache()); 579 } 580 581 return $this->cache; 582 } 583 584 /** 585 * Get the current Lambda Cache instance. 586 * 587 * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache. 588 * 589 * @see Mustache_Engine::getCache 590 * 591 * @return Mustache_Cache 592 */ 593 protected function getLambdaCache() 594 { 595 if ($this->cacheLambdaTemplates) { 596 return $this->getCache(); 597 } 598 599 if (!isset($this->lambdaCache)) { 600 $this->lambdaCache = new Mustache_Cache_NoopCache(); 601 } 602 603 return $this->lambdaCache; 604 } 605 606 /** 607 * Helper method to generate a Mustache template class. 608 * 609 * This method must be updated any time options are added which make it so 610 * the same template could be parsed and compiled multiple different ways. 611 * 612 * @param string|Mustache_Source $source 613 * 614 * @return string Mustache Template class name 615 */ 616 public function getTemplateClassName($source) 617 { 618 // For the most part, adding a new option here should do the trick. 619 // 620 // Pick a value here which is unique for each possible way the template 621 // could be compiled... but not necessarily unique per option value. See 622 // escape below, which only needs to differentiate between 'custom' and 623 // 'default' escapes. 624 // 625 // Keep this list in alphabetical order :) 626 $chunks = array( 627 'charset' => $this->charset, 628 'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}', 629 'entityFlags' => $this->entityFlags, 630 'escape' => isset($this->escape) ? 'custom' : 'default', 631 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source', 632 'pragmas' => $this->getPragmas(), 633 'strictCallables' => $this->strictCallables, 634 'version' => self::VERSION, 635 ); 636 637 $key = json_encode($chunks); 638 639 // Template Source instances have already provided their own source key. For strings, just include the whole 640 // source string in the md5 hash. 641 if (!$source instanceof Mustache_Source) { 642 $key .= "\n" . $source; 643 } 644 645 return $this->templateClassPrefix . md5($key); 646 } 647 648 /** 649 * Load a Mustache Template by name. 650 * 651 * @param string $name 652 * 653 * @return Mustache_Template 654 */ 655 public function loadTemplate($name) 656 { 657 return $this->loadSource($this->getLoader()->load($name)); 658 } 659 660 /** 661 * Load a Mustache partial Template by name. 662 * 663 * This is a helper method used internally by Template instances for loading partial templates. You can most likely 664 * ignore it completely. 665 * 666 * @param string $name 667 * 668 * @return Mustache_Template 669 */ 670 public function loadPartial($name) 671 { 672 try { 673 if (isset($this->partialsLoader)) { 674 $loader = $this->partialsLoader; 675 } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) { 676 $loader = $this->loader; 677 } else { 678 throw new Mustache_Exception_UnknownTemplateException($name); 679 } 680 681 return $this->loadSource($loader->load($name)); 682 } catch (Mustache_Exception_UnknownTemplateException $e) { 683 // If the named partial cannot be found, log then return null. 684 $this->log( 685 Mustache_Logger::WARNING, 686 'Partial not found: "{name}"', 687 array('name' => $e->getTemplateName()) 688 ); 689 } 690 } 691 692 /** 693 * Load a Mustache lambda Template by source. 694 * 695 * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most 696 * likely ignore it completely. 697 * 698 * @param string $source 699 * @param string $delims (default: null) 700 * 701 * @return Mustache_Template 702 */ 703 public function loadLambda($source, $delims = null) 704 { 705 if ($delims !== null) { 706 $source = $delims . "\n" . $source; 707 } 708 709 return $this->loadSource($source, $this->getLambdaCache()); 710 } 711 712 /** 713 * Instantiate and return a Mustache Template instance by source. 714 * 715 * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect 716 * the 'cache_lambda_templates' configuration option. 717 * 718 * @see Mustache_Engine::loadTemplate 719 * @see Mustache_Engine::loadPartial 720 * @see Mustache_Engine::loadLambda 721 * 722 * @param string|Mustache_Source $source 723 * @param Mustache_Cache $cache (default: null) 724 * 725 * @return Mustache_Template 726 */ 727 private function loadSource($source, Mustache_Cache $cache = null) 728 { 729 $className = $this->getTemplateClassName($source); 730 731 if (!isset($this->templates[$className])) { 732 if ($cache === null) { 733 $cache = $this->getCache(); 734 } 735 736 if (!class_exists($className, false)) { 737 if (!$cache->load($className)) { 738 $compiled = $this->compile($source); 739 $cache->cache($className, $compiled); 740 } 741 } 742 743 $this->log( 744 Mustache_Logger::DEBUG, 745 'Instantiating template: "{className}"', 746 array('className' => $className) 747 ); 748 749 $this->templates[$className] = new $className($this); 750 } 751 752 return $this->templates[$className]; 753 } 754 755 /** 756 * Helper method to tokenize a Mustache template. 757 * 758 * @see Mustache_Tokenizer::scan 759 * 760 * @param string $source 761 * 762 * @return array Tokens 763 */ 764 private function tokenize($source) 765 { 766 return $this->getTokenizer()->scan($source, $this->delimiters); 767 } 768 769 /** 770 * Helper method to parse a Mustache template. 771 * 772 * @see Mustache_Parser::parse 773 * 774 * @param string $source 775 * 776 * @return array Token tree 777 */ 778 private function parse($source) 779 { 780 $parser = $this->getParser(); 781 $parser->setPragmas($this->getPragmas()); 782 783 return $parser->parse($this->tokenize($source)); 784 } 785 786 /** 787 * Helper method to compile a Mustache template. 788 * 789 * @see Mustache_Compiler::compile 790 * 791 * @param string|Mustache_Source $source 792 * 793 * @return string generated Mustache template class code 794 */ 795 private function compile($source) 796 { 797 $name = $this->getTemplateClassName($source); 798 799 $this->log( 800 Mustache_Logger::INFO, 801 'Compiling template to "{className}" class', 802 array('className' => $name) 803 ); 804 805 if ($source instanceof Mustache_Source) { 806 $source = $source->getSource(); 807 } 808 $tree = $this->parse($source); 809 810 $compiler = $this->getCompiler(); 811 $compiler->setPragmas($this->getPragmas()); 812 813 return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); 814 } 815 816 /** 817 * Add a log record if logging is enabled. 818 * 819 * @param int $level The logging level 820 * @param string $message The log message 821 * @param array $context The log context 822 */ 823 private function log($level, $message, array $context = array()) 824 { 825 if (isset($this->logger)) { 826 $this->logger->log($level, $message, $context); 827 } 828 } 829} 830