Selaa lähdekoodia

Merge branch 'release/2.4.0'

Justin Hileman 12 vuotta sitten
vanhempi
sitoutus
6cd3145073

+ 26 - 26
bin/build_bootstrap.php

@@ -33,32 +33,32 @@ if (file_exists($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',
+    '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'));
 
 /**

+ 15 - 14
src/Mustache/Compiler.php

@@ -21,6 +21,7 @@ class Mustache_Compiler
     private $source;
     private $indentNextLine;
     private $customEscape;
+    private $entityFlags;
     private $charset;
     private $strictCallables;
     private $pragmas;
@@ -32,18 +33,20 @@ class Mustache_Compiler
      * @param string $tree            Parse tree of Mustache tokens
      * @param string $name            Mustache Template class name
      * @param bool   $customEscape    (default: false)
+     * @param int    $entityFlags     (default: ENT_COMPAT)
      * @param string $charset         (default: 'UTF-8')
      * @param bool   $strictCallables (default: false)
      *
      * @return string Generated PHP source code
      */
-    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false)
+    public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT)
     {
         $this->pragmas         = array();
         $this->sections        = array();
         $this->source          = $source;
         $this->indentNextLine  = true;
         $this->customEscape    = $customEscape;
+        $this->entityFlags     = $entityFlags;
         $this->charset         = $charset;
         $this->strictCallables = $strictCallables;
 
@@ -370,13 +373,11 @@ class Mustache_Compiler
      */
     private function text($text, $level)
     {
-        if ($text === "\n") {
-            $this->indentNextLine = true;
+        $indentNextLine = (substr($text, -1) === "\n");
+        $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
+        $this->indentNextLine = $indentNextLine;
 
-            return $this->prepare(self::LINE, $level);
-        } else {
-            return sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true));
-        }
+        return $code;
     }
 
     /**
@@ -402,7 +403,7 @@ class Mustache_Compiler
         return preg_replace("/\n( {8})?/", "\n".str_repeat(" ", $bonus * 4), $text);
     }
 
-    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
+    const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)';
     const CUSTOM_ESCAPE  = 'call_user_func($this->mustache->getEscape(), %s)';
 
     /**
@@ -417,7 +418,7 @@ class Mustache_Compiler
         if ($this->customEscape) {
             return sprintf(self::CUSTOM_ESCAPE, $value);
         } else {
-            return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
+            return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true));
         }
     }
 
@@ -464,12 +465,12 @@ class Mustache_Compiler
      */
     private function flushIndent()
     {
-        if ($this->indentNextLine) {
-            $this->indentNextLine = false;
-
-            return self::LINE_INDENT;
-        } else {
+        if (!$this->indentNextLine) {
             return '';
         }
+
+        $this->indentNextLine = false;
+
+        return self::LINE_INDENT;
     }
 }

+ 22 - 3
src/Mustache/Engine.php

@@ -23,7 +23,7 @@
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.3.1';
+    const VERSION        = '2.4.0';
     const SPEC_VERSION   = '1.1.2';
 
     const PRAGMA_FILTERS = 'FILTERS';
@@ -39,6 +39,7 @@ class Mustache_Engine
     private $partialsLoader;
     private $helpers;
     private $escape;
+    private $entityFlags = ENT_COMPAT;
     private $charset = 'UTF-8';
     private $logger;
     private $strictCallables = false;
@@ -81,6 +82,9 @@ class Mustache_Engine
      *             return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
      *         },
      *
+     *         // Type argument for `htmlspecialchars`.  Defaults to ENT_COMPAT.  You may prefer ENT_QUOTES.
+     *         'entity_flags' => ENT_QUOTES,
+     *
      *         // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
      *         'charset' => 'ISO-8859-1',
      *
@@ -139,6 +143,10 @@ class Mustache_Engine
             $this->escape = $options['escape'];
         }
 
+        if (isset($options['entity_flags'])) {
+          $this->entityFlags = $options['entity_flags'];
+        }
+
         if (isset($options['charset'])) {
             $this->charset = $options['charset'];
         }
@@ -180,6 +188,16 @@ class Mustache_Engine
         return $this->escape;
     }
 
+    /**
+     * Get the current Mustache entitity type to escape.
+     *
+     * @return int
+     */
+    public function getEntityFlags()
+    {
+      return $this->entityFlags;
+    }
+
     /**
      * Get the current Mustache character set.
      *
@@ -471,9 +489,10 @@ class Mustache_Engine
     public function getTemplateClassName($source)
     {
         return $this->templateClassPrefix . md5(sprintf(
-            'version:%s,escape:%s,charset:%s,strict_callables:%s,source:%s',
+            'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,source:%s',
             self::VERSION,
             isset($this->escape) ? 'custom' : 'default',
+            $this->entityFlags,
             $this->charset,
             $this->strictCallables ? 'true' : 'false',
             $source
@@ -644,7 +663,7 @@ class Mustache_Engine
             array('className' => $name)
         );
 
-        return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables);
+        return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
     }
 
     /**

+ 5 - 1
src/Mustache/Loader/FilesystemLoader.php

@@ -47,7 +47,11 @@ class Mustache_Loader_FilesystemLoader implements Mustache_Loader
      */
     public function __construct($baseDir, array $options = array())
     {
-        $this->baseDir = rtrim(realpath($baseDir), '/');
+        $this->baseDir = $baseDir;
+
+        if (strpos($this->baseDir, '://') === -1) {
+            $this->baseDir = realpath($this->baseDir);
+        }
 
         if (!is_dir($this->baseDir)) {
             throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir));

+ 3 - 1
src/Mustache/Loader/InlineLoader.php

@@ -35,11 +35,13 @@
  *         'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__)
  *     ));
  *
- *     $app->get('/{name}', function() use ($app) {
+ *     $app->get('/{name}', function($name) use ($app) {
  *         return $app['mustache']->render('hello', compact('name'));
  *     })
  *     ->value('name', 'world');
  *
+ *     // ...
+ *
  *     __halt_compiler();
  *
  *     @@ hello

+ 140 - 37
src/Mustache/Parser.php

@@ -16,6 +16,8 @@
  */
 class Mustache_Parser
 {
+    private $lineNum;
+    private $lineTokens;
 
     /**
      * Process an array of Mustache tokens and convert them into a parse tree.
@@ -26,7 +28,10 @@ class Mustache_Parser
      */
     public function parse(array $tokens = array())
     {
-        return $this->buildTree(new ArrayIterator($tokens));
+        $this->lineNum    = -1;
+        $this->lineTokens = 0;
+
+        return $this->buildTree($tokens);
     }
 
     /**
@@ -34,52 +39,74 @@ class Mustache_Parser
      *
      * @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)
+     * @param array &$tokens Set of Mustache tokens
+     * @param array  $parent Parent token (default: null)
      *
      * @return array Mustache Token parse tree
      */
-    private function buildTree(ArrayIterator $tokens, array $parent = null)
+    private function buildTree(array &$tokens, array $parent = null)
     {
         $nodes = array();
 
-        do {
-            $token = $tokens->current();
-            $tokens->next();
+        while (!empty($tokens)) {
+            $token = array_shift($tokens);
 
-            if ($token === null) {
-                continue;
+            if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) {
+                $this->lineTokens++;
             } 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)) {
-                            $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]) {
-                            $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];
-                        $parent[Mustache_Tokenizer::NODES] = $nodes;
-
-                        return $parent;
-                        break;
-
-                    default:
-                        $nodes[] = $token;
-                        break;
-                }
+                $this->lineNum    = $token[Mustache_Tokenizer::LINE];
+                $this->lineTokens = 0;
             }
 
-        } while ($tokens->valid());
+            switch ($token[Mustache_Tokenizer::TYPE]) {
+                case Mustache_Tokenizer::T_DELIM_CHANGE:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    break;
+
+                case Mustache_Tokenizer::T_SECTION:
+                case Mustache_Tokenizer::T_INVERTED:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_END_SECTION:
+                    if (!isset($parent)) {
+                        $msg = sprintf('Unexpected closing tag: /%s', $token[Mustache_Tokenizer::NAME]);
+                        throw new Mustache_Exception_SyntaxException($msg, $token);
+                    }
+
+                    if ($token[Mustache_Tokenizer::NAME] !== $parent[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);
+                    }
+
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $parent[Mustache_Tokenizer::END]   = $token[Mustache_Tokenizer::INDEX];
+                    $parent[Mustache_Tokenizer::NODES] = $nodes;
+
+                    return $parent;
+                    break;
+
+                case Mustache_Tokenizer::T_PARTIAL:
+                case Mustache_Tokenizer::T_PARTIAL_2:
+                    // store the whitespace prefix for laters!
+                    if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
+                        $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
+                    }
+                    $nodes[] = $token;
+                    break;
+
+                case Mustache_Tokenizer::T_PRAGMA:
+                case Mustache_Tokenizer::T_COMMENT:
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $token;
+                    break;
+
+                default:
+                    $nodes[] = $token;
+                    break;
+            }
+        }
 
         if (isset($parent)) {
             $msg = sprintf('Missing closing tag: %s', $parent[Mustache_Tokenizer::NAME]);
@@ -88,4 +115,80 @@ class Mustache_Parser
 
         return $nodes;
     }
+
+    /**
+     * Clear standalone line tokens.
+     *
+     * Returns a whitespace token for indenting partials, if applicable.
+     *
+     * @param array  $nodes  Parsed nodes.
+     * @param array  $tokens Tokens to be parsed.
+     *
+     * @return array Resulting indent token, if any.
+     */
+    private function clearStandaloneLines(array &$nodes, array &$tokens)
+    {
+        if ($this->lineTokens > 1) {
+            // this is the third or later node on this line, so it can't be standalone
+            return;
+        }
+
+        $prev = null;
+        if ($this->lineTokens === 1) {
+            // this is the second node on this line, so it can't be standalone
+            // unless the previous node is whitespace.
+            if ($prev = end($nodes)) {
+                if (!$this->tokenIsWhitespace($prev)) {
+                    return;
+                }
+            }
+        }
+
+        $next = null;
+        if ($next = reset($tokens)) {
+            // If we're on a new line, bail.
+            if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) {
+                return;
+            }
+
+            // If the next token isn't whitespace, bail.
+            if (!$this->tokenIsWhitespace($next)) {
+                return;
+            }
+
+            if (count($tokens) !== 1) {
+                // Unless it's the last token in the template, the next token
+                // must end in newline for this to be standalone.
+                if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+                    return;
+                }
+            }
+
+            // Discard the whitespace suffix
+            array_shift($tokens);
+        }
+
+        if ($prev) {
+            // Return the whitespace prefix, if any
+            return array_pop($nodes);
+        }
+    }
+
+    /**
+     * Check whether token is a whitespace token.
+     *
+     * True if token type is T_TEXT and value is all whitespace characters.
+     *
+     * @param array $token
+     *
+     * @return boolean True if token is a whitespace token
+     */
+    private function tokenIsWhitespace(array $token)
+    {
+        if ($token[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_TEXT) {
+            return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+        }
+
+        return false;
+    }
 }

+ 38 - 70
src/Mustache/Tokenizer.php

@@ -63,20 +63,20 @@ class Mustache_Tokenizer
     const NAME   = 'name';
     const OTAG   = 'otag';
     const CTAG   = 'ctag';
+    const LINE   = 'line';
     const INDEX  = 'index';
     const END    = 'end';
     const INDENT = 'indent';
     const NODES  = 'nodes';
     const VALUE  = 'value';
 
-    private $pragmas;
     private $state;
     private $tagType;
     private $tag;
     private $buffer;
     private $tokens;
     private $seenTag;
-    private $lineStart;
+    private $line;
     private $otag;
     private $ctag;
 
@@ -108,16 +108,15 @@ class Mustache_Tokenizer
                         $this->state = self::IN_TAG_TYPE;
                     } else {
                         $char = substr($text, $i, 1);
+                        $this->buffer .= $char;
                         if ($char == "\n") {
-                            $this->filterLine();
-                        } else {
-                            $this->buffer .= $char;
+                            $this->flushBuffer();
+                            $this->line++;
                         }
                     }
                     break;
 
                 case self::IN_TAG_TYPE:
-
                     $i += strlen($this->otag) - 1;
                     $char = substr($text, $i + 1, 1);
                     if (isset(self::$tagTypes[$char])) {
@@ -150,6 +149,7 @@ class Mustache_Tokenizer
                             self::NAME  => trim($this->buffer),
                             self::OTAG  => $this->otag,
                             self::CTAG  => $this->ctag,
+                            self::LINE  => $this->line,
                             self::INDEX => ($this->tagType == self::T_END_SECTION) ? $this->seenTag - strlen($this->otag) : $i + strlen($this->ctag)
                         );
 
@@ -174,14 +174,7 @@ class Mustache_Tokenizer
             }
         }
 
-        $this->filterLine(true);
-
-        foreach ($this->pragmas as $pragma) {
-            array_unshift($this->tokens, array(
-                self::TYPE => self::T_PRAGMA,
-                self::NAME => $pragma,
-            ));
-        }
+        $this->flushBuffer();
 
         return $this->tokens;
     }
@@ -197,10 +190,9 @@ class Mustache_Tokenizer
         $this->buffer    = '';
         $this->tokens    = array();
         $this->seenTag   = false;
-        $this->lineStart = 0;
+        $this->line      = 0;
         $this->otag      = '{{';
         $this->ctag      = '}}';
-        $this->pragmas   = array();
     }
 
     /**
@@ -209,62 +201,15 @@ class Mustache_Tokenizer
     private function flushBuffer()
     {
         if (!empty($this->buffer)) {
-            $this->tokens[] = array(self::TYPE  => self::T_TEXT, self::VALUE => $this->buffer);
+            $this->tokens[] = array(
+                self::TYPE  => self::T_TEXT,
+                self::LINE  => $this->line,
+                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.
      *
@@ -283,13 +228,36 @@ class Mustache_Tokenizer
         $this->otag = $otag;
         $this->ctag = $ctag;
 
+        $this->tokens[] = array(
+            self::TYPE => self::T_DELIM_CHANGE,
+            self::LINE => $this->line,
+        );
+
         return $closeIndex + strlen($close) - 1;
     }
 
+    /**
+     * Add pragma token.
+     *
+     * Pragmas are hoisted to the front of the template, so all pragma tokens
+     * will appear at the front of the token list.
+     *
+     * @param string $text
+     * @param int    $index
+     *
+     * @return int New index value
+     */
     private function addPragma($text, $index)
     {
-        $end = strpos($text, $this->ctag, $index);
-        $this->pragmas[] = trim(substr($text, $index + 2, $end - $index - 2));
+        $end    = strpos($text, $this->ctag, $index);
+        $pragma = trim(substr($text, $index + 2, $end - $index - 2));
+
+        // Pragmas are hoisted to the front of the template.
+        array_unshift($this->tokens, array(
+            self::TYPE => self::T_PRAGMA,
+            self::NAME => $pragma,
+            self::LINE => 0,
+        ));
 
         return $end + strlen($this->ctag) - 1;
     }

+ 31 - 10
test/Mustache/Test/CompilerTest.php

@@ -18,11 +18,11 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
     /**
      * @dataProvider getCompileValues
      */
-    public function testCompile($source, array $tree, $name, $customEscaper, $charset, $expected)
+    public function testCompile($source, array $tree, $name, $customEscaper, $entityFlags, $charset, $expected)
     {
         $compiler = new Mustache_Compiler;
 
-        $compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset);
+        $compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset, false, $entityFlags);
         foreach ($expected as $contains) {
             $this->assertContains($contains, $compiled);
         }
@@ -31,12 +31,12 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
     public function getCompileValues()
     {
         return array(
-            array('', array(), 'Banana', false, 'ISO-8859-1', array(
+            array('', array(), 'Banana', false, ENT_COMPAT, 'ISO-8859-1', array(
                 "\nclass Banana extends Mustache_Template",
                 'return $buffer;',
             )),
 
-            array('', array($this->createTextToken('TEXT')), 'Monkey', false, 'UTF-8', array(
+            array('', array($this->createTextToken('TEXT')), 'Monkey', false, ENT_COMPAT, 'UTF-8', array(
                 "\nclass Monkey extends Mustache_Template",
                 '$buffer .= $indent . \'TEXT\';',
                 'return $buffer;',
@@ -52,6 +52,7 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
                 ),
                 'Monkey',
                 true,
+                ENT_COMPAT,
                 'ISO-8859-1',
                 array(
                     "\nclass Monkey extends Mustache_Template",
@@ -71,11 +72,12 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
                 ),
                 'Monkey',
                 false,
+                ENT_COMPAT,
                 '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\');',
+                    '$buffer .= $indent . htmlspecialchars($value, '.ENT_COMPAT.', \'ISO-8859-1\');',
                     'return $buffer;',
                 )
             ),
@@ -83,8 +85,27 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
             array(
                 '',
                 array(
-                    $this->createTextToken('foo'),
-                    $this->createTextToken("\n"),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::NAME => 'name',
+                    )
+                ),
+                'Monkey',
+                false,
+                ENT_QUOTES,
+                'ISO-8859-1',
+                array(
+                    "\nclass Monkey extends Mustache_Template",
+                    '$value = $this->resolveValue($context->find(\'name\'), $context, $indent);',
+                    '$buffer .= $indent . htmlspecialchars($value, '.ENT_QUOTES.', \'ISO-8859-1\');',
+                    'return $buffer;',
+                )
+            ),
+
+            array(
+                '',
+                array(
+                    $this->createTextToken("foo\n"),
                     array(
                         Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
                         Mustache_Tokenizer::NAME => 'name',
@@ -97,13 +118,13 @@ class Mustache_Test_CompilerTest extends PHPUnit_Framework_TestCase
                 ),
                 'Monkey',
                 false,
+                ENT_COMPAT,
                 'UTF-8',
                 array(
                     "\nclass Monkey extends Mustache_Template",
-                    '$buffer .= $indent . \'foo\'',
-                    '$buffer .= "\n"',
+                    "\$buffer .= \$indent . 'foo\n';",
                     '$value = $this->resolveValue($context->find(\'name\'), $context, $indent);',
-                    '$buffer .= htmlspecialchars($value, ENT_COMPAT, \'UTF-8\');',
+                    '$buffer .= htmlspecialchars($value, '.ENT_COMPAT.', \'UTF-8\');',
                     '$value = $this->resolveValue($context->last(), $context, $indent);',
                     '$buffer .= \'\\\'bar\\\'\';',
                     'return $buffer;',

+ 7 - 0
test/Mustache/Test/Loader/FilesystemLoaderTest.php

@@ -22,6 +22,13 @@ class Mustache_Test_Loader_FilesystemLoaderTest extends PHPUnit_Framework_TestCa
         $this->assertEquals('beta contents', $loader->load('beta.ms'));
     }
 
+    public function testTrailingSlashes()
+    {
+        $baseDir = dirname(__FILE__).'/../../../fixtures/templates/';
+        $loader = new Mustache_Loader_FilesystemLoader($baseDir);
+        $this->assertEquals('one contents', $loader->load('one'));
+    }
+
     public function testLoadTemplates()
     {
         $baseDir = realpath(dirname(__FILE__).'/../../../fixtures/templates');

+ 22 - 2
test/Mustache/Test/ParserTest.php

@@ -35,21 +35,25 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
             array(
                 array(array(
                     Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
-                    Mustache_Tokenizer::VALUE => 'text'
+                    Mustache_Tokenizer::LINE  => 0,
+                    Mustache_Tokenizer::VALUE => 'text',
                 )),
                 array(array(
                     Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
-                    Mustache_Tokenizer::VALUE => 'text'
+                    Mustache_Tokenizer::LINE  => 0,
+                    Mustache_Tokenizer::VALUE => 'text',
                 )),
             ),
 
             array(
                 array(array(
                     Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                    Mustache_Tokenizer::LINE => 0,
                     Mustache_Tokenizer::NAME => 'name'
                 )),
                 array(array(
                     Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                    Mustache_Tokenizer::LINE => 0,
                     Mustache_Tokenizer::NAME => 'name'
                 )),
             ),
@@ -58,46 +62,55 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'foo'
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                         Mustache_Tokenizer::NAME  => 'parent'
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::NAME  => 'name'
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 456,
                         Mustache_Tokenizer::NAME  => 'parent'
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'bar'
                     ),
                 ),
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'foo'
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                         Mustache_Tokenizer::END   => 456,
                         Mustache_Tokenizer::NODES => array(
                             array(
                                 Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED,
+                                Mustache_Tokenizer::LINE => 0,
                                 Mustache_Tokenizer::NAME => 'name'
                             ),
                         ),
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'bar'
                     ),
                 ),
@@ -125,6 +138,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                 ),
@@ -136,6 +150,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_INVERTED,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                 ),
@@ -147,6 +162,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                 ),
@@ -158,21 +174,25 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_SECTION,
                         Mustache_Tokenizer::NAME  => 'child',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
                         Mustache_Tokenizer::NAME  => 'parent',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
                         Mustache_Tokenizer::NAME  => 'child',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 123,
                     ),
                 ),

+ 21 - 1
test/Mustache/Test/TokenizerTest.php

@@ -33,6 +33,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'text',
                     ),
                 ),
@@ -44,6 +45,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => 'text',
                     ),
                 ),
@@ -58,6 +60,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'name',
                         Mustache_Tokenizer::OTAG  => '{{',
                         Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 10,
                     )
                 )
@@ -69,6 +72,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => '{{ name }}',
                     ),
                 ),
@@ -83,6 +87,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'name',
                         Mustache_Tokenizer::OTAG  => '<<<',
                         Mustache_Tokenizer::CTAG  => '>>>',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 12,
                     )
                 )
@@ -97,10 +102,12 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'a',
                         Mustache_Tokenizer::OTAG  => '{{',
                         Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::INDEX => 8,
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 0,
                         Mustache_Tokenizer::VALUE => "\n",
                     ),
                     array(
@@ -108,14 +115,24 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'b',
                         Mustache_Tokenizer::OTAG  => '{{',
                         Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::LINE  => 1,
                         Mustache_Tokenizer::INDEX => 18,
                     ),
-                    null,
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 1,
+                        Mustache_Tokenizer::VALUE => "  \n",
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_DELIM_CHANGE,
+                        Mustache_Tokenizer::LINE  => 2,
+                    ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_ESCAPED,
                         Mustache_Tokenizer::NAME  => 'c',
                         Mustache_Tokenizer::OTAG  => '|',
                         Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::LINE  => 2,
                         Mustache_Tokenizer::INDEX => 37,
                     ),
                     array(
@@ -123,10 +140,12 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'b',
                         Mustache_Tokenizer::OTAG  => '|',
                         Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::LINE  => 2,
                         Mustache_Tokenizer::INDEX => 37,
                     ),
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE  => 2,
                         Mustache_Tokenizer::VALUE => "\n",
                     ),
                     array(
@@ -134,6 +153,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::NAME  => 'd',
                         Mustache_Tokenizer::OTAG  => '|',
                         Mustache_Tokenizer::CTAG  => '|',
+                        Mustache_Tokenizer::LINE  => 3,
                         Mustache_Tokenizer::INDEX => 51,
                     ),