Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/View/Antlers/Language/Runtime/NodeProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
use Statamic\View\Cascade;
use Statamic\View\Slot;
use Statamic\View\State\CachesOutput;
use Stringable;
use Throwable;

class NodeProcessor
Expand Down Expand Up @@ -110,6 +111,13 @@ class NodeProcessor
*/
protected $isInterpolationProcessor = false;

/**
* Indicates if the processor is reducing a value being assigned to a variable.
*
* @var bool
*/
protected $isAssignmentProcessor = false;

/**
* Indicates if the processor is providing results for a parameter.
*
Expand Down Expand Up @@ -291,6 +299,19 @@ public function setIsInterpolationProcessor($isInterpolation)
return $this;
}

/**
* Sets whether the NodeProcessor is reducing a value being assigned to a variable.
*
* @param bool $isAssignment The value.
* @return $this
*/
public function setIsAssignmentProcessor($isAssignment)
{
$this->isAssignmentProcessor = $isAssignment;

return $this;
}

/**
* Sets whether the NodeProcessor is processing conditions.
*
Expand Down Expand Up @@ -967,6 +988,23 @@ public function reduceInterpolatedVariable(VariableNode $node)
return $this->interpolationCache[$node->name];
}

/**
* Evaluates an interpolated variable being assigned to a variable, keeping tag objects intact.
*
* @param VariableNode $node The interpolated variable.
* @return mixed
*
* @throws RuntimeException
* @throws SyntaxErrorException
*/
public function reduceAssignedInterpolatedVariable(VariableNode $node)
{
return $this->cloneProcessor()
->setIsInterpolationProcessor(true)
->setIsAssignmentProcessor(true)
->setData($this->getActiveData())->reduce($node->interpolationNodes);
}

/**
* Executes the requested tag within the context of the current processor and provided node.
*
Expand Down Expand Up @@ -1802,7 +1840,9 @@ public function reduce($processNodes)
$output = RuntimeValues::resolveWithRuntimeIsolation($output);
}

$output = PathDataManager::reduceForAntlers($output, $this->antlersParser, $this->getActiveData(), $node->isClosedBy != null);
if (! $this->assigningAugmentable($output)) {
$output = PathDataManager::reduceForAntlers($output, $this->antlersParser, $this->getActiveData(), $node->isClosedBy != null);
}
}

if ($this->isInterpolationProcessor) {
Expand Down Expand Up @@ -2512,6 +2552,13 @@ public function reduce($processNodes)
return $buffer;
}

private function assigningAugmentable($output): bool
{
return $this->isAssignmentProcessor
&& $output instanceof Augmentable
&& $output instanceof Stringable;
}

/**
* Executes any PHP within the provided buffer and returns the result.
*
Expand Down
11 changes: 10 additions & 1 deletion src/View/Antlers/Language/Runtime/Sandbox/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -978,7 +978,7 @@ public function process($nodes)
if ($operand instanceof LeftAssignmentOperator) {
$varName = $this->nameOf($left);

$right = $this->checkForFieldValue($this->getValue($rightNode));
$right = $this->checkForFieldValue($this->getAssignedValue($rightNode));

$this->dataRetriever->setRuntimeValue($varName, $this->data, $right);
$lastPath = $this->dataRetriever->lastPath();
Expand Down Expand Up @@ -1177,6 +1177,15 @@ public function process($nodes)
return $stack;
}

private function getAssignedValue($node)
{
if ($node instanceof VariableNode && $node->isInterpolationReference && ! $node->hasModifiers()) {
return $this->nodeProcessor->reduceAssignedInterpolatedVariable($node);
}

return $this->getValue($node);
}

/**
* Evaluates the provided null coalescence group.
*
Expand Down
108 changes: 108 additions & 0 deletions tests/Antlers/Runtime/TagsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

namespace Tests\Antlers\Runtime;

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use PHPUnit\Framework\Attributes\DataProvider;
use Statamic\Facades\Asset;
use Statamic\Facades\AssetContainer;
use Statamic\Fields\Value;
use Statamic\Fields\Values;
use Statamic\Tags\Tags;
use Tests\Antlers\ParserTestCase;

Expand Down Expand Up @@ -46,4 +53,105 @@ public function index()

$this->assertSame('b', $this->renderString('{{ test_tag }}{{ a }}{{ /test_tag }}', [], true));
}

/**
* @see https://github.com/statamic/cms/issues/11257
*/
public function test_objects_returned_from_tags_keep_their_data_when_assigned_to_a_variable()
{
$this->createAsset();

$template = <<<'EOT'
{{ img = { asset url="/assets/a.jpg" } }}
{{ img }}|{{ img.url }}|{{ img.alt }}|{{ img:alt }}
EOT;

$this->assertSame('/assets/a.jpg|/assets/a.jpg|Alpha|Alpha', trim($this->renderString($template, [], true)));
Comment thread
duncanmcclean marked this conversation as resolved.
}

public function test_objects_returned_from_tags_can_be_looped_over_after_being_assigned_to_a_variable()
{
$this->createAsset();

$template = <<<'EOT'
{{ img = { asset url="/assets/a.jpg" } }}
{{ img }}[{{ alt }}]{{ /img }}
EOT;

$this->assertSame('[Alpha]', trim($this->renderString($template, [], true)));
}

#[DataProvider('dynamicKeyProvider')]
public function test_objects_returned_from_tags_are_strings_when_used_as_dynamic_keys($template)
{
$this->createAsset();

$this->assertSame('matched', $this->renderString($template, ['items' => ['/assets/a.jpg' => 'matched']], true));
}

public static function dynamicKeyProvider()
{
return [
'dot syntax' => ['{{ items.{asset url="/assets/a.jpg"} }}'],
'bracket syntax' => ['{{ items[{asset url="/assets/a.jpg"}] }}'],
];
}

#[DataProvider('comparisonProvider')]
public function test_objects_returned_from_tags_are_strings_when_compared_in_conditions($template, $expected)
{
$this->createAsset();

$this->assertSame($expected, $this->renderString($template, [], true));
}

public static function comparisonProvider()
{
return [
'identical' => ['{{ if {asset url="/assets/a.jpg"} === "/assets/a.jpg" }}yes{{ else }}no{{ /if }}', 'yes'],
'not identical' => ['{{ if {asset url="/assets/a.jpg"} !== "/assets/a.jpg" }}yes{{ else }}no{{ /if }}', 'no'],
'ternary' => ['{{ {asset url="/assets/a.jpg"} === "/assets/a.jpg" ? "yes" : "no" }}', 'yes'],
];
}

public function test_values_returned_from_tags_are_unwrapped_when_assigned_to_a_variable()
{
(new class extends Tags
{
public static $handle = 'test_value';

public function index()
{
return new Value('wrapped');
}
})::register();

(new class extends Tags
{
public static $handle = 'test_values';

public function index()
{
return new Values(['a' => 'b']);
}
})::register();

$this->assertSame('wrapped', $this->renderString('{{ value = {test_value} }}{{ value }}', [], true));
$this->assertSame('b', $this->renderString('{{ values = {test_values} }}{{ values:a }}', [], true));
}

public function test_missing_assets_assign_nothing_to_a_variable()
{
$this->createAsset();

$this->assertSame('[]', $this->renderString('{{ img = { asset url="/assets/missing.jpg" } }}[{{ img }}]', [], true));
}

private function createAsset()
{
Storage::fake('test', ['url' => '/assets']);
Storage::disk('test')->put('a.jpg', UploadedFile::fake()->image('a.jpg')->getContent());
tap(AssetContainer::make('test')->disk('test'))->save();
Asset::find('test::a.jpg')->data(['alt' => 'Alpha'])->save();
}
}
Loading