Skip to content

Commit d7c1145

Browse files
feat: enhance template rendering and add new string manipulation functions
- Update the render method to accept nullable associative arrays for user-defined variables. - Introduce a new 'mapping' filter in the Environment class to check for ObjectValue instances. - Add a 'join' filter in the Interpreter class for concatenating array or string values with a specified separator. - Improve error handling in array filters to provide clearer messages. - Refactor the Parser class to streamline member and call expression parsing.
1 parent 0d2ecf1 commit d7c1145

5 files changed

Lines changed: 53 additions & 23 deletions

File tree

src/Core/Environment.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public function __construct(?Environment $parent = null)
8080
return is_int($operand->value);
8181
},
8282
'iterable' => fn(RuntimeValue $operand) => $operand instanceof ArrayValue || $operand instanceof StringValue,
83+
'mapping' => fn(RuntimeValue $operand) => $operand instanceof ObjectValue,
8384
'lower' => function (RuntimeValue $operand) {
8485
if ($operand->type !== "StringValue") {
8586
return false;

src/Core/Interpreter.php

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -385,29 +385,45 @@ private function evaluateFilterExpression(FilterExpression $node, Environment $e
385385

386386
if ($filterName === "tojson") {
387387
return new StringValue(json_encode($operand));
388+
} elseif ($filterName === "join") {
389+
if ($operand instanceof StringValue) {
390+
$value = mb_str_split($operand->value);
391+
} elseif ($operand instanceof ArrayValue) {
392+
$value = $operand->value;
393+
} else {
394+
throw new \Exception("Cannot apply join filter to type: $operand->type");
395+
}
396+
397+
[$args, $kwargs] = $this->evaluateArguments($filter->args, $environment);
398+
$separator = $args[0] ?? $kwargs["separator"] ?? new StringValue("");
399+
400+
if (!($separator instanceof StringValue)) {
401+
throw new \Exception("separator must be a string");
402+
}
403+
404+
return new StringValue(implode($separator->value, $value));
388405
}
389406

390407
if ($operand instanceof ArrayValue) {
391408
switch ($filterName) {
392409
case "selectattr":
393-
// Check if all items in the array are instances of ObjectValue
410+
case "rejectattr":
394411
if (array_some($operand->value, fn($x) => !($x instanceof ObjectValue))) {
395-
throw new \Exception("`selectattr` can only be applied to an array of objects");
412+
throw new \Exception("`$filterName` can only be applied to an array of objects");
396413
}
397414

398415
if (array_some($filter->args, fn($x) => $x->type !== "StringLiteral")) {
399-
throw new \Exception("The arguments of `selectattr` must be strings");
416+
throw new \Exception("The arguments of `$filterName` must be strings");
400417
}
401418

402-
403419
[$attrExpr, $testNameExpr, $valueExpr] = $filter->args;
404420

405421
$attr = $this->evaluate($attrExpr, $environment);
406422
$testName = isset($testNameExpr) ? $this->evaluate($testNameExpr, $environment) : null;
407423
$value = isset($valueExpr) ? $this->evaluate($valueExpr, $environment) : null;
408424

409425
if (!($attr instanceof StringValue)) {
410-
throw new \Exception("The attribute name of `selectattr` must be a string");
426+
throw new \Exception("The attribute name of `$filterName` must be a string");
411427
}
412428

413429
$testFunction = null;
@@ -424,11 +440,16 @@ private function evaluateFilterExpression(FilterExpression $node, Environment $e
424440

425441
$filtered = [];
426442
foreach ($operand->value as $item) {
427-
$a = $item->value[$attr->value] ?? null;
428-
if ($a !== null) {
429-
if ($testFunction($a, $value)) {
430-
$filtered[] = $item;
431-
}
443+
$attrValue = $item->value[$attr->value] ?? null;
444+
if ($attrValue === null) {
445+
continue;
446+
}
447+
448+
$testResult = $testFunction($attrValue, $value);
449+
$shouldInclude = ($filterName === "selectattr") ? $testResult : !$testResult;
450+
451+
if ($shouldInclude) {
452+
$filtered[] = $item;
432453
}
433454
}
434455

src/Core/Parser.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,26 @@ private function parseAdditiveExpression(): Statement
417417

418418
private function parseCallMemberExpression(): Statement
419419
{
420-
$member = $this->parseMemberExpression();
420+
$member = $this->parseMemberExpression($this->parsePrimaryExpression());
421+
421422
if ($this->is(TokenType::OpenParen)) {
422423
return $this->parseCallExpression($member);
423424
}
425+
424426
return $member;
425427
}
426428

427-
private function parseCallExpression($callee): CallExpression
429+
private function parseCallExpression(Statement $callee): CallExpression
428430
{
429-
$callExpression = new CallExpression($callee, $this->parseArgs());
431+
$expression = new CallExpression($callee, $this->parseArgs());
432+
433+
$expression = $this->parseMemberExpression($expression); // foo->x()->y
434+
430435
if ($this->is(TokenType::OpenParen)) {
431-
$callExpression = $this->parseCallExpression($callExpression);
436+
$expression = $this->parseCallExpression($expression); // foo->x()()
432437
}
433-
return $callExpression;
438+
439+
return $expression;
434440
}
435441

436442
/**
@@ -502,9 +508,8 @@ private function parseMemberExpressionArgumentsList(): Statement
502508
return $slices[0];
503509
}
504510

505-
private function parseMemberExpression(): Statement
511+
private function parseMemberExpression(Statement $object): Statement
506512
{
507-
$object = $this->parsePrimaryExpression();
508513

509514
while ($this->is(TokenType::Dot) || $this->is(TokenType::OpenSquareBracket)) {
510515
$operator = $this->tokens[$this->current]; // . or [

src/Runtime/StringValue.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ public function __construct(string $value)
2121
"title" => new FunctionValue(fn() => new StringValue(toTitleCase($this->value))),
2222
"length" => new NumericValue(strlen($this->value)),
2323
"rstrip" => new FunctionValue(fn() => new StringValue(rtrim($this->value))),
24-
"lstrip" => new FunctionValue(fn() => new StringValue(ltrim($this->value)))
24+
"lstrip" => new FunctionValue(fn() => new StringValue(ltrim($this->value))),
25+
"split" => new FunctionValue(fn($args) => new ArrayValue(explode($args[0] ?? ' ', $this->value, $args[1] ?? -1))),
2526
];
2627
}
2728
}

src/Template.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ public function __construct(string $template)
3131
/**
3232
* Renders the template with the provided items as variables.
3333
*
34-
* @param array $items Associative array of user-defined variables.
34+
* @param ?array $items Associative array of user-defined variables.
3535
* @return string The rendered template.
3636
*/
37-
public function render(array $items): string
37+
public function render(?array $items): string
3838
{
3939
// Create a new environment for this template
4040
$env = new Environment();
@@ -43,11 +43,13 @@ public function render(array $items): string
4343
$env->set('false', false);
4444
$env->set('true', true);
4545
$env->set('raise_exception', fn($args) => throw new RuntimeException($args));
46-
$env->set('range', fn($args) => range($args[0], $args[1]));
46+
$env->set('range', range(...));
4747

4848
// Add user-defined variables
49-
foreach ($items as $key => $value) {
50-
$env->set($key, $value);
49+
if ($items) {
50+
foreach ($items as $key => $value) {
51+
$env->set($key, $value);
52+
}
5153
}
5254

5355
$interpreter = new Interpreter($env);

0 commit comments

Comments
 (0)