From 17388efaba9b805705aade079e2db6f5324c2369 Mon Sep 17 00:00:00 2001 From: James Joffe Date: Fri, 3 Oct 2025 21:32:32 +1000 Subject: [PATCH 1/6] Fix object literal interpretation --- src/AST/ObjectLiteral.php | 6 ++++-- src/Core/Interpreter.php | 4 ++-- src/Core/Parser.php | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/AST/ObjectLiteral.php b/src/AST/ObjectLiteral.php index 58524fd..23be946 100644 --- a/src/AST/ObjectLiteral.php +++ b/src/AST/ObjectLiteral.php @@ -5,6 +5,8 @@ namespace Codewithkyrian\Jinja\AST; +use SplObjectStorage; + /** * Represents an object literal in the template. */ @@ -13,9 +15,9 @@ class ObjectLiteral extends Literal public string $type = "ObjectLiteral"; /** - * @param array $value + * @param SplObjectStorage $value */ - public function __construct(array $value) + public function __construct(SplObjectStorage $value) { parent::__construct($value); } diff --git a/src/Core/Interpreter.php b/src/Core/Interpreter.php index 01fa482..cc02fda 100644 --- a/src/Core/Interpreter.php +++ b/src/Core/Interpreter.php @@ -91,12 +91,12 @@ function evaluate(?Statement $statement, Environment $environment): RuntimeValue TupleLiteral::class => fn(TupleLiteral $s, Environment $environment) => new TupleValue(array_map(fn($x) => $this->evaluate($x, $environment), $s->value)), ObjectLiteral::class => function (ObjectLiteral $s, Environment $environment): ObjectValue { $mapping = []; - foreach ($s->value as $key => $value) { + foreach ($s->value as $key) { $evaluatedKey = $this->evaluate($key, $environment); if (!($evaluatedKey instanceof StringValue)) { throw new RuntimeException("Object keys must be strings"); } - $mapping[$evaluatedKey->value] = $this->evaluate($value, $environment); + $mapping[$evaluatedKey->value] = $this->evaluate($s->value[$key], $environment); } return new ObjectValue($mapping); }, diff --git a/src/Core/Parser.php b/src/Core/Parser.php index 7d75fd5..1d56450 100644 --- a/src/Core/Parser.php +++ b/src/Core/Parser.php @@ -4,6 +4,7 @@ namespace Codewithkyrian\Jinja\Core; +use SplObjectStorage; use Codewithkyrian\Jinja\AST\ArrayLiteral; use Codewithkyrian\Jinja\AST\BinaryExpression; use Codewithkyrian\Jinja\AST\BreakStatement; @@ -762,12 +763,12 @@ private function parsePrimaryExpression(): Statement return new ArrayLiteral($values); case TokenType::OpenCurlyBracket: - $values = []; + $values = new SplObjectStorage(); while (!$this->is(TokenType::CloseCurlyBracket)) { $key = $this->parseExpression(); $this->expect(TokenType::Colon, "Expected colon between key and value in object literal"); $value = $this->parseExpression(); - $values[] = ['key' => $key, 'value' => $value]; // TODO: Use SPLObjectStorage + $values->attach($key, $value); if ($this->is(TokenType::Comma)) { $this->current++; // consume comma } From a3732b8488911e6e92648e3e064524b315d6ed9c Mon Sep 17 00:00:00 2001 From: James Joffe Date: Fri, 3 Oct 2025 22:07:33 +1000 Subject: [PATCH 2/6] Add test cases --- tests/Datasets/InterpreterDataset.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/Datasets/InterpreterDataset.php b/tests/Datasets/InterpreterDataset.php index 54953f0..18d12ad 100644 --- a/tests/Datasets/InterpreterDataset.php +++ b/tests/Datasets/InterpreterDataset.php @@ -8,6 +8,7 @@ const EXAMPLE_FOR_TEMPLATE_3 = "{% for item in seq %}\n {{ item }}\n{%- endfor %}"; const EXAMPLE_FOR_TEMPLATE_4 = "{% for item in seq -%}\n {{ item }}\n{%- endfor %}"; const EXAMPLE_COMMENT_TEMPLATE = " {# comment #}\n {# {% if true %} {% endif %} #}\n"; +const EXAMPLE_OBJECT_LITERAL_TEMPLATE = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.key1 }} - {{ obj.key2 }}"; dataset('interpreterTestData', [ // If tests @@ -129,4 +130,20 @@ // 'trim_blocks' => true, // 'target' => "", // ], + + // Object literal tests + 'object literal (no strip or trim)' => [ + 'template' => EXAMPLE_OBJECT_LITERAL_TEMPLATE, + 'data' => [], + 'lstrip_blocks' => false, + 'trim_blocks' => false, + 'target' => "value1 - value2", + ], + 'object literal (strip and trim)' => [ + 'template' => EXAMPLE_OBJECT_LITERAL_TEMPLATE, + 'data' => [], + 'lstrip_blocks' => true, + 'trim_blocks' => true, + 'target' => "value1 - value2", + ], ]); \ No newline at end of file From ad9d5597b6b7c3fd47806a918007f981c913a388 Mon Sep 17 00:00:00 2001 From: James Joffe Date: Sat, 11 Oct 2025 11:48:14 +1100 Subject: [PATCH 3/6] Misc fixes --- src/Core/Environment.php | 11 +++++------ src/Runtime/FunctionValue.php | 2 +- tests/Datasets/InterpreterDataset.php | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Core/Environment.php b/src/Core/Environment.php index 97b91f6..f080744 100644 --- a/src/Core/Environment.php +++ b/src/Core/Environment.php @@ -11,6 +11,7 @@ use Codewithkyrian\Jinja\Runtime\FloatValue; use Codewithkyrian\Jinja\Runtime\FunctionValue; use Codewithkyrian\Jinja\Runtime\IntegerValue; +use Codewithkyrian\Jinja\Runtime\KeywordArgumentsValue; use Codewithkyrian\Jinja\Runtime\NullValue; use Codewithkyrian\Jinja\Runtime\NumericValue; use Codewithkyrian\Jinja\Runtime\ObjectValue; @@ -43,14 +44,12 @@ public function __construct(?Environment $parent = null) $this->parent = $parent; $this->variables = [ - 'namespace' => new FunctionValue(function ($args) { - if (count($args) === 0) { + 'namespace' => new FunctionValue(function (?KeywordArgumentsValue $args = null) { + if (!$args) { return new ObjectValue([]); } - if (count($args) !== 1 || !($args[0] instanceof ObjectValue)) { - throw new RuntimeException("`namespace` expects either zero arguments or a single object argument"); - } - return $args[0]; + + return new ObjectValue($args->value); }) ]; diff --git a/src/Runtime/FunctionValue.php b/src/Runtime/FunctionValue.php index ecd9897..aa4b7c3 100644 --- a/src/Runtime/FunctionValue.php +++ b/src/Runtime/FunctionValue.php @@ -17,6 +17,6 @@ public function __construct(callable $value) public function call(array $args, Environment $env): RuntimeValue { - return call_user_func($this->value, $args, $env); + return call_user_func_array($this->value, [...$args, $env]); } } diff --git a/tests/Datasets/InterpreterDataset.php b/tests/Datasets/InterpreterDataset.php index 18d12ad..b6acac3 100644 --- a/tests/Datasets/InterpreterDataset.php +++ b/tests/Datasets/InterpreterDataset.php @@ -9,6 +9,7 @@ const EXAMPLE_FOR_TEMPLATE_4 = "{% for item in seq -%}\n {{ item }}\n{%- endfor %}"; const EXAMPLE_COMMENT_TEMPLATE = " {# comment #}\n {# {% if true %} {% endif %} #}\n"; const EXAMPLE_OBJECT_LITERAL_TEMPLATE = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.key1 }} - {{ obj.key2 }}"; +const EXAMPLE_OBJECT_GET = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.get('key1') }} - {{ obj.get('key3', 'default') }}"; dataset('interpreterTestData', [ // If tests @@ -146,4 +147,18 @@ 'trim_blocks' => true, 'target' => "value1 - value2", ], + 'object get method (no strip or trim)' => [ + 'template' => EXAMPLE_OBJECT_GET, + 'data' => [], + 'lstrip_blocks' => false, + 'trim_blocks' => false, + 'target' => "value1 - default", + ], + 'object get method (strip and trim)' => [ + 'template' => EXAMPLE_OBJECT_GET, + 'data' => [], + 'lstrip_blocks' => true, + 'trim_blocks' => true, + 'target' => "value1 - default", + ], ]); \ No newline at end of file From 8f2bf4eb8beb50433a7ce2902414d65dd0c15c66 Mon Sep 17 00:00:00 2001 From: James Joffe Date: Sat, 11 Oct 2025 12:26:58 +1100 Subject: [PATCH 4/6] Expose trim blocks --- src/Template.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Template.php b/src/Template.php index 780809d..2b4891d 100644 --- a/src/Template.php +++ b/src/Template.php @@ -22,9 +22,9 @@ class Template * * @param string $template The template string. */ - public function __construct(string $template) + public function __construct(string $template, bool $lstripBlocks = true, bool $trimBlocks = true) { - $tokens = Lexer::tokenize($template, lstripBlocks: true, trimBlocks: true); + $tokens = Lexer::tokenize($template, lstripBlocks: $lstripBlocks, trimBlocks: $trimBlocks); $this->parsed = Parser::make($tokens)->parse(); } From a2728c33339f34af295eac0916b8cb73d2cd92fc Mon Sep 17 00:00:00 2001 From: James Joffe Date: Wed, 15 Oct 2025 12:11:08 +1100 Subject: [PATCH 5/6] Add NumericValue string filter support --- src/Core/Interpreter.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Interpreter.php b/src/Core/Interpreter.php index cc02fda..da48a7a 100644 --- a/src/Core/Interpreter.php +++ b/src/Core/Interpreter.php @@ -375,6 +375,7 @@ private function applyFilter(RuntimeValue $operand, Identifier|CallExpression $f "abs" => $operand instanceof IntegerValue ? new IntegerValue(abs($operand->value)) : new FloatValue(abs($operand->value)), "int" => new IntegerValue((int)floor($operand->value)), "float" => new FloatValue((float)$operand->value), + "string" => new StringValue((string)$operand->value), default => throw new \Exception("Unknown NumericValue filter: {$filter->value}"), }; } From f82b76f4ca910f2437a8bf9b47407b29c1272e28 Mon Sep 17 00:00:00 2001 From: James Joffe Date: Fri, 24 Oct 2025 23:47:20 +1100 Subject: [PATCH 6/6] Default to null values for missing object keys --- src/Core/Interpreter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Interpreter.php b/src/Core/Interpreter.php index da48a7a..50376f9 100644 --- a/src/Core/Interpreter.php +++ b/src/Core/Interpreter.php @@ -758,7 +758,7 @@ private function evaluateMemberExpression(MemberExpression $expr, Environment $e throw new RuntimeException("Cannot access property with non-string: got {$property->type}"); } - $value = $object->value[$property->value] ?? $object->builtins[$property->value]; + $value = $object->value[$property->value] ?? $object->builtins[$property->value] ?? new NullValue(); } else if ($object instanceof ArrayValue || $object instanceof StringValue) { if ($property instanceof IntegerValue) { $index = $property->value;