Skip to content

Commit

Permalink
add support for conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
matronator committed May 22, 2024
1 parent ad179ea commit df84c37
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 8 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,30 @@ To get syntax highlighting for template files (highlight `<% variable|placeholde

### Template syntax

#### Conditions

You can use conditions in your templates by using the `<% if %>` and `<% endif %>` tags. The condition must be a valid PHP expression that will be evaluated and if it returns `true`, the content between the tags will be included in the final output.

To use a variable provided in the arguments array in a condition, you must use the `$` sign before the variable name, like this: `<% if $variable == 'value' %>`. The `$` sign is used to differentiate between the template variable and a keyword such as `true` or `null`.

##### Example:

```yaml
some:
key
<% if $variable == 'value' %>
with value
<% endif %>
```
If you provide an argument `['variable' => 'value']`, the final output will be this:

```yaml
some:
key
with value
```

#### Variables

Variables are wrapped in `<%` and `%>` with optional space on either side (both `<%nospace%>` and `<% space %>` are valid) and the name must be an alphanumeric string with optional underscore/s (this regex `[a-zA-Z0-9_]+?`).
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"config": {
"platform": {
"php": "8.1"
"php": "8.2"
}
},
"scripts": {
Expand Down
134 changes: 127 additions & 7 deletions src/Parsem/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@ final class Parser
* 3: filter:10,'arg','another' --> (filter with args)
* 4: filter --> (only filter name)
*/
public const PATTERN = '/<%\s?([a-zA-Z0-9_]+)(=.+?)?\|?(([a-zA-Z0-9_]+?)(?:\:(?:(?:\\?\'|\\?")?.?(?:\\?\'|\\?")?,?)+?)*?)?\s?%>/m';
public const VARIABLE_PATTERN = '/<%\s?([a-zA-Z0-9_]+)(=.+?)?\|?(([a-zA-Z0-9_]+?)(?:\:(?:(?:\\?\'|\\?")?.?(?:\\?\'|\\?")?,?)+?)*?)?\s?%>/m';

/**
* Matches:
* 0: <% if a > 10 %> --> (full match)
* 1: a > 10 --> (only condition)
* 2: ! --> (negation)
* 3: a --> (left side)
* 4: > 10 --> (right side with operator)
* 5: > --> (operator)
* 6: 10 --> (right side)
*/
public const CONDITION_PATTERN = '/(?<all><%\s?if\s(?<condition>(?<negation>!?)(?<left>\S+?)\s?(?<right>(?<operator>(?:<=|<|===|==|>=|>|!==|!=))\s?(?<value>.+?))?)\s?%>)/m';

public const LITERALLY_NULL = '__:-LITERALLY_NULL-:__';

Expand All @@ -40,7 +52,9 @@ public static function parseString(mixed $string, array $arguments = [], ?string
{
if (!is_string($string)) return $string;

preg_match_all($pattern ?? static::PATTERN, $string, $matches);
$string = self::parseConditions($string, $arguments);

preg_match_all($pattern ?? static::VARIABLE_PATTERN, $string, $matches);
$args = [];
foreach ($matches[1] as $key => $match) {
if (isset($arguments[$match])) {
Expand All @@ -60,6 +74,114 @@ public static function parseString(mixed $string, array $arguments = [], ?string
return str_replace($matches[0], $args, $string);
}

public static function parseConditions(string $string, array $arguments = [], int $offset = 0): string
{
preg_match(static::CONDITION_PATTERN, $string, $matches, PREG_OFFSET_CAPTURE, $offset);
if (!$matches) {
return $string;
}

$left = $matches['left'][0];
$negation = $matches['negation'][0];
$operator = $matches['operator'][0];
$right = $matches['value'][0];

var_dump($matches);

if (str_starts_with($left, '$')) {
$left = substr($left, 1);
if (!isset($arguments[$left])) {
throw new RuntimeException("Variable '$left' not found in arguments.");
}

if ($negation === '!') {
$left = !$arguments[$left];
} else {
$left = $arguments[$left];
}
} else {
if ((str_starts_with($left, '"') && str_ends_with($left, '"')) || (str_starts_with($left, "'") && str_ends_with($left, "'"))) {
$left = substr($left, 1, -1);
} else if ($left === 'true') {
$left = true;
} else if ($left === 'false') {
$left = false;
} else if ($left === 'null') {
$left = null;
} else if (str_contains($left, '.')) {
$left = floatval($left);
} else if (is_numeric($left) && !str_contains($left, '.')) {
$left = intval($left);
} else {
$left = (string)$left;
}
}

if (str_starts_with($right, '$')) {
$right = substr($right, 1);
if (!isset($arguments[$right])) {
throw new RuntimeException("Variable '$right' not found in arguments.");
}

$right = $arguments[$right];
} else {
if ((str_starts_with($right, '"') && str_ends_with($right, '"')) || (str_starts_with($right, "'") && str_ends_with($right, "'"))) {
$right = substr($right, 1, -1);
} else if ($right === 'true') {
$right = true;
} else if ($right === 'false') {
$right = false;
} else if ($right === 'null') {
$right = null;
} else if (str_contains($right, '.')) {
$right = floatval($right);
} else if (is_numeric($right) && !str_contains($right, '.')) {
$right = intval($right);
} else {
$right = (string)$right;
}
}

echo "Left: $left\n";
echo "Operator: $operator\n";
echo "Right: $right\n";

if ($operator === '==') {
$result = $left == $right;
} elseif ($operator === '===') {
$result = $left === $right;
} elseif ($operator === '!=') {
$result = $left != $right;
} elseif ($operator === '!==') {
$result = $left !== $right;
} elseif ($operator === '<') {
$result = $left < $right;
} elseif ($operator === '<=') {
$result = $left <= $right;
} elseif ($operator === '>') {
$result = $left > $right;
} elseif ($operator === '>=') {
$result = $left >= $right;
} else {
throw new RuntimeException("Unsupported operator '$operator'.");
}

preg_match('/<%\s?endif\s?%>/', $string, $endMatches, PREG_OFFSET_CAPTURE, $offset);
if (!$endMatches) {
throw new RuntimeException("Missing <% endif %> tag.");
}

$insideBlock = substr($string, $matches[0][1] + strlen($matches[0][0]), $endMatches[0][1] - $matches[0][1] - strlen($matches[0][0]));
$string = substr_replace($string, $result ? $insideBlock : '', (int)$matches[0][1], $endMatches[0][1] - $matches[0][1] + strlen($endMatches[0][0]));

preg_match(static::CONDITION_PATTERN, $string, $matches, PREG_OFFSET_CAPTURE, $offset);
if (!$matches) {
return $string;
}

return self::parseConditions($string, $arguments, (int)$matches[0][1]);
}

/**
* Converts a YAML, JSON or NEON file to a corresponding PHP object, replacing all template variables with the provided `$arguments` values.
* @return object
Expand Down Expand Up @@ -123,7 +245,7 @@ public static function decodeByExtension(string $filename, ?string $contents = n
}

/**
* Parse a file of any type to object using a cutom provided parser function.
* Parse a file of any type to object using a custom provided parser function.
* @return object
* @param string $filename
* @param callable $function The parsing function with the following signature `function(string $contents): object` where `$contents` will be the string content of `$filename`.
Expand Down Expand Up @@ -200,7 +322,7 @@ public static function isValidBundle(string $filename, ?string $contents = null)
*/
public static function getArguments(string $string, ?string $pattern = null): object
{
preg_match_all($pattern ?? self::PATTERN, $string, $matches);
preg_match_all($pattern ?? self::VARIABLE_PATTERN, $string, $matches);

$arguments = static::removeDuplicates($matches[1]);
$defaults = [];
Expand Down Expand Up @@ -240,11 +362,9 @@ public static function applyFilters(array $matches, array $arguments): array
if (is_numeric($item) && !preg_match('/([\'"])/', $item)) {
return strpos($item, '.') === false ? (int) $item : (float) $item;
}

if (in_array($item, ['false', 'true'])) {
return (bool) $item;
}

if ($item === 'null') {
return null;
}
Expand Down Expand Up @@ -273,7 +393,7 @@ public static function applyFilters(array $matches, array $arguments): array

public static function needsArguments(string $string, ?string $pattern = null): bool
{
preg_match_all($pattern ?? self::PATTERN, $string, $matches);
preg_match_all($pattern ?? self::VARIABLE_PATTERN, $string, $matches);
foreach ($matches[2] as $match) {
if ($match === '') {
return true;
Expand Down
58 changes: 58 additions & 0 deletions tests/Parsem/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,64 @@ public function testDefaultValueWithFilter()
$parsed = Parser::parseString($string3, $args);
Assert::equal('Hello !', $parsed, 'Filter applied correctly to empty default value.');
}

/** @testCase */
public function testSimpleCondition()
{
$string = '<% if $foo === true %><% bar %> <% endif %>World!';
$args = ['foo' => true, 'bar' => 'Hello'];
$args2 = ['foo' => false, 'bar' => 'Hello'];

$parsed = Parser::parseString($string, $args);
Assert::equal('Hello World!', $parsed, 'Condition applied correctly to default value.');

$parsed = Parser::parseString($string, $args2);
Assert::equal('World!', $parsed, 'Condition applied correctly to default value.');
}

/** @testCase */
public function testNestedConditions()
{
$string = '<% if $foo === true %>Hello<% if $bar === true %> Cruel<% endif %><% endif %> World!';
$args = ['foo' => true, 'bar' => true];
$args2 = ['foo' => true, 'bar' => false];
$args3 = ['foo' => false, 'bar' => true];
$args4 = ['foo' => false, 'bar' => false];

$parsed = Parser::parseString($string, $args);
Assert::equal('Hello Cruel World!', $parsed, 'All are true');

$parsed = Parser::parseString($string, $args2);
Assert::equal('Hello World!', $parsed, 'True and false');

$parsed = Parser::parseString($string, $args3);
Assert::equal(' World!', $parsed, 'False and true');

$parsed = Parser::parseString($string, $args4);
Assert::equal(' World!', $parsed, 'All are false');
}

/** @testCase */
public function testNestedNumericConditions()
{
$string = '<% if $foo === "asdf" %>Hello<% if $bar === 2 %> Cruel<% endif %><% endif %> World!';
$args = ['foo' => 'asdf', 'bar' => 2];
$args2 = ['foo' => 'asdf', 'bar' => 1];
$args3 = ['foo' => 2, 'bar' => 2];
$args4 = ['foo' => 2, 'bar' => 1];

$parsed = Parser::parseString($string, $args);
Assert::equal('Hello Cruel World!', $parsed, 'All are true');

$parsed = Parser::parseString($string, $args2);
Assert::equal('Hello World!', $parsed, 'True and false');

$parsed = Parser::parseString($string, $args3);
Assert::equal(' World!', $parsed, 'False and true');

$parsed = Parser::parseString($string, $args4);
Assert::equal(' World!', $parsed, 'All are false');
}
}

(new ParserTest())->run();

0 comments on commit df84c37

Please sign in to comment.