Переглянути джерело

Merge branch 'dynamic-names'

Justin Hileman 3 роки тому
батько
коміт
b03b889490

+ 36 - 7
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)
         );
     }

+ 10 - 8
src/Mustache/Engine.php

@@ -23,18 +23,20 @@
  */
 class Mustache_Engine
 {
-    const VERSION        = '2.14.2';
-    const SPEC_VERSION   = '1.2.2';
+    const VERSION      = '2.14.2';
+    const SPEC_VERSION = '1.3.0';
 
-    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

+ 74 - 8
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,11 +86,21 @@ class Mustache_Parser
                 $this->lineTokens = 0;
             }
 
-            if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) {
-                list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
-                if (!empty($filters)) {
-                    $token[Mustache_Tokenizer::NAME]    = $name;
-                    $token[Mustache_Tokenizer::FILTERS] = $filters;
+            if ($token[Mustache_Tokenizer::TYPE] !== Mustache_Tokenizer::T_COMMENT) {
+                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)) {
+                        $token[Mustache_Tokenizer::NAME]    = $name;
+                        $token[Mustache_Tokenizer::FILTERS] = $filters;
+                    }
                 }
             }
 
@@ -115,7 +127,11 @@ class Mustache_Parser
                         throw new Mustache_Exception_SyntaxException($msg, $token);
                     }
 
-                    if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) {
+                    $sameName = $token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME];
+                    $tokenDynamic = isset($token[Mustache_Tokenizer::DYNAMIC]) && $token[Mustache_Tokenizer::DYNAMIC];
+                    $parentDynamic = isset($parent[Mustache_Tokenizer::DYNAMIC]) && $parent[Mustache_Tokenizer::DYNAMIC];
+
+                    if ($sameName || ($tokenDynamic !== $parentDynamic)) {
                         $msg = sprintf(
                             'Nesting error: %s (on line %d) vs. %s (on line %d)',
                             $parent[Mustache_Tokenizer::NAME],
@@ -280,6 +296,52 @@ 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;
+        }
+
+        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:
+            case Mustache_Tokenizer::T_END_SECTION:
+                return;
+        }
+
+        $msg = sprintf(
+            'Invalid dynamic name: %s in %s tag',
+            $token[Mustache_Tokenizer::NAME],
+            Mustache_Tokenizer::getTagName($token[Mustache_Tokenizer::TYPE])
+        );
+
+        throw new Mustache_Exception_SyntaxException($msg, $token);
+    }
+
+
     /**
      * Split a tag name into name and filters.
      *
@@ -312,6 +374,10 @@ class Mustache_Parser
             case Mustache_Engine::PRAGMA_FILTERS:
                 $this->pragmaFilters = true;
                 break;
+
+            case Mustache_Engine::PRAGMA_DYNAMIC_NAMES:
+                $this->pragmaDynamicNames = true;
+                break;
         }
     }
 }

+ 30 - 0
src/Mustache/Tokenizer.php

@@ -53,9 +53,26 @@ class Mustache_Tokenizer
         self::T_BLOCK_VAR    => true,
     );
 
+    private static $tagNames = array(
+        self::T_SECTION      => 'section',
+        self::T_INVERTED     => 'inverted section',
+        self::T_END_SECTION  => 'section end',
+        self::T_COMMENT      => 'comment',
+        self::T_PARTIAL      => 'partial',
+        self::T_PARENT       => 'parent',
+        self::T_DELIM_CHANGE => 'set delimiter',
+        self::T_ESCAPED      => 'variable',
+        self::T_UNESCAPED    => 'unescaped variable',
+        self::T_UNESCAPED_2  => 'unescaped variable',
+        self::T_PRAGMA       => 'pragma',
+        self::T_BLOCK_VAR    => 'block variable',
+        self::T_BLOCK_ARG    => 'block variable',
+    );
+
     // Token properties
     const TYPE    = 'type';
     const NAME    = 'name';
+    const DYNAMIC = 'dynamic';
     const OTAG    = 'otag';
     const CTAG    = 'ctag';
     const LINE    = 'line';
@@ -357,6 +374,7 @@ class Mustache_Tokenizer
         return $end + $this->ctagLen - 1;
     }
 
+
     private function throwUnclosedTagException()
     {
         $name = trim($this->buffer);
@@ -375,4 +393,16 @@ class Mustache_Tokenizer
             self::INDEX => $this->seenTag - $this->otagLen,
         ));
     }
+
+    /**
+     * Get the human readable name for a tag type.
+     *
+     * @param string $tagType One of the tokenizer T_* constants
+     *
+     * @return string
+     */
+    static function getTagName($tagType)
+    {
+        return isset(self::$tagNames[$tagType]) ? self::$tagNames[$tagType] : 'unknown';
+    }
 }

+ 86 - 0
test/Mustache/Test/Functional/DynamicPartialsTest.php

@@ -0,0 +1,86 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * @group dynamic-names
+ * @group functional
+ */
+class Mustache_Test_Functional_DynamicPartialsTest extends PHPUnit_Framework_TestCase
+{
+    private $mustache;
+
+    public function setUp()
+    {
+        $this->mustache = new Mustache_Engine(array(
+            'pragmas' => array(Mustache_Engine::PRAGMA_DYNAMIC_NAMES),
+        ));
+    }
+
+    public function getValidDynamicNamesExamples()
+    {
+      // technically not all dynamic names, but also not invalid
+        return array(
+            array('{{>* foo }}'),
+            array('{{>* foo.bar.baz }}'),
+            array('{{=* *=}}'),
+            array('{{! *foo }}'),
+            array('{{! foo.*bar }}'),
+            array('{{% FILTERS }}{{! foo | *bar }}'),
+            array('{{% BLOCKS }}{{< *foo }}{{/ *foo }}'),
+        );
+    }
+
+    /**
+     * @dataProvider getValidDynamicNamesExamples
+     */
+    public function testLegalInheritanceExamples($template)
+    {
+        $this->assertSame('', $this->mustache->render($template));
+    }
+
+    public function getDynamicNameParseErrors()
+    {
+        return array(
+            array('{{# foo }}{{/ *foo }}'),
+            array('{{^ foo }}{{/ *foo }}'),
+            array('{{% BLOCKS }}{{< foo }}{{/ *foo }}'),
+            array('{{% BLOCKS }}{{$ foo }}{{/ *foo }}'),
+        );
+    }
+
+    /**
+     * @dataProvider getDynamicNameParseErrors
+     * @expectedException Mustache_Exception_SyntaxException
+     * @expectedExceptionMessage Nesting error:
+     */
+    public function testDynamicNameParseErrors($template)
+    {
+        $this->mustache->render($template);
+    }
+
+
+    public function testDynamicBlocks()
+    {
+        $tpl = '{{% BLOCKS }}{{< *partial }}{{$ bar }}{{ value }}{{/ bar }}{{/ *partial }}';
+
+        $this->mustache->setPartials(array(
+            'foobarbaz' => '{{% BLOCKS }}{{$ foo }}foo{{/ foo }}{{$ bar }}bar{{/ bar }}{{$ baz }}baz{{/ baz }}',
+            'qux' => 'qux',
+        ));
+
+        $result = $this->mustache->render($tpl, array(
+            'partial' => 'foobarbaz',
+            'value' => 'BAR',
+        ));
+
+        $this->assertSame($result, 'fooBARbaz');
+    }
+}

+ 52 - 0
test/Mustache/Test/Functional/MustacheDynamicNamesSpecTest.php

@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of Mustache.php.
+ *
+ * (c) 2010-2017 Justin Hileman
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * A PHPUnit test case wrapping the Mustache Spec.
+ *
+ * @group mustache-spec
+ * @group functional
+ */
+class Mustache_Test_Functional_MustacheDynamicNamesSpecTest extends Mustache_Test_SpecTestCase
+{
+    public static function setUpBeforeClass()
+    {
+        self::$mustache = new Mustache_Engine(array(
+          'pragmas' => 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');
+    }
+}

+ 1 - 1
vendor/spec

@@ -1 +1 @@
-Subproject commit b2aeb3c283de931a7004b5f7a2cb394b89382369
+Subproject commit 5d3b58ea35ae309c40d7a8111bfedc4c5bcd43a6