Merge branch 'MDL-67377-master' of git://github.com/ferranrecio/moodle
[moodle.git] / lib / mustache / src / Mustache / Engine.php
1 <?php
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  */
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  */
24 class Mustache_Engine
25 {
26     const VERSION        = '2.13.0';
27     const SPEC_VERSION   = '1.1.2';
29     const PRAGMA_FILTERS      = 'FILTERS';
30     const PRAGMA_BLOCKS       = 'BLOCKS';
31     const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT';
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     );
40     // Template cache
41     private $templates = array();
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;
59     // Services
60     private $tokenizer;
61     private $parser;
62     private $compiler;
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             }
149             $this->templateClassPrefix = $options['template_class_prefix'];
150         }
152         if (isset($options['cache'])) {
153             $cache = $options['cache'];
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             }
160             $this->setCache($cache);
161         }
163         if (isset($options['cache_lambda_templates'])) {
164             $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates'];
165         }
167         if (isset($options['loader'])) {
168             $this->setLoader($options['loader']);
169         }
171         if (isset($options['partials_loader'])) {
172             $this->setPartialsLoader($options['partials_loader']);
173         }
175         if (isset($options['partials'])) {
176             $this->setPartials($options['partials']);
177         }
179         if (isset($options['helpers'])) {
180             $this->setHelpers($options['helpers']);
181         }
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             }
188             $this->escape = $options['escape'];
189         }
191         if (isset($options['entity_flags'])) {
192             $this->entityFlags = $options['entity_flags'];
193         }
195         if (isset($options['charset'])) {
196             $this->charset = $options['charset'];
197         }
199         if (isset($options['logger'])) {
200             $this->setLogger($options['logger']);
201         }
203         if (isset($options['strict_callables'])) {
204             $this->strictCallables = $options['strict_callables'];
205         }
207         if (isset($options['delimiters'])) {
208             $this->delimiters = $options['delimiters'];
209         }
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     }
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     }
239     /**
240      * Get the current Mustache escape callback.
241      *
242      * @return callable|null
243      */
244     public function getEscape()
245     {
246         return $this->escape;
247     }
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     }
259     /**
260      * Get the current Mustache character set.
261      *
262      * @return string
263      */
264     public function getCharset()
265     {
266         return $this->charset;
267     }
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     }
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     }
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         }
303         return $this->loader;
304     }
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     }
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         }
330         return $this->partialsLoader;
331     }
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         }
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         }
350         $this->partialsLoader->setTemplates($partials);
351     }
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         }
370         $this->getHelpers()->clear();
372         foreach ($helpers as $name => $helper) {
373             $this->addHelper($name, $helper);
374         }
375     }
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         }
390         return $this->helpers;
391     }
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     }
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     }
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     }
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     }
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         }
459         if ($this->getCache()->getLogger() === null) {
460             $this->getCache()->setLogger($logger);
461         }
463         $this->logger = $logger;
464     }
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     }
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     }
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         }
499         return $this->tokenizer;
500     }
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     }
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         }
525         return $this->parser;
526     }
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     }
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         }
551         return $this->compiler;
552     }
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         }
565         $this->cache = $cache;
566     }
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         }
581         return $this->cache;
582     }
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         }
599         if (!isset($this->lambdaCache)) {
600             $this->lambdaCache = new Mustache_Cache_NoopCache();
601         }
603         return $this->lambdaCache;
604     }
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         );
637         $key = json_encode($chunks);
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         }
645         return $this->templateClassPrefix . md5($key);
646     }
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     }
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             }
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     }
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         }
709         return $this->loadSource($source, $this->getLambdaCache());
710     }
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);
731         if (!isset($this->templates[$className])) {
732             if ($cache === null) {
733                 $cache = $this->getCache();
734             }
736             if (!class_exists($className, false)) {
737                 if (!$cache->load($className)) {
738                     $compiled = $this->compile($source);
739                     $cache->cache($className, $compiled);
740                 }
741             }
743             $this->log(
744                 Mustache_Logger::DEBUG,
745                 'Instantiating template: "{className}"',
746                 array('className' => $className)
747             );
749             $this->templates[$className] = new $className($this);
750         }
752         return $this->templates[$className];
753     }
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     }
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());
783         return $parser->parse($this->tokenize($source));
784     }
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);
799         $this->log(
800             Mustache_Logger::INFO,
801             'Compiling template to "{className}" class',
802             array('className' => $name)
803         );
805         if ($source instanceof Mustache_Source) {
806             $source = $source->getSource();
807         }
808         $tree = $this->parse($source);
810         $compiler = $this->getCompiler();
811         $compiler->setPragmas($this->getPragmas());
813         return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
814     }
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     }