Browse Source

Merge branch 'release/2.3.0'

Justin Hileman 12 years ago
parent
commit
fb5e4ecb30
37 changed files with 994 additions and 101 deletions
  1. 1 0
      .gitignore
  2. 7 1
      README.markdown
  3. 175 0
      bin/build_bootstrap.php
  4. 37 25
      src/Mustache/Compiler.php
  5. 32 17
      src/Mustache/Engine.php
  6. 18 0
      src/Mustache/Exception.php
  7. 18 0
      src/Mustache/Exception/InvalidArgumentException.php
  8. 18 0
      src/Mustache/Exception/LogicException.php
  9. 18 0
      src/Mustache/Exception/RuntimeException.php
  10. 29 0
      src/Mustache/Exception/SyntaxException.php
  11. 29 0
      src/Mustache/Exception/UnknownFilterException.php
  12. 29 0
      src/Mustache/Exception/UnknownHelperException.php
  13. 29 0
      src/Mustache/Exception/UnknownTemplateException.php
  14. 7 5
      src/Mustache/HelperCollection.php
  15. 3 2
      src/Mustache/LambdaHelper.php
  16. 2 0
      src/Mustache/Loader.php
  17. 3 4
      src/Mustache/Loader/ArrayLoader.php
  18. 69 0
      src/Mustache/Loader/CascadingLoader.php
  19. 4 6
      src/Mustache/Loader/FilesystemLoader.php
  20. 121 0
      src/Mustache/Loader/InlineLoader.php
  21. 0 2
      src/Mustache/Loader/StringLoader.php
  22. 12 7
      src/Mustache/Logger/StreamLogger.php
  23. 8 5
      src/Mustache/Parser.php
  24. 30 2
      src/Mustache/Template.php
  25. 41 13
      test/Mustache/Test/CompilerTest.php
  26. 27 4
      test/Mustache/Test/EngineTest.php
  27. 27 0
      test/Mustache/Test/Exception/SyntaxExceptionTest.php
  28. 32 0
      test/Mustache/Test/Exception/UnknownFilterExceptionTest.php
  29. 32 0
      test/Mustache/Test/Exception/UnknownHelperExceptionTest.php
  30. 32 0
      test/Mustache/Test/Exception/UnknownTemplateExceptionTest.php
  31. 1 1
      test/Mustache/Test/FiveThree/Functional/FiltersTest.php
  32. 1 1
      test/Mustache/Test/Loader/ArrayLoaderTest.php
  33. 40 0
      test/Mustache/Test/Loader/CascadingLoaderTest.php
  34. 2 2
      test/Mustache/Test/Loader/FilesystemLoaderTest.php
  35. 56 0
      test/Mustache/Test/Loader/InlineLoaderTest.php
  36. 3 3
      test/Mustache/Test/Logger/StreamLoggerTest.php
  37. 1 1
      test/Mustache/Test/ParserTest.php

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 composer.lock
 vendor
+mustache.php

+ 7 - 1
README.markdown

@@ -5,6 +5,7 @@ A [Mustache](http://mustache.github.com/) implementation in PHP.
 
 [![Build Status](https://secure.travis-ci.org/bobthecow/mustache.php.png?branch=dev)](http://travis-ci.org/bobthecow/mustache.php)
 
+
 Usage
 -----
 
@@ -55,9 +56,14 @@ echo $m->render($template, $chris);
 ```
 
 
+And That's Not All!
+-------------------
+
+Read [the Mustache.php documentation](https://github.com/bobthecow/mustache.php/wiki/Home) for more information.
+
+
 See Also
 --------
 
- * [Mustache.php wiki](https://github.com/bobthecow/mustache.php/wiki/Home).
  * [Readme for the Ruby Mustache implementation](http://github.com/defunkt/mustache/blob/master/README.md).
  * [mustache(5)](http://mustache.github.com/mustache.5.html) man page.

+ 175 - 0
bin/build_bootstrap.php

@@ -0,0 +1,175 @@
+#!/usr/bin/env php
+<?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 shell script to create a single-file class cache of the entire Mustache
+ * library:
+ *
+ *     $ bin/build_bootstrap.php
+ *
+ * ... will create a `mustache.php` bootstrap file in the project directory,
+ * containing all Mustache library classes. This file can then be included in
+ * your project, rather than requiring the Mustache Autoloader.
+ */
+$baseDir = realpath(dirname(__FILE__).'/..');
+
+require $baseDir.'/src/Mustache/Autoloader.php';
+Mustache_Autoloader::register();
+
+// delete the old file
+$file = $baseDir.'/mustache.php';
+if (file_exists($file)) {
+    unlink($file);
+}
+
+// and load the new one
+SymfonyClassCollectionLoader::load(array(
+    '\Mustache_Engine',
+    '\Mustache_Compiler',
+    '\Mustache_Context',
+    '\Mustache_Exception',
+    '\Mustache_Exception_InvalidArgumentException',
+    '\Mustache_Exception_LogicException',
+    '\Mustache_Exception_RuntimeException',
+    '\Mustache_Exception_SyntaxException',
+    '\Mustache_Exception_UnknownFilterException',
+    '\Mustache_Exception_UnknownHelperException',
+    '\Mustache_Exception_UnknownTemplateException',
+    '\Mustache_HelperCollection',
+    '\Mustache_LambdaHelper',
+    '\Mustache_Loader',
+    '\Mustache_Loader_ArrayLoader',
+    '\Mustache_Loader_CascadingLoader',
+    '\Mustache_Loader_FilesystemLoader',
+    '\Mustache_Loader_InlineLoader',
+    '\Mustache_Loader_MutableLoader',
+    '\Mustache_Loader_StringLoader',
+    '\Mustache_Logger',
+    '\Mustache_Logger_AbstractLogger',
+    '\Mustache_Logger_StreamLogger',
+    '\Mustache_Parser',
+    '\Mustache_Template',
+    '\Mustache_Tokenizer',
+), dirname($file), basename($file, '.php'));
+
+/**
+ * SymfonyClassCollectionLoader.
+ *
+ * Based heavily on the Symfony ClassCollectionLoader component, with all
+ * the unnecessary bits removed.
+ *
+ * @license http://www.opensource.org/licenses/MIT
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class SymfonyClassCollectionLoader
+{
+    static private $loaded;
+
+    const HEADER = <<<EOS
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) %d Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+EOS;
+
+    /**
+     * Loads a list of classes and caches them in one big file.
+     *
+     * @param array   $classes    An array of classes to load
+     * @param string  $cacheDir   A cache directory
+     * @param string  $name       The cache name prefix
+     * @param string  $extension  File extension of the resulting file
+     *
+     * @throws InvalidArgumentException When class can't be loaded
+     */
+    static public function load(array $classes, $cacheDir, $name, $extension = '.php')
+    {
+        // each $name can only be loaded once per PHP process
+        if (isset(self::$loaded[$name])) {
+            return;
+        }
+
+        self::$loaded[$name] = true;
+
+        $content = '';
+        foreach ($classes as $class) {
+            if (!class_exists($class) && !interface_exists($class) && (!function_exists('trait_exists') || !trait_exists($class))) {
+                throw new InvalidArgumentException(sprintf('Unable to load class "%s"', $class));
+            }
+
+            $r = new ReflectionClass($class);
+            $content .= preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName()));
+        }
+
+        $cache  = $cacheDir.'/'.$name.$extension;
+        $header = sprintf(self::HEADER, strftime('%Y'));
+        self::writeCacheFile($cache, $header . substr(self::stripComments('<?php '.$content), 5));
+    }
+
+    /**
+     * Writes a cache file.
+     *
+     * @param string $file    Filename
+     * @param string $content Temporary file content
+     *
+     * @throws RuntimeException when a cache file cannot be written
+     */
+    static private function writeCacheFile($file, $content)
+    {
+        $tmpFile = tempnam(dirname($file), basename($file));
+        if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) {
+            chmod($file, 0666 & ~umask());
+
+            return;
+        }
+
+        throw new RuntimeException(sprintf('Failed to write cache file "%s".', $file));
+    }
+
+    /**
+     * Removes comments from a PHP source string.
+     *
+     * We don't use the PHP php_strip_whitespace() function
+     * as we want the content to be readable and well-formatted.
+     *
+     * @param string $source A PHP string
+     *
+     * @return string The PHP string with the comments removed
+     */
+    static private function stripComments($source)
+    {
+        if (!function_exists('token_get_all')) {
+            return $source;
+        }
+
+        $output = '';
+        foreach (token_get_all($source) as $token) {
+            if (is_string($token)) {
+                $output .= $token;
+            } elseif (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
+                $output .= $token[1];
+            }
+        }
+
+        // replace multiple new lines with a single newline
+        $output = preg_replace(array('/\s+$/Sm', '/\n+/S'), "\n", $output);
+
+        return $output;
+    }
+}

+ 37 - 25
src/Mustache/Compiler.php

@@ -53,7 +53,7 @@ class Mustache_Compiler
     /**
      * Helper function for walking the Mustache token parse tree.
      *
-     * @throws InvalidArgumentException upon encountering unknown token types.
+     * @throws Mustache_Exception_SyntaxException upon encountering unknown token types.
      *
      * @param array $tree  Parse tree of Mustache tokens
      * @param int   $level (default: 0)
@@ -116,7 +116,7 @@ class Mustache_Compiler
                     break;
 
                 default:
-                    throw new InvalidArgumentException('Unknown node type: '.json_encode($node));
+                    throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node);
             }
         }
 
@@ -127,23 +127,34 @@ class Mustache_Compiler
 
         class %s extends Mustache_Template
         {
-            private $lambdaHelper;
+            private $lambdaHelper;%s
 
-            public function renderInternal(Mustache_Context $context, $indent = \'\', $escape = false)
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
             {
                 $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
                 $buffer = \'\';
         %s
 
-                if ($escape) {
-                    return %s;
-                } else {
-                    return $buffer;
-                }
+                return $buffer;
             }
         %s
         }';
 
+    const KLASS_NO_LAMBDAS = '<?php
+
+        class %s extends Mustache_Template
+        {%s
+            public function renderInternal(Mustache_Context $context, $indent = \'\')
+            {
+                $buffer = \'\';
+        %s
+
+                return $buffer;
+            }
+        }';
+
+    const STRICT_CALLABLE = 'protected $strictCallables = true;';
+
     /**
      * Generate Mustache Template class PHP source.
      *
@@ -156,8 +167,10 @@ class Mustache_Compiler
     {
         $code     = $this->walk($tree);
         $sections = implode("\n", $this->sections);
+        $klass    = empty($this->sections) ? self::KLASS_NO_LAMBDAS : self::KLASS;
+        $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : '';
 
-        return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $this->getEscape('$buffer'), $sections);
+        return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections);
     }
 
     const SECTION_CALL = '
@@ -166,7 +179,8 @@ class Mustache_Compiler
     ';
 
     const SECTION = '
-        private function section%s(Mustache_Context $context, $indent, $value) {
+        private function section%s(Mustache_Context $context, $indent, $value)
+        {
             $buffer = \'\';
             if (%s) {
                 $source = %s;
@@ -268,12 +282,7 @@ class Mustache_Compiler
     }
 
     const VARIABLE = '
-        $value = $context->%s(%s);
-        if (%s) {
-            $value = $this->mustache
-                ->loadLambda((string) call_user_func($value))
-                ->renderInternal($context, $indent);
-        }%s
+        $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
         $buffer .= %s%s;
     ';
 
@@ -294,12 +303,11 @@ class Mustache_Compiler
             list($id, $filters) = $this->getFilters($id, $level);
         }
 
-        $method   = $this->getFindMethod($id);
-        $id       = ($method !== 'last') ? var_export($id, true) : '';
-        $callable = $this->getCallable();
-        $value    = $escape ? $this->getEscape() : '$value';
+        $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, $callable, $filters, $this->flushIndent(), $value);
+        return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value);
     }
 
     /**
@@ -321,7 +329,7 @@ class Mustache_Compiler
     const FILTER = '
         $filter = $context->%s(%s);
         if (!(%s)) {
-            throw new UnexpectedValueException(%s);
+            throw new Mustache_Exception_UnknownFilterException(%s);
         }
         $value = call_user_func($filter, $value);%s
     ';
@@ -344,7 +352,7 @@ class Mustache_Compiler
         $method   = $this->getFindMethod($name);
         $filter   = ($method !== 'last') ? var_export($name, true) : '';
         $callable = $this->getCallable('$filter');
-        $msg      = var_export(sprintf('Filter not found: %s', $name), true);
+        $msg      = var_export($name, true);
 
         return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilter($filters, $level));
     }
@@ -377,15 +385,19 @@ class Mustache_Compiler
      * @param string  $text
      * @param int     $bonus          Additional indent level (default: 0)
      * @param boolean $prependNewline Prepend a newline to the snippet? (default: true)
+     * @param boolean $appendNewline  Append a newline to the snippet? (default: false)
      *
      * @return string PHP source code snippet
      */
-    private function prepare($text, $bonus = 0, $prependNewline = true)
+    private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false)
     {
         $text = ($prependNewline ? "\n" : '').trim($text);
         if ($prependNewline) {
             $bonus++;
         }
+        if ($appendNewline) {
+            $text .= "\n";
+        }
 
         return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
     }

+ 32 - 17
src/Mustache/Engine.php

@@ -23,7 +23,7 @@
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.2.0';
+    const VERSION        = '2.3.0';
     const SPEC_VERSION   = '1.1.2';
 
     const PRAGMA_FILTERS = 'FILTERS';
@@ -87,7 +87,7 @@ class Mustache_Engine
      *         // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
      *         // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
      *         // available as well:
-     *         'logger' => new Mustache_StreamLogger('php://stderr'),
+     *         'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
      *
      *         // Only treat Closure instances and invokable classes as callable. If true, values like
      *         // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
@@ -97,6 +97,8 @@ class Mustache_Engine
      *         'strict_callables' => true,
      *     );
      *
+     * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
+     *
      * @param array $options (default: array())
      */
     public function __construct(array $options = array())
@@ -131,7 +133,7 @@ class Mustache_Engine
 
         if (isset($options['escape'])) {
             if (!is_callable($options['escape'])) {
-                throw new InvalidArgumentException('Mustache Constructor "escape" option must be callable');
+                throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
             }
 
             $this->escape = $options['escape'];
@@ -245,18 +247,21 @@ class Mustache_Engine
     /**
      * Set partials for the current partials Loader instance.
      *
-     * @throws RuntimeException If the current Loader instance is immutable
+     * @throws Mustache_Exception_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');
+        if (!isset($this->partialsLoader)) {
+            $this->partialsLoader = new Mustache_Loader_ArrayLoader;
+        }
+
+        if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
+            throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
         }
 
-        $loader->setTemplates($partials);
+        $this->partialsLoader->setTemplates($partials);
     }
 
     /**
@@ -266,14 +271,14 @@ class Mustache_Engine
      * 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
+     * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
      *
      * @param array|Traversable $helpers
      */
     public function setHelpers($helpers)
     {
         if (!is_array($helpers) && !$helpers instanceof Traversable) {
-            throw new InvalidArgumentException('setHelpers expects an array of helpers');
+            throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
         }
 
         $this->getHelpers()->clear();
@@ -355,12 +360,14 @@ class Mustache_Engine
     /**
      * Set the Mustache Logger instance.
      *
+     * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
+     *
      * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
      */
     public function setLogger($logger = null)
     {
         if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
-            throw new InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
+            throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
         }
 
         $this->logger = $logger;
@@ -498,13 +505,21 @@ class Mustache_Engine
     public function loadPartial($name)
     {
         try {
-            return $this->loadSource($this->getPartialsLoader()->load($name));
-        } catch (InvalidArgumentException $e) {
+            if (isset($this->partialsLoader)) {
+                $loader = $this->partialsLoader;
+            } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
+                $loader = $this->loader;
+            } else {
+                throw new Mustache_Exception_UnknownTemplateException($name);
+            }
+
+            return $this->loadSource($loader->load($name));
+        } catch (Mustache_Exception_UnknownTemplateException $e) {
             // If the named partial cannot be found, log then return null.
             $this->log(
                 Mustache_Logger::WARNING,
                 'Partial not found: "{name}"',
-                array('name' => $name)
+                array('name' => $e->getTemplateName())
             );
         }
     }
@@ -649,7 +664,7 @@ class Mustache_Engine
     /**
      * Helper method to dump a generated Mustache Template subclass to the file cache.
      *
-     * @throws RuntimeException if unable to create the cache directory or write to $fileName.
+     * @throws Mustache_Exception_RuntimeException if unable to create the cache directory or write to $fileName.
      *
      * @param string $fileName
      * @param string $source
@@ -668,7 +683,7 @@ class Mustache_Engine
 
             @mkdir($dirName, 0777, true);
             if (!is_dir($dirName)) {
-                throw new RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
+                throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
             }
 
         }
@@ -695,7 +710,7 @@ class Mustache_Engine
             );
         }
 
-        throw new RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
+        throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
     }
 
     /**

+ 18 - 0
src/Mustache/Exception.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Exception interface.
+ */
+interface Mustache_Exception
+{
+    // This space intentionally left blank.
+}

+ 18 - 0
src/Mustache/Exception/InvalidArgumentException.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Invalid argument exception.
+ */
+class Mustache_Exception_InvalidArgumentException extends InvalidArgumentException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}

+ 18 - 0
src/Mustache/Exception/LogicException.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Logic exception.
+ */
+class Mustache_Exception_LogicException extends LogicException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}

+ 18 - 0
src/Mustache/Exception/RuntimeException.php

@@ -0,0 +1,18 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Runtime exception.
+ */
+class Mustache_Exception_RuntimeException extends RuntimeException implements Mustache_Exception
+{
+    // This space intentionally left blank.
+}

+ 29 - 0
src/Mustache/Exception/SyntaxException.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Mustache syntax exception.
+ */
+class Mustache_Exception_SyntaxException extends LogicException implements Mustache_Exception
+{
+    protected $token;
+
+    public function __construct($msg, array $token)
+    {
+        $this->token = $token;
+        parent::__construct($msg);
+    }
+
+    public function getToken()
+    {
+        return $this->token;
+    }
+}

+ 29 - 0
src/Mustache/Exception/UnknownFilterException.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown filter exception.
+ */
+class Mustache_Exception_UnknownFilterException extends UnexpectedValueException implements Mustache_Exception
+{
+    protected $filterName;
+
+    public function __construct($filterName)
+    {
+        $this->filterName = $filterName;
+        parent::__construct(sprintf('Unknown filter: %s', $filterName));
+    }
+
+    public function getFilterName()
+    {
+        return $this->filterName;
+    }
+}

+ 29 - 0
src/Mustache/Exception/UnknownHelperException.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown helper exception.
+ */
+class Mustache_Exception_UnknownHelperException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $helperName;
+
+    public function __construct($helperName)
+    {
+        $this->helperName = $helperName;
+        parent::__construct(sprintf('Unknown helper: %s', $helperName));
+    }
+
+    public function getHelperName()
+    {
+        return $this->helperName;
+    }
+}

+ 29 - 0
src/Mustache/Exception/UnknownTemplateException.php

@@ -0,0 +1,29 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Unknown template exception.
+ */
+class Mustache_Exception_UnknownTemplateException extends InvalidArgumentException implements Mustache_Exception
+{
+    protected $templateName;
+
+    public function __construct($templateName)
+    {
+        $this->templateName = $templateName;
+        parent::__construct(sprintf('Unknown template: %s', $templateName));
+    }
+
+    public function getTemplateName()
+    {
+        return $this->templateName;
+    }
+}

+ 7 - 5
src/Mustache/HelperCollection.php

@@ -21,7 +21,7 @@ class Mustache_HelperCollection
      *
      * Optionally accepts an array (or Traversable) of `$name => $helper` pairs.
      *
-     * @throws InvalidArgumentException if the $helpers argument isn't an array or Traversable
+     * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable
      *
      * @param array|Traversable $helpers (default: null)
      */
@@ -29,7 +29,7 @@ class Mustache_HelperCollection
     {
         if ($helpers !== null) {
             if (!is_array($helpers) && !$helpers instanceof Traversable) {
-                throw new InvalidArgumentException('HelperCollection constructor expects an array of helpers');
+                throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers');
             }
 
             foreach ($helpers as $name => $helper) {
@@ -79,6 +79,8 @@ class Mustache_HelperCollection
     /**
      * Get a helper by name.
      *
+     * @throws Mustache_Exception_UnknownHelperException If helper does not exist.
+     *
      * @param string $name
      *
      * @return mixed Helper
@@ -86,7 +88,7 @@ class Mustache_HelperCollection
     public function get($name)
     {
         if (!$this->has($name)) {
-            throw new InvalidArgumentException('Unknown helper: '.$name);
+            throw new Mustache_Exception_UnknownHelperException($name);
         }
 
         return $this->helpers[$name];
@@ -133,14 +135,14 @@ class Mustache_HelperCollection
     /**
      * Check whether a given helper is present in the collection.
      *
-     * @throws InvalidArgumentException if the requested helper is not present.
+     * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present.
      *
      * @param string $name
      */
     public function remove($name)
     {
         if (!$this->has($name)) {
-            throw new InvalidArgumentException('Unknown helper: '.$name);
+            throw new Mustache_Exception_UnknownHelperException($name);
         }
 
         unset($this->helpers[$name]);

+ 3 - 2
src/Mustache/LambdaHelper.php

@@ -12,8 +12,9 @@
 /**
  * Mustache Lambda Helper.
  *
- * Passed to section and interpolation lambdas, giving them access to a `render`
- * method for rendering a string with the current context.
+ * Passed as the second argument to section lambdas (higher order sections),
+ * giving them access to a `render` method for rendering a string with the
+ * current context.
  */
 class Mustache_LambdaHelper
 {

+ 2 - 0
src/Mustache/Loader.php

@@ -18,6 +18,8 @@ interface Mustache_Loader
     /**
      * Load a Template by name.
      *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
      * @param string $name
      *
      * @return string Mustache Template source

+ 3 - 4
src/Mustache/Loader/ArrayLoader.php

@@ -23,9 +23,6 @@
  *
  * 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
 {
@@ -43,6 +40,8 @@ class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_Mu
     /**
      * Load a Template.
      *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
      * @param string $name
      *
      * @return string Mustache Template source
@@ -50,7 +49,7 @@ class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_Mu
     public function load($name)
     {
         if (!isset($this->templates[$name])) {
-            throw new InvalidArgumentException('Template '.$name.' not found.');
+            throw new Mustache_Exception_UnknownTemplateException($name);
         }
 
         return $this->templates[$name];

+ 69 - 0
src/Mustache/Loader/CascadingLoader.php

@@ -0,0 +1,69 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template cascading loader implementation, which delegates to other
+ * Loader instances.
+ */
+class Mustache_Loader_CascadingLoader implements Mustache_Loader
+{
+    private $loaders;
+
+    /**
+     * Construct a CascadingLoader with an array of loaders:
+     *
+     *     $loader = new Mustache_Loader_CascadingLoader(array(
+     *         new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__),
+     *         new Mustache_Loader_FilesystemLoader(__DIR__.'/templates')
+     *     ));
+     *
+     * @param array $loaders An array of Mustache Loader instances
+     */
+    public function __construct(array $loaders = array())
+    {
+        $this->loaders = array();
+        foreach ($loaders as $loader) {
+            $this->addLoader($loader);
+        }
+    }
+
+    /**
+     * Add a Loader instance.
+     *
+     * @param Mustache_Loader $loader A Mustache Loader instance
+     */
+    public function addLoader(Mustache_Loader $loader)
+    {
+        $this->loaders[] = $loader;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        foreach ($this->loaders as $loader) {
+            try {
+                return $loader->load($name);
+            } catch (Mustache_Exception_UnknownTemplateException $e) {
+                // do nothing, check the next loader.
+            }
+        }
+
+        throw new Mustache_Exception_UnknownTemplateException($name);
+    }
+}

+ 4 - 6
src/Mustache/Loader/FilesystemLoader.php

@@ -23,8 +23,6 @@
  *          'loader'          => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
  *          'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
  *     ));
- *
- * @implements Mustache_Loader
  */
 class Mustache_Loader_FilesystemLoader implements Mustache_Loader
 {
@@ -42,7 +40,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader
      *         'extension' => '.ms',
      *     );
      *
-     * @throws RuntimeException if $baseDir does not exist.
+     * @throws Mustache_Exception_RuntimeException if $baseDir does not exist.
      *
      * @param string $baseDir Base directory containing Mustache template files.
      * @param array  $options Array of Loader options (default: array())
@@ -52,7 +50,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader
         $this->baseDir = rtrim(realpath($baseDir), '/');
 
         if (!is_dir($this->baseDir)) {
-            throw new RuntimeException('FilesystemLoader baseDir must be a directory: '.$baseDir);
+            throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));
         }
 
         if (array_key_exists('extension', $options)) {
@@ -86,7 +84,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader
     /**
      * Helper function for loading a Mustache file by name.
      *
-     * @throws InvalidArgumentException if a template file is not found.
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
      *
      * @param string $name
      *
@@ -97,7 +95,7 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader
         $fileName = $this->getFileName($name);
 
         if (!file_exists($fileName)) {
-            throw new InvalidArgumentException('Template '.$name.' not found.');
+            throw new Mustache_Exception_UnknownTemplateException($name);
         }
 
         return file_get_contents($fileName);

+ 121 - 0
src/Mustache/Loader/InlineLoader.php

@@ -0,0 +1,121 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A Mustache Template loader for inline templates.
+ *
+ * With the InlineLoader, templates can be defined at the end of any PHP source
+ * file:
+ *
+ *     $loader  = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+ *     $hello   = $loader->load('hello');
+ *     $goodbye = $loader->load('goodbye');
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ planet }}!
+ *
+ *     @@ goodbye
+ *     Goodbye, cruel {{ planet }}
+ *
+ * Templates are deliniated by lines containing only `@@ name`.
+ *
+ * The InlineLoader is well-suited to micro-frameworks such as Silex:
+ *
+ *     $app->register(new MustacheServiceProvider, array(
+ *         'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
+ *     ));
+ *
+ *     $app->get('/{name}', function() use ($app) {
+ *         return $app['mustache']->render('hello', compact('name'));
+ *     })
+ *     ->value('name', 'world');
+ *
+ *     __halt_compiler();
+ *
+ *     @@ hello
+ *     Hello, {{ name }}!
+ *
+ */
+class Mustache_Loader_InlineLoader implements Mustache_Loader
+{
+    protected $fileName;
+    protected $offset;
+    protected $templates;
+
+    /**
+     * The InlineLoader requires a filename and offset to process templates.
+     * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually
+     * perfectly suited to the job:
+     *
+     *     $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+     *
+     * Note that this only works if the loader is instantiated inside the same
+     * file as the inline templates. If the templates are located in another
+     * file, it would be necessary to manually specify the filename and offset.
+     *
+     * @param string $fileName The file to parse for inline templates
+     * @param int    $offset   A string offset for the start of the templates.
+     *                         This usually coincides with the `__halt_compiler`
+     *                         call, and the `__COMPILER_HALT_OFFSET__`.
+     */
+    public function __construct($fileName, $offset)
+    {
+        if (!is_file($fileName)) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.');
+        }
+
+        if (!is_int($offset) || $offset < 0) {
+            throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.');
+        }
+
+        $this->fileName = $fileName;
+        $this->offset   = $offset;
+    }
+
+    /**
+     * Load a Template by name.
+     *
+     * @throws Mustache_Exception_UnknownTemplateException If a template file is not found.
+     *
+     * @param string $name
+     *
+     * @return string Mustache Template source
+     */
+    public function load($name)
+    {
+        $this->loadTemplates();
+
+        if (!array_key_exists($name, $this->templates)) {
+            throw new Mustache_Exception_UnknownTemplateException($name);
+        }
+
+        return $this->templates[$name];
+    }
+
+    /**
+     * Parse and load templates from the end of a source file.
+     */
+    protected function loadTemplates()
+    {
+        if ($this->templates === null) {
+            $this->templates = array();
+            $data = file_get_contents($this->fileName, false, null, $this->offset);
+            foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
+                if (trim($chunk)) {
+                    list($name, $content)         = explode("\n", $chunk, 2);
+                    $this->templates[trim($name)] = trim($content);
+                }
+            }
+        }
+    }
+}

+ 0 - 2
src/Mustache/Loader/StringLoader.php

@@ -22,8 +22,6 @@
  *     $m = new Mustache;
  *     $tpl = $m->loadTemplate('{{ foo }}');
  *     echo $tpl->render(array('foo' => 'bar')); // "bar"
- *
- * @implements Loader
  */
 class Mustache_Loader_StringLoader implements Mustache_Loader
 {

+ 12 - 7
src/Mustache/Logger/StreamLogger.php

@@ -64,14 +64,14 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     /**
      * Set the minimum logging level.
      *
-     * @throws InvalidArgumentException if the logging level is unknown.
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
      *
      * @param  integer $level The minimum logging level which will be written
      */
     public function setLevel($level)
     {
         if (!array_key_exists($level, self::$levels)) {
-            throw new InvalidArgumentException('Unexpected logging level: ' . $level);
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
         }
 
         $this->level = $level;
@@ -90,7 +90,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     /**
      * Logs with an arbitrary level.
      *
-     * @throws InvalidArgumentException if the logging level is unknown.
+     * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown.
      *
      * @param mixed $level
      * @param string $message
@@ -99,7 +99,7 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     public function log($level, $message, array $context = array())
     {
         if (!array_key_exists($level, self::$levels)) {
-            throw new InvalidArgumentException('Unexpected logging level: ' . $level);
+            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level));
         }
 
         if (self::$levels[$level] >= self::$levels[$this->level]) {
@@ -110,6 +110,9 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     /**
      * Write a record to the log.
      *
+     * @throws Mustache_Exception_LogicException   If neither a stream resource nor url is present.
+     * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened.
+     *
      * @param  integer $level   The logging level
      * @param  string  $message The log message
      * @param  array   $context The log context
@@ -118,13 +121,13 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
     {
         if (!is_resource($this->stream)) {
             if (!isset($this->url)) {
-                throw new LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
+                throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');
             }
 
             $this->stream = fopen($this->url, 'a');
             if (!is_resource($this->stream)) {
                 // @codeCoverageIgnoreStart
-                throw new UnexpectedValueException(sprintf('The stream or file "%s" could not be opened.', $this->url));
+                throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url));
                 // @codeCoverageIgnoreEnd
             }
         }
@@ -174,7 +177,9 @@ class Mustache_Logger_StreamLogger extends Mustache_Logger_AbstractLogger
      */
     protected static function interpolateMessage($message, array $context = array())
     {
-        $message = (string) $message;
+        if (strpos($message, '{') === false) {
+            return $message;
+        }
 
         // build a replacement array with braces around the context keys
         $replace = array();

+ 8 - 5
src/Mustache/Parser.php

@@ -32,12 +32,12 @@ class Mustache_Parser
     /**
      * Helper method for recursively building a parse tree.
      *
+     * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered.
+     *
      * @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)
     {
@@ -58,11 +58,13 @@ class Mustache_Parser
 
                     case Mustache_Tokenizer::T_END_SECTION:
                         if (!isset($parent)) {
-                            throw new LogicException('Unexpected closing tag: /'. $token[Mustache_Tokenizer::NAME]);
+                            $msg = sprintf('Unexpected closing tag: /%s', $token[Mustache_Tokenizer::NAME]);
+                            throw new Mustache_Exception_SyntaxException($msg, $token);
                         }
 
                         if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
-                            throw new LogicException('Nesting error: ' . $parent[Mustache_Tokenizer::NAME] . ' vs. ' . $token[Mustache_Tokenizer::NAME]);
+                            $msg = sprintf('Nesting error: %s vs. %s', $parent[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::NAME]);
+                            throw new Mustache_Exception_SyntaxException($msg, $token);
                         }
 
                         $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
@@ -80,7 +82,8 @@ class Mustache_Parser
         } while ($tokens->valid());
 
         if (isset($parent)) {
-            throw new LogicException('Missing closing tag: ' . $parent[Mustache_Tokenizer::NAME]);
+            $msg = sprintf('Missing closing tag: %s', $parent[Mustache_Tokenizer::NAME]);
+            throw new Mustache_Exception_SyntaxException($msg, $parent);
         }
 
         return $nodes;

+ 30 - 2
src/Mustache/Template.php

@@ -22,6 +22,11 @@ abstract class Mustache_Template
      */
     protected $mustache;
 
+    /**
+     * @var boolean
+     */
+    protected $strictCallables = false;
+
     /**
      * Mustache Template constructor.
      *
@@ -67,13 +72,14 @@ abstract class Mustache_Template
      *
      * This is where the magic happens :)
      *
+     * NOTE: This method is not part of the Mustache.php public API.
+     *
      * @param Mustache_Context $context
      * @param string           $indent  (default: '')
-     * @param bool             $escape  (default: false)
      *
      * @return string Rendered template
      */
-    abstract public function renderInternal(Mustache_Context $context, $indent = '', $escape = false);
+    abstract public function renderInternal(Mustache_Context $context, $indent = '');
 
     /**
      * Tests whether a value should be iterated over (e.g. in a section context).
@@ -146,4 +152,26 @@ abstract class Mustache_Template
 
         return $stack;
     }
+
+    /**
+     * Resolve a context value.
+     *
+     * Invoke the value if it is callable, otherwise return the value.
+     *
+     * @param  mixed            $value
+     * @param  Mustache_Context $context
+     * @param  string           $indent
+     *
+     * @return string
+     */
+    protected function resolveValue($value, Mustache_Context $context, $indent = '')
+    {
+        if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) {
+            return $this->mustache
+                ->loadLambda((string) call_user_func($value))
+                ->renderInternal($context, $indent);
+        }
+
+        return $value;
+    }
 }

+ 41 - 13
test/Mustache/Test/CompilerTest.php

@@ -33,23 +33,52 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
         return array(
             array('', array(), 'Banana', false, 'ISO-8859-1', array(
                 "\nclass Banana extends Mustache_Template",
-                'return htmlspecialchars($buffer, ENT_COMPAT, \'ISO-8859-1\');',
                 'return $buffer;',
             )),
 
             array('', array($this->createTextToken('TEXT')), 'Monkey', false, 'UTF-8', array(
                 "\nclass Monkey extends Mustache_Template",
-                'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
                 '$buffer .= $indent . \'TEXT\';',
                 'return $buffer;',
             )),
 
-            array('', array($this->createTextToken('TEXT')), 'Monkey', true, 'ISO-8859-1', array(
-                "\nclass Monkey extends Mustache_Template",
-                '$buffer .= $indent . \'TEXT\';',
-                'return call_user_func($this->mustache->getEscape(), $buffer);',
-                'return $buffer;',
-            )),
+            array(
+                '',
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME => 'name',
+                    )
+                ),
+                'Monkey',
+                true,
+                'ISO-8859-1',
+                array(
+                    "\nclass Monkey extends Mustache_Template",
+                    '$value = $this->resolveValue($context->find(\'name\'), $context, $indent);',
+                    '$buffer .= $indent . call_user_func($this->mustache->getEscape(), $value);',
+                    'return $buffer;',
+                )
+            ),
+
+            array(
+                '',
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME => 'name',
+                    )
+                ),
+                'Monkey',
+                false,
+                'ISO-8859-1',
+                array(
+                    "\nclass Monkey extends Mustache_Template",
+                    '$value = $this->resolveValue($context->find(\'name\'), $context, $indent);',
+                    '$buffer .= $indent . htmlspecialchars($value, ENT_COMPAT, \'ISO-8859-1\');',
+                    'return $buffer;',
+                )
+            ),
 
             array(
                 '',
@@ -73,11 +102,10 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
                     "\nclass Monkey extends Mustache_Template",
                     '$buffer .= $indent . \'foo\'',
                     '$buffer .= "\n"',
-                    '$value = $context->find(\'name\');',
+                    '$value = $this->resolveValue($context->find(\'name\'), $context, $indent);',
                     '$buffer .= htmlspecialchars($value, ENT_COMPAT, \'UTF-8\');',
-                    '$value = $context->last();',
+                    '$value = $this->resolveValue($context->last(), $context, $indent);',
                     '$buffer .= \'\\\'bar\\\'\';',
-                    'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
                     'return $buffer;',
                 )
             ),
@@ -85,9 +113,9 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_SyntaxException
      */
-    public function testCompilerThrowsUnknownNodeTypeException()
+    public function testCompilerThrowsSyntaxException()
     {
         $compiler = new Mustache_Compiler;
         $compiler->compile('', array(array(Mustache_Tokenizer::TYPE => 'invalid')), 'SomeClass');

+ 27 - 4
test/Mustache/Test/EngineTest.php

@@ -141,7 +141,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_InvalidArgumentException
      * @dataProvider getBadEscapers
      */
     public function testNonCallableEscapeThrowsException($escape)
@@ -158,7 +158,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException RuntimeException
+     * @expectedException Mustache_Exception_RuntimeException
      */
     public function testImmutablePartialsLoadersThrowException()
     {
@@ -223,7 +223,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_InvalidArgumentException
      */
     public function testSetHelpersThrowsExceptions()
     {
@@ -232,7 +232,7 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_InvalidArgumentException
      */
     public function testSetLoggerThrowsExceptions()
     {
@@ -240,6 +240,29 @@ class Mustache_Test_EngineTest extends PHPUnit_Framework_TestCase
         $mustache->setLogger(new StdClass);
     }
 
+    public function testLoadPartialCascading()
+    {
+        $loader = new Mustache_Loader_ArrayLoader(array(
+            'foo' => 'FOO',
+        ));
+
+        $mustache = new Mustache_Engine(array('loader' => $loader));
+
+        $tpl = $mustache->loadTemplate('foo');
+
+        $this->assertSame($tpl, $mustache->loadPartial('foo'));
+
+        $mustache->setPartials(array(
+            'foo' => 'f00',
+        ));
+
+        // setting partials overrides the default template loading fallback.
+        $this->assertNotSame($tpl, $mustache->loadPartial('foo'));
+
+        // but it didn't overwrite the original template loader templates.
+        $this->assertSame($tpl, $mustache->loadTemplate('foo'));
+    }
+
     public function testPartialLoadFailLogging()
     {
         $name     = tempnam(sys_get_temp_dir(), 'mustache-test');

+ 27 - 0
test/Mustache/Test/Exception/SyntaxExceptionTest.php

@@ -0,0 +1,27 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Test_Exception_SyntaxExceptionTest extends PHPUnit_Framework_TestCase
+{
+    public function testInstance()
+    {
+        $e = new Mustache_Exception_SyntaxException('whot', array('is' => 'this'));
+        $this->assertTrue($e instanceof LogicException);
+        $this->assertTrue($e instanceof Mustache_Exception);
+    }
+
+    public function testGetToken()
+    {
+        $token = array(Mustache_Tokenizer::TYPE => 'whatever');
+        $e = new Mustache_Exception_SyntaxException('ignore this', $token);
+        $this->assertEquals($token, $e->getToken());
+    }
+}

+ 32 - 0
test/Mustache/Test/Exception/UnknownFilterExceptionTest.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Test_Exception_UnknownFilterExceptionTest extends PHPUnit_Framework_TestCase
+{
+    public function testInstance()
+    {
+        $e = new Mustache_Exception_UnknownFilterException('bacon');
+        $this->assertTrue($e instanceof UnexpectedValueException);
+        $this->assertTrue($e instanceof Mustache_Exception);
+    }
+
+    public function testMessage()
+    {
+        $e = new Mustache_Exception_UnknownFilterException('sausage');
+        $this->assertEquals('Unknown filter: sausage', $e->getMessage());
+    }
+
+    public function testGetFilterName()
+    {
+        $e = new Mustache_Exception_UnknownFilterException('eggs');
+        $this->assertEquals('eggs', $e->getFilterName());
+    }
+}

+ 32 - 0
test/Mustache/Test/Exception/UnknownHelperExceptionTest.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Test_Exception_UnknownHelperExceptionTest extends PHPUnit_Framework_TestCase
+{
+    public function testInstance()
+    {
+        $e = new Mustache_Exception_UnknownHelperException('alpha');
+        $this->assertTrue($e instanceof InvalidArgumentException);
+        $this->assertTrue($e instanceof Mustache_Exception);
+    }
+
+    public function testMessage()
+    {
+        $e = new Mustache_Exception_UnknownHelperException('beta');
+        $this->assertEquals('Unknown helper: beta', $e->getMessage());
+    }
+
+    public function testGetHelperName()
+    {
+        $e = new Mustache_Exception_UnknownHelperException('gamma');
+        $this->assertEquals('gamma', $e->getHelperName());
+    }
+}

+ 32 - 0
test/Mustache/Test/Exception/UnknownTemplateExceptionTest.php

@@ -0,0 +1,32 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2013 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Mustache_Test_Exception_UnknownTemplateExceptionTest extends PHPUnit_Framework_TestCase
+{
+    public function testInstance()
+    {
+        $e = new Mustache_Exception_UnknownTemplateException('mario');
+        $this->assertTrue($e instanceof InvalidArgumentException);
+        $this->assertTrue($e instanceof Mustache_Exception);
+    }
+
+    public function testMessage()
+    {
+        $e = new Mustache_Exception_UnknownTemplateException('luigi');
+        $this->assertEquals('Unknown template: luigi', $e->getMessage());
+    }
+
+    public function testGetTemplateName()
+    {
+        $e = new Mustache_Exception_UnknownTemplateException('yoshi');
+        $this->assertEquals('yoshi', $e->getTemplateName());
+    }
+}

+ 1 - 1
test/Mustache/Test/FiveThree/Functional/FiltersTest.php

@@ -67,7 +67,7 @@ class Mustache_Test_FiveThree_Functional_FiltersTest extends PHPUnit_Framework_T
     }
 
     /**
-     * @expectedException UnexpectedValueException
+     * @expectedException Mustache_Exception_UnknownFilterException
      * @dataProvider getBrokenPipes
      */
     public function testThrowsExceptionForBrokenPipes($tpl, $data)

+ 1 - 1
test/Mustache/Test/Loader/ArrayLoaderTest.php

@@ -42,7 +42,7 @@ class Mustache_Test_Loader_ArrayLoaderTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_UnknownTemplateException
      */
     public function testMissingTemplatesThrowExceptions()
     {

+ 40 - 0
test/Mustache/Test/Loader/CascadingLoaderTest.php

@@ -0,0 +1,40 @@
+<?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.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_CascadingLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testLoadTemplates()
+    {
+        $loader = new Mustache_Loader_CascadingLoader(array(
+            new Mustache_Loader_ArrayLoader(array('foo' => '{{ foo }}')),
+            new Mustache_Loader_ArrayLoader(array('bar' => '{{#bar}}BAR{{/bar}}')),
+        ));
+
+        $this->assertEquals('{{ foo }}', $loader->load('foo'));
+        $this->assertEquals('{{#bar}}BAR{{/bar}}', $loader->load('bar'));
+    }
+
+    /**
+     * @expectedException Mustache_Exception_UnknownTemplateException
+     */
+    public function testMissingTemplatesThrowExceptions()
+    {
+        $loader = new Mustache_Loader_CascadingLoader(array(
+            new Mustache_Loader_ArrayLoader(array('foo' => '{{ foo }}')),
+            new Mustache_Loader_ArrayLoader(array('bar' => '{{#bar}}BAR{{/bar}}')),
+        ));
+
+        $loader->load('not_a_real_template');
+    }
+}

+ 2 - 2
test/Mustache/Test/Loader/FilesystemLoaderTest.php

@@ -44,7 +44,7 @@ class Mustache_Test_Loader_FilesystemLoaderTest extends PHPUnit_Framework_TestCa
     }
 
     /**
-     * @expectedException RuntimeException
+     * @expectedException Mustache_Exception_RuntimeException
      */
     public function testMissingBaseDirThrowsException()
     {
@@ -52,7 +52,7 @@ class Mustache_Test_Loader_FilesystemLoaderTest extends PHPUnit_Framework_TestCa
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_UnknownTemplateException
      */
     public function testMissingTemplateThrowsException()
     {

+ 56 - 0
test/Mustache/Test/Loader/InlineLoaderTest.php

@@ -0,0 +1,56 @@
+<?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.
+ */
+
+/**
+ * @group unit
+ */
+class Mustache_Test_Loader_InlineLoaderTest extends PHPUnit_Framework_TestCase
+{
+    public function testLoadTemplates()
+    {
+        $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+        $this->assertEquals('{{ foo }}', $loader->load('foo'));
+        $this->assertEquals('{{#bar}}BAR{{/bar}}', $loader->load('bar'));
+    }
+
+    /**
+     * @expectedException Mustache_Exception_UnknownTemplateException
+     */
+    public function testMissingTemplatesThrowExceptions()
+    {
+        $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__);
+        $loader->load('not_a_real_template');
+    }
+
+    /**
+     * @expectedException Mustache_Exception_InvalidArgumentException
+     */
+    public function testInvalidOffsetThrowsException()
+    {
+        $loader = new Mustache_Loader_InlineLoader(__FILE__, 'notanumber');
+    }
+
+    /**
+     * @expectedException Mustache_Exception_InvalidArgumentException
+     */
+    public function testInvalidFileThrowsException()
+    {
+        $loader = new Mustache_Loader_InlineLoader('notarealfile', __COMPILER_HALT_OFFSET__);
+    }
+}
+
+__halt_compiler();
+
+@@ foo
+{{ foo }}
+
+@@ bar
+{{#bar}}BAR{{/bar}}

+ 3 - 3
test/Mustache/Test/Logger/StreamLoggerTest.php

@@ -34,7 +34,7 @@ class Mustache_Test_Logger_StreamLoggerTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException LogicException
+     * @expectedException Mustache_Exception_LogicException
      */
     public function testPrematurelyClosedStreamThrowsException()
     {
@@ -187,7 +187,7 @@ class Mustache_Test_Logger_StreamLoggerTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_InvalidArgumentException
      */
     public function testThrowsInvalidArgumentExceptionWhenSettingUnknownLevels()
     {
@@ -196,7 +196,7 @@ class Mustache_Test_Logger_StreamLoggerTest extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @expectedException InvalidArgumentException
+     * @expectedException Mustache_Exception_InvalidArgumentException
      */
     public function testThrowsInvalidArgumentExceptionWhenLoggingUnknownLevels()
     {

+ 1 - 1
test/Mustache/Test/ParserTest.php

@@ -108,7 +108,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
 
     /**
      * @dataProvider getBadParseTrees
-     * @expectedException LogicException
+     * @expectedException Mustache_Exception_SyntaxException
      */
     public function testParserThrowsExceptions($tokens)
     {