From 43e406068af8f2ae3d77301926bb5d31f392c961 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 15 Oct 2013 19:24:01 +0200 Subject: Initial commit --- Mustache/Autoloader.php | 69 ++++ Mustache/Compiler.php | 386 +++++++++++++++++++++++ Mustache/Context.php | 149 +++++++++ Mustache/Engine.php | 589 +++++++++++++++++++++++++++++++++++ Mustache/HelperCollection.php | 168 ++++++++++ Mustache/LICENSE | 22 ++ Mustache/Loader.php | 26 ++ Mustache/Loader/ArrayLoader.php | 79 +++++ Mustache/Loader/FilesystemLoader.php | 118 +++++++ Mustache/Loader/MutableLoader.php | 32 ++ Mustache/Loader/StringLoader.php | 42 +++ Mustache/Parser.php | 88 ++++++ Mustache/Template.php | 149 +++++++++ Mustache/Tokenizer.php | 286 +++++++++++++++++ 14 files changed, 2203 insertions(+) create mode 100644 Mustache/Autoloader.php create mode 100644 Mustache/Compiler.php create mode 100644 Mustache/Context.php create mode 100644 Mustache/Engine.php create mode 100644 Mustache/HelperCollection.php create mode 100644 Mustache/LICENSE create mode 100644 Mustache/Loader.php create mode 100644 Mustache/Loader/ArrayLoader.php create mode 100644 Mustache/Loader/FilesystemLoader.php create mode 100644 Mustache/Loader/MutableLoader.php create mode 100644 Mustache/Loader/StringLoader.php create mode 100644 Mustache/Parser.php create mode 100644 Mustache/Template.php create mode 100644 Mustache/Tokenizer.php (limited to 'Mustache') diff --git a/Mustache/Autoloader.php b/Mustache/Autoloader.php new file mode 100644 index 00000000..707a0ffa --- /dev/null +++ b/Mustache/Autoloader.php @@ -0,0 +1,69 @@ +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; + } + } +} diff --git a/Mustache/Compiler.php b/Mustache/Compiler.php new file mode 100644 index 00000000..dd5307d4 --- /dev/null +++ b/Mustache/Compiler.php @@ -0,0 +1,386 @@ +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 = '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 ''; + } + } +} diff --git a/Mustache/Context.php b/Mustache/Context.php new file mode 100644 index 00000000..7bc75719 --- /dev/null +++ b/Mustache/Context.php @@ -0,0 +1,149 @@ +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 ''; + } +} diff --git a/Mustache/Engine.php b/Mustache/Engine.php new file mode 100644 index 00000000..ca909bca --- /dev/null +++ b/Mustache/Engine.php @@ -0,0 +1,589 @@ + '__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. + } + } + + /** + * 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)); + } + } + + /** + * 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)); + } +} diff --git a/Mustache/HelperCollection.php b/Mustache/HelperCollection.php new file mode 100644 index 00000000..92bcde4a --- /dev/null +++ b/Mustache/HelperCollection.php @@ -0,0 +1,168 @@ + $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); + } +} diff --git a/Mustache/LICENSE b/Mustache/LICENSE new file mode 100644 index 00000000..63c95ace --- /dev/null +++ b/Mustache/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2010 Justin Hileman + +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. diff --git a/Mustache/Loader.php b/Mustache/Loader.php new file mode 100644 index 00000000..f082cf59 --- /dev/null +++ b/Mustache/Loader.php @@ -0,0 +1,26 @@ + '{{ 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; + } +} diff --git a/Mustache/Loader/FilesystemLoader.php b/Mustache/Loader/FilesystemLoader.php new file mode 100644 index 00000000..34a9ecfd --- /dev/null +++ b/Mustache/Loader/FilesystemLoader.php @@ -0,0 +1,118 @@ +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; + } +} diff --git a/Mustache/Loader/MutableLoader.php b/Mustache/Loader/MutableLoader.php new file mode 100644 index 00000000..952db2f0 --- /dev/null +++ b/Mustache/Loader/MutableLoader.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/Mustache/Parser.php b/Mustache/Parser.php new file mode 100644 index 00000000..39911d6b --- /dev/null +++ b/Mustache/Parser.php @@ -0,0 +1,88 @@ +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; + } +} diff --git a/Mustache/Template.php b/Mustache/Template.php new file mode 100644 index 00000000..ebb9df8c --- /dev/null +++ b/Mustache/Template.php @@ -0,0 +1,149 @@ +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; + } +} diff --git a/Mustache/Tokenizer.php b/Mustache/Tokenizer.php new file mode 100644 index 00000000..fd866e30 --- /dev/null +++ b/Mustache/Tokenizer.php @@ -0,0 +1,286 @@ +'; + 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; + } +} -- cgit v1.2.3-55-g7522