diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index c6fa24d..649c5ec 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -16,6 +16,9 @@ */ class Mustache_Compiler { + + private $pragmas; + private $defaultPragmas = array(); private $sections; private $source; private $indentNextLine; @@ -23,7 +26,6 @@ class Mustache_Compiler private $entityFlags; private $charset; private $strictCallables; - private $pragmas; /** * Compile a Mustache token parse tree into PHP source code. @@ -40,7 +42,7 @@ class Mustache_Compiler */ public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT) { - $this->pragmas = array(); + $this->pragmas = $this->defaultPragmas; $this->sections = array(); $this->source = $source; $this->indentNextLine = true; @@ -52,6 +54,23 @@ class Mustache_Compiler return $this->writeCode($tree, $name); } + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param array $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->pragmas[$pragma] = true; + } + $this->defaultPragmas = $this->pragmas; + } + /** * Helper function for walking the Mustache token parse tree. * @@ -93,7 +112,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] : '', @@ -101,6 +119,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] : '', + $node[Mustache_Tokenizer::NODES], + $level + ); + break; + + case Mustache_Tokenizer::T_BLOCK_ARG: + $code .= $this->blockArg( + $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_BLOCK_VAR: + $code .= $this->blockVar( + $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); @@ -135,6 +186,7 @@ class Mustache_Compiler { $this->lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); $buffer = \'\'; + $newContext = array(); %s return $buffer; @@ -149,6 +201,7 @@ class Mustache_Compiler public function renderInternal(Mustache_Context $context, $indent = \'\') { $buffer = \'\'; + $newContext = array(); %s return $buffer; @@ -170,11 +223,75 @@ 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 BLOCK_VAR = ' + $value = $this->resolveValue($context->findInBlock(%s), $context, $indent); + if ($value && !is_array($value) && !is_object($value)) { + $buffer .= $value; + } else { + %s + } + '; + + /** + * Generate Mustache Template inheritance block variable PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) + { + $id_str = var_export($id, true); + + return sprintf($this->prepare(self::BLOCK_VAR, $level), $id_str, $this->walk($nodes, 2)); + } + + const BLOCK_ARG = ' + // %s block_arg + $value = $this->section%s($context, $indent, true); + $newContext[%s] = %s$value; + '; + + /** + * Generate Mustache Template inheritance block argument PHP source. + * + * @param array $nodes Array of child tokens + * @param string $id Section name + * @param int $start Section start offset + * @param int $end Section end offset + * @param string $otag Current Mustache opening tag + * @param string $ctag Current Mustache closing tag + * @param int $level + * + * @return string Generated PHP source code + */ + private function blockArg($nodes, $id, $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])) { + list($id, $filters) = $this->getFilters($id, $level); + } + + $method = $this->getFindMethod($id); + $id = var_export($id, true); + + return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key, $id, $this->flushIndent()); + } + const SECTION_CALL = ' // %s section $value = $context->%s(%s);%s @@ -198,7 +315,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(); } } @@ -216,10 +334,11 @@ class Mustache_Compiler * @param string $otag Current Mustache opening tag * @param string $ctag Current Mustache closing tag * @param int $level + * @param bool $arg (default: false) * * @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 = ''; @@ -227,8 +346,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(); @@ -238,13 +355,20 @@ class Mustache_Compiler $delims = ''; } - $key = ucfirst(md5($delims."\n".$source)); + $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 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 = ' @@ -301,6 +425,50 @@ class Mustache_Compiler ); } + const PARENT = ' + %s + + if ($parent = $this->mustache->LoadPartial(%s)) { + $context->pushBlockContext($newContext); + $buffer .= $parent->renderInternal($context, $indent); + $context->popBlockContext(); + } + '; + + /** + * Generate Mustache Template inheritance parent call PHP source. + * + * @param string $id Parent tag name + * @param string $indent Whitespace indent to apply to parent + * @param array $children Child nodes + * @param int $level + * + * @return string Generated PHP source code + */ + private function parent($id, $indent, array $children, $level) + { + $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); + + return sprintf( + $this->prepare(self::PARENT, $level), + $this->walk($realChildren, $level), + var_export($id, true), + var_export($indent, true) + ); + } + + /** + * Helper method for filtering out non-block-arg tokens. + * + * @param array $node + * + * @return boolean True if $node is a block arg token. + */ + private static function onlyBlockArgs(array $node) + { + return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG; + } + const VARIABLE = ' $value = $this->resolveValue($context->%s(%s), $context, $indent);%s $buffer .= %s%s; @@ -466,6 +634,13 @@ class Mustache_Compiler const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; + /** + * Helper function to compile strict vs lax "is callable" logic. + * + * @param string $variable (default: '$value') + * + * @return string "is callable" logic + */ private function getCallable($variable = '$value') { $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index fe25692..7d1dc90 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -14,7 +14,8 @@ */ class Mustache_Context { - private $stack = array(); + private $stack = array(); + private $blockStack = array(); /** * Mustache rendering Context constructor. @@ -38,6 +39,16 @@ class Mustache_Context array_push($this->stack, $value); } + /** + * Push a new Context frame onto the block context stack. + * + * @param mixed $value Object or array to use for block context + */ + public function pushBlockContext($value) + { + array_push($this->blockStack, $value); + } + /** * Pop the last Context frame from the stack. * @@ -48,6 +59,16 @@ class Mustache_Context return array_pop($this->stack); } + /** + * Pop the last block Context frame from the stack. + * + * @return mixed Last block Context frame (object or array) + */ + public function popBlockContext() + { + return array_pop($this->blockStack); + } + /** * Get the last Context frame. * @@ -120,6 +141,24 @@ class Mustache_Context return $value; } + /** + * Find an argument in the block context stack. + * + * @param string $id + * + * @return mixed Variable value, or '' if not found. + */ + public function findInBlock($id) + { + foreach ($this->blockStack as $context) { + if (array_key_exists($id, $context)) { + return $context[$id]; + } + } + + return ''; + } + /** * Helper function to find a variable in the Context stack. * diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 01a1a30..1556caa 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -27,6 +27,13 @@ class Mustache_Engine const SPEC_VERSION = '1.1.2'; const PRAGMA_FILTERS = 'FILTERS'; + const PRAGMA_BLOCKS = 'BLOCKS'; + + // Known pragmas + private static $knownPragmas = array( + self::PRAGMA_FILTERS => true, + self::PRAGMA_BLOCKS => true, + ); // Template cache private $templates = array(); @@ -44,6 +51,7 @@ class Mustache_Engine private $charset = 'UTF-8'; private $logger; private $strictCallables = false; + private $pragmas = array(); // Services private $tokenizer; @@ -110,6 +118,10 @@ class Mustache_Engine * // helps protect against arbitrary code execution when user input is passed directly into the template. * // This currently defaults to false, but will default to true in v3.0. * 'strict_callables' => true, + * + * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual + * // templates. + * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], * ); * * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable. @@ -176,6 +188,15 @@ class Mustache_Engine if (isset($options['strict_callables'])) { $this->strictCallables = $options['strict_callables']; } + + if (isset($options['pragmas'])) { + foreach ($options['pragmas'] as $pragma) { + if (!isset(self::$knownPragmas[$pragma])) { + throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma)); + } + $this->pragmas[$pragma] = true; + } + } } /** @@ -226,6 +247,16 @@ class Mustache_Engine return $this->charset; } + /** + * Get the current globally enabled pragmas. + * + * @return array + */ + public function getPragmas() + { + return array_keys($this->pragmas); + } + /** * Set the Mustache template Loader instance. * @@ -563,12 +594,13 @@ class Mustache_Engine public function getTemplateClassName($source) { return $this->templateClassPrefix . md5(sprintf( - 'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,source:%s', + 'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,pragmas:%s,source:%s', self::VERSION, isset($this->escape) ? 'custom' : 'default', $this->entityFlags, $this->charset, $this->strictCallables ? 'true' : 'false', + implode(' ', array_keys($this->pragmas)), $source )); } @@ -705,7 +737,10 @@ class Mustache_Engine */ private function parse($source) { - return $this->getParser()->parse($this->tokenize($source)); + $parser = $this->getParser(); + $parser->setPragmas($this->getPragmas()); + + return $parser->parse($this->tokenize($source)); } /** @@ -728,7 +763,10 @@ class Mustache_Engine array('className' => $name) ); - return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); + $compiler = $this->getCompiler(); + $compiler->setPragmas($this->getPragmas()); + + return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); } /** diff --git a/src/Mustache/Parser.php b/src/Mustache/Parser.php index 54f52fc..ce3a880 100644 --- a/src/Mustache/Parser.php +++ b/src/Mustache/Parser.php @@ -18,6 +18,8 @@ class Mustache_Parser { private $lineNum; private $lineTokens; + private $pragmas; + private $defaultPragmas = array(); /** * Process an array of Mustache tokens and convert them into a parse tree. @@ -30,10 +32,28 @@ class Mustache_Parser { $this->lineNum = -1; $this->lineTokens = 0; + $this->pragmas = $this->defaultPragmas; return $this->buildTree($tokens); } + /** + * Enable pragmas across all templates, regardless of the presence of pragma + * tags in the individual templates. + * + * @internal Users should set global pragmas in Mustache_Engine, not here :) + * + * @param array $pragmas + */ + public function setPragmas(array $pragmas) + { + $this->pragmas = array(); + foreach ($pragmas as $pragma) { + $this->pragmas[$pragma] = true; + } + $this->defaultPragmas = $this->pragmas; + } + /** * Helper method for recursively building a parse tree. * @@ -60,11 +80,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,15 +119,40 @@ class Mustache_Parser return $parent; case Mustache_Tokenizer::T_PARTIAL: - case Mustache_Tokenizer::T_PARTIAL_2: - // store the whitespace prefix for laters! + $this->checkIfTokenIsAllowedInParent($parent, $token); + //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: + $this->checkIfTokenIsAllowedInParent($parent, $token); + $nodes[] = $this->buildTree($tokens, $token); + break; + + case Mustache_Tokenizer::T_BLOCK_VAR: + if (isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS])) { + // BLOCKS pragma is enabled, let's do this! + if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; + } + $this->clearStandaloneLines($nodes, $tokens); + $nodes[] = $this->buildTree($tokens, $token); + } else { + // pretend this was just a normal "escaped" token... + $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED; + // TODO: figure out how to figure out if there was a space after this dollar: + $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME]; + $nodes[] = $token; + } + break; + case Mustache_Tokenizer::T_PRAGMA: + $this->pragmas[$token[Mustache_Tokenizer::NAME]] = true; + // no break + case Mustache_Tokenizer::T_COMMENT: $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $token; @@ -197,10 +244,25 @@ class Mustache_Parser */ private function tokenIsWhitespace(array $token) { - if ($token[Mustache_Tokenizer::TYPE] == Mustache_Tokenizer::T_TEXT) { + if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) { return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); } return false; } + + /** + * Check whether a token is allowed inside a parent tag. + * + * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag. + * + * @param array|null $parent + * @param array $token + */ + private function checkIfTokenIsAllowedInParent($parent, array $token) + { + if ($parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { + throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); + } + } } diff --git a/src/Mustache/Template.php b/src/Mustache/Template.php index 35d652c..4d1273d 100644 --- a/src/Mustache/Template.php +++ b/src/Mustache/Template.php @@ -63,7 +63,9 @@ abstract class Mustache_Template */ 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 9a9afab..eef1580 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -27,13 +27,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_BLOCK_VAR = '$'; + const T_BLOCK_ARG = '$arg'; // Valid token types private static $tagTypes = array( @@ -42,19 +44,20 @@ 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_BLOCK_VAR => true, ); // Interpolated tags private static $interpolatedTags = array( - self::T_ESCAPED => true, - self::T_UNESCAPED => true, - self::T_UNESCAPED_2 => true, + self::T_ESCAPED => true, + self::T_UNESCAPED => true, + self::T_UNESCAPED_2 => true, ); // Token properties diff --git a/test/Mustache/Test/EngineTest.php b/test/Mustache/Test/EngineTest.php index 6904d18..6aca877 100644 --- a/test/Mustache/Test/EngineTest.php +++ b/test/Mustache/Test/EngineTest.php @@ -36,6 +36,7 @@ class Mustache_Test_EngineTest extends Mustache_Test_FunctionalTestCase 'escape' => 'strtoupper', 'entity_flags' => ENT_QUOTES, 'charset' => 'ISO-8859-1', + 'pragmas' => array(Mustache_Engine::PRAGMA_FILTERS), )); $this->assertSame($logger, $mustache->getLogger()); @@ -50,6 +51,7 @@ class Mustache_Test_EngineTest extends Mustache_Test_FunctionalTestCase $this->assertTrue($mustache->hasHelper('bar')); $this->assertFalse($mustache->hasHelper('baz')); $this->assertInstanceOf('Mustache_Cache_FilesystemCache', $mustache->getCache()); + $this->assertEquals(array(Mustache_Engine::PRAGMA_FILTERS), $mustache->getPragmas()); } public static function getFoo() diff --git a/test/Mustache/Test/FiveThree/Functional/EngineTest.php b/test/Mustache/Test/FiveThree/Functional/EngineTest.php new file mode 100644 index 0000000..3c80b2b --- /dev/null +++ b/test/Mustache/Test/FiveThree/Functional/EngineTest.php @@ -0,0 +1,50 @@ + $pragmas, + 'helpers' => $helpers, + )); + + $this->assertEquals($expect, $mustache->render($tpl, $data)); + } + + public function pragmaData() + { + $helpers = array( + 'longdate' => function (\DateTime $value) { + return $value->format('Y-m-d h:m:s'); + } + ); + + $data = array( + 'date' => new DateTime('1/1/2000', new DateTimeZone('UTC')), + ); + + $tpl = '{{ date | longdate }}'; + + return array( + array(array(Mustache_Engine::PRAGMA_FILTERS), $helpers, $data, $tpl, '2000-01-01 12:01:00'), + array(array(), $helpers, $data, $tpl, '' ), + ); + } +} diff --git a/test/Mustache/Test/Functional/InheritanceTest.php b/test/Mustache/Test/Functional/InheritanceTest.php new file mode 100644 index 0000000..dd524a5 --- /dev/null +++ b/test/Mustache/Test/Functional/InheritanceTest.php @@ -0,0 +1,437 @@ +mustache = new Mustache_Engine(array( + 'pragmas' => array(Mustache_Engine::PRAGMA_BLOCKS), + )); + } + + 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( + ), + '{{ '{{$baz}}defualt content{{/baz}}' + ), + array(), + '{{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 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( + '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 testIgnoreTextInsideSuperTemplates() + { + $partials = array( + 'include' => '{{$foo}}default content{{/foo}}' + ); + + $this->mustache->setPartials($partials); + + $tpl = $this->mustache->loadTemplate( + '{{assertEquals('default content', $tpl->render($data)); + } + + /** + * @dataProvider getIllegalInheritanceExamples + * @expectedException Mustache_Exception_SyntaxException + * @expectedExceptionMessage Illegal content in < parent tag + */ + public function testIllegalInheritanceExamples($partials, $data, $template) + { + $this->mustache->setPartials($partials); + $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)); + } +} diff --git a/test/Mustache/Test/ParserTest.php b/test/Mustache/Test/ParserTest.php index d51b0a6..a739930 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,166 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase ), ), + // This *would* be an invalid inheritance parse tree, but that pragma + // isn't enabled so it'll thunk it back into an "escaped" token: + array( + array( + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_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( + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_ESCAPED, + 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' + ), + ), + ), + ); + } + + /** + * @dataProvider getInheritanceTokenSets + */ + public function testParseWithInheritance($tokens, $expected) + { + $parser = new Mustache_Parser; + $parser->setPragmas(array(Mustache_Engine::PRAGMA_BLOCKS)); + $this->assertEquals($expected, $parser->parse($tokens)); + } + + public function getInheritanceTokenSets() + { + return array( + 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_BLOCK_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_BLOCK_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_BLOCK_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_BLOCK_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' + ) + ) + ) + ) + ), ); } @@ -197,6 +358,33 @@ class Mustache_Test_ParserTest extends PHPUnit_Framework_TestCase ), ), ), + + // This *would* be a valid inheritance parse tree, but that pragma + // isn't enabled here so it's going to fail :) + array( + array( + array( + Mustache_Tokenizer::TYPE => Mustache_Tokenizer::T_BLOCK_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, + ), + ), + ), ); } } diff --git a/test/Mustache/Test/TokenizerTest.php b/test/Mustache/Test/TokenizerTest.php index 67df941..9180f91 100644 --- a/test/Mustache/Test/TokenizerTest.php +++ b/test/Mustache/Test/TokenizerTest.php @@ -273,6 +273,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_BLOCK_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, + ) + ) + ), ); } }