diff options
Diffstat (limited to 'Mustache')
35 files changed, 4802 insertions, 2201 deletions
diff --git a/Mustache/Autoloader.php b/Mustache/Autoloader.php index 707a0ffa..e8ea3f4a 100644 --- a/Mustache/Autoloader.php +++ b/Mustache/Autoloader.php @@ -1,69 +1,88 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache class autoloader.
- */
-class Mustache_Autoloader
-{
-
- private $baseDir;
-
- /**
- * Autoloader constructor.
- *
- * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
- */
- public function __construct($baseDir = null)
- {
- if ($baseDir === null) {
- $this->baseDir = dirname(__FILE__).'/..';
- } else {
- $this->baseDir = rtrim($baseDir, '/');
- }
- }
-
- /**
- * Register a new instance as an SPL autoloader.
- *
- * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..')
- *
- * @return Mustache_Autoloader Registered Autoloader instance
- */
- public static function register($baseDir = null)
- {
- $loader = new self($baseDir);
- spl_autoload_register(array($loader, 'autoload'));
-
- return $loader;
- }
-
- /**
- * Autoload Mustache classes.
- *
- * @param string $class
- */
- public function autoload($class)
- {
- if ($class[0] === '\\') {
- $class = substr($class, 1);
- }
-
- if (strpos($class, 'Mustache') !== 0) {
- return;
- }
-
- $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class));
- if (is_file($file)) {
- require $file;
- }
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache class autoloader. + */ +class Mustache_Autoloader +{ + private $baseDir; + + /** + * An array where the key is the baseDir and the key is an instance of this + * class. + * + * @var array + */ + private static $instances; + + /** + * Autoloader constructor. + * + * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..') + */ + public function __construct($baseDir = null) + { + if ($baseDir === null) { + $baseDir = dirname(__FILE__) . '/..'; + } + + // realpath doesn't always work, for example, with stream URIs + $realDir = realpath($baseDir); + if (is_dir($realDir)) { + $this->baseDir = $realDir; + } else { + $this->baseDir = $baseDir; + } + } + + /** + * Register a new instance as an SPL autoloader. + * + * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..') + * + * @return Mustache_Autoloader Registered Autoloader instance + */ + public static function register($baseDir = null) + { + $key = $baseDir ? $baseDir : 0; + + if (!isset(self::$instances[$key])) { + self::$instances[$key] = new self($baseDir); + } + + $loader = self::$instances[$key]; + spl_autoload_register(array($loader, 'autoload')); + + return $loader; + } + + /** + * Autoload Mustache classes. + * + * @param string $class + */ + public function autoload($class) + { + if ($class[0] === '\\') { + $class = substr($class, 1); + } + + if (strpos($class, 'Mustache') !== 0) { + return; + } + + $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class)); + if (is_file($file)) { + require $file; + } + } +} diff --git a/Mustache/Cache.php b/Mustache/Cache.php new file mode 100644 index 00000000..3292efac --- /dev/null +++ b/Mustache/Cache.php @@ -0,0 +1,43 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Cache interface. + * + * Interface for caching and loading Mustache_Template classes + * generated by the Mustache_Compiler. + */ +interface Mustache_Cache +{ + /** + * Load a compiled Mustache_Template class from cache. + * + * @param string $key + * + * @return bool indicates successfully class load + */ + public function load($key); + + /** + * Cache and load a compiled Mustache_Template class. + * + * @param string $key + * @param string $value + */ + public function cache($key, $value); + + /** + * Set a logger instance. + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null); +} diff --git a/Mustache/Cache/AbstractCache.php b/Mustache/Cache/AbstractCache.php new file mode 100644 index 00000000..281038fa --- /dev/null +++ b/Mustache/Cache/AbstractCache.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Abstract Mustache Cache class. + * + * Provides logging support to child implementations. + * + * @abstract + */ +abstract class Mustache_Cache_AbstractCache implements Mustache_Cache +{ + private $logger = null; + + /** + * Get the current logger instance. + * + * @return Mustache_Logger|Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Set a logger instance. + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + $this->logger = $logger; + } + + /** + * Add a log record if logging is enabled. + * + * @param string $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } +} diff --git a/Mustache/Cache/FilesystemCache.php b/Mustache/Cache/FilesystemCache.php new file mode 100644 index 00000000..3e742b70 --- /dev/null +++ b/Mustache/Cache/FilesystemCache.php @@ -0,0 +1,161 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Cache filesystem implementation. + * + * A FilesystemCache instance caches Mustache Template classes from the filesystem by name: + * + * $cache = new Mustache_Cache_FilesystemCache(dirname(__FILE__).'/cache'); + * $cache->cache($className, $compiledSource); + * + * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k? + */ +class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache +{ + private $baseDir; + private $fileMode; + + /** + * Filesystem cache constructor. + * + * @param string $baseDir Directory for compiled templates + * @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask + */ + public function __construct($baseDir, $fileMode = null) + { + $this->baseDir = $baseDir; + $this->fileMode = $fileMode; + } + + /** + * Load the class from cache using `require_once`. + * + * @param string $key + * + * @return bool + */ + public function load($key) + { + $fileName = $this->getCacheFilename($key); + if (!is_file($fileName)) { + return false; + } + + require_once $fileName; + + return true; + } + + /** + * Cache and load the compiled class. + * + * @param string $key + * @param string $value + */ + public function cache($key, $value) + { + $fileName = $this->getCacheFilename($key); + + $this->log( + Mustache_Logger::DEBUG, + 'Writing to template cache: "{fileName}"', + array('fileName' => $fileName) + ); + + $this->writeFile($fileName, $value); + $this->load($key); + } + + /** + * Build the cache filename. + * Subclasses should override for custom cache directory structures. + * + * @param string $name + * + * @return string + */ + protected function getCacheFilename($name) + { + return sprintf('%s/%s.php', $this->baseDir, $name); + } + + /** + * Create cache directory. + * + * @throws Mustache_Exception_RuntimeException If unable to create directory + * + * @param string $fileName + * + * @return string + */ + private function buildDirectoryForFilename($fileName) + { + $dirName = dirname($fileName); + if (!is_dir($dirName)) { + $this->log( + Mustache_Logger::INFO, + 'Creating Mustache template cache directory: "{dirName}"', + array('dirName' => $dirName) + ); + + @mkdir($dirName, 0777, true); + // @codeCoverageIgnoreStart + if (!is_dir($dirName)) { + throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); + } + // @codeCoverageIgnoreEnd + } + + return $dirName; + } + + /** + * Write cache file. + * + * @throws Mustache_Exception_RuntimeException If unable to write file + * + * @param string $fileName + * @param string $value + */ + private function writeFile($fileName, $value) + { + $dirName = $this->buildDirectoryForFilename($fileName); + + $this->log( + Mustache_Logger::DEBUG, + 'Caching compiled template to "{fileName}"', + array('fileName' => $fileName) + ); + + $tempFile = tempnam($dirName, basename($fileName)); + if (false !== @file_put_contents($tempFile, $value)) { + if (@rename($tempFile, $fileName)) { + $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask()); + @chmod($fileName, $mode); + + return; + } + + // @codeCoverageIgnoreStart + $this->log( + Mustache_Logger::ERROR, + 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', + array('tempName' => $tempFile, 'fileName' => $fileName) + ); + // @codeCoverageIgnoreEnd + } + + // @codeCoverageIgnoreStart + throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); + // @codeCoverageIgnoreEnd + } +} diff --git a/Mustache/Cache/NoopCache.php b/Mustache/Cache/NoopCache.php new file mode 100644 index 00000000..ed9eec9d --- /dev/null +++ b/Mustache/Cache/NoopCache.php @@ -0,0 +1,47 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Cache in-memory implementation. + * + * The in-memory cache is used for uncached lambda section templates. It's also useful during development, but is not + * recommended for production use. + */ +class Mustache_Cache_NoopCache extends Mustache_Cache_AbstractCache +{ + /** + * Loads nothing. Move along. + * + * @param string $key + * + * @return bool + */ + public function load($key) + { + return false; + } + + /** + * Loads the compiled Mustache Template class without caching. + * + * @param string $key + * @param string $value + */ + public function cache($key, $value) + { + $this->log( + Mustache_Logger::WARNING, + 'Template cache disabled, evaluating "{className}" class at runtime', + array('className' => $key) + ); + eval('?>' . $value); + } +} diff --git a/Mustache/Compiler.php b/Mustache/Compiler.php index dd5307d4..2b0d1f93 100644 --- a/Mustache/Compiler.php +++ b/Mustache/Compiler.php @@ -1,386 +1,718 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Compiler class.
- *
- * This class is responsible for turning a Mustache token parse tree into normal PHP source code.
- */
-class Mustache_Compiler
-{
-
- private $sections;
- private $source;
- private $indentNextLine;
- private $customEscape;
- private $charset;
-
- /**
- * Compile a Mustache token parse tree into PHP source code.
- *
- * @param string $source Mustache Template source code
- * @param string $tree Parse tree of Mustache tokens
- * @param string $name Mustache Template class name
- * @param bool $customEscape (default: false)
- * @param string $charset (default: 'UTF-8')
- *
- * @return string Generated PHP source code
- */
- public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8')
- {
- $this->sections = array();
- $this->source = $source;
- $this->indentNextLine = true;
- $this->customEscape = $customEscape;
- $this->charset = $charset;
-
- return $this->writeCode($tree, $name);
- }
-
- /**
- * Helper function for walking the Mustache token parse tree.
- *
- * @throws InvalidArgumentException upon encountering unknown token types.
- *
- * @param array $tree Parse tree of Mustache tokens
- * @param int $level (default: 0)
- *
- * @return string Generated PHP source code
- */
- private function walk(array $tree, $level = 0)
- {
- $code = '';
- $level++;
- foreach ($tree as $node) {
- switch ($node[Mustache_Tokenizer::TYPE]) {
- case Mustache_Tokenizer::T_SECTION:
- $code .= $this->section(
- $node[Mustache_Tokenizer::NODES],
- $node[Mustache_Tokenizer::NAME],
- $node[Mustache_Tokenizer::INDEX],
- $node[Mustache_Tokenizer::END],
- $node[Mustache_Tokenizer::OTAG],
- $node[Mustache_Tokenizer::CTAG],
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_INVERTED:
- $code .= $this->invertedSection(
- $node[Mustache_Tokenizer::NODES],
- $node[Mustache_Tokenizer::NAME],
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_PARTIAL:
- case Mustache_Tokenizer::T_PARTIAL_2:
- $code .= $this->partial(
- $node[Mustache_Tokenizer::NAME],
- isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
- $level
- );
- break;
-
- case Mustache_Tokenizer::T_UNESCAPED:
- case Mustache_Tokenizer::T_UNESCAPED_2:
- $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
- break;
-
- case Mustache_Tokenizer::T_COMMENT:
- break;
-
- case Mustache_Tokenizer::T_ESCAPED:
- $code .= $this->variable($node[Mustache_Tokenizer::NAME], true, $level);
- break;
-
- case Mustache_Tokenizer::T_TEXT:
- $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
- break;
-
- default:
- throw new InvalidArgumentException('Unknown node type: '.json_encode($node));
- }
- }
-
- return $code;
- }
-
- const KLASS = '<?php
-
- class %s extends Mustache_Template
- {
- public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false)
- {
- $buffer = \'\';
- %s
-
- if ($escape) {
- return %s;
- } else {
- return $buffer;
- }
- }
- %s
- }';
-
- /**
- * Generate Mustache Template class PHP source.
- *
- * @param array $tree Parse tree of Mustache tokens
- * @param string $name Mustache Template class name
- *
- * @return string Generated PHP source code
- */
- private function writeCode($tree, $name)
- {
- $code = $this->walk($tree);
- $sections = implode("\n", $this->sections);
-
- return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $this->getEscape('$buffer'), $sections);
- }
-
- const SECTION_CALL = '
- // %s section
- $buffer .= $this->section%s($context, $indent, $context->%s(%s));
- ';
-
- const SECTION = '
- private function section%s(Mustache_Context $context, $indent, $value) {
- $buffer = \'\';
- if (!is_string($value) && is_callable($value)) {
- $source = %s;
- $buffer .= $this->mustache
- ->loadLambda((string) call_user_func($value, $source)%s)
- ->renderInternal($context, $indent);
- } elseif (!empty($value)) {
- $values = $this->isIterable($value) ? $value : array($value);
- foreach ($values as $value) {
- $context->push($value);%s
- $context->pop();
- }
- }
-
- return $buffer;
- }';
-
- /**
- * Generate Mustache Template section PHP source.
- *
- * @param array $nodes Array of child tokens
- * @param string $id Section name
- * @param int $start Section start offset
- * @param int $end Section end offset
- * @param string $otag Current Mustache opening tag
- * @param string $ctag Current Mustache closing tag
- * @param int $level
- *
- * @return string Generated section PHP source code
- */
- private function section($nodes, $id, $start, $end, $otag, $ctag, $level)
- {
- $method = $this->getFindMethod($id);
- $id = var_export($id, true);
- $source = var_export(substr($this->source, $start, $end - $start), true);
-
- if ($otag !== '{{' || $ctag !== '}}') {
- $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
- } else {
- $delims = '';
- }
-
- $key = ucfirst(md5($delims."\n".$source));
-
- if (!isset($this->sections[$key])) {
- $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $source, $delims, $this->walk($nodes, 2));
- }
-
- return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $key, $method, $id);
- }
-
- const INVERTED_SECTION = '
- // %s inverted section
- $value = $context->%s(%s);
- if (empty($value)) {
- %s
- }';
-
- /**
- * Generate Mustache Template inverted section PHP source.
- *
- * @param array $nodes Array of child tokens
- * @param string $id Section name
- * @param int $level
- *
- * @return string Generated inverted section PHP source code
- */
- private function invertedSection($nodes, $id, $level)
- {
- $method = $this->getFindMethod($id);
- $id = var_export($id, true);
-
- return sprintf($this->prepare(self::INVERTED_SECTION, $level), $id, $method, $id, $this->walk($nodes, $level));
- }
-
- const PARTIAL = '
- if ($partial = $this->mustache->loadPartial(%s)) {
- $buffer .= $partial->renderInternal($context, %s);
- }
- ';
-
- /**
- * Generate Mustache Template partial call PHP source.
- *
- * @param string $id Partial name
- * @param string $indent Whitespace indent to apply to partial
- * @param int $level
- *
- * @return string Generated partial call PHP source code
- */
- private function partial($id, $indent, $level)
- {
- return sprintf(
- $this->prepare(self::PARTIAL, $level),
- var_export($id, true),
- var_export($indent, true)
- );
- }
-
- const VARIABLE = '
- $value = $context->%s(%s);
- if (!is_string($value) && is_callable($value)) {
- $value = $this->mustache
- ->loadLambda((string) call_user_func($value))
- ->renderInternal($context, $indent);
- }
- $buffer .= %s%s;
- ';
-
- /**
- * Generate Mustache Template variable interpolation PHP source.
- *
- * @param string $id Variable name
- * @param boolean $escape Escape the variable value for output?
- * @param int $level
- *
- * @return string Generated variable interpolation PHP source
- */
- private function variable($id, $escape, $level)
- {
- $method = $this->getFindMethod($id);
- $id = ($method !== 'last') ? var_export($id, true) : '';
- $value = $escape ? $this->getEscape() : '$value';
-
- return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value);
- }
-
- const LINE = '$buffer .= "\n";';
- const TEXT = '$buffer .= %s%s;';
-
- /**
- * Generate Mustache Template output Buffer call PHP source.
- *
- * @param string $text
- * @param int $level
- *
- * @return string Generated output Buffer call PHP source
- */
- private function text($text, $level)
- {
- if ($text === "\n") {
- $this->indentNextLine = true;
-
- return $this->prepare(self::LINE, $level);
- } else {
- return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
- }
- }
-
- /**
- * Prepare PHP source code snippet for output.
- *
- * @param string $text
- * @param int $bonus Additional indent level (default: 0)
- * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
- *
- * @return string PHP source code snippet
- */
- private function prepare($text, $bonus = 0, $prependNewline = true)
- {
- $text = ($prependNewline ? "\n" : '').trim($text);
- if ($prependNewline) {
- $bonus++;
- }
-
- return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
- }
-
- const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
- const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
-
- /**
- * Get the current escaper.
- *
- * @param string $value (default: '$value')
- *
- * @return string Either a custom callback, or an inline call to `htmlspecialchars`
- */
- private function getEscape($value = '$value')
- {
- if ($this->customEscape) {
- return sprintf(self::CUSTOM_ESCAPE, $value);
- } else {
- return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
- }
- }
-
- /**
- * Select the appropriate Context `find` method for a given $id.
- *
- * The return value will be one of `find`, `findDot` or `last`.
- *
- * @see Mustache_Context::find
- * @see Mustache_Context::findDot
- * @see Mustache_Context::last
- *
- * @param string $id Variable name
- *
- * @return string `find` method name
- */
- private function getFindMethod($id)
- {
- if ($id === '.') {
- return 'last';
- } elseif (strpos($id, '.') === false) {
- return 'find';
- } else {
- return 'findDot';
- }
- }
-
- const LINE_INDENT = '$indent . ';
-
- /**
- * Get the current $indent prefix to write to the buffer.
- *
- * @return string "$indent . " or ""
- */
- private function flushIndent()
- {
- if ($this->indentNextLine) {
- $this->indentNextLine = false;
-
- return self::LINE_INDENT;
- } else {
- return '';
- }
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Compiler class. + * + * This class is responsible for turning a Mustache token parse tree into normal PHP source code. + */ +class Mustache_Compiler +{ + private $pragmas; + private $defaultPragmas = array(); + private $sections; + private $blocks; + private $source; + private $indentNextLine; + private $customEscape; + private $entityFlags; + private $charset; + private $strictCallables; + + /** + * Compile a Mustache token parse tree into PHP source code. + * + * @param string $source Mustache Template source code + * @param string $tree Parse tree of Mustache tokens + * @param string $name Mustache Template class name + * @param bool $customEscape (default: false) + * @param string $charset (default: 'UTF-8') + * @param bool $strictCallables (default: false) + * @param int $entityFlags (default: ENT_COMPAT) + * + * @return string Generated PHP source code + */ + public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT) + { + $this->pragmas = $this->defaultPragmas; + $this->sections = array(); + $this->blocks = array(); + $this->source = $source; + $this->indentNextLine = true; + $this->customEscape = $customEscape; + $this->entityFlags = $entityFlags; + $this->charset = $charset; + $this->strictCallables = $strictCallables; + + return $this->writeCode($tree, $name); + } + + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param string[] $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->pragmas[$pragma] = true; + } + $this->defaultPragmas = $this->pragmas; + } + + /** + * Helper function for walking the Mustache token parse tree. + * + * @throws Mustache_Exception_SyntaxException upon encountering unknown token types + * + * @param array $tree Parse tree of Mustache tokens + * @param int $level (default: 0) + * + * @return string Generated PHP source code + */ + private function walk(array $tree, $level = 0) + { + $code = ''; + $level++; + foreach ($tree as $node) { + switch ($node[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PRAGMA: + $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; + break; + + case Mustache_Tokenizer::T_SECTION: + $code .= $this->section( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_INVERTED: + $code .= $this->invertedSection( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $level + ); + break; + + case Mustache_Tokenizer::T_PARTIAL: + $code .= $this->partial( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false, + isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', + $level + ); + break; + + case Mustache_Tokenizer::T_PARENT: + $code .= $this->parent( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false, + isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', + $node[Mustache_Tokenizer::NODES], + $level + ); + break; + + case Mustache_Tokenizer::T_BLOCK_ARG: + $code .= $this->blockArg( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_BLOCK_VAR: + $code .= $this->blockVar( + $node[Mustache_Tokenizer::NODES], + $node[Mustache_Tokenizer::NAME], + $node[Mustache_Tokenizer::INDEX], + $node[Mustache_Tokenizer::END], + $node[Mustache_Tokenizer::OTAG], + $node[Mustache_Tokenizer::CTAG], + $level + ); + break; + + case Mustache_Tokenizer::T_COMMENT: + break; + + case Mustache_Tokenizer::T_ESCAPED: + case Mustache_Tokenizer::T_UNESCAPED: + case Mustache_Tokenizer::T_UNESCAPED_2: + $code .= $this->variable( + $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), + $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED, + $level + ); + break; + + case Mustache_Tokenizer::T_TEXT: + $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level); + break; + + default: + throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node); + } + } + + return $code; + } + + const KLASS = '<?php + + class %s extends Mustache_Template + { + private $lambdaHelper;%s + + public function renderInternal(Mustache_Context $context, $indent = \'\') + { + $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); + $buffer = \'\'; + %s + + return $buffer; + } + %s + %s + }'; + + const KLASS_NO_LAMBDAS = '<?php + + class %s extends Mustache_Template + {%s + public function renderInternal(Mustache_Context $context, $indent = \'\') + { + $buffer = \'\'; + %s + + return $buffer; + } + }'; + + const STRICT_CALLABLE = 'protected $strictCallables = true;'; + + /** + * Generate Mustache Template class PHP source. + * + * @param array $tree Parse tree of Mustache tokens + * @param string $name Mustache Template class name + * + * @return string Generated PHP source code + */ + private function writeCode($tree, $name) + { + $code = $this->walk($tree); + $sections = implode("\n", $this->sections); + $blocks = implode("\n", $this->blocks); + $klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS; + + $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; + + return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks); + } + + const BLOCK_VAR = ' + $blockFunction = $context->findInBlock(%s); + if (is_callable($blockFunction)) { + $buffer .= call_user_func($blockFunction, $context); + %s} + '; + + const BLOCK_VAR_ELSE = '} else {%s'; + + /** + * Generate Mustache Template inheritance block variable PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) + { + $id = var_export($id, true); + + $else = $this->walk($nodes, $level); + if ($else !== '') { + $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else); + } + + return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else); + } + + const BLOCK_ARG = '%s => array($this, \'block%s\'),'; + + /** + * Generate Mustache Template inheritance block argument PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level) + { + $key = $this->block($nodes); + $id = var_export($id, true); + + return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key); + } + + const BLOCK_FUNCTION = ' + public function block%s($context) + { + $indent = $buffer = \'\';%s + + return $buffer; + } + '; + + /** + * Generate Mustache Template inheritance block function PHP source. + * + * @param array $nodes Array of child tokens + * + * @return string key of new block function + */ + private function block($nodes) + { + $code = $this->walk($nodes, 0); + $key = ucfirst(md5($code)); + + if (!isset($this->blocks[$key])) { + $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code); + } + + return $key; + } + + const SECTION_CALL = ' + $value = $context->%s(%s);%s + $buffer .= $this->section%s($context, $indent, $value); + '; + + const SECTION = ' + private function section%s(Mustache_Context $context, $indent, $value) + { + $buffer = \'\'; + + if (%s) { + $source = %s; + $result = (string) call_user_func($value, $source, %s); + if (strpos($result, \'{{\') === false) { + $buffer .= $result; + } else { + $buffer .= $this->mustache + ->loadLambda($result%s) + ->renderInternal($context); + } + } elseif (!empty($value)) { + $values = $this->isIterable($value) ? $value : array($value); + foreach ($values as $value) { + $context->push($value); + %s + $context->pop(); + } + } + + return $buffer; + } + '; + + /** + * Generate Mustache Template section PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param string[] $filters Array of filters + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated section PHP source code + */ + private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level) + { + $source = var_export(substr($this->source, $start, $end - $start), true); + $callable = $this->getCallable(); + + if ($otag !== '{{' || $ctag !== '}}') { + $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true); + $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag); + $delims = ', ' . $delimTag; + } else { + $helper = '$this->lambdaHelper'; + $delims = ''; + } + + $key = ucfirst(md5($delims . "\n" . $source)); + + if (!isset($this->sections[$key])) { + $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2)); + } + + $method = $this->getFindMethod($id); + $id = var_export($id, true); + $filters = $this->getFilters($filters, $level); + + return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key); + } + + const INVERTED_SECTION = ' + $value = $context->%s(%s);%s + if (empty($value)) { + %s + } + '; + + /** + * Generate Mustache Template inverted section PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param string[] $filters Array of filters + * @param int $level + * + * @return string Generated inverted section PHP source code + */ + private function invertedSection($nodes, $id, $filters, $level) + { + $method = $this->getFindMethod($id); + $id = var_export($id, true); + $filters = $this->getFilters($filters, $level); + + return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level)); + } + + const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s), $context)'; + + /** + * Generate Mustache Template dynamic name resolution PHP source. + * + * @param string $id Tag name + * @param bool $dynamic True if the name is dynamic + * + * @return string Dynamic name resolution PHP source code + */ + private function resolveDynamicName($id, $dynamic) + { + if (!$dynamic) { + return var_export($id, true); + } + + $method = $this->getFindMethod($id); + $id = ($method !== 'last') ? var_export($id, true) : ''; + + // TODO: filters? + + return sprintf(self::DYNAMIC_NAME, $method, $id); + } + + const PARTIAL_INDENT = ', $indent . %s'; + const PARTIAL = ' + if ($partial = $this->mustache->loadPartial(%s)) { + $buffer .= $partial->renderInternal($context%s); + } + '; + + /** + * Generate Mustache Template partial call PHP source. + * + * @param string $id Partial name + * @param bool $dynamic Partial name is dynamic + * @param string $indent Whitespace indent to apply to partial + * @param int $level + * + * @return string Generated partial call PHP source code + */ + private function partial($id, $dynamic, $indent, $level) + { + if ($indent !== '') { + $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true)); + } else { + $indentParam = ''; + } + + return sprintf( + $this->prepare(self::PARTIAL, $level), + $this->resolveDynamicName($id, $dynamic), + $indentParam + ); + } + + const PARENT = ' + if ($parent = $this->mustache->loadPartial(%s)) { + $context->pushBlockContext(array(%s + )); + $buffer .= $parent->renderInternal($context, $indent); + $context->popBlockContext(); + } + '; + + const PARENT_NO_CONTEXT = ' + if ($parent = $this->mustache->loadPartial(%s)) { + $buffer .= $parent->renderInternal($context, $indent); + } + '; + + /** + * Generate Mustache Template inheritance parent call PHP source. + * + * @param string $id Parent tag name + * @param bool $dynamic Tag name is dynamic + * @param string $indent Whitespace indent to apply to parent + * @param array $children Child nodes + * @param int $level + * + * @return string Generated PHP source code + */ + private function parent($id, $dynamic, $indent, array $children, $level) + { + $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); + $partialName = $this->resolveDynamicName($id, $dynamic); + + if (empty($realChildren)) { + return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), $partialName); + } + + return sprintf( + $this->prepare(self::PARENT, $level), + $partialName, + $this->walk($realChildren, $level + 1) + ); + } + + /** + * Helper method for filtering out non-block-arg tokens. + * + * @param array $node + * + * @return bool True if $node is a block arg token + */ + private static function onlyBlockArgs(array $node) + { + return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG; + } + + const VARIABLE = ' + $value = $this->resolveValue($context->%s(%s), $context);%s + $buffer .= %s($value === null ? \'\' : %s); + '; + + /** + * Generate Mustache Template variable interpolation PHP source. + * + * @param string $id Variable name + * @param string[] $filters Array of filters + * @param bool $escape Escape the variable value for output? + * @param int $level + * + * @return string Generated variable interpolation PHP source + */ + private function variable($id, $filters, $escape, $level) + { + $method = $this->getFindMethod($id); + $id = ($method !== 'last') ? var_export($id, true) : ''; + $filters = $this->getFilters($filters, $level); + $value = $escape ? $this->getEscape() : '$value'; + + return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); + } + + const FILTER = ' + $filter = $context->%s(%s); + if (!(%s)) { + throw new Mustache_Exception_UnknownFilterException(%s); + } + $value = call_user_func($filter, $value);%s + '; + + /** + * Generate Mustache Template variable filtering PHP source. + * + * @param string[] $filters Array of filters + * @param int $level + * + * @return string Generated filter PHP source + */ + private function getFilters(array $filters, $level) + { + if (empty($filters)) { + return ''; + } + + $name = array_shift($filters); + $method = $this->getFindMethod($name); + $filter = ($method !== 'last') ? var_export($name, true) : ''; + $callable = $this->getCallable('$filter'); + $msg = var_export($name, true); + + return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level)); + } + + const LINE = '$buffer .= "\n";'; + const TEXT = '$buffer .= %s%s;'; + + /** + * Generate Mustache Template output Buffer call PHP source. + * + * @param string $text + * @param int $level + * + * @return string Generated output Buffer call PHP source + */ + private function text($text, $level) + { + $indentNextLine = (substr($text, -1) === "\n"); + $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true)); + $this->indentNextLine = $indentNextLine; + + return $code; + } + + /** + * Prepare PHP source code snippet for output. + * + * @param string $text + * @param int $bonus Additional indent level (default: 0) + * @param bool $prependNewline Prepend a newline to the snippet? (default: true) + * @param bool $appendNewline Append a newline to the snippet? (default: false) + * + * @return string PHP source code snippet + */ + private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false) + { + $text = ($prependNewline ? "\n" : '') . trim($text); + if ($prependNewline) { + $bonus++; + } + if ($appendNewline) { + $text .= "\n"; + } + + return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text); + } + + const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)'; + const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)'; + + /** + * Get the current escaper. + * + * @param string $value (default: '$value') + * + * @return string Either a custom callback, or an inline call to `htmlspecialchars` + */ + private function getEscape($value = '$value') + { + if ($this->customEscape) { + return sprintf(self::CUSTOM_ESCAPE, $value); + } + + return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true)); + } + + /** + * Select the appropriate Context `find` method for a given $id. + * + * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`. + * + * @see Mustache_Context::find + * @see Mustache_Context::findDot + * @see Mustache_Context::last + * + * @param string $id Variable name + * + * @return string `find` method name + */ + private function getFindMethod($id) + { + if ($id === '.') { + return 'last'; + } + + if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) { + if (substr($id, 0, 1) === '.') { + return 'findAnchoredDot'; + } + } + + if (strpos($id, '.') === false) { + return 'find'; + } + + return 'findDot'; + } + + const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; + const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; + + /** + * Helper function to compile strict vs lax "is callable" logic. + * + * @param string $variable (default: '$value') + * + * @return string "is callable" logic + */ + private function getCallable($variable = '$value') + { + $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; + + return sprintf($tpl, $variable, $variable); + } + + const LINE_INDENT = '$indent . '; + + /** + * Get the current $indent prefix to write to the buffer. + * + * @return string "$indent . " or "" + */ + private function flushIndent() + { + if (!$this->indentNextLine) { + return ''; + } + + $this->indentNextLine = false; + + return self::LINE_INDENT; + } +} diff --git a/Mustache/Context.php b/Mustache/Context.php index 7bc75719..69c02e01 100644 --- a/Mustache/Context.php +++ b/Mustache/Context.php @@ -1,149 +1,242 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template rendering Context.
- */
-class Mustache_Context
-{
- private $stack = array();
-
- /**
- * Mustache rendering Context constructor.
- *
- * @param mixed $context Default rendering context (default: null)
- */
- public function __construct($context = null)
- {
- if ($context !== null) {
- $this->stack = array($context);
- }
- }
-
- /**
- * Push a new Context frame onto the stack.
- *
- * @param mixed $value Object or array to use for context
- */
- public function push($value)
- {
- array_push($this->stack, $value);
- }
-
- /**
- * Pop the last Context frame from the stack.
- *
- * @return mixed Last Context frame (object or array)
- */
- public function pop()
- {
- return array_pop($this->stack);
- }
-
- /**
- * Get the last Context frame.
- *
- * @return mixed Last Context frame (object or array)
- */
- public function last()
- {
- return end($this->stack);
- }
-
- /**
- * Find a variable in the Context stack.
- *
- * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
- * rendering context, look for a variable with the given name:
- *
- * * If the Context frame is an associative array which contains the key $id, returns the value of that element.
- * * If the Context frame is an object, this will check first for a public method, then a public property named
- * $id. Failing both of these, it will try `__isset` and `__get` magic methods.
- * * If a value named $id is not found in any Context frame, returns an empty string.
- *
- * @param string $id Variable name
- *
- * @return mixed Variable value, or '' if not found
- */
- public function find($id)
- {
- return $this->findVariableInStack($id, $this->stack);
- }
-
- /**
- * Find a 'dot notation' variable in the Context stack.
- *
- * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
- * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
- * result. For example, given the following context stack:
- *
- * $data = array(
- * 'name' => 'Fred',
- * 'child' => array(
- * 'name' => 'Bob'
- * ),
- * );
- *
- * ... and the Mustache following template:
- *
- * {{ child.name }}
- *
- * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
- * Context frames.
- *
- * @param string $id Dotted variable selector
- *
- * @return mixed Variable value, or '' if not found
- */
- public function findDot($id)
- {
- $chunks = explode('.', $id);
- $first = array_shift($chunks);
- $value = $this->findVariableInStack($first, $this->stack);
-
- foreach ($chunks as $chunk) {
- if ($value === '') {
- return $value;
- }
-
- $value = $this->findVariableInStack($chunk, array($value));
- }
-
- return $value;
- }
-
- /**
- * Helper function to find a variable in the Context stack.
- *
- * @see Mustache_Context::find
- *
- * @param string $id Variable name
- * @param array $stack Context stack
- *
- * @return mixed Variable value, or '' if not found
- */
- private function findVariableInStack($id, array $stack)
- {
- for ($i = count($stack) - 1; $i >= 0; $i--) {
- if (is_object($stack[$i])) {
- if (method_exists($stack[$i], $id)) {
- return $stack[$i]->$id();
- } elseif (isset($stack[$i]->$id)) {
- return $stack[$i]->$id;
- }
- } elseif (is_array($stack[$i]) && array_key_exists($id, $stack[$i])) {
- return $stack[$i][$id];
- }
- }
-
- return '';
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template rendering Context. + */ +class Mustache_Context +{ + private $stack = array(); + private $blockStack = array(); + + /** + * Mustache rendering Context constructor. + * + * @param mixed $context Default rendering context (default: null) + */ + public function __construct($context = null) + { + if ($context !== null) { + $this->stack = array($context); + } + } + + /** + * Push a new Context frame onto the stack. + * + * @param mixed $value Object or array to use for context + */ + public function push($value) + { + array_push($this->stack, $value); + } + + /** + * Push a new Context frame onto the block context stack. + * + * @param mixed $value Object or array to use for block context + */ + public function pushBlockContext($value) + { + array_push($this->blockStack, $value); + } + + /** + * Pop the last Context frame from the stack. + * + * @return mixed Last Context frame (object or array) + */ + public function pop() + { + return array_pop($this->stack); + } + + /** + * Pop the last block Context frame from the stack. + * + * @return mixed Last block Context frame (object or array) + */ + public function popBlockContext() + { + return array_pop($this->blockStack); + } + + /** + * Get the last Context frame. + * + * @return mixed Last Context frame (object or array) + */ + public function last() + { + return end($this->stack); + } + + /** + * Find a variable in the Context stack. + * + * Starting with the last Context frame (the context of the innermost section), and working back to the top-level + * rendering context, look for a variable with the given name: + * + * * If the Context frame is an associative array which contains the key $id, returns the value of that element. + * * If the Context frame is an object, this will check first for a public method, then a public property named + * $id. Failing both of these, it will try `__isset` and `__get` magic methods. + * * If a value named $id is not found in any Context frame, returns an empty string. + * + * @param string $id Variable name + * + * @return mixed Variable value, or '' if not found + */ + public function find($id) + { + return $this->findVariableInStack($id, $this->stack); + } + + /** + * Find a 'dot notation' variable in the Context stack. + * + * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding + * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous + * result. For example, given the following context stack: + * + * $data = array( + * 'name' => 'Fred', + * 'child' => array( + * 'name' => 'Bob' + * ), + * ); + * + * ... and the Mustache following template: + * + * {{ child.name }} + * + * ... the `name` value is only searched for within the `child` value of the global Context, not within parent + * Context frames. + * + * @param string $id Dotted variable selector + * + * @return mixed Variable value, or '' if not found + */ + public function findDot($id) + { + $chunks = explode('.', $id); + $first = array_shift($chunks); + $value = $this->findVariableInStack($first, $this->stack); + + foreach ($chunks as $chunk) { + if ($value === '') { + return $value; + } + + $value = $this->findVariableInStack($chunk, array($value)); + } + + return $value; + } + + /** + * Find an 'anchored dot notation' variable in the Context stack. + * + * This is the same as findDot(), except it looks in the top of the context + * stack for the first value, rather than searching the whole context stack + * and starting from there. + * + * @see Mustache_Context::findDot + * + * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id + * + * @param string $id Dotted variable selector + * + * @return mixed Variable value, or '' if not found + */ + public function findAnchoredDot($id) + { + $chunks = explode('.', $id); + $first = array_shift($chunks); + if ($first !== '') { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id)); + } + + $value = $this->last(); + + foreach ($chunks as $chunk) { + if ($value === '') { + return $value; + } + + $value = $this->findVariableInStack($chunk, array($value)); + } + + return $value; + } + + /** + * Find an argument in the block context stack. + * + * @param string $id + * + * @return mixed Variable value, or '' if not found + */ + public function findInBlock($id) + { + foreach ($this->blockStack as $context) { + if (array_key_exists($id, $context)) { + return $context[$id]; + } + } + + return ''; + } + + /** + * Helper function to find a variable in the Context stack. + * + * @see Mustache_Context::find + * + * @param string $id Variable name + * @param array $stack Context stack + * + * @return mixed Variable value, or '' if not found + */ + private function findVariableInStack($id, array $stack) + { + for ($i = count($stack) - 1; $i >= 0; $i--) { + $frame = &$stack[$i]; + + switch (gettype($frame)) { + case 'object': + if (!($frame instanceof Closure)) { + // Note that is_callable() *will not work here* + // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods + if (method_exists($frame, $id)) { + return $frame->$id(); + } + + if (isset($frame->$id)) { + return $frame->$id; + } + + if ($frame instanceof ArrayAccess && isset($frame[$id])) { + return $frame[$id]; + } + } + break; + + case 'array': + if (array_key_exists($id, $frame)) { + return $frame[$id]; + } + break; + } + } + + return ''; + } +} diff --git a/Mustache/Engine.php b/Mustache/Engine.php index e1f6b8cf..7a31ac03 100644 --- a/Mustache/Engine.php +++ b/Mustache/Engine.php @@ -1,591 +1,831 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * A Mustache implementation in PHP.
- *
- * {@link http://defunkt.github.com/mustache}
- *
- * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
- * logic from template files. In fact, it is not even possible to embed logic in the template.
- *
- * This is very, very rad.
- *
- * @author Justin Hileman {@link http://justinhileman.com}
- */
-class Mustache_Engine
-{
- const VERSION = '2.0.2';
- const SPEC_VERSION = '1.1.2';
-
- // Template cache
- private $templates = array();
-
- // Environment
- private $templateClassPrefix = '__Mustache_';
- private $cache = null;
- private $loader;
- private $partialsLoader;
- private $helpers;
- private $escape;
- private $charset = 'UTF-8';
-
- /**
- * Mustache class constructor.
- *
- * Passing an $options array allows overriding certain Mustache options during instantiation:
- *
- * $options = array(
- * // The class prefix for compiled templates. Defaults to '__Mustache_'
- * 'template_class_prefix' => '__MyTemplates_',
- *
- * // A cache directory for compiled templates. Mustache will not cache templates unless this is set
- * 'cache' => dirname(__FILE__).'/tmp/cache/mustache',
- *
- * // A Mustache template loader instance. Uses a StringLoader if not specified
- * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
- *
- * // A Mustache loader instance for partials.
- * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
- *
- * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
- * // efficient or lazy as a Filesystem (or database) loader.
- * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
- *
- * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
- * // sections), or any other valid Mustache context value. They will be prepended to the context stack,
- * // so they will be available in any template loaded by this Mustache instance.
- * 'helpers' => array('i18n' => function($text) {
- * // do something translatey here...
- * }),
- *
- * // An 'escape' callback, responsible for escaping double-mustache variables.
- * 'escape' => function($value) {
- * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
- * },
- *
- * // character set for `htmlspecialchars`. Defaults to 'UTF-8'
- * 'charset' => 'ISO-8859-1',
- * );
- *
- * @param array $options (default: array())
- */
- public function __construct(array $options = array())
- {
- if (isset($options['template_class_prefix'])) {
- $this->templateClassPrefix = $options['template_class_prefix'];
- }
-
- if (isset($options['cache'])) {
- $this->cache = $options['cache'];
- }
-
- if (isset($options['loader'])) {
- $this->setLoader($options['loader']);
- }
-
- if (isset($options['partials_loader'])) {
- $this->setPartialsLoader($options['partials_loader']);
- }
-
- if (isset($options['partials'])) {
- $this->setPartials($options['partials']);
- }
-
- if (isset($options['helpers'])) {
- $this->setHelpers($options['helpers']);
- }
-
- if (isset($options['escape'])) {
- if (!is_callable($options['escape'])) {
- throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
- }
-
- $this->escape = $options['escape'];
- }
-
- if (isset($options['charset'])) {
- $this->charset = $options['charset'];
- }
- }
-
- /**
- * Shortcut 'render' invocation.
- *
- * Equivalent to calling `$mustache->loadTemplate($template)->render($data);`
- *
- * @see Mustache_Engine::loadTemplate
- * @see Mustache_Template::render
- *
- * @param string $template
- * @param mixed $data
- *
- * @return string Rendered template
- */
- public function render($template, $data)
- {
- return $this->loadTemplate($template)->render($data);
- }
-
- /**
- * Get the current Mustache escape callback.
- *
- * @return mixed Callable or null
- */
- public function getEscape()
- {
- return $this->escape;
- }
-
- /**
- * Get the current Mustache character set.
- *
- * @return string
- */
- public function getCharset()
- {
- return $this->charset;
- }
-
- /**
- * Set the Mustache template Loader instance.
- *
- * @param Mustache_Loader $loader
- */
- public function setLoader(Mustache_Loader $loader)
- {
- $this->loader = $loader;
- }
-
- /**
- * Get the current Mustache template Loader instance.
- *
- * If no Loader instance has been explicitly specified, this method will instantiate and return
- * a StringLoader instance.
- *
- * @return Mustache_Loader
- */
- public function getLoader()
- {
- if (!isset($this->loader)) {
- $this->loader = new Mustache_Loader_StringLoader;
- }
-
- return $this->loader;
- }
-
- /**
- * Set the Mustache partials Loader instance.
- *
- * @param Mustache_Loader $partialsLoader
- */
- public function setPartialsLoader(Mustache_Loader $partialsLoader)
- {
- $this->partialsLoader = $partialsLoader;
- }
-
- /**
- * Get the current Mustache partials Loader instance.
- *
- * If no Loader instance has been explicitly specified, this method will instantiate and return
- * an ArrayLoader instance.
- *
- * @return Mustache_Loader
- */
- public function getPartialsLoader()
- {
- if (!isset($this->partialsLoader)) {
- $this->partialsLoader = new Mustache_Loader_ArrayLoader;
- }
-
- return $this->partialsLoader;
- }
-
- /**
- * Set partials for the current partials Loader instance.
- *
- * @throws RuntimeException If the current Loader instance is immutable
- *
- * @param array $partials (default: array())
- */
- public function setPartials(array $partials = array())
- {
- $loader = $this->getPartialsLoader();
- if (!$loader instanceof Mustache_Loader_MutableLoader) {
- throw new RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
- }
-
- $loader->setTemplates($partials);
- }
-
- /**
- * Set an array of Mustache helpers.
- *
- * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
- * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
- * any template loaded by this Mustache instance.
- *
- * @throws InvalidArgumentException if $helpers is not an array or Traversable
- *
- * @param array|Traversable $helpers
- */
- public function setHelpers($helpers)
- {
- if (!is_array($helpers) && !$helpers instanceof Traversable) {
- throw new InvalidArgumentException('setHelpers expects an array of helpers');
- }
-
- $this->getHelpers()->clear();
-
- foreach ($helpers as $name => $helper) {
- $this->addHelper($name, $helper);
- }
- }
-
- /**
- * Get the current set of Mustache helpers.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @return Mustache_HelperCollection
- */
- public function getHelpers()
- {
- if (!isset($this->helpers)) {
- $this->helpers = new Mustache_HelperCollection;
- }
-
- return $this->helpers;
- }
-
- /**
- * Add a new Mustache helper.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- * @param mixed $helper
- */
- public function addHelper($name, $helper)
- {
- $this->getHelpers()->add($name, $helper);
- }
-
- /**
- * Get a Mustache helper by name.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function getHelper($name)
- {
- return $this->getHelpers()->get($name);
- }
-
- /**
- * Check whether this Mustache instance has a helper.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- *
- * @return boolean True if the helper is present
- */
- public function hasHelper($name)
- {
- return $this->getHelpers()->has($name);
- }
-
- /**
- * Remove a helper by name.
- *
- * @see Mustache_Engine::setHelpers
- *
- * @param string $name
- */
- public function removeHelper($name)
- {
- $this->getHelpers()->remove($name);
- }
-
- /**
- * Set the Mustache Tokenizer instance.
- *
- * @param Mustache_Tokenizer $tokenizer
- */
- public function setTokenizer(Mustache_Tokenizer $tokenizer)
- {
- $this->tokenizer = $tokenizer;
- }
-
- /**
- * Get the current Mustache Tokenizer instance.
- *
- * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Tokenizer
- */
- public function getTokenizer()
- {
- if (!isset($this->tokenizer)) {
- $this->tokenizer = new Mustache_Tokenizer;
- }
-
- return $this->tokenizer;
- }
-
- /**
- * Set the Mustache Parser instance.
- *
- * @param Mustache_Parser $parser
- */
- public function setParser(Mustache_Parser $parser)
- {
- $this->parser = $parser;
- }
-
- /**
- * Get the current Mustache Parser instance.
- *
- * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Parser
- */
- public function getParser()
- {
- if (!isset($this->parser)) {
- $this->parser = new Mustache_Parser;
- }
-
- return $this->parser;
- }
-
- /**
- * Set the Mustache Compiler instance.
- *
- * @param Mustache_Compiler $compiler
- */
- public function setCompiler(Mustache_Compiler $compiler)
- {
- $this->compiler = $compiler;
- }
-
- /**
- * Get the current Mustache Compiler instance.
- *
- * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
- *
- * @return Mustache_Compiler
- */
- public function getCompiler()
- {
- if (!isset($this->compiler)) {
- $this->compiler = new Mustache_Compiler;
- }
-
- return $this->compiler;
- }
-
- /**
- * Helper method to generate a Mustache template class.
- *
- * @param string $source
- *
- * @return string Mustache Template class name
- */
- public function getTemplateClassName($source)
- {
- return $this->templateClassPrefix . md5(sprintf(
- 'version:%s,escape:%s,charset:%s,source:%s',
- self::VERSION,
- isset($this->escape) ? 'custom' : 'default',
- $this->charset,
- $source
- ));
- }
-
- /**
- * Load a Mustache Template by name.
- *
- * @param string $name
- *
- * @return Mustache_Template
- */
- public function loadTemplate($name)
- {
- return $this->loadSource($this->getLoader()->load($name));
- }
-
- /**
- * Load a Mustache partial Template by name.
- *
- * This is a helper method used internally by Template instances for loading partial templates. You can most likely
- * ignore it completely.
- *
- * @param string $name
- *
- * @return Mustache_Template
- */
- public function loadPartial($name)
- {
- try {
- return $this->loadSource($this->getPartialsLoader()->load($name));
- } catch (InvalidArgumentException $e) {
- // If the named partial cannot be found, return null.
- }
- return null;
- }
-
- /**
- * Load a Mustache lambda Template by source.
- *
- * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
- * likely ignore it completely.
- *
- * @param string $source
- * @param string $delims (default: null)
- *
- * @return Mustache_Template
- */
- public function loadLambda($source, $delims = null)
- {
- if ($delims !== null) {
- $source = $delims . "\n" . $source;
- }
-
- return $this->loadSource($source);
- }
-
- /**
- * Instantiate and return a Mustache Template instance by source.
- *
- * @see Mustache_Engine::loadTemplate
- * @see Mustache_Engine::loadPartial
- * @see Mustache_Engine::loadLambda
- *
- * @param string $source
- *
- * @return Mustache_Template
- */
- private function loadSource($source)
- {
- $className = $this->getTemplateClassName($source);
-
- if (!isset($this->templates[$className])) {
- if (!class_exists($className, false)) {
- if ($fileName = $this->getCacheFilename($source)) {
- if (!is_file($fileName)) {
- $this->writeCacheFile($fileName, $this->compile($source));
- }
-
- require_once $fileName;
- } else {
- eval('?>'.$this->compile($source));
- }
- }
-
- $this->templates[$className] = new $className($this);
- }
-
- return $this->templates[$className];
- }
-
- /**
- * Helper method to tokenize a Mustache template.
- *
- * @see Mustache_Tokenizer::scan
- *
- * @param string $source
- *
- * @return array Tokens
- */
- private function tokenize($source)
- {
- return $this->getTokenizer()->scan($source);
- }
-
- /**
- * Helper method to parse a Mustache template.
- *
- * @see Mustache_Parser::parse
- *
- * @param string $source
- *
- * @return array Token tree
- */
- private function parse($source)
- {
- return $this->getParser()->parse($this->tokenize($source));
- }
-
- /**
- * Helper method to compile a Mustache template.
- *
- * @see Mustache_Compiler::compile
- *
- * @param string $source
- *
- * @return string generated Mustache template class code
- */
- private function compile($source)
- {
- $tree = $this->parse($source);
- $name = $this->getTemplateClassName($source);
-
- return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
- }
-
- /**
- * Helper method to generate a Mustache Template class cache filename.
- *
- * @param string $source
- *
- * @return string Mustache Template class cache filename
- */
- private function getCacheFilename($source)
- {
- if ($this->cache) {
- return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source));
- }
- return false;
- }
-
- /**
- * Helper method to dump a generated Mustache Template subclass to the file cache.
- *
- * @throws RuntimeException if unable to write to $fileName.
- *
- * @param string $fileName
- * @param string $source
- *
- * @codeCoverageIgnore
- */
- private function writeCacheFile($fileName, $source)
- {
- if (!is_dir(dirname($fileName))) {
- mkdir(dirname($fileName), 0777, true);
- }
-
- $tempFile = tempnam(dirname($fileName), basename($fileName));
- if (false !== @file_put_contents($tempFile, $source)) {
- if (@rename($tempFile, $fileName)) {
- chmod($fileName, 0644);
-
- return;
- }
- }
-
- throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A Mustache implementation in PHP. + * + * {@link http://defunkt.github.com/mustache} + * + * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view + * logic from template files. In fact, it is not even possible to embed logic in the template. + * + * This is very, very rad. + * + * @author Justin Hileman {@link http://justinhileman.com} + */ +class Mustache_Engine +{ + const VERSION = '2.14.2'; + const SPEC_VERSION = '1.3.0'; + + const PRAGMA_FILTERS = 'FILTERS'; + const PRAGMA_BLOCKS = 'BLOCKS'; + const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT'; + const PRAGMA_DYNAMIC_NAMES = 'DYNAMIC-NAMES'; + + // Known pragmas + private static $knownPragmas = array( + self::PRAGMA_FILTERS => true, + self::PRAGMA_BLOCKS => true, + self::PRAGMA_ANCHORED_DOT => true, + self::PRAGMA_DYNAMIC_NAMES => true, + ); + + // Template cache + private $templates = array(); + + // Environment + private $templateClassPrefix = '__Mustache_'; + private $cache; + private $lambdaCache; + private $cacheLambdaTemplates = false; + private $loader; + private $partialsLoader; + private $helpers; + private $escape; + private $entityFlags = ENT_COMPAT; + private $charset = 'UTF-8'; + private $logger; + private $strictCallables = false; + private $pragmas = array(); + private $delimiters; + + // Services + private $tokenizer; + private $parser; + private $compiler; + + /** + * Mustache class constructor. + * + * Passing an $options array allows overriding certain Mustache options during instantiation: + * + * $options = array( + * // The class prefix for compiled templates. Defaults to '__Mustache_'. + * 'template_class_prefix' => '__MyTemplates_', + * + * // A Mustache cache instance or a cache directory string for compiled templates. + * // Mustache will not cache templates unless this is set. + * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', + * + * // Override default permissions for cache files. Defaults to using the system-defined umask. It is + * // *strongly* recommended that you configure your umask properly rather than overriding permissions here. + * 'cache_file_mode' => 0666, + * + * // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda + * // sections are often too dynamic to benefit from caching. + * 'cache_lambda_templates' => true, + * + * // Customize the tag delimiters used by this engine instance. Note that overriding here changes the + * // delimiters used to parse all templates and partials loaded by this instance. To override just for a + * // single template, use an inline "change delimiters" tag at the start of the template file: + * // + * // {{=<% %>=}} + * // + * 'delimiters' => '<% %>', + * + * // A Mustache template loader instance. Uses a StringLoader if not specified. + * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), + * + * // A Mustache loader instance for partials. + * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), + * + * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as + * // efficient or lazy as a Filesystem (or database) loader. + * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')), + * + * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order + * // sections), or any other valid Mustache context value. They will be prepended to the context stack, + * // so they will be available in any template loaded by this Mustache instance. + * 'helpers' => array('i18n' => function ($text) { + * // do something translatey here... + * }), + * + * // An 'escape' callback, responsible for escaping double-mustache variables. + * 'escape' => function ($value) { + * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); + * }, + * + * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES. + * 'entity_flags' => ENT_QUOTES, + * + * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. + * 'charset' => 'ISO-8859-1', + * + * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible + * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is + * // available as well: + * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'), + * + * // Only treat Closure instances and invokable classes as callable. If true, values like + * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally + * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This + * // helps protect against arbitrary code execution when user input is passed directly into the template. + * // This currently defaults to false, but will default to true in v3.0. + * 'strict_callables' => true, + * + * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual + * // templates. + * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], + * ); + * + * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable + * + * @param array $options (default: array()) + */ + public function __construct(array $options = array()) + { + if (isset($options['template_class_prefix'])) { + if ((string) $options['template_class_prefix'] === '') { + throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty'); + } + + $this->templateClassPrefix = $options['template_class_prefix']; + } + + if (isset($options['cache'])) { + $cache = $options['cache']; + + if (is_string($cache)) { + $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null; + $cache = new Mustache_Cache_FilesystemCache($cache, $mode); + } + + $this->setCache($cache); + } + + if (isset($options['cache_lambda_templates'])) { + $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates']; + } + + if (isset($options['loader'])) { + $this->setLoader($options['loader']); + } + + if (isset($options['partials_loader'])) { + $this->setPartialsLoader($options['partials_loader']); + } + + if (isset($options['partials'])) { + $this->setPartials($options['partials']); + } + + if (isset($options['helpers'])) { + $this->setHelpers($options['helpers']); + } + + if (isset($options['escape'])) { + if (!is_callable($options['escape'])) { + throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable'); + } + + $this->escape = $options['escape']; + } + + if (isset($options['entity_flags'])) { + $this->entityFlags = $options['entity_flags']; + } + + if (isset($options['charset'])) { + $this->charset = $options['charset']; + } + + if (isset($options['logger'])) { + $this->setLogger($options['logger']); + } + + if (isset($options['strict_callables'])) { + $this->strictCallables = $options['strict_callables']; + } + + if (isset($options['delimiters'])) { + $this->delimiters = $options['delimiters']; + } + + if (isset($options['pragmas'])) { + foreach ($options['pragmas'] as $pragma) { + if (!isset(self::$knownPragmas[$pragma])) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma)); + } + $this->pragmas[$pragma] = true; + } + } + } + + /** + * Shortcut 'render' invocation. + * + * Equivalent to calling `$mustache->loadTemplate($template)->render($context);` + * + * @see Mustache_Engine::loadTemplate + * @see Mustache_Template::render + * + * @param string $template + * @param mixed $context (default: array()) + * + * @return string Rendered template + */ + public function render($template, $context = array()) + { + return $this->loadTemplate($template)->render($context); + } + + /** + * Get the current Mustache escape callback. + * + * @return callable|null + */ + public function getEscape() + { + return $this->escape; + } + + /** + * Get the current Mustache entitity type to escape. + * + * @return int + */ + public function getEntityFlags() + { + return $this->entityFlags; + } + + /** + * Get the current Mustache character set. + * + * @return string + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Get the current globally enabled pragmas. + * + * @return array + */ + public function getPragmas() + { + return array_keys($this->pragmas); + } + + /** + * Set the Mustache template Loader instance. + * + * @param Mustache_Loader $loader + */ + public function setLoader(Mustache_Loader $loader) + { + $this->loader = $loader; + } + + /** + * Get the current Mustache template Loader instance. + * + * If no Loader instance has been explicitly specified, this method will instantiate and return + * a StringLoader instance. + * + * @return Mustache_Loader + */ + public function getLoader() + { + if (!isset($this->loader)) { + $this->loader = new Mustache_Loader_StringLoader(); + } + + return $this->loader; + } + + /** + * Set the Mustache partials Loader instance. + * + * @param Mustache_Loader $partialsLoader + */ + public function setPartialsLoader(Mustache_Loader $partialsLoader) + { + $this->partialsLoader = $partialsLoader; + } + + /** + * Get the current Mustache partials Loader instance. + * + * If no Loader instance has been explicitly specified, this method will instantiate and return + * an ArrayLoader instance. + * + * @return Mustache_Loader + */ + public function getPartialsLoader() + { + if (!isset($this->partialsLoader)) { + $this->partialsLoader = new Mustache_Loader_ArrayLoader(); + } + + return $this->partialsLoader; + } + + /** + * Set partials for the current partials Loader instance. + * + * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable + * + * @param array $partials (default: array()) + */ + public function setPartials(array $partials = array()) + { + if (!isset($this->partialsLoader)) { + $this->partialsLoader = new Mustache_Loader_ArrayLoader(); + } + + if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) { + throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance'); + } + + $this->partialsLoader->setTemplates($partials); + } + + /** + * Set an array of Mustache helpers. + * + * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or + * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in + * any template loaded by this Mustache instance. + * + * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable + * + * @param array|Traversable $helpers + */ + public function setHelpers($helpers) + { + if (!is_array($helpers) && !$helpers instanceof Traversable) { + throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers'); + } + + $this->getHelpers()->clear(); + + foreach ($helpers as $name => $helper) { + $this->addHelper($name, $helper); + } + } + + /** + * Get the current set of Mustache helpers. + * + * @see Mustache_Engine::setHelpers + * + * @return Mustache_HelperCollection + */ + public function getHelpers() + { + if (!isset($this->helpers)) { + $this->helpers = new Mustache_HelperCollection(); + } + + return $this->helpers; + } + + /** + * Add a new Mustache helper. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * @param mixed $helper + */ + public function addHelper($name, $helper) + { + $this->getHelpers()->add($name, $helper); + } + + /** + * Get a Mustache helper by name. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * + * @return mixed Helper + */ + public function getHelper($name) + { + return $this->getHelpers()->get($name); + } + + /** + * Check whether this Mustache instance has a helper. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + * + * @return bool True if the helper is present + */ + public function hasHelper($name) + { + return $this->getHelpers()->has($name); + } + + /** + * Remove a helper by name. + * + * @see Mustache_Engine::setHelpers + * + * @param string $name + */ + public function removeHelper($name) + { + $this->getHelpers()->remove($name); + } + + /** + * Set the Mustache Logger instance. + * + * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface + * + * @param Mustache_Logger|Psr\Log\LoggerInterface $logger + */ + public function setLogger($logger = null) + { + if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { + throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); + } + + if ($this->getCache()->getLogger() === null) { + $this->getCache()->setLogger($logger); + } + + $this->logger = $logger; + } + + /** + * Get the current Mustache Logger instance. + * + * @return Mustache_Logger|Psr\Log\LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + + /** + * Set the Mustache Tokenizer instance. + * + * @param Mustache_Tokenizer $tokenizer + */ + public function setTokenizer(Mustache_Tokenizer $tokenizer) + { + $this->tokenizer = $tokenizer; + } + + /** + * Get the current Mustache Tokenizer instance. + * + * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Tokenizer + */ + public function getTokenizer() + { + if (!isset($this->tokenizer)) { + $this->tokenizer = new Mustache_Tokenizer(); + } + + return $this->tokenizer; + } + + /** + * Set the Mustache Parser instance. + * + * @param Mustache_Parser $parser + */ + public function setParser(Mustache_Parser $parser) + { + $this->parser = $parser; + } + + /** + * Get the current Mustache Parser instance. + * + * If no Parser instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Parser + */ + public function getParser() + { + if (!isset($this->parser)) { + $this->parser = new Mustache_Parser(); + } + + return $this->parser; + } + + /** + * Set the Mustache Compiler instance. + * + * @param Mustache_Compiler $compiler + */ + public function setCompiler(Mustache_Compiler $compiler) + { + $this->compiler = $compiler; + } + + /** + * Get the current Mustache Compiler instance. + * + * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Compiler + */ + public function getCompiler() + { + if (!isset($this->compiler)) { + $this->compiler = new Mustache_Compiler(); + } + + return $this->compiler; + } + + /** + * Set the Mustache Cache instance. + * + * @param Mustache_Cache $cache + */ + public function setCache(Mustache_Cache $cache) + { + if (isset($this->logger) && $cache->getLogger() === null) { + $cache->setLogger($this->getLogger()); + } + + $this->cache = $cache; + } + + /** + * Get the current Mustache Cache instance. + * + * If no Cache instance has been explicitly specified, this method will instantiate and return a new one. + * + * @return Mustache_Cache + */ + public function getCache() + { + if (!isset($this->cache)) { + $this->setCache(new Mustache_Cache_NoopCache()); + } + + return $this->cache; + } + + /** + * Get the current Lambda Cache instance. + * + * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache. + * + * @see Mustache_Engine::getCache + * + * @return Mustache_Cache + */ + protected function getLambdaCache() + { + if ($this->cacheLambdaTemplates) { + return $this->getCache(); + } + + if (!isset($this->lambdaCache)) { + $this->lambdaCache = new Mustache_Cache_NoopCache(); + } + + return $this->lambdaCache; + } + + /** + * Helper method to generate a Mustache template class. + * + * This method must be updated any time options are added which make it so + * the same template could be parsed and compiled multiple different ways. + * + * @param string|Mustache_Source $source + * + * @return string Mustache Template class name + */ + public function getTemplateClassName($source) + { + // For the most part, adding a new option here should do the trick. + // + // Pick a value here which is unique for each possible way the template + // could be compiled... but not necessarily unique per option value. See + // escape below, which only needs to differentiate between 'custom' and + // 'default' escapes. + // + // Keep this list in alphabetical order :) + $chunks = array( + 'charset' => $this->charset, + 'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}', + 'entityFlags' => $this->entityFlags, + 'escape' => isset($this->escape) ? 'custom' : 'default', + 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source', + 'pragmas' => $this->getPragmas(), + 'strictCallables' => $this->strictCallables, + 'version' => self::VERSION, + ); + + $key = json_encode($chunks); + + // Template Source instances have already provided their own source key. For strings, just include the whole + // source string in the md5 hash. + if (!$source instanceof Mustache_Source) { + $key .= "\n" . $source; + } + + return $this->templateClassPrefix . md5($key); + } + + /** + * Load a Mustache Template by name. + * + * @param string $name + * + * @return Mustache_Template + */ + public function loadTemplate($name) + { + return $this->loadSource($this->getLoader()->load($name)); + } + + /** + * Load a Mustache partial Template by name. + * + * This is a helper method used internally by Template instances for loading partial templates. You can most likely + * ignore it completely. + * + * @param string $name + * + * @return Mustache_Template + */ + public function loadPartial($name) + { + try { + if (isset($this->partialsLoader)) { + $loader = $this->partialsLoader; + } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) { + $loader = $this->loader; + } else { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->loadSource($loader->load($name)); + } catch (Mustache_Exception_UnknownTemplateException $e) { + // If the named partial cannot be found, log then return null. + $this->log( + Mustache_Logger::WARNING, + 'Partial not found: "{name}"', + array('name' => $e->getTemplateName()) + ); + } + } + + /** + * Load a Mustache lambda Template by source. + * + * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most + * likely ignore it completely. + * + * @param string $source + * @param string $delims (default: null) + * + * @return Mustache_Template + */ + public function loadLambda($source, $delims = null) + { + if ($delims !== null) { + $source = $delims . "\n" . $source; + } + + return $this->loadSource($source, $this->getLambdaCache()); + } + + /** + * Instantiate and return a Mustache Template instance by source. + * + * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect + * the 'cache_lambda_templates' configuration option. + * + * @see Mustache_Engine::loadTemplate + * @see Mustache_Engine::loadPartial + * @see Mustache_Engine::loadLambda + * + * @param string|Mustache_Source $source + * @param Mustache_Cache $cache (default: null) + * + * @return Mustache_Template + */ + private function loadSource($source, Mustache_Cache $cache = null) + { + $className = $this->getTemplateClassName($source); + + if (!isset($this->templates[$className])) { + if ($cache === null) { + $cache = $this->getCache(); + } + + if (!class_exists($className, false)) { + if (!$cache->load($className)) { + $compiled = $this->compile($source); + $cache->cache($className, $compiled); + } + } + + $this->log( + Mustache_Logger::DEBUG, + 'Instantiating template: "{className}"', + array('className' => $className) + ); + + $this->templates[$className] = new $className($this); + } + + return $this->templates[$className]; + } + + /** + * Helper method to tokenize a Mustache template. + * + * @see Mustache_Tokenizer::scan + * + * @param string $source + * + * @return array Tokens + */ + private function tokenize($source) + { + return $this->getTokenizer()->scan($source, $this->delimiters); + } + + /** + * Helper method to parse a Mustache template. + * + * @see Mustache_Parser::parse + * + * @param string $source + * + * @return array Token tree + */ + private function parse($source) + { + $parser = $this->getParser(); + $parser->setPragmas($this->getPragmas()); + + return $parser->parse($this->tokenize($source)); + } + + /** + * Helper method to compile a Mustache template. + * + * @see Mustache_Compiler::compile + * + * @param string|Mustache_Source $source + * + * @return string generated Mustache template class code + */ + private function compile($source) + { + $name = $this->getTemplateClassName($source); + + $this->log( + Mustache_Logger::INFO, + 'Compiling template to "{className}" class', + array('className' => $name) + ); + + if ($source instanceof Mustache_Source) { + $source = $source->getSource(); + } + $tree = $this->parse($source); + + $compiler = $this->getCompiler(); + $compiler->setPragmas($this->getPragmas()); + + return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); + } + + /** + * Add a log record if logging is enabled. + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + private function log($level, $message, array $context = array()) + { + if (isset($this->logger)) { + $this->logger->log($level, $message, $context); + } + } +} diff --git a/Mustache/Exception.php b/Mustache/Exception.php new file mode 100644 index 00000000..d4001a9b --- /dev/null +++ b/Mustache/Exception.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A Mustache Exception interface. + */ +interface Mustache_Exception +{ + // This space intentionally left blank. +} diff --git a/Mustache/Exception/InvalidArgumentException.php b/Mustache/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..becf2ed1 --- /dev/null +++ b/Mustache/Exception/InvalidArgumentException.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Invalid argument exception. + */ +class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception +{ + // This space intentionally left blank. +} diff --git a/Mustache/Exception/LogicException.php b/Mustache/Exception/LogicException.php new file mode 100644 index 00000000..b2424d67 --- /dev/null +++ b/Mustache/Exception/LogicException.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Logic exception. + */ +class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception +{ + // This space intentionally left blank. +} diff --git a/Mustache/Exception/RuntimeException.php b/Mustache/Exception/RuntimeException.php new file mode 100644 index 00000000..b6369f4b --- /dev/null +++ b/Mustache/Exception/RuntimeException.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Runtime exception. + */ +class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception +{ + // This space intentionally left blank. +} diff --git a/Mustache/Exception/SyntaxException.php b/Mustache/Exception/SyntaxException.php new file mode 100644 index 00000000..b1879a3d --- /dev/null +++ b/Mustache/Exception/SyntaxException.php @@ -0,0 +1,41 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache syntax exception. + */ +class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception +{ + protected $token; + + /** + * @param string $msg + * @param array $token + * @param Exception $previous + */ + public function __construct($msg, array $token, Exception $previous = null) + { + $this->token = $token; + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($msg, 0, $previous); + } else { + parent::__construct($msg); // @codeCoverageIgnore + } + } + + /** + * @return array + */ + public function getToken() + { + return $this->token; + } +} diff --git a/Mustache/Exception/UnknownFilterException.php b/Mustache/Exception/UnknownFilterException.php new file mode 100644 index 00000000..0651c173 --- /dev/null +++ b/Mustache/Exception/UnknownFilterException.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Unknown filter exception. + */ +class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception +{ + protected $filterName; + + /** + * @param string $filterName + * @param Exception $previous + */ + public function __construct($filterName, Exception $previous = null) + { + $this->filterName = $filterName; + $message = sprintf('Unknown filter: %s', $filterName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getFilterName() + { + return $this->filterName; + } +} diff --git a/Mustache/Exception/UnknownHelperException.php b/Mustache/Exception/UnknownHelperException.php new file mode 100644 index 00000000..193be782 --- /dev/null +++ b/Mustache/Exception/UnknownHelperException.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Unknown helper exception. + */ +class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception +{ + protected $helperName; + + /** + * @param string $helperName + * @param Exception $previous + */ + public function __construct($helperName, Exception $previous = null) + { + $this->helperName = $helperName; + $message = sprintf('Unknown helper: %s', $helperName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getHelperName() + { + return $this->helperName; + } +} diff --git a/Mustache/Exception/UnknownTemplateException.php b/Mustache/Exception/UnknownTemplateException.php new file mode 100644 index 00000000..32a778a5 --- /dev/null +++ b/Mustache/Exception/UnknownTemplateException.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Unknown template exception. + */ +class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception +{ + protected $templateName; + + /** + * @param string $templateName + * @param Exception $previous + */ + public function __construct($templateName, Exception $previous = null) + { + $this->templateName = $templateName; + $message = sprintf('Unknown template: %s', $templateName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + public function getTemplateName() + { + return $this->templateName; + } +} diff --git a/Mustache/HelperCollection.php b/Mustache/HelperCollection.php index 92bcde4a..5d8f73c1 100644 --- a/Mustache/HelperCollection.php +++ b/Mustache/HelperCollection.php @@ -1,168 +1,172 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * A collection of helpers for a Mustache instance.
- */
-class Mustache_HelperCollection
-{
- private $helpers = array();
-
- /**
- * Helper Collection constructor.
- *
- * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
- *
- * @throws InvalidArgumentException if the $helpers argument isn't an array or Traversable
- *
- * @param array|Traversable $helpers (default: null)
- */
- public function __construct($helpers = null)
- {
- if ($helpers !== null) {
- if (!is_array($helpers) && !$helpers instanceof Traversable) {
- throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
- }
-
- foreach ($helpers as $name => $helper) {
- $this->add($name, $helper);
- }
- }
- }
-
- /**
- * Magic mutator.
- *
- * @see Mustache_HelperCollection::add
- *
- * @param string $name
- * @param mixed $helper
- */
- public function __set($name, $helper)
- {
- $this->add($name, $helper);
- }
-
- /**
- * Add a helper to this collection.
- *
- * @param string $name
- * @param mixed $helper
- */
- public function add($name, $helper)
- {
- $this->helpers[$name] = $helper;
- }
-
- /**
- * Magic accessor.
- *
- * @see Mustache_HelperCollection::get
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function __get($name)
- {
- return $this->get($name);
- }
-
- /**
- * Get a helper by name.
- *
- * @param string $name
- *
- * @return mixed Helper
- */
- public function get($name)
- {
- if (!$this->has($name)) {
- throw new InvalidArgumentException('Unknown helper: '.$name);
- }
-
- return $this->helpers[$name];
- }
-
- /**
- * Magic isset().
- *
- * @see Mustache_HelperCollection::has
- *
- * @param string $name
- *
- * @return boolean True if helper is present
- */
- public function __isset($name)
- {
- return $this->has($name);
- }
-
- /**
- * Check whether a given helper is present in the collection.
- *
- * @param string $name
- *
- * @return boolean True if helper is present
- */
- public function has($name)
- {
- return array_key_exists($name, $this->helpers);
- }
-
- /**
- * Magic unset().
- *
- * @see Mustache_HelperCollection::remove
- *
- * @param string $name
- */
- public function __unset($name)
- {
- $this->remove($name);
- }
-
- /**
- * Check whether a given helper is present in the collection.
- *
- * @throws InvalidArgumentException if the requested helper is not present.
- *
- * @param string $name
- */
- public function remove($name)
- {
- if (!$this->has($name)) {
- throw new InvalidArgumentException('Unknown helper: '.$name);
- }
-
- unset($this->helpers[$name]);
- }
-
- /**
- * Clear the helper collection.
- *
- * Removes all helpers from this collection
- */
- public function clear()
- {
- $this->helpers = array();
- }
-
- /**
- * Check whether the helper collection is empty.
- *
- * @return boolean True if the collection is empty
- */
- public function isEmpty()
- {
- return empty($this->helpers);
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A collection of helpers for a Mustache instance. + */ +class Mustache_HelperCollection +{ + private $helpers = array(); + + /** + * Helper Collection constructor. + * + * Optionally accepts an array (or Traversable) of `$name => $helper` pairs. + * + * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable + * + * @param array|Traversable $helpers (default: null) + */ + public function __construct($helpers = null) + { + if ($helpers === null) { + return; + } + + if (!is_array($helpers) && !$helpers instanceof Traversable) { + throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers'); + } + + foreach ($helpers as $name => $helper) { + $this->add($name, $helper); + } + } + + /** + * Magic mutator. + * + * @see Mustache_HelperCollection::add + * + * @param string $name + * @param mixed $helper + */ + public function __set($name, $helper) + { + $this->add($name, $helper); + } + + /** + * Add a helper to this collection. + * + * @param string $name + * @param mixed $helper + */ + public function add($name, $helper) + { + $this->helpers[$name] = $helper; + } + + /** + * Magic accessor. + * + * @see Mustache_HelperCollection::get + * + * @param string $name + * + * @return mixed Helper + */ + public function __get($name) + { + return $this->get($name); + } + + /** + * Get a helper by name. + * + * @throws Mustache_Exception_UnknownHelperException If helper does not exist + * + * @param string $name + * + * @return mixed Helper + */ + public function get($name) + { + if (!$this->has($name)) { + throw new Mustache_Exception_UnknownHelperException($name); + } + + return $this->helpers[$name]; + } + + /** + * Magic isset(). + * + * @see Mustache_HelperCollection::has + * + * @param string $name + * + * @return bool True if helper is present + */ + public function __isset($name) + { + return $this->has($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @param string $name + * + * @return bool True if helper is present + */ + public function has($name) + { + return array_key_exists($name, $this->helpers); + } + + /** + * Magic unset(). + * + * @see Mustache_HelperCollection::remove + * + * @param string $name + */ + public function __unset($name) + { + $this->remove($name); + } + + /** + * Check whether a given helper is present in the collection. + * + * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present + * + * @param string $name + */ + public function remove($name) + { + if (!$this->has($name)) { + throw new Mustache_Exception_UnknownHelperException($name); + } + + unset($this->helpers[$name]); + } + + /** + * Clear the helper collection. + * + * Removes all helpers from this collection + */ + public function clear() + { + $this->helpers = array(); + } + + /** + * Check whether the helper collection is empty. + * + * @return bool True if the collection is empty + */ + public function isEmpty() + { + return empty($this->helpers); + } +} diff --git a/Mustache/LICENSE b/Mustache/LICENSE index 63c95ace..e0aecc94 100644 --- a/Mustache/LICENSE +++ b/Mustache/LICENSE @@ -1,22 +1,21 @@ -Copyright (c) 2010 Justin Hileman +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: +Copyright (c) 2010-2015 Justin Hileman -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Mustache/LambdaHelper.php b/Mustache/LambdaHelper.php new file mode 100644 index 00000000..e93dbfa3 --- /dev/null +++ b/Mustache/LambdaHelper.php @@ -0,0 +1,76 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Lambda Helper. + * + * Passed as the second argument to section lambdas (higher order sections), + * giving them access to a `render` method for rendering a string with the + * current context. + */ +class Mustache_LambdaHelper +{ + private $mustache; + private $context; + private $delims; + + /** + * Mustache Lambda Helper constructor. + * + * @param Mustache_Engine $mustache Mustache engine instance + * @param Mustache_Context $context Rendering context + * @param string $delims Optional custom delimiters, in the format `{{= <% %> =}}`. (default: null) + */ + public function __construct(Mustache_Engine $mustache, Mustache_Context $context, $delims = null) + { + $this->mustache = $mustache; + $this->context = $context; + $this->delims = $delims; + } + + /** + * Render a string as a Mustache template with the current rendering context. + * + * @param string $string + * + * @return string Rendered template + */ + public function render($string) + { + return $this->mustache + ->loadLambda((string) $string, $this->delims) + ->renderInternal($this->context); + } + + /** + * Render a string as a Mustache template with the current rendering context. + * + * @param string $string + * + * @return string Rendered template + */ + public function __invoke($string) + { + return $this->render($string); + } + + /** + * Get a Lambda Helper with custom delimiters. + * + * @param string $delims Custom delimiters, in the format `{{= <% %> =}}` + * + * @return Mustache_LambdaHelper + */ + public function withDelimiters($delims) + { + return new self($this->mustache, $this->context, $delims); + } +} diff --git a/Mustache/Loader.php b/Mustache/Loader.php index f082cf59..23adba1a 100644 --- a/Mustache/Loader.php +++ b/Mustache/Loader.php @@ -1,26 +1,27 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template Loader interface.
- */
-interface Mustache_Loader
-{
-
- /**
- * Load a Template by name.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name);
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template Loader interface. + */ +interface Mustache_Loader +{ + /** + * Load a Template by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string|Mustache_Source Mustache Template source + */ + public function load($name); +} diff --git a/Mustache/Loader/ArrayLoader.php b/Mustache/Loader/ArrayLoader.php index 0a9ceefb..4276493a 100644 --- a/Mustache/Loader/ArrayLoader.php +++ b/Mustache/Loader/ArrayLoader.php @@ -1,79 +1,79 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template array Loader implementation.
- *
- * An ArrayLoader instance loads Mustache Template source by name from an initial array:
- *
- * $loader = new ArrayLoader(
- * 'foo' => '{{ bar }}',
- * 'baz' => 'Hey {{ qux }}!'
- * );
- *
- * $tpl = $loader->load('foo'); // '{{ bar }}'
- *
- * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials
- * is set. It can also be used as a quick-and-dirty Template loader.
- *
- * @implements Loader
- * @implements MutableLoader
- */
-class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader
-{
-
- /**
- * ArrayLoader constructor.
- *
- * @param array $templates Associative array of Template source (default: array())
- */
- public function __construct(array $templates = array())
- {
- $this->templates = $templates;
- }
-
- /**
- * Load a Template.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- if (!isset($this->templates[$name])) {
- throw new InvalidArgumentException('Template '.$name.' not found.');
- }
-
- return $this->templates[$name];
- }
-
- /**
- * Set an associative array of Template sources for this loader.
- *
- * @param array $templates
- */
- public function setTemplates(array $templates)
- {
- $this->templates = $templates;
- }
-
- /**
- * Set a Template source by name.
- *
- * @param string $name
- * @param string $template Mustache Template source
- */
- public function setTemplate($name, $template)
- {
- $this->templates[$name] = $template;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template array Loader implementation. + * + * An ArrayLoader instance loads Mustache Template source by name from an initial array: + * + * $loader = new ArrayLoader( + * 'foo' => '{{ bar }}', + * 'baz' => 'Hey {{ qux }}!' + * ); + * + * $tpl = $loader->load('foo'); // '{{ bar }}' + * + * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials + * is set. It can also be used as a quick-and-dirty Template loader. + */ +class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader +{ + private $templates; + + /** + * ArrayLoader constructor. + * + * @param array $templates Associative array of Template source (default: array()) + */ + public function __construct(array $templates = array()) + { + $this->templates = $templates; + } + + /** + * Load a Template. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + if (!isset($this->templates[$name])) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->templates[$name]; + } + + /** + * Set an associative array of Template sources for this loader. + * + * @param array $templates + */ + public function setTemplates(array $templates) + { + $this->templates = $templates; + } + + /** + * Set a Template source by name. + * + * @param string $name + * @param string $template Mustache Template source + */ + public function setTemplate($name, $template) + { + $this->templates[$name] = $template; + } +} diff --git a/Mustache/Loader/CascadingLoader.php b/Mustache/Loader/CascadingLoader.php new file mode 100644 index 00000000..3fb6353c --- /dev/null +++ b/Mustache/Loader/CascadingLoader.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A Mustache Template cascading loader implementation, which delegates to other + * Loader instances. + */ +class Mustache_Loader_CascadingLoader implements Mustache_Loader +{ + private $loaders; + + /** + * Construct a CascadingLoader with an array of loaders. + * + * $loader = new Mustache_Loader_CascadingLoader(array( + * new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__), + * new Mustache_Loader_FilesystemLoader(__DIR__.'/templates') + * )); + * + * @param Mustache_Loader[] $loaders + */ + public function __construct(array $loaders = array()) + { + $this->loaders = array(); + foreach ($loaders as $loader) { + $this->addLoader($loader); + } + } + + /** + * Add a Loader instance. + * + * @param Mustache_Loader $loader + */ + public function addLoader(Mustache_Loader $loader) + { + $this->loaders[] = $loader; + } + + /** + * Load a Template by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + foreach ($this->loaders as $loader) { + try { + return $loader->load($name); + } catch (Mustache_Exception_UnknownTemplateException $e) { + // do nothing, check the next loader. + } + } + + throw new Mustache_Exception_UnknownTemplateException($name); + } +} diff --git a/Mustache/Loader/FilesystemLoader.php b/Mustache/Loader/FilesystemLoader.php index 34a9ecfd..e366df70 100644 --- a/Mustache/Loader/FilesystemLoader.php +++ b/Mustache/Loader/FilesystemLoader.php @@ -1,118 +1,135 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template filesystem Loader implementation.
- *
- * An ArrayLoader instance loads Mustache Template source from the filesystem by name:
- *
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
- * $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache');
- *
- * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates:
- *
- * $m = new Mustache(array(
- * 'loader' => new FilesystemLoader(dirname(__FILE__).'/views'),
- * 'partials_loader' => new FilesystemLoader(dirname(__FILE__).'/views/partials'),
- * ));
- *
- * @implements Loader
- */
-class Mustache_Loader_FilesystemLoader implements Mustache_Loader
-{
- private $baseDir;
- private $extension = '.mustache';
- private $templates = array();
-
- /**
- * Mustache filesystem Loader constructor.
- *
- * Passing an $options array allows overriding certain Loader options during instantiation:
- *
- * $options = array(
- * // The filename extension used for Mustache templates. Defaults to '.mustache'
- * 'extension' => '.ms',
- * );
- *
- * @throws RuntimeException if $baseDir does not exist.
- *
- * @param string $baseDir Base directory containing Mustache template files.
- * @param array $options Array of Loader options (default: array())
- */
- public function __construct($baseDir, array $options = array())
- {
- $this->baseDir = rtrim(realpath($baseDir), '/');
-
- if (!is_dir($this->baseDir)) {
- throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
- }
-
- if (isset($options['extension'])) {
- $this->extension = '.' . ltrim($options['extension'], '.');
- }
- }
-
- /**
- * Load a Template by name.
- *
- * $loader = new FilesystemLoader(dirname(__FILE__).'/views');
- * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache";
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- if (!isset($this->templates[$name])) {
- $this->templates[$name] = $this->loadFile($name);
- }
-
- return $this->templates[$name];
- }
-
- /**
- * Helper function for loading a Mustache file by name.
- *
- * @throws InvalidArgumentException if a template file is not found.
- *
- * @param string $name
- *
- * @return string Mustache Template source
- */
- protected function loadFile($name)
- {
- $fileName = $this->getFileName($name);
-
- if (!file_exists($fileName)) {
- throw new InvalidArgumentException('Template '.$name.' not found.');
- }
-
- return file_get_contents($fileName);
- }
-
- /**
- * Helper function for getting a Mustache template file name.
- *
- * @param string $name
- *
- * @return string Template file name
- */
- protected function getFileName($name)
- {
- $fileName = $this->baseDir . '/' . $name;
- if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) {
- $fileName .= $this->extension;
- }
-
- return $fileName;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template filesystem Loader implementation. + * + * A FilesystemLoader instance loads Mustache Template source from the filesystem by name: + * + * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); + * $tpl = $loader->load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache'); + * + * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates: + * + * $m = new Mustache(array( + * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), + * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), + * )); + */ +class Mustache_Loader_FilesystemLoader implements Mustache_Loader +{ + private $baseDir; + private $extension = '.mustache'; + private $templates = array(); + + /** + * Mustache filesystem Loader constructor. + * + * Passing an $options array allows overriding certain Loader options during instantiation: + * + * $options = array( + * // The filename extension used for Mustache templates. Defaults to '.mustache' + * 'extension' => '.ms', + * ); + * + * @throws Mustache_Exception_RuntimeException if $baseDir does not exist + * + * @param string $baseDir Base directory containing Mustache template files + * @param array $options Array of Loader options (default: array()) + */ + public function __construct($baseDir, array $options = array()) + { + $this->baseDir = $baseDir; + + if (strpos($this->baseDir, '://') === false) { + $this->baseDir = realpath($this->baseDir); + } + + if ($this->shouldCheckPath() && !is_dir($this->baseDir)) { + throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir)); + } + + if (array_key_exists('extension', $options)) { + if (empty($options['extension'])) { + $this->extension = ''; + } else { + $this->extension = '.' . ltrim($options['extension'], '.'); + } + } + } + + /** + * Load a Template by name. + * + * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); + * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache"; + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + if (!isset($this->templates[$name])) { + $this->templates[$name] = $this->loadFile($name); + } + + return $this->templates[$name]; + } + + /** + * Helper function for loading a Mustache file by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if ($this->shouldCheckPath() && !file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return file_get_contents($fileName); + } + + /** + * Helper function for getting a Mustache template file name. + * + * @param string $name + * + * @return string Template file name + */ + protected function getFileName($name) + { + $fileName = $this->baseDir . '/' . $name; + if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) { + $fileName .= $this->extension; + } + + return $fileName; + } + + /** + * Only check if baseDir is a directory and requested templates are files if + * baseDir is using the filesystem stream wrapper. + * + * @return bool Whether to check `is_dir` and `file_exists` + */ + protected function shouldCheckPath() + { + return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0; + } +} diff --git a/Mustache/Loader/InlineLoader.php b/Mustache/Loader/InlineLoader.php new file mode 100644 index 00000000..ae297fec --- /dev/null +++ b/Mustache/Loader/InlineLoader.php @@ -0,0 +1,123 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A Mustache Template loader for inline templates. + * + * With the InlineLoader, templates can be defined at the end of any PHP source + * file: + * + * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); + * $hello = $loader->load('hello'); + * $goodbye = $loader->load('goodbye'); + * + * __halt_compiler(); + * + * @@ hello + * Hello, {{ planet }}! + * + * @@ goodbye + * Goodbye, cruel {{ planet }} + * + * Templates are deliniated by lines containing only `@@ name`. + * + * The InlineLoader is well-suited to micro-frameworks such as Silex: + * + * $app->register(new MustacheServiceProvider, array( + * 'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__) + * )); + * + * $app->get('/{name}', function ($name) use ($app) { + * return $app['mustache']->render('hello', compact('name')); + * }) + * ->value('name', 'world'); + * + * // ... + * + * __halt_compiler(); + * + * @@ hello + * Hello, {{ name }}! + */ +class Mustache_Loader_InlineLoader implements Mustache_Loader +{ + protected $fileName; + protected $offset; + protected $templates; + + /** + * The InlineLoader requires a filename and offset to process templates. + * + * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually + * perfectly suited to the job: + * + * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); + * + * Note that this only works if the loader is instantiated inside the same + * file as the inline templates. If the templates are located in another + * file, it would be necessary to manually specify the filename and offset. + * + * @param string $fileName The file to parse for inline templates + * @param int $offset A string offset for the start of the templates. + * This usually coincides with the `__halt_compiler` + * call, and the `__COMPILER_HALT_OFFSET__` + */ + public function __construct($fileName, $offset) + { + if (!is_file($fileName)) { + throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.'); + } + + if (!is_int($offset) || $offset < 0) { + throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.'); + } + + $this->fileName = $fileName; + $this->offset = $offset; + } + + /** + * Load a Template by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found + * + * @param string $name + * + * @return string Mustache Template source + */ + public function load($name) + { + $this->loadTemplates(); + + if (!array_key_exists($name, $this->templates)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return $this->templates[$name]; + } + + /** + * Parse and load templates from the end of a source file. + */ + protected function loadTemplates() + { + if ($this->templates === null) { + $this->templates = array(); + $data = file_get_contents($this->fileName, false, null, $this->offset); + foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) { + if (trim($chunk)) { + list($name, $content) = explode("\n", $chunk, 2); + $this->templates[trim($name)] = trim($content); + } + } + } + } +} diff --git a/Mustache/Loader/MutableLoader.php b/Mustache/Loader/MutableLoader.php index 952db2f0..57fe5be3 100644 --- a/Mustache/Loader/MutableLoader.php +++ b/Mustache/Loader/MutableLoader.php @@ -1,32 +1,31 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template mutable Loader interface.
- */
-interface Mustache_Loader_MutableLoader
-{
-
- /**
- * Set an associative array of Template sources for this loader.
- *
- * @param array $templates
- */
- public function setTemplates(array $templates);
-
- /**
- * Set a Template source by name.
- *
- * @param string $name
- * @param string $template Mustache Template source
- */
- public function setTemplate($name, $template);
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template mutable Loader interface. + */ +interface Mustache_Loader_MutableLoader +{ + /** + * Set an associative array of Template sources for this loader. + * + * @param array $templates + */ + public function setTemplates(array $templates); + + /** + * Set a Template source by name. + * + * @param string $name + * @param string $template Mustache Template source + */ + public function setTemplate($name, $template); +} diff --git a/Mustache/Loader/ProductionFilesystemLoader.php b/Mustache/Loader/ProductionFilesystemLoader.php new file mode 100644 index 00000000..e7353327 --- /dev/null +++ b/Mustache/Loader/ProductionFilesystemLoader.php @@ -0,0 +1,86 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template production filesystem Loader implementation. + * + * A production-ready FilesystemLoader, which doesn't require reading a file if it already exists in the template cache. + * + * {@inheritdoc} + */ +class Mustache_Loader_ProductionFilesystemLoader extends Mustache_Loader_FilesystemLoader +{ + private $statProps; + + /** + * Mustache production filesystem Loader constructor. + * + * Passing an $options array allows overriding certain Loader options during instantiation: + * + * $options = array( + * // The filename extension used for Mustache templates. Defaults to '.mustache' + * 'extension' => '.ms', + * 'stat_props' => array('size', 'mtime'), + * ); + * + * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this + * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat(): + * + * http://php.net/manual/en/function.stat.php + * + * You can also disable filesystem stat entirely: + * + * $options = array('stat_props' => null); + * + * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation, + * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy + * process so you don't forget! + * + * @throws Mustache_Exception_RuntimeException if $baseDir does not exist. + * + * @param string $baseDir Base directory containing Mustache template files. + * @param array $options Array of Loader options (default: array()) + */ + public function __construct($baseDir, array $options = array()) + { + parent::__construct($baseDir, $options); + + if (array_key_exists('stat_props', $options)) { + if (empty($options['stat_props'])) { + $this->statProps = array(); + } else { + $this->statProps = $options['stat_props']; + } + } else { + $this->statProps = array('size', 'mtime'); + } + } + + /** + * Helper function for loading a Mustache file by name. + * + * @throws Mustache_Exception_UnknownTemplateException If a template file is not found. + * + * @param string $name + * + * @return Mustache_Source Mustache Template source + */ + protected function loadFile($name) + { + $fileName = $this->getFileName($name); + + if (!file_exists($fileName)) { + throw new Mustache_Exception_UnknownTemplateException($name); + } + + return new Mustache_Source_FilesystemSource($fileName, $this->statProps); + } +} diff --git a/Mustache/Loader/StringLoader.php b/Mustache/Loader/StringLoader.php index 8f18062f..7012c03b 100644 --- a/Mustache/Loader/StringLoader.php +++ b/Mustache/Loader/StringLoader.php @@ -1,42 +1,39 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Template string Loader implementation.
- *
- * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through:
- *
- * $loader = new StringLoader;
- * $tpl = $loader->load('{{ foo }}'); // '{{ foo }}'
- *
- * This is the default Template Loader instance used by Mustache:
- *
- * $m = new Mustache;
- * $tpl = $m->loadTemplate('{{ foo }}');
- * echo $tpl->render(array('foo' => 'bar')); // "bar"
- *
- * @implements Loader
- */
-class Mustache_Loader_StringLoader implements Mustache_Loader
-{
-
- /**
- * Load a Template by source.
- *
- * @param string $name Mustache Template source
- *
- * @return string Mustache Template source
- */
- public function load($name)
- {
- return $name;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Template string Loader implementation. + * + * A StringLoader instance is essentially a noop. It simply passes the 'name' argument straight through: + * + * $loader = new StringLoader; + * $tpl = $loader->load('{{ foo }}'); // '{{ foo }}' + * + * This is the default Template Loader instance used by Mustache: + * + * $m = new Mustache; + * $tpl = $m->loadTemplate('{{ foo }}'); + * echo $tpl->render(array('foo' => 'bar')); // "bar" + */ +class Mustache_Loader_StringLoader implements Mustache_Loader +{ + /** + * Load a Template by source. + * + * @param string $name Mustache Template source + * + * @return string Mustache Template source + */ + public function load($name) + { + return $name; + } +} diff --git a/Mustache/Logger.php b/Mustache/Logger.php new file mode 100644 index 00000000..cb4037a2 --- /dev/null +++ b/Mustache/Logger.php @@ -0,0 +1,126 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Describes a Mustache logger instance. + * + * This is identical to the Psr\Log\LoggerInterface. + * + * The message MUST be a string or object implementing __toString(). + * + * The message MAY contain placeholders in the form: {foo} where foo + * will be replaced by the context data in key "foo". + * + * The context array can contain arbitrary data, the only assumption that + * can be made by implementors is that if an Exception instance is given + * to produce a stack trace, it MUST be in a key named "exception". + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md + * for the full interface specification. + */ +interface Mustache_Logger +{ + /** + * Psr\Log compatible log levels. + */ + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; + + /** + * System is unusable. + * + * @param string $message + * @param array $context + */ + public function emergency($message, array $context = array()); + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = array()); + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = array()); + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = array()); + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = array()); + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = array()); + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = array()); + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = array()); + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()); +} diff --git a/Mustache/Logger/AbstractLogger.php b/Mustache/Logger/AbstractLogger.php new file mode 100644 index 00000000..a169f9c6 --- /dev/null +++ b/Mustache/Logger/AbstractLogger.php @@ -0,0 +1,121 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * This is a simple Logger implementation that other Loggers can inherit from. + * + * This is identical to the Psr\Log\AbstractLogger. + * + * It simply delegates all log-level-specific methods to the `log` method to + * reduce boilerplate code that a simple Logger that does the same thing with + * messages regardless of the error level has to implement. + */ +abstract class Mustache_Logger_AbstractLogger implements Mustache_Logger +{ + /** + * System is unusable. + * + * @param string $message + * @param array $context + */ + public function emergency($message, array $context = array()) + { + $this->log(Mustache_Logger::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + */ + public function alert($message, array $context = array()) + { + $this->log(Mustache_Logger::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + */ + public function critical($message, array $context = array()) + { + $this->log(Mustache_Logger::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + */ + public function error($message, array $context = array()) + { + $this->log(Mustache_Logger::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + */ + public function warning($message, array $context = array()) + { + $this->log(Mustache_Logger::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + */ + public function notice($message, array $context = array()) + { + $this->log(Mustache_Logger::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + */ + public function info($message, array $context = array()) + { + $this->log(Mustache_Logger::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + */ + public function debug($message, array $context = array()) + { + $this->log(Mustache_Logger::DEBUG, $message, $context); + } +} diff --git a/Mustache/Logger/StreamLogger.php b/Mustache/Logger/StreamLogger.php new file mode 100644 index 00000000..402a148e --- /dev/null +++ b/Mustache/Logger/StreamLogger.php @@ -0,0 +1,194 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A Mustache Stream Logger. + * + * The Stream Logger wraps a file resource instance (such as a stream) or a + * stream URL. All log messages over the threshold level will be appended to + * this stream. + * + * Hint: Try `php://stderr` for your stream URL. + */ +class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger +{ + protected static $levels = array( + self::DEBUG => 100, + self::INFO => 200, + self::NOTICE => 250, + self::WARNING => 300, + self::ERROR => 400, + self::CRITICAL => 500, + self::ALERT => 550, + self::EMERGENCY => 600, + ); + + protected $level; + protected $stream = null; + protected $url = null; + + /** + * @throws InvalidArgumentException if the logging level is unknown + * + * @param resource|string $stream Resource instance or URL + * @param int $level The minimum logging level at which this handler will be triggered + */ + public function __construct($stream, $level = Mustache_Logger::ERROR) + { + $this->setLevel($level); + + if (is_resource($stream)) { + $this->stream = $stream; + } else { + $this->url = $stream; + } + } + + /** + * Close stream resources. + */ + public function __destruct() + { + if (is_resource($this->stream)) { + fclose($this->stream); + } + } + + /** + * Set the minimum logging level. + * + * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown + * + * @param int $level The minimum logging level which will be written + */ + public function setLevel($level) + { + if (!array_key_exists($level, self::$levels)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); + } + + $this->level = $level; + } + + /** + * Get the current minimum logging level. + * + * @return int + */ + public function getLevel() + { + return $this->level; + } + + /** + * Logs with an arbitrary level. + * + * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown + * + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = array()) + { + if (!array_key_exists($level, self::$levels)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); + } + + if (self::$levels[$level] >= self::$levels[$this->level]) { + $this->writeLog($level, $message, $context); + } + } + + /** + * Write a record to the log. + * + * @throws Mustache_Exception_LogicException If neither a stream resource nor url is present + * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + */ + protected function writeLog($level, $message, array $context = array()) + { + if (!is_resource($this->stream)) { + if (!isset($this->url)) { + throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); + } + + $this->stream = fopen($this->url, 'a'); + if (!is_resource($this->stream)) { + // @codeCoverageIgnoreStart + throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url)); + // @codeCoverageIgnoreEnd + } + } + + fwrite($this->stream, self::formatLine($level, $message, $context)); + } + + /** + * Gets the name of the logging level. + * + * @throws InvalidArgumentException if the logging level is unknown + * + * @param int $level + * + * @return string + */ + protected static function getLevelName($level) + { + return strtoupper($level); + } + + /** + * Format a log line for output. + * + * @param int $level The logging level + * @param string $message The log message + * @param array $context The log context + * + * @return string + */ + protected static function formatLine($level, $message, array $context = array()) + { + return sprintf( + "%s: %s\n", + self::getLevelName($level), + self::interpolateMessage($message, $context) + ); + } + + /** + * Interpolate context values into the message placeholders. + * + * @param string $message + * @param array $context + * + * @return string + */ + protected static function interpolateMessage($message, array $context = array()) + { + if (strpos($message, '{') === false) { + return $message; + } + + // build a replacement array with braces around the context keys + $replace = array(); + foreach ($context as $key => $val) { + $replace['{' . $key . '}'] = $val; + } + + // interpolate replacement values into the the message and return + return strtr($message, $replace); + } +} diff --git a/Mustache/Parser.php b/Mustache/Parser.php index 39911d6b..5db58244 100644 --- a/Mustache/Parser.php +++ b/Mustache/Parser.php @@ -1,88 +1,385 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Parser class.
- *
- * This class is responsible for turning a set of Mustache tokens into a parse tree.
- */
-class Mustache_Parser
-{
-
- /**
- * Process an array of Mustache tokens and convert them into a parse tree.
- *
- * @param array $tokens Set of Mustache tokens
- *
- * @return array Mustache token parse tree
- */
- public function parse(array $tokens = array())
- {
- return $this->buildTree(new ArrayIterator($tokens));
- }
-
- /**
- * Helper method for recursively building a parse tree.
- *
- * @param ArrayIterator $tokens Stream of Mustache tokens
- * @param array $parent Parent token (default: null)
- *
- * @return array Mustache Token parse tree
- *
- * @throws LogicException when nesting errors or mismatched section tags are encountered.
- */
- private function buildTree(ArrayIterator $tokens, array $parent = null)
- {
- $nodes = array();
-
- do {
- $token = $tokens->current();
- $tokens->next();
-
- if ($token === null) {
- continue;
- } else {
- switch ($token[Mustache_Tokenizer::TYPE]) {
- case Mustache_Tokenizer::T_SECTION:
- case Mustache_Tokenizer::T_INVERTED:
- $nodes[] = $this->buildTree($tokens, $token);
- break;
-
- case Mustache_Tokenizer::T_END_SECTION:
- if (!isset($parent)) {
- throw new LogicException('Unexpected closing tag: /'. $token[Mustache_Tokenizer::NAME]);
- }
-
- if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
- throw new LogicException('Nesting error: ' . $parent[Mustache_Tokenizer::NAME] . ' vs. ' . $token[Mustache_Tokenizer::NAME]);
- }
-
- $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX];
- $parent[Mustache_Tokenizer::NODES] = $nodes;
-
- return $parent;
- break;
-
- default:
- $nodes[] = $token;
- break;
- }
- }
-
- } while ($tokens->valid());
-
- if (isset($parent)) {
- throw new LogicException('Missing closing tag: ' . $parent[Mustache_Tokenizer::NAME]);
- }
-
- return $nodes;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Parser class. + * + * This class is responsible for turning a set of Mustache tokens into a parse tree. + */ +class Mustache_Parser +{ + private $lineNum; + private $lineTokens; + private $pragmas; + private $defaultPragmas = array(); + + private $pragmaFilters; + private $pragmaBlocks; + private $pragmaDynamicNames; + + /** + * Process an array of Mustache tokens and convert them into a parse tree. + * + * @param array $tokens Set of Mustache tokens + * + * @return array Mustache token parse tree + */ + public function parse(array $tokens = array()) + { + $this->lineNum = -1; + $this->lineTokens = 0; + $this->pragmas = $this->defaultPragmas; + + $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); + $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); + $this->pragmaDynamicNames = isset($this->pragmas[Mustache_Engine::PRAGMA_DYNAMIC_NAMES]); + + return $this->buildTree($tokens); + } + + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param string[] $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->enablePragma($pragma); + } + $this->defaultPragmas = $this->pragmas; + } + + /** + * Helper method for recursively building a parse tree. + * + * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered + * + * @param array &$tokens Set of Mustache tokens + * @param array $parent Parent token (default: null) + * + * @return array Mustache Token parse tree + */ + private function buildTree(array &$tokens, array $parent = null) + { + $nodes = array(); + + while (!empty($tokens)) { + $token = array_shift($tokens); + + if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) { + $this->lineTokens++; + } else { + $this->lineNum = $token[Mustache_Tokenizer::LINE]; + $this->lineTokens = 0; + } + + if ($token[Mustache_Tokenizer::TYPE] !== Mustache_Tokenizer::T_COMMENT) { + if ($this->pragmaDynamicNames && isset($token[Mustache_Tokenizer::NAME])) { + list($name, $isDynamic) = $this->getDynamicName($token); + if ($isDynamic) { + $token[Mustache_Tokenizer::NAME] = $name; + $token[Mustache_Tokenizer::DYNAMIC] = true; + } + } + + if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) { + list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]); + if (!empty($filters)) { + $token[Mustache_Tokenizer::NAME] = $name; + $token[Mustache_Tokenizer::FILTERS] = $filters; + } + } + } + + switch ($token[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_DELIM_CHANGE: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $this->clearStandaloneLines($nodes, $tokens); + break; + + case Mustache_Tokenizer::T_SECTION: + case Mustache_Tokenizer::T_INVERTED: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $this->buildTree($tokens, $token); + break; + + case Mustache_Tokenizer::T_END_SECTION: + if (!isset($parent)) { + $msg = sprintf( + 'Unexpected closing tag: /%s on line %d', + $token[Mustache_Tokenizer::NAME], + $token[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + $sameName = $token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]; + $tokenDynamic = isset($token[Mustache_Tokenizer::DYNAMIC]) && $token[Mustache_Tokenizer::DYNAMIC]; + $parentDynamic = isset($parent[Mustache_Tokenizer::DYNAMIC]) && $parent[Mustache_Tokenizer::DYNAMIC]; + + if ($sameName || ($tokenDynamic !== $parentDynamic)) { + $msg = sprintf( + 'Nesting error: %s%s (on line %d) vs. %s%s (on line %d)', + $parentDynamic ? '*' : '', + $parent[Mustache_Tokenizer::NAME], + $parent[Mustache_Tokenizer::LINE], + $tokenDynamic ? '*' : '', + $token[Mustache_Tokenizer::NAME], + $token[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + $this->clearStandaloneLines($nodes, $tokens); + $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX]; + $parent[Mustache_Tokenizer::NODES] = $nodes; + + return $parent; + + case Mustache_Tokenizer::T_PARTIAL: + $this->checkIfTokenIsAllowedInParent($parent, $token); + //store the whitespace prefix for laters! + if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { + $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; + } + $nodes[] = $token; + break; + + case Mustache_Tokenizer::T_PARENT: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $nodes[] = $this->buildTree($tokens, $token); + break; + + case Mustache_Tokenizer::T_BLOCK_VAR: + if ($this->pragmaBlocks) { + // BLOCKS pragma is enabled, let's do this! + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; + } + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $this->buildTree($tokens, $token); + } else { + // pretend this was just a normal "escaped" token... + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED; + // TODO: figure out how to figure out if there was a space after this dollar: + $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME]; + $nodes[] = $token; + } + break; + + case Mustache_Tokenizer::T_PRAGMA: + $this->enablePragma($token[Mustache_Tokenizer::NAME]); + // no break + + case Mustache_Tokenizer::T_COMMENT: + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $token; + break; + + default: + $nodes[] = $token; + break; + } + } + + if (isset($parent)) { + $msg = sprintf( + 'Missing closing tag: %s opened on line %d', + $parent[Mustache_Tokenizer::NAME], + $parent[Mustache_Tokenizer::LINE] + ); + throw new Mustache_Exception_SyntaxException($msg, $parent); + } + + return $nodes; + } + + /** + * Clear standalone line tokens. + * + * Returns a whitespace token for indenting partials, if applicable. + * + * @param array $nodes Parsed nodes + * @param array $tokens Tokens to be parsed + * + * @return array|null Resulting indent token, if any + */ + private function clearStandaloneLines(array &$nodes, array &$tokens) + { + if ($this->lineTokens > 1) { + // this is the third or later node on this line, so it can't be standalone + return; + } + + $prev = null; + if ($this->lineTokens === 1) { + // this is the second node on this line, so it can't be standalone + // unless the previous node is whitespace. + if ($prev = end($nodes)) { + if (!$this->tokenIsWhitespace($prev)) { + return; + } + } + } + + if ($next = reset($tokens)) { + // If we're on a new line, bail. + if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) { + return; + } + + // If the next token isn't whitespace, bail. + if (!$this->tokenIsWhitespace($next)) { + return; + } + + if (count($tokens) !== 1) { + // Unless it's the last token in the template, the next token + // must end in newline for this to be standalone. + if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") { + return; + } + } + + // Discard the whitespace suffix + array_shift($tokens); + } + + if ($prev) { + // Return the whitespace prefix, if any + return array_pop($nodes); + } + } + + /** + * Check whether token is a whitespace token. + * + * True if token type is T_TEXT and value is all whitespace characters. + * + * @param array $token + * + * @return bool True if token is a whitespace token + */ + private function tokenIsWhitespace(array $token) + { + if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) { + return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); + } + + return false; + } + + /** + * Check whether a token is allowed inside a parent tag. + * + * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag + * + * @param array|null $parent + * @param array $token + */ + private function checkIfTokenIsAllowedInParent($parent, array $token) + { + if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); + } + } + + /** + * Parse dynamic names. + * + * @throws Mustache_Exception_SyntaxException when a tag does not allow * + * @throws Mustache_Exception_SyntaxException on multiple *s, or dots or filters with * + */ + private function getDynamicName(array $token) + { + $name = $token[Mustache_Tokenizer::NAME]; + $isDynamic = false; + + if (preg_match('/^\s*\*\s*/', $name)) { + $this->ensureTagAllowsDynamicNames($token); + $name = preg_replace('/^\s*\*\s*/', '', $name); + $isDynamic = true; + } + + return array($name, $isDynamic); + } + + /** + * Check whether the given token supports dynamic tag names. + * + * @throws Mustache_Exception_SyntaxException when a tag does not allow * + * + * @param array $token + */ + private function ensureTagAllowsDynamicNames(array $token) + { + switch ($token[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PARTIAL: + case Mustache_Tokenizer::T_PARENT: + case Mustache_Tokenizer::T_END_SECTION: + return; + } + + $msg = sprintf( + 'Invalid dynamic name: %s in %s tag', + $token[Mustache_Tokenizer::NAME], + Mustache_Tokenizer::getTagName($token[Mustache_Tokenizer::TYPE]) + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + + /** + * Split a tag name into name and filters. + * + * @param string $name + * + * @return array [Tag name, Array of filters] + */ + private function getNameAndFilters($name) + { + $filters = array_map('trim', explode('|', $name)); + $name = array_shift($filters); + + return array($name, $filters); + } + + /** + * Enable a pragma. + * + * @param string $name + */ + private function enablePragma($name) + { + $this->pragmas[$name] = true; + + switch ($name) { + case Mustache_Engine::PRAGMA_BLOCKS: + $this->pragmaBlocks = true; + break; + + case Mustache_Engine::PRAGMA_FILTERS: + $this->pragmaFilters = true; + break; + + case Mustache_Engine::PRAGMA_DYNAMIC_NAMES: + $this->pragmaDynamicNames = true; + break; + } + } +} diff --git a/Mustache/Source.php b/Mustache/Source.php new file mode 100644 index 00000000..278c2cb3 --- /dev/null +++ b/Mustache/Source.php @@ -0,0 +1,40 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache template Source interface. + */ +interface Mustache_Source +{ + /** + * Get the Source key (used to generate the compiled class name). + * + * This must return a distinct key for each template source. For example, an + * MD5 hash of the template contents would probably do the trick. The + * ProductionFilesystemLoader uses mtime and file path. If your production + * source directory is under version control, you could use the current Git + * rev and the file path... + * + * @throws RuntimeException when a source file cannot be read + * + * @return string + */ + public function getKey(); + + /** + * Get the template Source. + * + * @throws RuntimeException when a source file cannot be read + * + * @return string + */ + public function getSource(); +} diff --git a/Mustache/Source/FilesystemSource.php b/Mustache/Source/FilesystemSource.php new file mode 100644 index 00000000..270f584e --- /dev/null +++ b/Mustache/Source/FilesystemSource.php @@ -0,0 +1,77 @@ +<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache template Filesystem Source. + * + * This template Source uses stat() to generate the Source key, so that using + * pre-compiled templates doesn't require hitting the disk to read the source. + * It is more suitable for production use, and is used by default in the + * ProductionFilesystemLoader. + */ +class Mustache_Source_FilesystemSource implements Mustache_Source +{ + private $fileName; + private $statProps; + private $stat; + + /** + * Filesystem Source constructor. + * + * @param string $fileName + * @param array $statProps + */ + public function __construct($fileName, array $statProps) + { + $this->fileName = $fileName; + $this->statProps = $statProps; + } + + /** + * Get the Source key (used to generate the compiled class name). + * + * @throws Mustache_Exception_RuntimeException when a source file cannot be read + * + * @return string + */ + public function getKey() + { + $chunks = array( + 'fileName' => $this->fileName, + ); + + if (!empty($this->statProps)) { + if (!isset($this->stat)) { + $this->stat = @stat($this->fileName); + } + + if ($this->stat === false) { + throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName)); + } + + foreach ($this->statProps as $prop) { + $chunks[$prop] = $this->stat[$prop]; + } + } + + return json_encode($chunks); + } + + /** + * Get the template Source. + * + * @return string + */ + public function getSource() + { + return file_get_contents($this->fileName); + } +} diff --git a/Mustache/Template.php b/Mustache/Template.php index ebb9df8c..4de82393 100644 --- a/Mustache/Template.php +++ b/Mustache/Template.php @@ -1,149 +1,180 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Abstract Mustache Template class.
- *
- * @abstract
- */
-abstract class Mustache_Template
-{
-
- /**
- * @var Mustache_Engine
- */
- protected $mustache;
-
- /**
- * Mustache Template constructor.
- *
- * @param Mustache_Engine $mustache
- */
- public function __construct(Mustache_Engine $mustache)
- {
- $this->mustache = $mustache;
- }
-
- /**
- * Mustache Template instances can be treated as a function and rendered by simply calling them:
- *
- * $m = new Mustache_Engine;
- * $tpl = $m->loadTemplate('Hello, {{ name }}!');
- * echo $tpl(array('name' => 'World')); // "Hello, World!"
- *
- * @see Mustache_Template::render
- *
- * @param mixed $context Array or object rendering context (default: array())
- *
- * @return string Rendered template
- */
- public function __invoke($context = array())
- {
- return $this->render($context);
- }
-
- /**
- * Render this template given the rendering context.
- *
- * @param mixed $context Array or object rendering context (default: array())
- *
- * @return string Rendered template
- */
- public function render($context = array())
- {
- return $this->renderInternal($this->prepareContextStack($context));
- }
-
- /**
- * Internal rendering method implemented by Mustache Template concrete subclasses.
- *
- * This is where the magic happens :)
- *
- * @param Mustache_Context $context
- * @param string $indent (default: '')
- * @param bool $escape (default: false)
- *
- * @return string Rendered template
- */
- abstract public function renderInternal(Mustache_Context $context, $indent = '', $escape = false);
-
- /**
- * Tests whether a value should be iterated over (e.g. in a section context).
- *
- * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists
- * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript,
- * Java, Python, etc.
- *
- * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish
- * between between a list of things (numeric, normalized array) and a set of variables to be used as section context
- * (associative array). In other words, this will be iterated over:
- *
- * $items = array(
- * array('name' => 'foo'),
- * array('name' => 'bar'),
- * array('name' => 'baz'),
- * );
- *
- * ... but this will be used as a section context block:
- *
- * $items = array(
- * 1 => array('name' => 'foo'),
- * 'banana' => array('name' => 'bar'),
- * 42 => array('name' => 'baz'),
- * );
- *
- * @param mixed $value
- *
- * @return boolean True if the value is 'iterable'
- */
- protected function isIterable($value)
- {
- if (is_object($value)) {
- return $value instanceof Traversable;
- } elseif (is_array($value)) {
- $i = 0;
- foreach ($value as $k => $v) {
- if ($k !== $i++) {
- return false;
- }
- }
-
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Helper method to prepare the Context stack.
- *
- * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present.
- *
- * @param mixed $context Optional first context frame (default: null)
- *
- * @return Mustache_Context
- */
- protected function prepareContextStack($context = null)
- {
- $stack = new Mustache_Context;
-
- $helpers = $this->mustache->getHelpers();
- if (!$helpers->isEmpty()) {
- $stack->push($helpers);
- }
-
- if (!empty($context)) {
- $stack->push($context);
- }
-
- return $stack;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Abstract Mustache Template class. + * + * @abstract + */ +abstract class Mustache_Template +{ + /** + * @var Mustache_Engine + */ + protected $mustache; + + /** + * @var bool + */ + protected $strictCallables = false; + + /** + * Mustache Template constructor. + * + * @param Mustache_Engine $mustache + */ + public function __construct(Mustache_Engine $mustache) + { + $this->mustache = $mustache; + } + + /** + * Mustache Template instances can be treated as a function and rendered by simply calling them. + * + * $m = new Mustache_Engine; + * $tpl = $m->loadTemplate('Hello, {{ name }}!'); + * echo $tpl(array('name' => 'World')); // "Hello, World!" + * + * @see Mustache_Template::render + * + * @param mixed $context Array or object rendering context (default: array()) + * + * @return string Rendered template + */ + public function __invoke($context = array()) + { + return $this->render($context); + } + + /** + * Render this template given the rendering context. + * + * @param mixed $context Array or object rendering context (default: array()) + * + * @return string Rendered template + */ + public function render($context = array()) + { + return $this->renderInternal( + $this->prepareContextStack($context) + ); + } + + /** + * Internal rendering method implemented by Mustache Template concrete subclasses. + * + * This is where the magic happens :) + * + * NOTE: This method is not part of the Mustache.php public API. + * + * @param Mustache_Context $context + * @param string $indent (default: '') + * + * @return string Rendered template + */ + abstract public function renderInternal(Mustache_Context $context, $indent = ''); + + /** + * Tests whether a value should be iterated over (e.g. in a section context). + * + * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists + * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript, + * Java, Python, etc. + * + * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish + * between between a list of things (numeric, normalized array) and a set of variables to be used as section context + * (associative array). In other words, this will be iterated over: + * + * $items = array( + * array('name' => 'foo'), + * array('name' => 'bar'), + * array('name' => 'baz'), + * ); + * + * ... but this will be used as a section context block: + * + * $items = array( + * 1 => array('name' => 'foo'), + * 'banana' => array('name' => 'bar'), + * 42 => array('name' => 'baz'), + * ); + * + * @param mixed $value + * + * @return bool True if the value is 'iterable' + */ + protected function isIterable($value) + { + switch (gettype($value)) { + case 'object': + return $value instanceof Traversable; + + case 'array': + $i = 0; + foreach ($value as $k => $v) { + if ($k !== $i++) { + return false; + } + } + + return true; + + default: + return false; + } + } + + /** + * Helper method to prepare the Context stack. + * + * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present. + * + * @param mixed $context Optional first context frame (default: null) + * + * @return Mustache_Context + */ + protected function prepareContextStack($context = null) + { + $stack = new Mustache_Context(); + + $helpers = $this->mustache->getHelpers(); + if (!$helpers->isEmpty()) { + $stack->push($helpers); + } + + if (!empty($context)) { + $stack->push($context); + } + + return $stack; + } + + /** + * Resolve a context value. + * + * Invoke the value if it is callable, otherwise return the value. + * + * @param mixed $value + * @param Mustache_Context $context + * + * @return string + */ + protected function resolveValue($value, Mustache_Context $context) + { + if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) { + return $this->mustache + ->loadLambda((string) call_user_func($value)) + ->renderInternal($context); + } + + return $value; + } +} diff --git a/Mustache/Tokenizer.php b/Mustache/Tokenizer.php index fd866e30..d96f1298 100644 --- a/Mustache/Tokenizer.php +++ b/Mustache/Tokenizer.php @@ -1,286 +1,408 @@ -<?php
-
-/*
- * This file is part of Mustache.php.
- *
- * (c) 2012 Justin Hileman
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-/**
- * Mustache Tokenizer class.
- *
- * This class is responsible for turning raw template source into a set of Mustache tokens.
- */
-class Mustache_Tokenizer
-{
-
- // Finite state machine states
- const IN_TEXT = 0;
- const IN_TAG_TYPE = 1;
- const IN_TAG = 2;
-
- // Token types
- const T_SECTION = '#';
- const T_INVERTED = '^';
- const T_END_SECTION = '/';
- const T_COMMENT = '!';
- const T_PARTIAL = '>';
- const T_PARTIAL_2 = '<';
- const T_DELIM_CHANGE = '=';
- const T_ESCAPED = '_v';
- const T_UNESCAPED = '{';
- const T_UNESCAPED_2 = '&';
- const T_TEXT = '_t';
-
- // Valid token types
- private static $tagTypes = array(
- self::T_SECTION => true,
- self::T_INVERTED => true,
- self::T_END_SECTION => true,
- self::T_COMMENT => true,
- self::T_PARTIAL => true,
- self::T_PARTIAL_2 => true,
- self::T_DELIM_CHANGE => true,
- self::T_ESCAPED => true,
- self::T_UNESCAPED => true,
- self::T_UNESCAPED_2 => true,
- );
-
- // Interpolated tags
- private static $interpolatedTags = array(
- self::T_ESCAPED => true,
- self::T_UNESCAPED => true,
- self::T_UNESCAPED_2 => true,
- );
-
- // Token properties
- const TYPE = 'type';
- const NAME = 'name';
- const OTAG = 'otag';
- const CTAG = 'ctag';
- const INDEX = 'index';
- const END = 'end';
- const INDENT = 'indent';
- const NODES = 'nodes';
- const VALUE = 'value';
-
- private $state;
- private $tagType;
- private $tag;
- private $buffer;
- private $tokens;
- private $seenTag;
- private $lineStart;
- private $otag;
- private $ctag;
-
- /**
- * Scan and tokenize template source.
- *
- * @param string $text Mustache template source to tokenize
- * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: null)
- *
- * @return array Set of Mustache tokens
- */
- public function scan($text, $delimiters = null)
- {
- $this->reset();
-
- if ($delimiters = trim($delimiters)) {
- list($otag, $ctag) = explode(' ', $delimiters);
- $this->otag = $otag;
- $this->ctag = $ctag;
- }
-
- $len = strlen($text);
- for ($i = 0; $i < $len; $i++) {
- switch ($this->state) {
- case self::IN_TEXT:
- if ($this->tagChange($this->otag, $text, $i)) {
- $i--;
- $this->flushBuffer();
- $this->state = self::IN_TAG_TYPE;
- } else {
- if ($text[$i] == "\n") {
- $this->filterLine();
- } else {
- $this->buffer .= $text[$i];
- }
- }
- break;
-
- case self::IN_TAG_TYPE:
-
- $i += strlen($this->otag) - 1;
- if (isset(self::$tagTypes[$text[$i + 1]])) {
- $tag = $text[$i + 1];
- $this->tagType = $tag;
- } else {
- $tag = null;
- $this->tagType = self::T_ESCAPED;
- }
-
- if ($this->tagType === self::T_DELIM_CHANGE) {
- $i = $this->changeDelimiters($text, $i);
- $this->state = self::IN_TEXT;
- } else {
- if ($tag !== null) {
- $i++;
- }
- $this->state = self::IN_TAG;
- }
- $this->seenTag = $i;
- break;
-
- default:
- if ($this->tagChange($this->ctag, $text, $i)) {
- $this->tokens[] = array(
- self::TYPE => $this->tagType,
- self::NAME => trim($this->buffer),
- self::OTAG => $this->otag,
- self::CTAG => $this->ctag,
- self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
- );
-
- $this->buffer = '';
- $i += strlen($this->ctag) - 1;
- $this->state = self::IN_TEXT;
- if ($this->tagType == self::T_UNESCAPED) {
- if ($this->ctag == '}}') {
- $i++;
- } else {
- // Clean up `{{{ tripleStache }}}` style tokens.
- $lastName = $this->tokens[count($this->tokens) - 1][self::NAME];
- if (substr($lastName, -1) === '}') {
- $this->tokens[count($this->tokens) - 1][self::NAME] = trim(substr($lastName, 0, -1));
- }
- }
- }
- } else {
- $this->buffer .= $text[$i];
- }
- break;
- }
- }
-
- $this->filterLine(true);
-
- return $this->tokens;
- }
-
- /**
- * Helper function to reset tokenizer internal state.
- */
- private function reset()
- {
- $this->state = self::IN_TEXT;
- $this->tagType = null;
- $this->tag = null;
- $this->buffer = '';
- $this->tokens = array();
- $this->seenTag = false;
- $this->lineStart = 0;
- $this->otag = '{{';
- $this->ctag = '}}';
- }
-
- /**
- * Flush the current buffer to a token.
- */
- private function flushBuffer()
- {
- if (!empty($this->buffer)) {
- $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => $this->buffer);
- $this->buffer = '';
- }
- }
-
- /**
- * Test whether the current line is entirely made up of whitespace.
- *
- * @return boolean True if the current line is all whitespace
- */
- private function lineIsWhitespace()
- {
- $tokensCount = count($this->tokens);
- for ($j = $this->lineStart; $j < $tokensCount; $j++) {
- $token = $this->tokens[$j];
- if (isset(self::$tagTypes[$token[self::TYPE]])) {
- if (isset(self::$interpolatedTags[$token[self::TYPE]])) {
- return false;
- }
- } elseif ($token[self::TYPE] == self::T_TEXT) {
- if (preg_match('/\S/', $token[self::VALUE])) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Filter out whitespace-only lines and store indent levels for partials.
- *
- * @param bool $noNewLine Suppress the newline? (default: false)
- */
- private function filterLine($noNewLine = false)
- {
- $this->flushBuffer();
- if ($this->seenTag && $this->lineIsWhitespace()) {
- $tokensCount = count($this->tokens);
- for ($j = $this->lineStart; $j < $tokensCount; $j++) {
- if ($this->tokens[$j][self::TYPE] == self::T_TEXT) {
- if (isset($this->tokens[$j+1]) && $this->tokens[$j+1][self::TYPE] == self::T_PARTIAL) {
- $this->tokens[$j+1][self::INDENT] = $this->tokens[$j][self::VALUE];
- }
-
- $this->tokens[$j] = null;
- }
- }
- } elseif (!$noNewLine) {
- $this->tokens[] = array(self::TYPE => self::T_TEXT, self::VALUE => "\n");
- }
-
- $this->seenTag = false;
- $this->lineStart = count($this->tokens);
- }
-
- /**
- * Change the current Mustache delimiters. Set new `otag` and `ctag` values.
- *
- * @param string $text Mustache template source
- * @param int $index Current tokenizer index
- *
- * @return int New index value
- */
- private function changeDelimiters($text, $index)
- {
- $startIndex = strpos($text, '=', $index) + 1;
- $close = '='.$this->ctag;
- $closeIndex = strpos($text, $close, $index);
-
- list($otag, $ctag) = explode(' ', trim(substr($text, $startIndex, $closeIndex - $startIndex)));
- $this->otag = $otag;
- $this->ctag = $ctag;
-
- return $closeIndex + strlen($close) - 1;
- }
-
- /**
- * Test whether it's time to change tags.
- *
- * @param string $tag Current tag name
- * @param string $text Mustache template source
- * @param int $index Current tokenizer index
- *
- * @return boolean True if this is a closing section tag
- */
- private function tagChange($tag, $text, $index)
- {
- return substr($text, $index, strlen($tag)) === $tag;
- }
-}
+<?php + +/* + * This file is part of Mustache.php. + * + * (c) 2010-2017 Justin Hileman + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Mustache Tokenizer class. + * + * This class is responsible for turning raw template source into a set of Mustache tokens. + */ +class Mustache_Tokenizer +{ + // Finite state machine states + const IN_TEXT = 0; + const IN_TAG_TYPE = 1; + const IN_TAG = 2; + + // Token types + const T_SECTION = '#'; + const T_INVERTED = '^'; + const T_END_SECTION = '/'; + const T_COMMENT = '!'; + const T_PARTIAL = '>'; + const T_PARENT = '<'; + const T_DELIM_CHANGE = '='; + const T_ESCAPED = '_v'; + const T_UNESCAPED = '{'; + const T_UNESCAPED_2 = '&'; + const T_TEXT = '_t'; + const T_PRAGMA = '%'; + const T_BLOCK_VAR = '$'; + const T_BLOCK_ARG = '$arg'; + + // Valid token types + private static $tagTypes = array( + self::T_SECTION => true, + self::T_INVERTED => true, + self::T_END_SECTION => true, + self::T_COMMENT => true, + self::T_PARTIAL => true, + self::T_PARENT => true, + self::T_DELIM_CHANGE => true, + self::T_ESCAPED => true, + self::T_UNESCAPED => true, + self::T_UNESCAPED_2 => true, + self::T_PRAGMA => true, + self::T_BLOCK_VAR => true, + ); + + private static $tagNames = array( + self::T_SECTION => 'section', + self::T_INVERTED => 'inverted section', + self::T_END_SECTION => 'section end', + self::T_COMMENT => 'comment', + self::T_PARTIAL => 'partial', + self::T_PARENT => 'parent', + self::T_DELIM_CHANGE => 'set delimiter', + self::T_ESCAPED => 'variable', + self::T_UNESCAPED => 'unescaped variable', + self::T_UNESCAPED_2 => 'unescaped variable', + self::T_PRAGMA => 'pragma', + self::T_BLOCK_VAR => 'block variable', + self::T_BLOCK_ARG => 'block variable', + ); + + // Token properties + const TYPE = 'type'; + const NAME = 'name'; + const DYNAMIC = 'dynamic'; + const OTAG = 'otag'; + const CTAG = 'ctag'; + const LINE = 'line'; + const INDEX = 'index'; + const END = 'end'; + const INDENT = 'indent'; + const NODES = 'nodes'; + const VALUE = 'value'; + const FILTERS = 'filters'; + + private $state; + private $tagType; + private $buffer; + private $tokens; + private $seenTag; + private $line; + + private $otag; + private $otagChar; + private $otagLen; + + private $ctag; + private $ctagChar; + private $ctagLen; + + /** + * Scan and tokenize template source. + * + * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered + * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid + * + * @param string $text Mustache template source to tokenize + * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string) + * + * @return array Set of Mustache tokens + */ + public function scan($text, $delimiters = '') + { + // Setting mbstring.func_overload makes things *really* slow. + // Let's do everyone a favor and scan this string as ASCII instead. + // + // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it + // when we remove support for older versions of PHP). + // + // @codeCoverageIgnoreStart + $encoding = null; + if (version_compare(PHP_VERSION, '8.0.0', '<')) { + if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) { + $encoding = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + } + // @codeCoverageIgnoreEnd + + $this->reset(); + + if (is_string($delimiters) && $delimiters = trim($delimiters)) { + $this->setDelimiters($delimiters); + } + + $len = strlen($text); + for ($i = 0; $i < $len; $i++) { + switch ($this->state) { + case self::IN_TEXT: + $char = $text[$i]; + // Test whether it's time to change tags. + if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) { + $i--; + $this->flushBuffer(); + $this->state = self::IN_TAG_TYPE; + } else { + $this->buffer .= $char; + if ($char === "\n") { + $this->flushBuffer(); + $this->line++; + } + } + break; + + case self::IN_TAG_TYPE: + $i += $this->otagLen - 1; + $char = $text[$i + 1]; + if (isset(self::$tagTypes[$char])) { + $tag = $char; + $this->tagType = $tag; + } else { + $tag = null; + $this->tagType = self::T_ESCAPED; + } + + if ($this->tagType === self::T_DELIM_CHANGE) { + $i = $this->changeDelimiters($text, $i); + $this->state = self::IN_TEXT; + } elseif ($this->tagType === self::T_PRAGMA) { + $i = $this->addPragma($text, $i); + $this->state = self::IN_TEXT; + } else { + if ($tag !== null) { + $i++; + } + $this->state = self::IN_TAG; + } + $this->seenTag = $i; + break; + + default: + $char = $text[$i]; + // Test whether it's time to change tags. + if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) { + $token = array( + self::TYPE => $this->tagType, + self::NAME => trim($this->buffer), + self::OTAG => $this->otag, + self::CTAG => $this->ctag, + self::LINE => $this->line, + self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen, + ); + + if ($this->tagType === self::T_UNESCAPED) { + // Clean up `{{{ tripleStache }}}` style tokens. + if ($this->ctag === '}}') { + if (($i + 2 < $len) && $text[$i + 2] === '}') { + $i++; + } else { + $msg = sprintf( + 'Mismatched tag delimiters: %s on line %d', + $token[self::NAME], + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + } else { + $lastName = $token[self::NAME]; + if (substr($lastName, -1) === '}') { + $token[self::NAME] = trim(substr($lastName, 0, -1)); + } else { + $msg = sprintf( + 'Mismatched tag delimiters: %s on line %d', + $token[self::NAME], + $token[self::LINE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + } + } + + $this->buffer = ''; + $i += $this->ctagLen - 1; + $this->state = self::IN_TEXT; + $this->tokens[] = $token; + } else { + $this->buffer .= $char; + } + break; + } + } + + if ($this->state !== self::IN_TEXT) { + $this->throwUnclosedTagException(); + } + + $this->flushBuffer(); + + // Restore the user's encoding... + // @codeCoverageIgnoreStart + if ($encoding) { + mb_internal_encoding($encoding); + } + // @codeCoverageIgnoreEnd + + return $this->tokens; + } + + /** + * Helper function to reset tokenizer internal state. + */ + private function reset() + { + $this->state = self::IN_TEXT; + $this->tagType = null; + $this->buffer = ''; + $this->tokens = array(); + $this->seenTag = false; + $this->line = 0; + + $this->otag = '{{'; + $this->otagChar = '{'; + $this->otagLen = 2; + + $this->ctag = '}}'; + $this->ctagChar = '}'; + $this->ctagLen = 2; + } + + /** + * Flush the current buffer to a token. + */ + private function flushBuffer() + { + if (strlen($this->buffer) > 0) { + $this->tokens[] = array( + self::TYPE => self::T_TEXT, + self::LINE => $this->line, + self::VALUE => $this->buffer, + ); + $this->buffer = ''; + } + } + + /** + * Change the current Mustache delimiters. Set new `otag` and `ctag` values. + * + * @throws Mustache_Exception_SyntaxException when delimiter string is invalid + * + * @param string $text Mustache template source + * @param int $index Current tokenizer index + * + * @return int New index value + */ + private function changeDelimiters($text, $index) + { + $startIndex = strpos($text, '=', $index) + 1; + $close = '=' . $this->ctag; + $closeIndex = strpos($text, $close, $index); + + if ($closeIndex === false) { + $this->throwUnclosedTagException(); + } + + $token = array( + self::TYPE => self::T_DELIM_CHANGE, + self::LINE => $this->line, + ); + + try { + $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex))); + } catch (Mustache_Exception_InvalidArgumentException $e) { + throw new Mustache_Exception_SyntaxException($e->getMessage(), $token); + } + + $this->tokens[] = $token; + + return $closeIndex + strlen($close) - 1; + } + + /** + * Set the current Mustache `otag` and `ctag` delimiters. + * + * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid + * + * @param string $delimiters + */ + private function setDelimiters($delimiters) + { + if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters)); + } + + list($_, $otag, $ctag) = $matches; + + $this->otag = $otag; + $this->otagChar = $otag[0]; + $this->otagLen = strlen($otag); + + $this->ctag = $ctag; + $this->ctagChar = $ctag[0]; + $this->ctagLen = strlen($ctag); + } + + /** + * Add pragma token. + * + * Pragmas are hoisted to the front of the template, so all pragma tokens + * will appear at the front of the token list. + * + * @param string $text + * @param int $index + * + * @return int New index value + */ + private function addPragma($text, $index) + { + $end = strpos($text, $this->ctag, $index); + if ($end === false) { + $this->throwUnclosedTagException(); + } + + $pragma = trim(substr($text, $index + 2, $end - $index - 2)); + + // Pragmas are hoisted to the front of the template. + array_unshift($this->tokens, array( + self::TYPE => self::T_PRAGMA, + self::NAME => $pragma, + self::LINE => 0, + )); + + return $end + $this->ctagLen - 1; + } + + + private function throwUnclosedTagException() + { + $name = trim($this->buffer); + if ($name !== '') { + $msg = sprintf('Unclosed tag: %s on line %d', $name, $this->line); + } else { + $msg = sprintf('Unclosed tag on line %d', $this->line); + } + + throw new Mustache_Exception_SyntaxException($msg, array( + self::TYPE => $this->tagType, + self::NAME => $name, + self::OTAG => $this->otag, + self::CTAG => $this->ctag, + self::LINE => $this->line, + self::INDEX => $this->seenTag - $this->otagLen, + )); + } + + /** + * Get the human readable name for a tag type. + * + * @param string $tagType One of the tokenizer T_* constants + * + * @return string + */ + static function getTagName($tagType) + { + return isset(self::$tagNames[$tagType]) ? self::$tagNames[$tagType] : 'unknown'; + } +} |