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
6 changes: 4 additions & 2 deletions src/AST/ObjectLiteral.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

namespace Codewithkyrian\Jinja\AST;

use SplObjectStorage;

/**
* Represents an object literal in the template.
*/
Expand All @@ -13,9 +15,9 @@ class ObjectLiteral extends Literal
public string $type = "ObjectLiteral";

/**
* @param array<Expression, Expression> $value
* @param SplObjectStorage $value
*/
public function __construct(array $value)
public function __construct(SplObjectStorage $value)
{
parent::__construct($value);
}
Expand Down
11 changes: 5 additions & 6 deletions src/Core/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
})
];

Expand Down
7 changes: 4 additions & 3 deletions src/Core/Interpreter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down Expand Up @@ -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}"),
};
}
Expand Down Expand Up @@ -757,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;
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion src/Runtime/FunctionValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
}
4 changes: 2 additions & 2 deletions src/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
32 changes: 32 additions & 0 deletions tests/Datasets/InterpreterDataset.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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 }}";
const EXAMPLE_OBJECT_GET = "{% set obj = { 'key1': 'value1', 'key2': 'value2' } %}{{ obj.get('key1') }} - {{ obj.get('key3', 'default') }}";

dataset('interpreterTestData', [
// If tests
Expand Down Expand Up @@ -129,4 +131,34 @@
// '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",
],
'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",
],
]);