diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 93a295a..2b0d1f9 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -117,6 +117,7 @@ class Mustache_Compiler case Mustache_Tokenizer::T_PARTIAL: $code .= $this->partial( $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false, isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', $level ); @@ -125,6 +126,7 @@ class Mustache_Compiler case Mustache_Tokenizer::T_PARENT: $code .= $this->parent( $node[Mustache_Tokenizer::NAME], + isset($node[Mustache_Tokenizer::DYNAMIC]) ? $node[Mustache_Tokenizer::DYNAMIC] : false, isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', $node[Mustache_Tokenizer::NODES], $level @@ -419,6 +421,30 @@ class Mustache_Compiler return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level)); } + const DYNAMIC_NAME = '$this->resolveValue($context->%s(%s), $context)'; + + /** + * Generate Mustache Template dynamic name resolution PHP source. + * + * @param string $id Tag name + * @param bool $dynamic True if the name is dynamic + * + * @return string Dynamic name resolution PHP source code + */ + private function resolveDynamicName($id, $dynamic) + { + if (!$dynamic) { + return var_export($id, true); + } + + $method = $this->getFindMethod($id); + $id = ($method !== 'last') ? var_export($id, true) : ''; + + // TODO: filters? + + return sprintf(self::DYNAMIC_NAME, $method, $id); + } + const PARTIAL_INDENT = ', $indent . %s'; const PARTIAL = ' if ($partial = $this->mustache->loadPartial(%s)) { @@ -429,13 +455,14 @@ class Mustache_Compiler /** * Generate Mustache Template partial call PHP source. * - * @param string $id Partial name - * @param string $indent Whitespace indent to apply to partial + * @param string $id Partial name + * @param bool $dynamic Partial name is dynamic + * @param string $indent Whitespace indent to apply to partial * @param int $level * * @return string Generated partial call PHP source code */ - private function partial($id, $indent, $level) + private function partial($id, $dynamic, $indent, $level) { if ($indent !== '') { $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true)); @@ -445,7 +472,7 @@ class Mustache_Compiler return sprintf( $this->prepare(self::PARTIAL, $level), - var_export($id, true), + $this->resolveDynamicName($id, $dynamic), $indentParam ); } @@ -469,23 +496,25 @@ class Mustache_Compiler * Generate Mustache Template inheritance parent call PHP source. * * @param string $id Parent tag name + * @param bool $dynamic Tag name is dynamic * @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) + private function parent($id, $dynamic, $indent, array $children, $level) { $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); + $partialName = $this->resolveDynamicName($id, $dynamic); if (empty($realChildren)) { - return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true)); + return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), $partialName); } return sprintf( $this->prepare(self::PARENT, $level), - var_export($id, true), + $partialName, $this->walk($realChildren, $level + 1) ); } diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index bbdc975..dc6d7db 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -26,15 +26,17 @@ class Mustache_Engine const VERSION = '2.14.2'; const SPEC_VERSION = '1.2.2'; - const PRAGMA_FILTERS = 'FILTERS'; - const PRAGMA_BLOCKS = 'BLOCKS'; - const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT'; + const PRAGMA_FILTERS = 'FILTERS'; + const PRAGMA_BLOCKS = 'BLOCKS'; + const PRAGMA_ANCHORED_DOT = 'ANCHORED-DOT'; + const PRAGMA_DYNAMIC_NAMES = 'DYNAMIC-NAMES'; // Known pragmas private static $knownPragmas = array( - self::PRAGMA_FILTERS => true, - self::PRAGMA_BLOCKS => true, - self::PRAGMA_ANCHORED_DOT => true, + self::PRAGMA_FILTERS => true, + self::PRAGMA_BLOCKS => true, + self::PRAGMA_ANCHORED_DOT => true, + self::PRAGMA_DYNAMIC_NAMES => true, ); // Template cache diff --git a/src/Mustache/Parser.php b/src/Mustache/Parser.php index 0ec4192..bf8bb40 100644 --- a/src/Mustache/Parser.php +++ b/src/Mustache/Parser.php @@ -23,6 +23,7 @@ class Mustache_Parser private $pragmaFilters; private $pragmaBlocks; + private $pragmaDynamicNames; /** * Process an array of Mustache tokens and convert them into a parse tree. @@ -37,8 +38,9 @@ class Mustache_Parser $this->lineTokens = 0; $this->pragmas = $this->defaultPragmas; - $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); - $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); + $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); + $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); + $this->pragmaDynamicNames = isset($this->pragmas[Mustache_Engine::PRAGMA_DYNAMIC_NAMES]); return $this->buildTree($tokens); } @@ -84,6 +86,14 @@ class Mustache_Parser $this->lineTokens = 0; } + if ($this->pragmaDynamicNames && isset($token[Mustache_Tokenizer::NAME])) { + list($name, $isDynamic) = $this->getDynamicName($token); + if ($isDynamic) { + $token[Mustache_Tokenizer::NAME] = $name; + $token[Mustache_Tokenizer::DYNAMIC] = true; + } + } + if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) { list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]); if (!empty($filters)) { @@ -280,6 +290,57 @@ class Mustache_Parser } } + /** + * Parse dynamic names. + * + * @throws Mustache_Exception_SyntaxException when a tag does not allow * + * @throws Mustache_Exception_SyntaxException on multiple *s, or dots or filters with * + */ + private function getDynamicName(array $token) + { + $name = $token[Mustache_Tokenizer::NAME]; + $isDynamic = false; + + if (preg_match('/^\s*\*\s*/', $name)) { + $this->ensureTagAllowsDynamicNames($token); + $name = preg_replace('/^\s*\*\s*/', '', $name); + $isDynamic = true; + } + + // Two stars is two many! + if (preg_match('/^\s*\*\s*/', $name) || preg_match('/[|.]\s*\*/', $name)) { + $msg = sprintf('Invalid dynamic name: %s', $name); + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + return array($name, $isDynamic); + } + + /** + * Check whether the given token supports dynamic tag names. + * + * @throws Mustache_Exception_SyntaxException when a tag does not allow * + * + * @param array $token + */ + private function ensureTagAllowsDynamicNames(array $token) + { + switch ($token[Mustache_Tokenizer::TYPE]) { + case Mustache_Tokenizer::T_PARTIAL: + case Mustache_Tokenizer::T_PARENT: + return; + } + + $msg = sprintf( + 'Invalid dynamic name: %s in %s tag', + $token[Mustache_Tokenizer::NAME], + $token[Mustache_Tokenizer::TYPE] + ); + + throw new Mustache_Exception_SyntaxException($msg, $token); + } + + /** * Split a tag name into name and filters. * @@ -312,6 +373,10 @@ class Mustache_Parser case Mustache_Engine::PRAGMA_FILTERS: $this->pragmaFilters = true; break; + + case Mustache_Engine::PRAGMA_DYNAMIC_NAMES: + $this->pragmaDynamicNames = true; + break; } } } diff --git a/src/Mustache/Tokenizer.php b/src/Mustache/Tokenizer.php index e59d30b..d97e36f 100644 --- a/src/Mustache/Tokenizer.php +++ b/src/Mustache/Tokenizer.php @@ -56,6 +56,7 @@ class Mustache_Tokenizer // Token properties const TYPE = 'type'; const NAME = 'name'; + const DYNAMIC = 'dynamic'; const OTAG = 'otag'; const CTAG = 'ctag'; const LINE = 'line'; diff --git a/test/Mustache/Test/Functional/MustacheDynamicNamesSpecTest.php b/test/Mustache/Test/Functional/MustacheDynamicNamesSpecTest.php new file mode 100644 index 0000000..e26c029 --- /dev/null +++ b/test/Mustache/Test/Functional/MustacheDynamicNamesSpecTest.php @@ -0,0 +1,52 @@ + array(Mustache_Engine::PRAGMA_DYNAMIC_NAMES), + )); + } + + /** + * For some reason data providers can't mark tests skipped, so this test exists + * simply to provide a 'skipped' test if the `spec` submodule isn't initialized. + */ + public function testSpecInitialized() + { + if (!file_exists(dirname(__FILE__) . '/../../../../vendor/spec/specs/')) { + $this->markTestSkipped('Mustache spec submodule not initialized: run "git submodule update --init"'); + } + } + + /** + * @group dynamic-names + * @dataProvider loadDynamicNamesSpec + */ + public function testDynamicNamesSpec($desc, $source, $partials, $data, $expected) + { + $template = self::loadTemplate($source, $partials); + $this->assertEquals($expected, $template->render($data), $desc); + } + + public function loadDynamicNamesSpec() + { + return $this->loadSpec('~dynamic-names'); + } +} diff --git a/vendor/spec b/vendor/spec index b2aeb3c..1704964 160000 --- a/vendor/spec +++ b/vendor/spec @@ -1 +1 @@ -Subproject commit b2aeb3c283de931a7004b5f7a2cb394b89382369 +Subproject commit 1704964eb6cd0c8b1a3da3de6222e0f0480284ec