From db14a6c8633f1686a81c2c5dcc1890c656ec754e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 18:29:26 +0000 Subject: [PATCH 01/14] hogan.js style template inheritance --- src/Mustache/Compiler.php | 127 +++++++- src/Mustache/Context.php | 12 + src/Mustache/Parser.php | 16 +- src/Mustache/Tokenizer.php | 7 +- .../Test/Functional/InheritanceTest.php | 303 ++++++++++++++++++ .../Test/Functional/MustacheSpecTest.php | 15 + test/Mustache/Test/ParserTest.php | 113 +++++++ test/Mustache/Test/TokenizerTest.php | 29 ++ 8 files changed, 614 insertions(+), 8 deletions(-) create mode 100644 test/Mustache/Test/Functional/InheritanceTest.php diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index c13e60f..d7daa95 100644 --- a/src/Mustache/Compiler.php +++ b/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; diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index c6900d7..fbb22e0 100644 --- a/src/Mustache/Context.php +++ b/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. * diff --git a/src/Mustache/Parser.php b/src/Mustache/Parser.php index f44153c..83f71f1 100644 --- a/src/Mustache/Parser.php +++ b/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); diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index c701b25..1dac502 100644 --- a/src/Mustache/Tokenizer.php +++ b/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 diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php new file mode 100644 index 0000000..799246b --- /dev/null +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -0,0 +1,303 @@ +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' => '' + ); + + $this->assertEquals('default 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( + '{{assertEquals('default content', $tpl->render($data)); + } + + public function testOverriddenContent() + { + $partials = array( + 'super' => '...{{$title}}Default title{{/title}}...' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{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 {{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( + "{{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( + '{{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( + '{{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}}|{{assertEquals('default content|default content', $tpl->render($data)); + } + + public function testRecursionInInheritedTemplates() + { + $partials = array( + 'include' => '{{$foo}}default content{{/foo}} {{$bar}}{{ '{{$foo}}include2 default content{{/foo}} {{mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('override override override don\'t recurse', $tpl->render($data)); + } + + public function testTopLevelSubstitutionsTakePrecedenceInMultilevelInheritance() + { + $partials = array( + 'parent' => '{{ '{{ '{{$a}}g{{/a}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('c', $tpl->render($data)); + } + + public function testMultiLevelInheritanceNoSubChild() + { + $partials = array( + 'parent' => '{{ '{{ '{{$a}}g{{/a}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('p', $tpl->render($data)); + } + + public function testIgnoreTextInsideSuperTemplatesButParseArgs() + { + $partials = array( + 'include' => '{{$foo}}default content{{/foo}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('hmm', $tpl->render($data)); + } + + public function IgnoreTextInsideSuperTemplates() + { + $partials = array( + 'include' => '{{$foo}}default content{{/foo}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('default content', $tpl->render($data)); + } +} diff --git a/test/Mustache/Test/Functional/MustacheSpecTest.php b/test/Mustache/Test/Functional/MustacheSpecTest.php index 312a4c9..c128278 100644 --- a/test/Mustache/Test/Functional/MustacheSpecTest.php +++ b/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. * diff --git a/test/Mustache/Test/ParserTest.php b/test/Mustache/Test/ParserTest.php index 57e418a..b780dca 100644 --- a/test/Mustache/Test/ParserTest.php +++ b/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' + ) + ) + ) + ) + ) ); } diff --git a/test/Mustache/Test/TokenizerTest.php b/test/Mustache/Test/TokenizerTest.php index 63a94d4..dab379d 100644 --- a/test/Mustache/Test/TokenizerTest.php +++ b/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, + ) + ) + ), ); } } From c6c67ce609903765e0a4807b4cd556b6f895f75e Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 19:53:06 +0000 Subject: [PATCH 02/14] referencing nodes by class constant rather than string literal --- src/Mustache/Compiler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index d7daa95..da634d3 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -106,7 +106,7 @@ class Mustache_Compiler $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', $level, - $node['nodes'] + $node[Mustache_Tokenizer::NODES] ); break; From f47a09538e4deb70da9a58bb48de2f2da0efe836 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 20:07:50 +0000 Subject: [PATCH 03/14] php 5.2.x doesn't support closures, so added a private static funtion to be used from array_filter instead --- src/Mustache/Compiler.php | 9 ++++++--- vendor/spec | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index da634d3..f7091cc 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -410,9 +410,7 @@ class Mustache_Compiler { $block = ''; - $real_children = array_filter($children, function($ch) { - return $ch[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT_ARG; - }); + $real_children = array_filter($children, array(__CLASS__, 'return_only_parent_args')); $block = $this->walk($real_children, $level); @@ -423,6 +421,11 @@ class Mustache_Compiler ); } + private static function return_only_parent_args($child) + { + return $child[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT_ARG; + } + const VARIABLE = ' $value = $this->resolveValue($context->%s(%s), $context, $indent);%s $buffer .= %s%s; diff --git a/vendor/spec b/vendor/spec index bf6288e..7c3923d 160000 --- a/vendor/spec +++ b/vendor/spec @@ -1 +1 @@ -Subproject commit bf6288ed6bd0ce8ccea6f1dac070b3d779132c3b +Subproject commit 7c3923d0079a345136325f7d1895f5d98efa9701 From 31800de88f7b240a013cb98396a371db2ef935ed Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 20:42:56 +0000 Subject: [PATCH 04/14] removing unnecessary local variable in findFromParent function --- src/Mustache/Context.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index fbb22e0..58461e5 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -122,8 +122,7 @@ class Mustache_Context public function findFromParent($id) { - $stack = $this->stack; - foreach($stack as $context) { + foreach($this->stack as $context) { if (is_array($context) && array_key_exists($id, $context)) { return $context[$id]; } From a631de174ca807281b3d00f0448160b34b8f38ad Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 20:43:47 +0000 Subject: [PATCH 05/14] s/new_context/newContext/g --- src/Mustache/Compiler.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index f7091cc..a5ff392 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -168,7 +168,7 @@ class Mustache_Compiler { $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); $buffer = \'\'; - $new_context = array(); + $newContext = array(); %s return $buffer; @@ -183,7 +183,7 @@ class Mustache_Compiler public function renderInternal(Mustache_Context $context, $indent = \'\') { $buffer = \'\'; - $new_context = array(); + $newContext = array(); %s return $buffer; @@ -252,7 +252,7 @@ class Mustache_Compiler const PARENT_ARG = ' // %s parent_arg $value = $this->section%s($context, $indent, true); - $new_context[%s] = %s$value; + $newContext[%s] = %s$value; '; private function parentArg($nodes, $id, $start, $end, $otag, $ctag, $level) @@ -400,7 +400,7 @@ class Mustache_Compiler const PARENT = ' if ($parent = $this->mustache->LoadPartial(%s)) { - $context->push($new_context); + $context->push($newContext); $buffer .= $parent->renderInternal($context, $indent); $context->pop(); } From ebb7fda2e56bfba4d238c7f0f892bd02d1e7b7fc Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 20:45:21 +0000 Subject: [PATCH 06/14] removing deprecated code for loading non-existent inheritance tests from the mustache spec --- .../Mustache/Test/Functional/MustacheSpecTest.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/test/Mustache/Test/Functional/MustacheSpecTest.php b/test/Mustache/Test/Functional/MustacheSpecTest.php index c128278..312a4c9 100644 --- a/test/Mustache/Test/Functional/MustacheSpecTest.php +++ b/test/Mustache/Test/Functional/MustacheSpecTest.php @@ -127,21 +127,6 @@ 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. * From 3c57d5f2205e3148f7253b4cc9b750d67d8fded2 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 4 Apr 2014 20:57:07 +0000 Subject: [PATCH 07/14] reverting vendor spec change --- vendor/spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/spec b/vendor/spec index 7c3923d..bf6288e 160000 --- a/vendor/spec +++ b/vendor/spec @@ -1 +1 @@ -Subproject commit 7c3923d0079a345136325f7d1895f5d98efa9701 +Subproject commit bf6288ed6bd0ce8ccea6f1dac070b3d779132c3b From 63b835bd0963d65f00e958f37739b361ae669f82 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 7 Apr 2014 18:11:43 +0000 Subject: [PATCH 08/14] DRYing parentArgSection into the section method --- src/Mustache/Compiler.php | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index a5ff392..f0b6b71 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -229,26 +229,6 @@ class Mustache_Compiler 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); @@ -257,7 +237,7 @@ class Mustache_Compiler private function parentArg($nodes, $id, $start, $end, $otag, $ctag, $level) { - $key = $this->parentArgSection($nodes, $start, $end, $otag, $ctag, $level); + $key = $this->section($nodes, $id, $start, $end, $otag, $ctag, $level, true); $filters = ''; if (isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS])) { @@ -315,7 +295,7 @@ class Mustache_Compiler * * @return string Generated section PHP source code */ - private function section($nodes, $id, $start, $end, $otag, $ctag, $level) + private function section($nodes, $id, $start, $end, $otag, $ctag, $level, $arg=false) { $filters = ''; @@ -323,8 +303,6 @@ class Mustache_Compiler list($id, $filters) = $this->getFilters($id, $level); } - $method = $this->getFindMethod($id); - $id = var_export($id, true); $source = var_export(substr($this->source, $start, $end - $start), true); $callable = $this->getCallable(); @@ -340,7 +318,13 @@ class Mustache_Compiler $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $delims, $this->walk($nodes, 2)); } - return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key); + if ($arg === true) { + return $key; + } else { + $method = $this->getFindMethod($id); + $id = var_export($id, true); + return sprintf($this->prepare(self::SECTION_CALL, $level), $id, $method, $id, $filters, $key); + } } const INVERTED_SECTION = ' From af8a62c5e22cf126801e839ad578998ef1967271 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 7 Apr 2014 20:05:33 +0000 Subject: [PATCH 09/14] Separate stacks are necessary for blocks to be resolved independently of variables. Renamed parent_* to block_*. --- src/Mustache/Compiler.php | 35 +++++++++-------- src/Mustache/Context.php | 15 +++++++- src/Mustache/Parser.php | 4 +- src/Mustache/Template.php | 5 ++- src/Mustache/Tokenizer.php | 6 +-- .../Test/Functional/InheritanceTest.php | 38 +++++++++++++++++++ test/Mustache/Test/ParserTest.php | 8 ++-- test/Mustache/Test/TokenizerTest.php | 2 +- 8 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index f0b6b71..9c32fa0 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -110,8 +110,8 @@ class Mustache_Compiler ); break; - case Mustache_Tokenizer::T_PARENT_ARG: - $code .= $this->parentArg( + case Mustache_Tokenizer::T_BLOCK_ARG: + $code .= $this->blockArg( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], $node[Mustache_Tokenizer::INDEX], @@ -122,8 +122,8 @@ class Mustache_Compiler ); break; - case Mustache_Tokenizer::T_PARENT_VAR: - $code .= $this->parentVar( + case Mustache_Tokenizer::T_BLOCK_VAR: + $code .= $this->blockVar( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], $node[Mustache_Tokenizer::INDEX], @@ -154,7 +154,6 @@ class Mustache_Compiler throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node); } } - return $code; } @@ -211,7 +210,7 @@ class Mustache_Compiler return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections); } - const PARENT_VAR = ' + const BLOCK_VAR = ' $value = $this->resolveValue($context->%s(%s), $context, $indent); if($value && !is_array($value) && !is_object($value)) { $buffer .= %s; @@ -220,22 +219,22 @@ class Mustache_Compiler } '; - private function parentVar($nodes, $id, $start, $end, $otag, $ctag, $level) + private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) { - $method = 'findFromParent'; + $method = 'findInBlock'; $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)); + return sprintf($this->prepare(self::BLOCK_VAR, $level), $method, $id_str, $value, $this->walk($nodes, 2)); } - const PARENT_ARG = ' - // %s parent_arg + const BLOCK_ARG = ' + // %s block_arg $value = $this->section%s($context, $indent, true); $newContext[%s] = %s$value; '; - private function parentArg($nodes, $id, $start, $end, $otag, $ctag, $level) + private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level) { $key = $this->section($nodes, $id, $start, $end, $otag, $ctag, $level, true); $filters = ''; @@ -247,7 +246,7 @@ class Mustache_Compiler $method = $this->getFindMethod($id); $id = var_export($id, true); - return sprintf($this->prepare(self::PARENT_ARG, $level), $key, $key, $id, $this->flushIndent()); + return sprintf($this->prepare(self::BLOCK_ARG, $level), $key, $key, $id, $this->flushIndent()); } const SECTION_CALL = ' @@ -384,9 +383,9 @@ class Mustache_Compiler const PARENT = ' if ($parent = $this->mustache->LoadPartial(%s)) { - $context->push($newContext); + $context->pushBlockContext($newContext); $buffer .= $parent->renderInternal($context, $indent); - $context->pop(); + $context->popBlockContext(); } '; @@ -394,7 +393,7 @@ class Mustache_Compiler { $block = ''; - $real_children = array_filter($children, array(__CLASS__, 'return_only_parent_args')); + $real_children = array_filter($children, array(__CLASS__, 'return_only_block_args')); $block = $this->walk($real_children, $level); @@ -405,9 +404,9 @@ class Mustache_Compiler ); } - private static function return_only_parent_args($child) + private static function return_only_block_args($child) { - return $child[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT_ARG; + return $child[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_BLOCK_ARG; } const VARIABLE = ' diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index 58461e5..ca82c35 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -15,6 +15,7 @@ class Mustache_Context { private $stack = array(); + private $block_stack = array(); /** * Mustache rendering Context constructor. @@ -38,6 +39,11 @@ class Mustache_Context array_push($this->stack, $value); } + public function pushBlockContext($value) + { + array_push($this->block_stack, $value); + } + /** * Pop the last Context frame from the stack. * @@ -48,6 +54,11 @@ class Mustache_Context return array_pop($this->stack); } + public function popBlockContext() + { + return array_pop($this->block_stack); + } + /** * Get the last Context frame. * @@ -120,9 +131,9 @@ class Mustache_Context return $value; } - public function findFromParent($id) + public function findInBlock($id) { - foreach($this->stack as $context) { + foreach($this->block_stack as $context) { if (is_array($context) && array_key_exists($id, $context)) { return $context[$id]; } diff --git a/src/Mustache/Parser.php b/src/Mustache/Parser.php index 83f71f1..448e229 100644 --- a/src/Mustache/Parser.php +++ b/src/Mustache/Parser.php @@ -108,9 +108,9 @@ class Mustache_Parser $nodes[] = $this->buildTree($tokens, $token); break; - case Mustache_Tokenizer::T_PARENT_VAR: + case Mustache_Tokenizer::T_BLOCK_VAR: if ($parent[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT) { - $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_PARENT_ARG; + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; } $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $this->buildTree($tokens, $token); diff --git a/src/Mustache/Template.php b/src/Mustache/Template.php index 8c76916..07f49ab 100644 --- a/src/Mustache/Template.php +++ b/src/Mustache/Template.php @@ -61,10 +61,13 @@ abstract class Mustache_Template * @param mixed $context Array or object rendering context (default: array()) * * @return string Rendered template + * TODO construct a parent context stack here and pass it in */ public function render($context = array()) { - return $this->renderInternal($this->prepareContextStack($context)); + return $this->renderInternal( + $this->prepareContextStack($context) + ); } /** diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index 1dac502..c851434 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -35,8 +35,8 @@ class Mustache_Tokenizer const T_UNESCAPED_2 = '&'; const T_TEXT = '_t'; const T_PRAGMA = '%'; - const T_PARENT_VAR = '$'; - const T_PARENT_ARG = '$arg'; + const T_BLOCK_VAR = '$'; + const T_BLOCK_ARG = '$arg'; // Valid token types private static $tagTypes = array( @@ -51,7 +51,7 @@ class Mustache_Tokenizer self::T_UNESCAPED => true, self::T_UNESCAPED_2 => true, self::T_PRAGMA => true, - self::T_PARENT_VAR => true, + self::T_BLOCK_VAR => true, ); // Interpolated tags diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php index 799246b..d1e01af 100644 --- a/test/Mustache/Test/Functional/InheritanceTest.php +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -137,6 +137,44 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas $this->assertEquals('test |override1 default| |override2 default|', $tpl->render($data)); } + public function testDataDoesNotOverrideBlock() + { + $partials = array( + 'include' => '{{$var}}var in include{{/var}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{ 'var in data' + ); + + $this->assertEquals('var in template', $tpl->render($data)); + } + + public function testDataDoesNotOverrideDefaultBlockValue() + { + $partials = array( + 'include' => '{{$var}}var in include{{/var}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{ 'var in data' + ); + + $this->assertEquals('var in include', $tpl->render($data)); + } + public function testOverridePartialWithNewlines() { $partials = array( diff --git a/test/Mustache/Test/ParserTest.php b/test/Mustache/Test/ParserTest.php index b780dca..b54e427 100644 --- a/test/Mustache/Test/ParserTest.php +++ b/test/Mustache/Test/ParserTest.php @@ -128,7 +128,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase Mustache_Tokenizer::INDEX => 8 ), array( - Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR, + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR, Mustache_Tokenizer::NAME => 'bar', Mustache_Tokenizer::OTAG => '{{', Mustache_Tokenizer::CTAG => '}}', @@ -168,7 +168,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase Mustache_Tokenizer::END => 27, Mustache_Tokenizer::NODES => array( array( - Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_ARG, + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_ARG, Mustache_Tokenizer::NAME => 'bar', Mustache_Tokenizer::OTAG => '{{', Mustache_Tokenizer::CTAG => '}}', @@ -191,7 +191,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase array( array( array( - Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR, + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR, Mustache_Tokenizer::NAME => 'foo', Mustache_Tokenizer::OTAG => '{{', Mustache_Tokenizer::CTAG => '}}', @@ -213,7 +213,7 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase ), array( array( - Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR, + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR, Mustache_Tokenizer::NAME => 'foo', Mustache_Tokenizer::OTAG => '{{', Mustache_Tokenizer::CTAG => '}}', diff --git a/test/Mustache/Test/TokenizerTest.php b/test/Mustache/Test/TokenizerTest.php index dab379d..629e543 100644 --- a/test/Mustache/Test/TokenizerTest.php +++ b/test/Mustache/Test/TokenizerTest.php @@ -195,7 +195,7 @@ class Mustache_Test_TokenizerTest extends PHPUnit_Framework_TestCase null, array( array( - Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_PARENT_VAR, + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_VAR, Mustache_Tokenizer::NAME => 'arg', Mustache_Tokenizer::OTAG => '{{', Mustache_Tokenizer::CTAG => '}}', From d27840f8efa25248bd0c0eb3be5578686c00bc2a Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Wed, 9 Apr 2014 15:09:01 +0000 Subject: [PATCH 10/14] Throw exception when something other than text or block tags are encountered inside of a parent tag. Other small cleanups --- src/Mustache/Compiler.php | 2 +- src/Mustache/Context.php | 2 +- src/Mustache/Parser.php | 13 +++++++++- .../Test/Functional/InheritanceTest.php | 25 ++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 9c32fa0..dcc9469 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -246,7 +246,7 @@ class Mustache_Compiler $method = $this->getFindMethod($id); $id = var_export($id, true); - return sprintf($this->prepare(self::BLOCK_ARG, $level), $key, $key, $id, $this->flushIndent()); + return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent()); } const SECTION_CALL = ' diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index ca82c35..96089ce 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -134,7 +134,7 @@ class Mustache_Context public function findInBlock($id) { foreach($this->block_stack as $context) { - if (is_array($context) && array_key_exists($id, $context)) { + if (array_key_exists($id, $context)) { return $context[$id]; } } diff --git a/src/Mustache/Parser.php b/src/Mustache/Parser.php index 448e229..d331db2 100644 --- a/src/Mustache/Parser.php +++ b/src/Mustache/Parser.php @@ -60,11 +60,13 @@ class Mustache_Parser switch ($token[Mustache_Tokenizer::TYPE]) { case Mustache_Tokenizer::T_DELIM_CHANGE: + $this->checkIfTokenIsAllowedInParent($parent, $token); $this->clearStandaloneLines($nodes, $tokens); break; case Mustache_Tokenizer::T_SECTION: case Mustache_Tokenizer::T_INVERTED: + $this->checkIfTokenIsAllowedInParent($parent, $token); $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $this->buildTree($tokens, $token); break; @@ -97,6 +99,7 @@ class Mustache_Parser return $parent; case Mustache_Tokenizer::T_PARTIAL: + $this->checkIfTokenIsAllowedInParent($parent, $token); //store the whitespace prefix for laters! if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; @@ -105,11 +108,12 @@ class Mustache_Parser break; case Mustache_Tokenizer::T_PARENT: + $this->checkIfTokenIsAllowedInParent($parent, $token); $nodes[] = $this->buildTree($tokens, $token); break; case Mustache_Tokenizer::T_BLOCK_VAR: - if ($parent[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_PARENT) { + if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; } $this->clearStandaloneLines($nodes, $tokens); @@ -215,4 +219,11 @@ class Mustache_Parser return false; } + + private function checkIfTokenIsAllowedInParent($parent, $token) + { + if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); + } + } } diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php index d1e01af..bbd4969 100644 --- a/test/Mustache/Test/Functional/InheritanceTest.php +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -322,7 +322,7 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas $this->assertEquals('hmm', $tpl->render($data)); } - public function IgnoreTextInsideSuperTemplates() + public function testIgnoreTextInsideSuperTemplates() { $partials = array( 'include' => '{{$foo}}default content{{/foo}}' @@ -338,4 +338,27 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas $this->assertEquals('default content', $tpl->render($data)); } + + /** + * @expectedException Mustache_Exception_SyntaxException + * @expectedExceptionMessage Illegal content in < parent tag + */ + public function testOnlyBlockTagsAllowedInParent() + { + $partials = array( + 'foo' => '{{$baz}}default content{{/baz}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{< foo }}{{# bar }}{{$ baz }}{{/ baz }}{{/ bar }}{{/ foo }}' + ); + + $data = array( + 'bar' => 'set by user' + ); + + $tpl->render($data); + } } From efae42978dbce89b2989800649ba444982c4f621 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Fri, 11 Apr 2014 16:46:27 +0000 Subject: [PATCH 11/14] Adding data provider to test legal and illegal syntax inside of a parent tag --- .../Test/Functional/InheritanceTest.php | 94 ++++++++++++++++--- 1 file changed, 80 insertions(+), 14 deletions(-) diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php index bbd4969..2c57936 100644 --- a/test/Mustache/Test/Functional/InheritanceTest.php +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -14,6 +14,71 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas $this->mustache = new Mustache_Engine; } + public function getIllegalInheritanceExamples() + { + return array( + array( + array( + 'foo' => '{{$baz}}default content{{/baz}}', + ), + array( + 'bar' => 'set by user' + ), + '{{< foo }}{{# bar }}{{$ baz }}{{/ baz }}{{/ bar }}{{/ foo }}', + ), + array( + array( + 'foo' => '{{$baz}}default content{{/baz}}' + ), + array( + ), + '{{ '{{$baz}}default content{{/baz}}', + 'qux' => 'I am a partial' + ), + array( + ), + '{{qux}}{{$baz}}set by template{{/baz}}{{/foo}}' + ), + array( + array( + 'foo' => '{{$baz}}default content{{/baz}}' + ), + array( + ), + '{{=}}<%={{ }}=%>{{/foo}}' + ) + ); + } + + public function getLegalInheritanceExamples() + { + return array( + array( + array( + 'foo' => '{{$baz}}default content{{/baz}}', + ), + array( + 'bar' => 'set by user' + ), + '{{ '{{$baz}}default content{{/baz}}' + ), + array( + ), + '{{mustache->loadTemplate('{{$title}}Default title{{/title}}'); @@ -339,26 +404,27 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas $this->assertEquals('default content', $tpl->render($data)); } + + /** + * @dataProvider getIllegalInheritanceExamples * @expectedException Mustache_Exception_SyntaxException * @expectedExceptionMessage Illegal content in < parent tag */ - public function testOnlyBlockTagsAllowedInParent() + public function testIllegalInheritanceExamples($partials, $data, $template) { - $partials = array( - 'foo' => '{{$baz}}default content{{/baz}}' - ); - $this->mustache->setPartials($partials); - - $tpl = $this->mustache->loadTemplate( - '{{< foo }}{{# bar }}{{$ baz }}{{/ baz }}{{/ bar }}{{/ foo }}' - ); - - $data = array( - 'bar' => 'set by user' - ); - + $tpl = $this->mustache->loadTemplate($template); $tpl->render($data); } + + /** + * @dataProvider getLegalInheritanceExamples + */ + public function testLegalInheritanceExamples($partials, $data, $template, $expect) + { + $this->mustache->setPartials($partials); + $tpl = $this->mustache->loadTemplate($template); + $this->assertSame($expect, $tpl->render($data)); + } } From 56713e560f0ec1f90c75c23ccd879f89a90f77f0 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Mon, 14 Apr 2014 17:20:14 +0000 Subject: [PATCH 12/14] ensure that text is ignored outside of a block inside of a parent tag --- test/Mustache/Test/Functional/InheritanceTest.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php index 2c57936..1f5dd97 100644 --- a/test/Mustache/Test/Functional/InheritanceTest.php +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -47,8 +47,7 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas array( 'foo' => '{{$baz}}default content{{/baz}}' ), - array( - ), + array(), '{{=}}<%={{ }}=%>{{/foo}}' ) ); @@ -75,6 +74,14 @@ class Mustache_Test_Functional_InheritanceTest extends PHPUnit_Framework_TestCas ), '{{ '{{$baz}}defualt content{{/baz}}' + ), + array(), + '{{ Date: Mon, 14 Apr 2014 19:51:42 +0000 Subject: [PATCH 13/14] Don\'t escape html from blocks, removing unneeded variable assigns --- src/Mustache/Compiler.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index dcc9469..db17d90 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -211,9 +211,9 @@ class Mustache_Compiler } const BLOCK_VAR = ' - $value = $this->resolveValue($context->%s(%s), $context, $indent); + $value = $this->resolveValue($context->findInBlock(%s), $context, $indent); if($value && !is_array($value) && !is_object($value)) { - $buffer .= %s; + $buffer .= $value; } else { %s } @@ -221,11 +221,9 @@ class Mustache_Compiler private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) { - $method = 'findInBlock'; $id_str = var_export($id, true); - $value = $this->getEscape(); - return sprintf($this->prepare(self::BLOCK_VAR, $level), $method, $id_str, $value, $this->walk($nodes, 2)); + return sprintf($this->prepare(self::BLOCK_VAR, $level), $id_str, $this->walk($nodes, 2)); } const BLOCK_ARG = ' From 9cb926a607e4350e42c58e8fb7345a67b36cd713 Mon Sep 17 00:00:00 2001 From: Dan Miller Date: Tue, 15 Apr 2014 14:35:08 +0000 Subject: [PATCH 14/14] removing old todo --- src/Mustache/Template.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Mustache/Template.php b/src/Mustache/Template.php index 07f49ab..b64cd26 100644 --- a/src/Mustache/Template.php +++ b/src/Mustache/Template.php @@ -61,7 +61,6 @@ abstract class Mustache_Template * @param mixed $context Array or object rendering context (default: array()) * * @return string Rendered template - * TODO construct a parent context stack here and pass it in */ public function render($context = array()) {