summaryrefslogtreecommitdiffstats
path: root/Mustache
diff options
context:
space:
mode:
Diffstat (limited to 'Mustache')
-rw-r--r--Mustache/Autoloader.php69
-rw-r--r--Mustache/Compiler.php386
-rw-r--r--Mustache/Context.php149
-rw-r--r--Mustache/Engine.php589
-rw-r--r--Mustache/HelperCollection.php168
-rw-r--r--Mustache/LICENSE22
-rw-r--r--Mustache/Loader.php26
-rw-r--r--Mustache/Loader/ArrayLoader.php79
-rw-r--r--Mustache/Loader/FilesystemLoader.php118
-rw-r--r--Mustache/Loader/MutableLoader.php32
-rw-r--r--Mustache/Loader/StringLoader.php42
-rw-r--r--Mustache/Parser.php88
-rw-r--r--Mustache/Template.php149
-rw-r--r--Mustache/Tokenizer.php286
14 files changed, 2203 insertions, 0 deletions
diff --git a/Mustache/Autoloader.php b/Mustache/Autoloader.php
new file mode 100644
index 0000000..707a0ff
--- /dev/null
+++ b/Mustache/Autoloader.php
@@ -0,0 +1,69 @@
+<?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;
+ }
+ }
+}
diff --git a/Mustache/Compiler.php b/Mustache/Compiler.php
new file mode 100644
index 0000000..dd5307d
--- /dev/null
+++ b/Mustache/Compiler.php
@@ -0,0 +1,386 @@
+<?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 '';
+ }
+ }
+}
diff --git a/Mustache/Context.php b/Mustache/Context.php
new file mode 100644
index 0000000..7bc7571
--- /dev/null
+++ b/Mustache/Context.php
@@ -0,0 +1,149 @@
+<?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 '';
+ }
+}
diff --git a/Mustache/Engine.php b/Mustache/Engine.php
new file mode 100644
index 0000000..ca909bc
--- /dev/null
+++ b/Mustache/Engine.php
@@ -0,0 +1,589 @@
+<?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.
+ }
+ }
+
+ /**
+ * 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 0000000..92bcde4
--- /dev/null
+++ b/Mustache/HelperCollection.php
@@ -0,0 +1,168 @@
+<?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);
+ }
+}
diff --git a/Mustache/LICENSE b/Mustache/LICENSE
new file mode 100644
index 0000000..63c95ac
--- /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 0000000..f082cf5
--- /dev/null
+++ b/Mustache/Loader.php
@@ -0,0 +1,26 @@
+<?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);
+}
diff --git a/Mustache/Loader/ArrayLoader.php b/Mustache/Loader/ArrayLoader.php
new file mode 100644
index 0000000..0a9ceef
--- /dev/null
+++ b/Mustache/Loader/ArrayLoader.php
@@ -0,0 +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;
+ }
+}
diff --git a/Mustache/Loader/FilesystemLoader.php b/Mustache/Loader/FilesystemLoader.php
new file mode 100644
index 0000000..34a9ecf
--- /dev/null
+++ b/Mustache/Loader/FilesystemLoader.php
@@ -0,0 +1,118 @@
+<?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;
+ }
+}
diff --git a/Mustache/Loader/MutableLoader.php b/Mustache/Loader/MutableLoader.php
new file mode 100644
index 0000000..952db2f
--- /dev/null
+++ b/Mustache/Loader/MutableLoader.php
@@ -0,0 +1,32 @@
+<?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);
+}
diff --git a/Mustache/Loader/StringLoader.php b/Mustache/Loader/StringLoader.php
new file mode 100644
index 0000000..8f18062
--- /dev/null
+++ b/Mustache/Loader/StringLoader.php
@@ -0,0 +1,42 @@
+<?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;
+ }
+}
diff --git a/Mustache/Parser.php b/Mustache/Parser.php
new file mode 100644
index 0000000..39911d6
--- /dev/null
+++ b/Mustache/Parser.php
@@ -0,0 +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 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;
+ }
+}
diff --git a/Mustache/Template.php b/Mustache/Template.php
new file mode 100644
index 0000000..ebb9df8
--- /dev/null
+++ b/Mustache/Template.php
@@ -0,0 +1,149 @@
+<?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;
+ }
+}
diff --git a/Mustache/Tokenizer.php b/Mustache/Tokenizer.php
new file mode 100644
index 0000000..fd866e3
--- /dev/null
+++ b/Mustache/Tokenizer.php
@@ -0,0 +1,286 @@
+<?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;
+ }
+}