Skip to content

Commit

Permalink
Merge branch 'dynamic-names'
Browse files Browse the repository at this point in the history
  • Loading branch information
bobthecow committed Aug 23, 2022
2 parents e62b7c3 + 3085345 commit b03b889
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 24 deletions.
43 changes: 36 additions & 7 deletions src/Mustache/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ private function walk(array $tree, $level = 0)
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
);
Expand All @@ -125,6 +126,7 @@ private function walk(array $tree, $level = 0)
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
Expand Down Expand Up @@ -419,6 +421,30 @@ private function invertedSection($nodes, $id, $filters, $level)
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)) {
Expand All @@ -429,13 +455,14 @@ private function invertedSection($nodes, $id, $filters, $level)
/**
* 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));
Expand All @@ -445,7 +472,7 @@ private function partial($id, $indent, $level)

return sprintf(
$this->prepare(self::PARTIAL, $level),
var_export($id, true),
$this->resolveDynamicName($id, $dynamic),
$indentParam
);
}
Expand All @@ -469,23 +496,25 @@ private function partial($id, $indent, $level)
* 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)
);
}
Expand Down
18 changes: 10 additions & 8 deletions src/Mustache/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 74 additions & 8 deletions src/Mustache/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -37,8 +38,9 @@ public function parse(array $tokens = array())
$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);
}
Expand Down Expand Up @@ -84,11 +86,21 @@ private function buildTree(array &$tokens, array $parent = null)
$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;
}
}
}

Expand All @@ -115,7 +127,11 @@ private function buildTree(array &$tokens, array $parent = null)
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],
Expand Down Expand Up @@ -280,6 +296,52 @@ private function checkIfTokenIsAllowedInParent($parent, array $token)
}
}

/**
* 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.
*
Expand Down Expand Up @@ -312,6 +374,10 @@ private function enablePragma($name)
case Mustache_Engine::PRAGMA_FILTERS:
$this->pragmaFilters = true;
break;

case Mustache_Engine::PRAGMA_DYNAMIC_NAMES:
$this->pragmaDynamicNames = true;
break;
}
}
}
30 changes: 30 additions & 0 deletions src/Mustache/Tokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -357,6 +374,7 @@ private function addPragma($text, $index)
return $end + $this->ctagLen - 1;
}


private function throwUnclosedTagException()
{
$name = trim($this->buffer);
Expand All @@ -375,4 +393,16 @@ private function throwUnclosedTagException()
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 changes: 86 additions & 0 deletions test/Mustache/Test/Functional/DynamicPartialsTest.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
Loading

0 comments on commit b03b889

Please sign in to comment.