Skip to content

Commit 2f4cf5b

Browse files
authored
fix: Object literal interpretation (#4)
* Fix object literal interpretation * Add test cases * Misc fixes * Expose trim blocks * Add NumericValue string filter support * Default to null values for missing object keys
1 parent 5bc6f17 commit 2f4cf5b

File tree

7 files changed

+51
-16
lines changed

7 files changed

+51
-16
lines changed

src/AST/ObjectLiteral.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
namespace Codewithkyrian\Jinja\AST;
77

8+
use SplObjectStorage;
9+
810
/**
911
* Represents an object literal in the template.
1012
*/
@@ -13,9 +15,9 @@ class ObjectLiteral extends Literal
1315
public string $type = "ObjectLiteral";
1416

1517
/**
16-
* @param array<Expression, Expression> $value
18+
* @param SplObjectStorage $value
1719
*/
18-
public function __construct(array $value)
20+
public function __construct(SplObjectStorage $value)
1921
{
2022
parent::__construct($value);
2123
}

src/Core/Environment.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Codewithkyrian\Jinja\Runtime\FloatValue;
1212
use Codewithkyrian\Jinja\Runtime\FunctionValue;
1313
use Codewithkyrian\Jinja\Runtime\IntegerValue;
14+
use Codewithkyrian\Jinja\Runtime\KeywordArgumentsValue;
1415
use Codewithkyrian\Jinja\Runtime\NullValue;
1516
use Codewithkyrian\Jinja\Runtime\NumericValue;
1617
use Codewithkyrian\Jinja\Runtime\ObjectValue;
@@ -43,14 +44,12 @@ public function __construct(?Environment $parent = null)
4344
$this->parent = $parent;
4445

4546
$this->variables = [
46-
'namespace' => new FunctionValue(function ($args) {
47-
if (count($args) === 0) {
47+
'namespace' => new FunctionValue(function (?KeywordArgumentsValue $args = null) {
48+
if (!$args) {
4849
return new ObjectValue([]);
4950
}
50-
if (count($args) !== 1 || !($args[0] instanceof ObjectValue)) {
51-
throw new RuntimeException("`namespace` expects either zero arguments or a single object argument");
52-
}
53-
return $args[0];
51+
52+
return new ObjectValue($args->value);
5453
})
5554
];
5655

src/Core/Interpreter.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,12 @@ function evaluate(?Statement $statement, Environment $environment): RuntimeValue
9191
TupleLiteral::class => fn(TupleLiteral $s, Environment $environment) => new TupleValue(array_map(fn($x) => $this->evaluate($x, $environment), $s->value)),
9292
ObjectLiteral::class => function (ObjectLiteral $s, Environment $environment): ObjectValue {
9393
$mapping = [];
94-
foreach ($s->value as $key => $value) {
94+
foreach ($s->value as $key) {
9595
$evaluatedKey = $this->evaluate($key, $environment);
9696
if (!($evaluatedKey instanceof StringValue)) {
9797
throw new RuntimeException("Object keys must be strings");
9898
}
99-
$mapping[$evaluatedKey->value] = $this->evaluate($value, $environment);
99+
$mapping[$evaluatedKey->value] = $this->evaluate($s->value[$key], $environment);
100100
}
101101
return new ObjectValue($mapping);
102102
},
@@ -375,6 +375,7 @@ private function applyFilter(RuntimeValue $operand, Identifier|CallExpression $f
375375
"abs" => $operand instanceof IntegerValue ? new IntegerValue(abs($operand->value)) : new FloatValue(abs($operand->value)),
376376
"int" => new IntegerValue((int)floor($operand->value)),
377377
"float" => new FloatValue((float)$operand->value),
378+
"string" => new StringValue((string)$operand->value),
378379
default => throw new \Exception("Unknown NumericValue filter: {$filter->value}"),
379380
};
380381
}
@@ -757,7 +758,7 @@ private function evaluateMemberExpression(MemberExpression $expr, Environment $e
757758
throw new RuntimeException("Cannot access property with non-string: got {$property->type}");
758759
}
759760

760-
$value = $object->value[$property->value] ?? $object->builtins[$property->value];
761+
$value = $object->value[$property->value] ?? $object->builtins[$property->value] ?? new NullValue();
761762
} else if ($object instanceof ArrayValue || $object instanceof StringValue) {
762763
if ($property instanceof IntegerValue) {
763764
$index = $property->value;

src/Core/Parser.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Codewithkyrian\Jinja\Core;
66

7+
use SplObjectStorage;
78
use Codewithkyrian\Jinja\AST\ArrayLiteral;
89
use Codewithkyrian\Jinja\AST\BinaryExpression;
910
use Codewithkyrian\Jinja\AST\BreakStatement;
@@ -762,12 +763,12 @@ private function parsePrimaryExpression(): Statement
762763
return new ArrayLiteral($values);
763764

764765
case TokenType::OpenCurlyBracket:
765-
$values = [];
766+
$values = new SplObjectStorage();
766767
while (!$this->is(TokenType::CloseCurlyBracket)) {
767768
$key = $this->parseExpression();
768769
$this->expect(TokenType::Colon, "Expected colon between key and value in object literal");
769770
$value = $this->parseExpression();
770-
$values[] = ['key' => $key, 'value' => $value]; // TODO: Use SPLObjectStorage
771+
$values->attach($key, $value);
771772
if ($this->is(TokenType::Comma)) {
772773
$this->current++; // consume comma
773774
}

src/Runtime/FunctionValue.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public function __construct(callable $value)
1717

1818
public function call(array $args, Environment $env): RuntimeValue
1919
{
20-
return call_user_func($this->value, $args, $env);
20+
return call_user_func_array($this->value, [...$args, $env]);
2121
}
2222
}

src/Template.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ class Template
2222
*
2323
* @param string $template The template string.
2424
*/
25-
public function __construct(string $template)
25+
public function __construct(string $template, bool $lstripBlocks = true, bool $trimBlocks = true)
2626
{
27-
$tokens = Lexer::tokenize($template, lstripBlocks: true, trimBlocks: true);
27+
$tokens = Lexer::tokenize($template, lstripBlocks: $lstripBlocks, trimBlocks: $trimBlocks);
2828
$this->parsed = Parser::make($tokens)->parse();
2929
}
3030

tests/Datasets/InterpreterDataset.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
const EXAMPLE_FOR_TEMPLATE_3 = "{% for item in seq %}\n {{ item }}\n{%- endfor %}";
99
const EXAMPLE_FOR_TEMPLATE_4 = "{% for item in seq -%}\n {{ item }}\n{%- endfor %}";
1010
const EXAMPLE_COMMENT_TEMPLATE = " {# comment #}\n {# {% if true %} {% endif %} #}\n";
11+
const EXAMPLE_OBJECT_LITERAL_TEMPLATE = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.key1 }} - {{ obj.key2 }}";
12+
const EXAMPLE_OBJECT_GET = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.get('key1') }} - {{ obj.get('key3', 'default') }}";
1113

1214
dataset('interpreterTestData', [
1315
// If tests
@@ -129,4 +131,34 @@
129131
// 'trim_blocks' => true,
130132
// 'target' => "",
131133
// ],
134+
135+
// Object literal tests
136+
'object literal (no strip or trim)' => [
137+
'template' => EXAMPLE_OBJECT_LITERAL_TEMPLATE,
138+
'data' => [],
139+
'lstrip_blocks' => false,
140+
'trim_blocks' => false,
141+
'target' => "value1 - value2",
142+
],
143+
'object literal (strip and trim)' => [
144+
'template' => EXAMPLE_OBJECT_LITERAL_TEMPLATE,
145+
'data' => [],
146+
'lstrip_blocks' => true,
147+
'trim_blocks' => true,
148+
'target' => "value1 - value2",
149+
],
150+
'object get method (no strip or trim)' => [
151+
'template' => EXAMPLE_OBJECT_GET,
152+
'data' => [],
153+
'lstrip_blocks' => false,
154+
'trim_blocks' => false,
155+
'target' => "value1 - default",
156+
],
157+
'object get method (strip and trim)' => [
158+
'template' => EXAMPLE_OBJECT_GET,
159+
'data' => [],
160+
'lstrip_blocks' => true,
161+
'trim_blocks' => true,
162+
'target' => "value1 - default",
163+
],
132164
]);

0 commit comments

Comments
 (0)