Skip to content

Commit b154a77

Browse files
feat: add macro support, new filters, negative array indexing and improved error handling
- Add macro support with {% macro %} and {% endmacro %} blocks - Add new filters: tojson, map, and indent - Add support for Python-style negative array indices (e.g., messages[-1]) - Fix undefined array key errors in parser and lexer with proper bounds checking - Add support for for-else loops with {% else %} blocks - Add select expressions for conditional value selection - Improve error messages and exception handling throughout - Add JsonSerializable interface to runtime values - Support both lowercase and uppercase boolean literals (true/false, True/False)
1 parent 3702672 commit b154a77

17 files changed

Lines changed: 474 additions & 175 deletions

src/AST/ForStatement.php

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@
22

33
declare(strict_types=1);
44

5-
65
namespace Codewithkyrian\Jinja\AST;
76

7+
/**
8+
* Loop over each item in a sequence
9+
* https://jinja.palletsprojects.com/en/3.0.x/templates/#for
10+
*/
811
class ForStatement extends Statement
912
{
1013
public string $type = "For";
1114

1215
/**
13-
* @param Identifier|TupleLiteral $loopvar
14-
* @param Expression $iterable
15-
* @param Statement[] $body
16+
* @param Identifier|TupleLiteral $loopvar The variable to loop over
17+
* @param Expression $iterable The iterable to loop over
18+
* @param Statement[] $body The block to render for each item in the iterable
19+
* @param Statement[]|null $defaultBlock Optional default block to render if the iterable is empty
1620
*/
1721
public function __construct(
1822
public Identifier|TupleLiteral $loopvar,
19-
public Expression $iterable,
20-
public array $body
21-
)
22-
{
23-
}
24-
}
23+
public Expression|SelectExpression $iterable,
24+
public array $body,
25+
public ?array $defaultBlock = null
26+
) {}
27+
}

src/AST/Macro.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codewithkyrian\Jinja\AST;
6+
7+
class Macro extends Statement
8+
{
9+
public string $type = "Macro";
10+
11+
/**
12+
* @param Identifier $name The name of the macro
13+
* @param Expression[] $args The arguments of the macro
14+
* @param Statement[] $body The body of the macro
15+
*/
16+
public function __construct(public Identifier $name, public array $args, public array $body) {}
17+
}

src/AST/SelectExpression.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codewithkyrian\Jinja\AST;
6+
7+
class SelectExpression extends Expression
8+
{
9+
public string $type = "SelectExpression";
10+
11+
public function __construct(public Expression $lhs, public Expression $test) {}
12+
}

src/AST/SetStatement.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,5 @@ class SetStatement extends Statement
1212
public function __construct(
1313
public Expression $assignee,
1414
public Expression $value
15-
)
16-
{
17-
}
18-
}
15+
) {}
16+
}

src/Core/Environment.php

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public function __construct(?Environment $parent = null)
7070
},
7171
'false' => fn(RuntimeValue $operand) => $operand->type === "BooleanValue" && !$operand->value,
7272
'true' => fn(RuntimeValue $operand) => $operand->type === "BooleanValue" && $operand->value,
73+
'string' => fn(RuntimeValue $operand) => $operand->type === "StringValue",
7374
'number' => fn(RuntimeValue $operand) => $operand->type === "NumericValue",
7475
'integer' => function (RuntimeValue $operand) {
7576
if ($operand->type !== "NumericValue") {
@@ -102,6 +103,12 @@ public function __construct(?Environment $parent = null)
102103
'equalto' => function (RuntimeValue $a, RuntimeValue $b) {
103104
return $a->value === $b->value;
104105
},
106+
'eq' => function (RuntimeValue $a, RuntimeValue $b) {
107+
return $a->value === $b->value;
108+
},
109+
'ne' => function (RuntimeValue $a, RuntimeValue $b) {
110+
return $a->value !== $b->value;
111+
}
105112
];
106113
}
107114

@@ -155,6 +162,9 @@ public function lookupVariable(string $name): RuntimeValue
155162
}
156163

157164

165+
/**
166+
* Helper function to convert a PHP value to a runtime value.
167+
*/
158168
public static function convertToRuntimeValues(mixed $input): RuntimeValue
159169
{
160170
if (is_numeric($input)) {
@@ -170,30 +180,23 @@ public static function convertToRuntimeValues(mixed $input): RuntimeValue
170180
}
171181

172182
if (is_callable($input)) {
173-
// Wrap the PHP callable in a runtime function value
174183
return new FunctionValue(function ($args, $scope) use ($input) {
175-
// Assuming args are also runtime values; extract their inner values for the callable
176-
$plainArgs = array_map(function ($arg) {
177-
return $arg->value;
178-
}, $args);
184+
$plainArgs = array_map(fn($arg) => $arg->value, $args);
179185
$result = call_user_func_array($input, $plainArgs);
180-
// Convert the result back to a runtime value
181186
return $this->convertToRuntimeValues($result);
182187
});
183188
}
184189

185190
if (is_array($input)) {
186-
if (array_keys($input) !== range(0, count($input) - 1)) {
187-
// Associative array, treat as ObjectValue
188-
$convertedItems = [];
189-
foreach ($input as $key => $value) {
190-
$convertedItems[$key] = self::convertToRuntimeValues($value);
191-
}
192-
return new ObjectValue($convertedItems);
193-
} else {
194-
// Sequential array, treat as ArrayValue
191+
if (array_is_list($input)) {
195192
return new ArrayValue(array_map(self::convertToRuntimeValues(...), $input));
196193
}
194+
195+
$convertedItems = [];
196+
foreach ($input as $key => $value) {
197+
$convertedItems[$key] = self::convertToRuntimeValues($value);
198+
}
199+
return new ObjectValue($convertedItems);
197200
}
198201

199202
if (is_callable($input)) {
@@ -213,5 +216,4 @@ public static function convertToRuntimeValues(mixed $input): RuntimeValue
213216

214217
throw new RuntimeException("Cannot convert to runtime value: Unsupported type");
215218
}
216-
217219
}

0 commit comments

Comments
 (0)