diff --git a/README.md b/README.md index 2ec03e6..882aa42 100644 --- a/README.md +++ b/README.md @@ -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_]+?`). diff --git a/composer.json b/composer.json index c4ca6de..2e1ef13 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "config": { "platform": { - "php": "8.1" + "php": "8.2" } }, "scripts": { diff --git a/src/Parsem/Parser.php b/src/Parsem/Parser.php index 5154753..16fddad 100644 --- a/src/Parsem/Parser.php +++ b/src/Parsem/Parser.php @@ -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 = '/(?<%\s?if\s(?(?!?)(?\S+?)\s?(?(?(?:<=|<|===|==|>=|>|!==|!=))\s?(?.+?))?)\s?%>)/m'; public const LITERALLY_NULL = '__:-LITERALLY_NULL-:__'; @@ -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])) { @@ -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 @@ -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`. @@ -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 = []; @@ -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; } @@ -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; diff --git a/tests/Parsem/ParserTest.php b/tests/Parsem/ParserTest.php index 8f080b1..12626cb 100644 --- a/tests/Parsem/ParserTest.php +++ b/tests/Parsem/ParserTest.php @@ -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();