浏览代码

hogan.js style template inheritance

Dan Miller 11 年之前
父节点
当前提交
db14a6c863

+ 124 - 3
src/Mustache/Compiler.php

@@ -17,6 +17,7 @@
 class Mustache_Compiler
 {
 
+    private $pragmas;
     private $sections;
     private $source;
     private $indentNextLine;
@@ -24,7 +25,6 @@ class Mustache_Compiler
     private $entityFlags;
     private $charset;
     private $strictCallables;
-    private $pragmas;
 
     /**
      * Compile a Mustache token parse tree into PHP source code.
@@ -94,7 +94,6 @@ class Mustache_Compiler
                     break;
 
                 case Mustache_Tokenizer::T_PARTIAL:
-                case Mustache_Tokenizer::T_PARTIAL_2:
                     $code .= $this->partial(
                         $node[Mustache_Tokenizer::NAME],
                         isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
@@ -102,6 +101,39 @@ class Mustache_Compiler
                     );
                     break;
 
+                case Mustache_Tokenizer::T_PARENT:
+                    $code .= $this->parent(
+                        $node[Mustache_Tokenizer::NAME],
+                        isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
+                        $level,
+                        $node['nodes']
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT_ARG:
+                    $code .= $this->parentArg(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT_VAR:
+                    $code .= $this->parentVar(
+                        $node[Mustache_Tokenizer::NODES],
+                        $node[Mustache_Tokenizer::NAME],
+                        $node[Mustache_Tokenizer::INDEX],
+                        $node[Mustache_Tokenizer::END],
+                        $node[Mustache_Tokenizer::OTAG],
+                        $node[Mustache_Tokenizer::CTAG],
+                        $level
+                    );
+                    break;
+
                 case Mustache_Tokenizer::T_UNESCAPED:
                 case Mustache_Tokenizer::T_UNESCAPED_2:
                     $code .= $this->variable($node[Mustache_Tokenizer::NAME], false, $level);
@@ -136,6 +168,7 @@ class Mustache_Compiler
             {
                 $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context);
                 $buffer = \'\';
+                $new_context = array();
         %s
 
                 return $buffer;
@@ -150,6 +183,7 @@ class Mustache_Compiler
             public function renderInternal(Mustache_Context $context, $indent = \'\')
             {
                 $buffer = \'\';
+                $new_context = array();
         %s
 
                 return $buffer;
@@ -171,11 +205,71 @@ 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($klass, 0, false, true), $name, $callable, $code, $sections);
     }
 
+    const PARENT_VAR = '
+        $value = $this->resolveValue($context->%s(%s), $context, $indent);
+        if($value && !is_array($value) && !is_object($value)) {
+            $buffer .= %s;
+        } else {
+            %s
+        }
+    ';
+
+    private function parentVar($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $method = 'findFromParent';
+        $id_str = var_export($id, true);
+        $value  = $this->getEscape();
+
+        return sprintf($this->prepare(self::PARENT_VAR, $level), $method, $id_str, $value, $this->walk($nodes, 2));
+    }
+
+    function parentArgSection($nodes, $start, $end, $otag, $ctag, $level)
+    {
+        $source   = var_export(substr($this->source, $start, $end - $start), true);
+        $callable = $this->getCallable();
+
+        if ($otag !== '{{' || $ctag !== '}}') {
+            $delims = ', '.var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true);
+        } else {
+            $delims = '';
+        }
+
+        $key    = ucfirst(md5($delims."\n".$source));
+
+        if (!isset($this->sections[$key])) {
+            $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2));
+        }
+
+        return $key;
+    }
+
+    const PARENT_ARG = '
+        // %s parent_arg
+        $value = $this->section%s($context, $indent, true);
+        $new_context[%s] = %s$value;
+    ';
+
+    private function parentArg($nodes, $id, $start, $end, $otag, $ctag, $level)
+    {
+        $key = $this->parentArgSection($nodes, $start, $end, $otag, $ctag, $level);
+        $filters = '';
+
+        if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) {
+            list($id, $filters) = $this->getFilters($id, $level);
+        }
+
+        $method   = $this->getFindMethod($id);
+        $id       = var_export($id, true);
+
+        return sprintf($this->prepare(self::PARENT_ARG, $level), $key, $key, $id, $this->flushIndent());
+    }
+
     const SECTION_CALL = '
         // %s section
         $value = $context->%s(%s);%s
@@ -199,7 +293,8 @@ class Mustache_Compiler
             } elseif (!empty($value)) {
                 $values = $this->isIterable($value) ? $value : array($value);
                 foreach ($values as $value) {
-                    $context->push($value);%s
+                    $context->push($value);
+                    %s
                     $context->pop();
                 }
             }
@@ -302,6 +397,32 @@ class Mustache_Compiler
         );
     }
 
+    const PARENT = '
+
+        if ($parent = $this->mustache->LoadPartial(%s)) {
+            $context->push($new_context);
+            $buffer .= $parent->renderInternal($context, $indent);
+            $context->pop();
+        }
+    ';
+
+    private function parent($id, $indent, $level, $children)
+    {
+        $block = '';
+
+        $real_children = array_filter($children, function($ch) {
+            return $ch[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT_ARG;
+        });
+
+        $block = $this->walk($real_children, $level);
+
+        return $block. sprintf(
+            $this->prepare(self::PARENT, $level),
+            var_export($id, true),
+            var_export($indent, true)
+        );
+    }
+
     const VARIABLE = '
         $value = $this->resolveValue($context->%s(%s), $context, $indent);%s
         $buffer .= %s%s;

+ 12 - 0
src/Mustache/Context.php

@@ -120,6 +120,18 @@ class Mustache_Context
         return $value;
     }
 
+    public function findFromParent($id)
+    {
+        $stack = $this->stack;
+        foreach($stack as $context) {
+            if (is_array($context) && array_key_exists($id, $context)) {
+                return $context[$id];
+            }
+        }
+
+        return '';
+    }
+
     /**
      * Helper function to find a variable in the Context stack.
      *

+ 13 - 3
src/Mustache/Parser.php

@@ -95,17 +95,27 @@ class Mustache_Parser
                     $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!
+                    //store the whitespace prefix for laters!
                     if ($indent = $this->clearStandaloneLines($nodes, $tokens)) {
                         $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE];
                     }
                     $nodes[] = $token;
                     break;
 
+                case Mustache_Tokenizer::T_PARENT:
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
+                case Mustache_Tokenizer::T_PARENT_VAR:
+                    if ($parent[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT) {
+                        $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_PARENT_ARG;
+                    }
+                    $this->clearStandaloneLines($nodes, $tokens);
+                    $nodes[] = $this->buildTree($tokens, $token);
+                    break;
+
                 case Mustache_Tokenizer::T_PRAGMA:
                 case Mustache_Tokenizer::T_COMMENT:
                     $this->clearStandaloneLines($nodes, $tokens);

+ 5 - 2
src/Mustache/Tokenizer.php

@@ -28,13 +28,15 @@ class Mustache_Tokenizer
     const T_END_SECTION  = '/';
     const T_COMMENT      = '!';
     const T_PARTIAL      = '>';
-    const T_PARTIAL_2    = '<';
+    const T_PARENT       = '<';
     const T_DELIM_CHANGE = '=';
     const T_ESCAPED      = '_v';
     const T_UNESCAPED    = '{';
     const T_UNESCAPED_2  = '&';
     const T_TEXT         = '_t';
     const T_PRAGMA       = '%';
+    const T_PARENT_VAR   = '$';
+    const T_PARENT_ARG   = '$arg';
 
     // Valid token types
     private static $tagTypes = array(
@@ -43,12 +45,13 @@ class Mustache_Tokenizer
         self::T_END_SECTION  => true,
         self::T_COMMENT      => true,
         self::T_PARTIAL      => true,
-        self::T_PARTIAL_2    => true,
+        self::T_PARENT       => true,
         self::T_DELIM_CHANGE => true,
         self::T_ESCAPED      => true,
         self::T_UNESCAPED    => true,
         self::T_UNESCAPED_2  => true,
         self::T_PRAGMA       => true,
+        self::T_PARENT_VAR   => true,
     );
 
     // Interpolated tags

+ 303 - 0
test/Mustache/Test/Functional/InheritanceTest.php

@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * @group inheritance
+ * @group functional
+ */
+
+class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCase
+{
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->mustache = new Mustache_Engine;
+    }
+
+    public function testDefaultContent()
+    {
+        $tpl = $this->mustache->loadTemplate('{{$title}}Default title{{/title}}');
+
+        $data = array();
+
+        $this->assertEquals('Default title', $tpl->render($data));
+    }
+
+    public function testDefaultContentRendersVariables()
+    {
+        $tpl = $this->mustache->loadTemplate('{{$foo}}default {{bar}} content{{/foo}}');
+
+        $data = array(
+            'bar' => 'baz'
+        );
+
+        $this->assertEquals('default baz content', $tpl->render($data));
+    }
+
+    public function testDefaultContentRendersTripleMustacheVariables()
+    {
+        $tpl = $this->mustache->loadTemplate('{{$foo}}default {{{bar}}} content{{/foo}}');
+
+        $data = array(
+            'bar' => '<baz>'
+        );
+
+        $this->assertEquals('default <baz> content', $tpl->render($data));
+    }
+
+    public function testDefaultContentRendersSections()
+    {
+        $tpl = $this->mustache->loadTemplate(
+            '{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}'
+        );
+
+        $data = array(
+            'bar' => array('baz' => 'qux')
+        );
+
+        $this->assertEquals('default qux content', $tpl->render($data));
+    }
+
+    public function testDefaultContentRendersNegativeSections()
+    {
+        $tpl = $this->mustache->loadTemplate(
+            '{{$foo}}default {{^bar}}{{baz}}{{/bar}} content{{/foo}}'
+        );
+
+        $data = array(
+            'foo' => array('bar' => 'qux'),
+            'baz' => 'three'
+        );
+
+        $this->assertEquals('default three content', $tpl->render($data));
+
+    }
+
+    public function testMustacheInjectionInDefaultContent()
+    {
+        $tpl = $this->mustache->loadTemplate(
+            '{{$foo}}default {{#bar}}{{baz}}{{/bar}} content{{/foo}}'
+        );
+
+        $data = array(
+            'bar' => array('baz' => '{{qux}}')
+        );
+
+        $this->assertEquals('default {{qux}} content', $tpl->render($data));
+    }
+
+    public function testDefaultContentRenderedInsideIncludedTemplates()
+    {
+        $partials = array(
+            'include' => '{{$foo}}default content{{/foo}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<include}}{{/include}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('default content', $tpl->render($data));
+    }
+
+    public function testOverriddenContent()
+    {
+        $partials = array(
+            'super' => '...{{$title}}Default title{{/title}}...'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<super}}{{$title}}sub template title{{/title}}{{/super}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('...sub template title...', $tpl->render($data));
+    }
+
+    public function testOverriddenPartial()
+    {
+        $partials = array(
+            'partial' => '|{{$stuff}}...{{/stuff}}{{$default}} default{{/default}}|'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            'test {{<partial}}{{$stuff}}override1{{/stuff}}{{/partial}} {{<partial}}{{$stuff}}override2{{/stuff}}{{/partial}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('test |override1 default| |override2 default|', $tpl->render($data));
+    }
+
+    public function testOverridePartialWithNewlines()
+    {
+         $partials = array(
+            'partial' => '{{$ballmer}}peaking{{/ballmer}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            "{{<partial}}{{\$ballmer}}\npeaked\n\n:(\n{{/ballmer}}{{/partial}}"
+        );
+
+        $data = array();
+
+        $this->assertEquals("peaked\n\n:(\n", $tpl->render($data));
+    }
+
+    public function testInheritIndentationWhenOverridingAPartial()
+    {
+        $partials = array(
+            'partial' =>
+                'stop:
+                    {{$nineties}}collaborate and listen{{/nineties}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<partial}}{{$nineties}}hammer time{{/nineties}}{{/partial}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals(
+            'stop:
+                    hammer time',
+            $tpl->render($data)
+        );
+    }
+
+    public function testOverrideOneSubstitutionButNotTheOther()
+    {
+        $partials = array(
+            'partial' => '{{$stuff}}default one{{/stuff}}, {{$stuff2}}default two{{/stuff2}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<partial}}{{$stuff2}}override two{{/stuff2}}{{/partial}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('default one, override two', $tpl->render($data));
+    }
+
+    public function testSuperTemplatesWithNoParameters()
+    {
+        $partials = array(
+            'include' => '{{$foo}}default content{{/foo}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{>include}}|{{<include}}{{/include}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('default content|default content', $tpl->render($data));
+    }
+
+    public function testRecursionInInheritedTemplates()
+    {
+        $partials = array(
+            'include' => '{{$foo}}default content{{/foo}} {{$bar}}{{<include2}}{{/include2}}{{/bar}}',
+            'include2' => '{{$foo}}include2 default content{{/foo}} {{<include}}{{$bar}}don\'t recurse{{/bar}}{{/include}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<include}}{{$foo}}override{{/foo}}{{/include}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('override override override don\'t recurse', $tpl->render($data));
+    }
+
+    public function testTopLevelSubstitutionsTakePrecedenceInMultilevelInheritance()
+    {
+        $partials = array(
+            'parent' => '{{<older}}{{$a}}p{{/a}}{{/older}}',
+            'older' => '{{<grandParent}}{{$a}}o{{/a}}{{/grandParent}}',
+            'grandParent' => '{{$a}}g{{/a}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<parent}}{{$a}}c{{/a}}{{/parent}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('c', $tpl->render($data));
+    }
+
+    public function testMultiLevelInheritanceNoSubChild()
+    {
+        $partials = array(
+            'parent' => '{{<older}}{{$a}}p{{/a}}{{/older}}',
+            'older' => '{{<grandParent}}{{$a}}o{{/a}}{{/grandParent}}',
+            'grandParent' => '{{$a}}g{{/a}}'
+        );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<parent}}{{/parent}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('p', $tpl->render($data));
+    }
+
+    public function testIgnoreTextInsideSuperTemplatesButParseArgs()
+    {
+        $partials = array(
+            'include' => '{{$foo}}default content{{/foo}}'
+         );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<include}} asdfasd {{$foo}}hmm{{/foo}} asdfasdfasdf {{/include}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('hmm', $tpl->render($data));
+    }
+
+    public function IgnoreTextInsideSuperTemplates()
+    {
+        $partials = array(
+            'include' => '{{$foo}}default content{{/foo}}'
+         );
+
+        $this->mustache->setPartials($partials);
+
+        $tpl = $this->mustache->loadTemplate(
+            '{{<include}} asdfasd asdfasdfasdf {{/include}}'
+        );
+
+        $data = array();
+
+        $this->assertEquals('default content', $tpl->render($data));
+    }
+}

+ 15 - 0
test/Mustache/Test/Functional/MustacheSpecTest.php

@@ -127,6 +127,21 @@ class Mustache_Test_Functional_MustacheSpecTest extends PHPUnit_Framework_TestCa
         return $this->loadSpec('sections');
     }
 
+    /**
+     * @group inheritance
+     * @dataProvider loadInheritanceSpec
+     */
+    public function testInheritanceSpec($desc, $source, $partials, $data, $expected)
+    {
+        $template = self::loadTemplate($source, $partials);
+        $this->assertEquals($expected, $template->render($data), $desc);
+    }
+
+    public function loadInheritanceSpec()
+    {
+        return $this->loadSpec('inheritance');
+    }
+
     /**
      * Data provider for the mustache spec test.
      *

+ 113 - 0
test/Mustache/Test/ParserTest.php

@@ -88,6 +88,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                         Mustache_Tokenizer::VALUE => 'bar'
                     ),
                 ),
+
                 array(
                     array(
                         Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_TEXT,
@@ -116,6 +117,118 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase
                 ),
             ),
 
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 8
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR,
+                        Mustache_Tokenizer::NAME => 'bar',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 16
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::VALUE => 'baz'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME => 'bar',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 19
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 27
+                    )
+                ),
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 8,
+                        Mustache_Tokenizer::END => 27,
+                        Mustache_Tokenizer::NODES => array(
+                            array(
+                                Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_ARG,
+                                Mustache_Tokenizer::NAME => 'bar',
+                                Mustache_Tokenizer::OTAG => '{{',
+                                Mustache_Tokenizer::CTAG => '}}',
+                                Mustache_Tokenizer::LINE => 0,
+                                Mustache_Tokenizer::INDEX => 16,
+                                Mustache_Tokenizer::END => 19,
+                                Mustache_Tokenizer::NODES => array(
+                                    array(
+                                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
+                                        Mustache_Tokenizer::LINE => 0,
+                                        Mustache_Tokenizer::VALUE => 'baz'
+                                    )
+                                )
+                            )
+                        )
+                    )
+                )
+            ),
+
+            array(
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::VALUE => 'bar'
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::INDEX => 11,
+                    ),
+                ),
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR,
+                        Mustache_Tokenizer::NAME => 'foo',
+                        Mustache_Tokenizer::OTAG => '{{',
+                        Mustache_Tokenizer::CTAG => '}}',
+                        Mustache_Tokenizer::LINE => 0,
+                        Mustache_Tokenizer::END => 11,
+                        Mustache_Tokenizer::NODES => array(
+                            array(
+                                Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_TEXT,
+                                Mustache_Tokenizer::LINE => 0,
+                                Mustache_Tokenizer::VALUE => 'bar'
+                            )
+                        )
+                    )
+                )
+            )
         );
     }
 

+ 29 - 0
test/Mustache/Test/TokenizerTest.php

@@ -188,6 +188,35 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase
                     ),
                 )
             ),
+
+            // Ensure that $arg token is not picked up during tokenization
+            array(
+                '{{$arg}}default{{/arg}}',
+                null,
+                array(
+                    array(
+                        Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR,
+                        Mustache_Tokenizer::NAME => 'arg',
+                        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 => "default",
+                    ),
+                    array(
+                        Mustache_Tokenizer::TYPE  => Mustache_Tokenizer::T_END_SECTION,
+                        Mustache_Tokenizer::NAME  => 'arg',
+                        Mustache_Tokenizer::OTAG  => '{{',
+                        Mustache_Tokenizer::CTAG  => '}}',
+                        Mustache_Tokenizer::LINE  => 0,
+                        Mustache_Tokenizer::INDEX => 15,
+                    )
+                )
+            ),
         );
     }
 }