diff options
| author | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
|---|---|---|
| committer | Philipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de> | 2024-09-24 10:53:31 +0200 |
| commit | 4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch) | |
| tree | 5c07151ae61276d334e88f6309c30d439a85c12e /lib/exTpl | |
| parent | da0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff) | |
| parent | 97a188592c679890a25c37ab78463add76a52ff7 (diff) | |
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/exTpl')
| -rw-r--r-- | lib/exTpl/ArithExpression.php | 29 | ||||
| -rw-r--r-- | lib/exTpl/ArrayNode.php | 37 | ||||
| -rw-r--r-- | lib/exTpl/BinaryExpression.php | 27 | ||||
| -rw-r--r-- | lib/exTpl/BooleanExpression.php | 31 | ||||
| -rw-r--r-- | lib/exTpl/ConditionExpression.php | 38 | ||||
| -rw-r--r-- | lib/exTpl/ConditionNode.php | 59 | ||||
| -rw-r--r-- | lib/exTpl/ConstantExpression.php | 31 | ||||
| -rw-r--r-- | lib/exTpl/Context.php | 77 | ||||
| -rw-r--r-- | lib/exTpl/Expression.php | 27 | ||||
| -rw-r--r-- | lib/exTpl/ExpressionNode.php | 37 | ||||
| -rw-r--r-- | lib/exTpl/FunctionExpression.php | 45 | ||||
| -rw-r--r-- | lib/exTpl/IndexExpression.php | 22 | ||||
| -rw-r--r-- | lib/exTpl/IteratorNode.php | 55 | ||||
| -rw-r--r-- | lib/exTpl/MinusExpression.php | 19 | ||||
| -rw-r--r-- | lib/exTpl/Node.php | 28 | ||||
| -rw-r--r-- | lib/exTpl/NotExpression.php | 19 | ||||
| -rw-r--r-- | lib/exTpl/RawExpression.php | 19 | ||||
| -rw-r--r-- | lib/exTpl/Scanner.php | 87 | ||||
| -rw-r--r-- | lib/exTpl/SymbolExpression.php | 39 | ||||
| -rw-r--r-- | lib/exTpl/Template.php | 547 | ||||
| -rw-r--r-- | lib/exTpl/TemplateParserException.php | 19 | ||||
| -rw-r--r-- | lib/exTpl/TextNode.php | 31 | ||||
| -rw-r--r-- | lib/exTpl/UnaryExpression.php | 21 |
23 files changed, 1344 insertions, 0 deletions
diff --git a/lib/exTpl/ArithExpression.php b/lib/exTpl/ArithExpression.php new file mode 100644 index 0000000..4888b73 --- /dev/null +++ b/lib/exTpl/ArithExpression.php @@ -0,0 +1,29 @@ +<?php + +namespace exTpl; + +/** + * ArithExpression represents an arithmetic operator. + */ +class ArithExpression extends BinaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + $left = $this->left->value($context); + $right = $this->right->value($context); + + return match ($this->operator) { + '+' => $left + $right, + '-' => $left - $right, + '*' => $left * $right, + '/' => $left / $right, + '%' => $left % $right, + '~' => $left . $right, + }; + } +} diff --git a/lib/exTpl/ArrayNode.php b/lib/exTpl/ArrayNode.php new file mode 100644 index 0000000..3981598 --- /dev/null +++ b/lib/exTpl/ArrayNode.php @@ -0,0 +1,37 @@ +<?php + +namespace exTpl; + +/** + * ArrayNode represents a sequence of arbitrary nodes. + */ +class ArrayNode implements Node +{ + protected array $nodes = []; + + /** + * Adds a child node to this sequence node. + * + * @param Node $node child node to add + */ + public function addChild(Node $node): void + { + $this->nodes[] = $node; + } + + /** + * Returns a string representation of this node. + * + * @param Context $context symbol table + */ + public function render(Context $context): string + { + $result = ''; + + foreach ($this->nodes as $node) { + $result .= $node->render($context); + } + + return $result; + } +} diff --git a/lib/exTpl/BinaryExpression.php b/lib/exTpl/BinaryExpression.php new file mode 100644 index 0000000..1c82730 --- /dev/null +++ b/lib/exTpl/BinaryExpression.php @@ -0,0 +1,27 @@ +<?php + +namespace exTpl; + +/** + * BinaryExpression represents a binary operator. + */ +abstract class BinaryExpression implements Expression +{ + protected Expression $left; + protected Expression $right; + protected mixed $operator; + + /** + * Initializes a new Expression instance. + * + * @param Expression $left left operand + * @param Expression $right right operand + * @param mixed $operator operator token + */ + public function __construct(Expression $left, Expression $right, mixed $operator) + { + $this->left = $left; + $this->right = $right; + $this->operator = $operator; + } +} diff --git a/lib/exTpl/BooleanExpression.php b/lib/exTpl/BooleanExpression.php new file mode 100644 index 0000000..0e18df0 --- /dev/null +++ b/lib/exTpl/BooleanExpression.php @@ -0,0 +1,31 @@ +<?php + +namespace exTpl; + +/** + * BooleanExpression represents a boolean operator. + */ +class BooleanExpression extends BinaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): bool + { + $left = $this->left->value($context); + $right = $this->right->value($context); + + return match ($this->operator) { + T_IS_EQUAL => $left == $right, + T_IS_NOT_EQUAL => $left != $right, + '<' => $left < $right, + T_IS_SMALLER_OR_EQUAL => $left <= $right, + '>' => $left > $right, + T_IS_GREATER_OR_EQUAL => $left >= $right, + T_BOOLEAN_AND => $left && $right, + T_BOOLEAN_OR => $left || $right, + }; + } +} diff --git a/lib/exTpl/ConditionExpression.php b/lib/exTpl/ConditionExpression.php new file mode 100644 index 0000000..657ee31 --- /dev/null +++ b/lib/exTpl/ConditionExpression.php @@ -0,0 +1,38 @@ +<?php + +namespace exTpl; + +/** + * ConditionExpression represents the conditional operator ('?:'). + */ +class ConditionExpression implements Expression +{ + protected Expression $condition; + protected Expression $left; + protected Expression $right; + + /** + * Initializes a new Expression instance. + * + * @param Expression $condition expression + * @param Expression $left left alternative + * @param Expression $right right alternative + */ + public function __construct(Expression $condition, Expression $left, Expression $right) + { + $this->condition = $condition; + $this->left = $left; + $this->right = $right; + } + + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + return $this->condition->value($context) ? + $this->left->value($context) : $this->right->value($context); + } +} diff --git a/lib/exTpl/ConditionNode.php b/lib/exTpl/ConditionNode.php new file mode 100644 index 0000000..bab2121 --- /dev/null +++ b/lib/exTpl/ConditionNode.php @@ -0,0 +1,59 @@ +<?php + +namespace exTpl; + +/** + * ConditionNode represents a single condition tag: + * "{if CONDITION}...{else}...{endif}". + */ +class ConditionNode extends ArrayNode +{ + protected Expression $condition; + protected ArrayNode|null $else_node = null; + + /** + * Initializes a new Node instance with the given expression. + * + * @param Expression $condition expression object + */ + public function __construct(Expression $condition) + { + $this->condition = $condition; + } + + /** + * Adds an else block to this condition node. + */ + public function addElse(): void + { + $this->else_node = new ArrayNode(); + } + + /** + * Adds a child node to this condition node. + * + * @param Node $node child node to add + */ + public function addChild(Node $node): void + { + if ($this->else_node) { + $this->else_node->addChild($node); + } else { + parent::addChild($node); + } + } + + /** + * Returns a string representation of this node. + * + * @param Context $context symbol table + */ + public function render(Context $context): string + { + if ($this->condition->value($context)) { + return parent::render($context); + } + + return $this->else_node ? $this->else_node->render($context) : ''; + } +} diff --git a/lib/exTpl/ConstantExpression.php b/lib/exTpl/ConstantExpression.php new file mode 100644 index 0000000..7235647 --- /dev/null +++ b/lib/exTpl/ConstantExpression.php @@ -0,0 +1,31 @@ +<?php + +namespace exTpl; + +/** + * ConstantExpression represents a literal value. + */ +class ConstantExpression implements Expression +{ + protected mixed $value; + + /** + * Initializes a new Expression instance. + * + * @param mixed $value expression value + */ + public function __construct(mixed $value) + { + $this->value = $value; + } + + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + return $this->value; + } +} diff --git a/lib/exTpl/Context.php b/lib/exTpl/Context.php new file mode 100644 index 0000000..309174f --- /dev/null +++ b/lib/exTpl/Context.php @@ -0,0 +1,77 @@ +<?php +/** + * Context.php - template parser symbol table + * + * A Context object represents the symbol table used to resolve + * symbol names to their values in the local scope. Each context + * may inherit symbol definitions from its parent context. + * + * @copyright 2013 Elmar Ludwig + * @license GPL2 or any later version + */ + +namespace exTpl; + +use Closure; + +class Context +{ + private array $bindings; + private Closure|null $escape; + private Context|null $parent; + + /** + * Initializes a new Context instance with the given bindings. + * + * @param array $bindings symbol table + * @param Context|null $parent parent context (or NULL) + */ + public function __construct(array $bindings, Context $parent = null) + { + $this->bindings = $bindings; + $this->parent = $parent; + } + + /** + * Looks up the value of a symbol in this context and returns it. + * The reserved symbol "this" is an alias for the current context. + * + * @param string $key symbol name + */ + public function lookup(string $key): mixed + { + if (isset($this->bindings[$key])) { + return $this->bindings[$key]; + } else if ($this->parent) { + return $this->parent->lookup($key); + } + + return null; + } + + /** + * Enables or disables automatic escaping for template values. + * + * @param callable|null $escape escape callback or null + */ + public function autoescape(?callable $escape): void + { + $this->escape = $escape ? $escape(...) : null; + } + + /** + * Escapes the given value using the configured strategy. + * + * @param mixed $value expression value + */ + public function escape(mixed $value): mixed + { + if (isset($this->escape)) { + $value = call_user_func($this->escape, $value); + } else if ($this->parent) { + $value = $this->parent->escape($value); + } + + return $value; + } +} diff --git a/lib/exTpl/Expression.php b/lib/exTpl/Expression.php new file mode 100644 index 0000000..15a485d --- /dev/null +++ b/lib/exTpl/Expression.php @@ -0,0 +1,27 @@ +<?php +/* + * Expression.php - template parser expression interface and classes + * + * Copyright (c) 2013 Elmar Ludwig + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + */ + +namespace exTpl; + +/** + * Basic interface for expressions in the template parse tree. The + * only required method is "value" to get the expression's value. + */ +interface Expression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed; +} diff --git a/lib/exTpl/ExpressionNode.php b/lib/exTpl/ExpressionNode.php new file mode 100644 index 0000000..061edce --- /dev/null +++ b/lib/exTpl/ExpressionNode.php @@ -0,0 +1,37 @@ +<?php + +namespace exTpl; + +/** + * ExpressionNode represents an expression tag: "{...}". + */ +class ExpressionNode implements Node +{ + protected Expression $expr; + + /** + * Initializes a new Node instance with the given expression. + * + * @param Expression $expr expression object + */ + public function __construct(Expression $expr) + { + $this->expr = $expr; + } + + /** + * Returns a string representation of this node. + * + * @param Context $context symbol table + */ + public function render(Context $context): ?string + { + $value = $this->expr->value($context); + + if (!($this->expr instanceof RawExpression)) { + $value = $context->escape($value); + } + + return $value; + } +} diff --git a/lib/exTpl/FunctionExpression.php b/lib/exTpl/FunctionExpression.php new file mode 100644 index 0000000..48d4413 --- /dev/null +++ b/lib/exTpl/FunctionExpression.php @@ -0,0 +1,45 @@ +<?php + +namespace exTpl; + +/** + * FunctionExpression represents a function call. + */ +class FunctionExpression implements Expression +{ + protected Expression $name; + protected array $arguments; + + /** + * Initializes a new Expression instance. + * + * @param Expression $name function name + * @param array $arguments function arguments + */ + public function __construct(Expression $name, array $arguments) + { + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + $callable = $this->name->value($context); + $arguments = []; + + foreach ($this->arguments as $expr) { + $arguments[] = $expr->value($context); + } + + if (is_callable($callable)) { + return $callable(...$arguments); + } + + return null; + } +} diff --git a/lib/exTpl/IndexExpression.php b/lib/exTpl/IndexExpression.php new file mode 100644 index 0000000..ecb3241 --- /dev/null +++ b/lib/exTpl/IndexExpression.php @@ -0,0 +1,22 @@ +<?php + +namespace exTpl; + +/** + * IndexExpression represents the array index operator. + */ +class IndexExpression extends BinaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + $left = $this->left->value($context); + $right = $this->right->value($context); + + return $left[$right]; + } +} diff --git a/lib/exTpl/IteratorNode.php b/lib/exTpl/IteratorNode.php new file mode 100644 index 0000000..58dd593 --- /dev/null +++ b/lib/exTpl/IteratorNode.php @@ -0,0 +1,55 @@ +<?php + +namespace exTpl; + +/** + * IteratorNode represents a single iterator tag: + * "{foreach ARRAY [as [KEY =>] VALUE]}...{endforeach}". + */ +class IteratorNode extends ArrayNode +{ + protected Expression $expr; + protected string $key_name; + protected string $val_name; + + /** + * Initializes a new Node instance with the given expression. + * + * @param Expression $expr expression object + * @param string $key_name name of variable on each iteration + * @param string $val_name name of variable on each iteration + */ + public function __construct(Expression $expr, string $key_name, string $val_name) + { + $this->expr = $expr; + $this->key_name = $key_name; + $this->val_name = $val_name; + } + + /** + * Returns a string representation of this node. The IteratorNode + * renders the node sequence for each value in the expression list. + * + * @param Context $context symbol table + */ + public function render(Context $context): string + { + $values = $this->expr->value($context); + $result = ''; + + if (is_array($values) && is_int(key($values))) { + $bindings = [$this->key_name => &$key, $this->val_name => &$value]; + $context = new Context($bindings, $context); + + foreach ($values as $key => $value) { + $result .= parent::render(new Context($value, $context)); + } + } else if (is_array($values) && count($values)) { + return parent::render(new Context($values, $context)); + } else if ($values) { + return parent::render($context); + } + + return $result; + } +} diff --git a/lib/exTpl/MinusExpression.php b/lib/exTpl/MinusExpression.php new file mode 100644 index 0000000..6b81e91 --- /dev/null +++ b/lib/exTpl/MinusExpression.php @@ -0,0 +1,19 @@ +<?php + +namespace exTpl; + +/** + * MinusExpression represents the unary minus operator ('-'). + */ +class MinusExpression extends UnaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + return -$this->expr->value($context); + } +} diff --git a/lib/exTpl/Node.php b/lib/exTpl/Node.php new file mode 100644 index 0000000..5c24cab --- /dev/null +++ b/lib/exTpl/Node.php @@ -0,0 +1,28 @@ +<?php +/* + * Node.php - template parser node interface and classes + * + * Copyright (c) 2013 Elmar Ludwig + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + */ + +namespace exTpl; + + +/** + * Basic interface for nodes in the template parse tree. The only + * required method is "render" to render a node and its children. + */ +interface Node +{ + /** + * Returns a string representation of this node. + * + * @param Context $context symbol table + */ + public function render(Context $context): ?string; +} diff --git a/lib/exTpl/NotExpression.php b/lib/exTpl/NotExpression.php new file mode 100644 index 0000000..cf7472d --- /dev/null +++ b/lib/exTpl/NotExpression.php @@ -0,0 +1,19 @@ +<?php + +namespace exTpl; + +/** + * NotExpression represents the logical negation operator ('!'). + */ +class NotExpression extends UnaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): bool + { + return !$this->expr->value($context); + } +} diff --git a/lib/exTpl/RawExpression.php b/lib/exTpl/RawExpression.php new file mode 100644 index 0000000..275f239 --- /dev/null +++ b/lib/exTpl/RawExpression.php @@ -0,0 +1,19 @@ +<?php + +namespace exTpl; + +/** + * RawExpression represents the "raw" filter function. + */ +class RawExpression extends UnaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + return $this->expr->value($context); + } +} diff --git a/lib/exTpl/Scanner.php b/lib/exTpl/Scanner.php new file mode 100644 index 0000000..76cbc01 --- /dev/null +++ b/lib/exTpl/Scanner.php @@ -0,0 +1,87 @@ +<?php +/** + * Scanner.php - template parser lexical scanner + * + * Simple wrapper class around the Zend engine's lexical scanner. It + * automatically skips whitespace. + * + * @copyright 2013 Elmar Ludwig + * @license GPL2 or any later version + */ +namespace exTpl; + +class Scanner +{ + private array $tokens; + private mixed $token_type; + private mixed $token_value; + + /** + * Initializes a new Scanner instance for the given text. + * + * @param string $text string to parse + */ + public function __construct(string $text) + { + $this->tokens = token_get_all('<?php ' . $text); + } + + /** + * Advances the scanner to the next token and returns its token type. + * The valid token types are those defined for token_get_all() in the + * PHP documentation. Returns false when the end of input is reached. + */ + public function nextToken(): mixed + { + do { + $token = next($this->tokens); + $key = key($this->tokens); + + // FIXME this workaround should be dropped + while ( + $token && $token[0] === T_STRING + && isset($this->tokens[$key + 2]) + && $this->tokens[++$key] === '-' + && $this->tokens[++$key][0] === T_STRING + ) { + $token[1] .= '-' . $this->tokens[$key][1]; + next($this->tokens); + next($this->tokens); + } + } while (is_array($token) && $token[0] === T_WHITESPACE); + + if (is_string($token) || $token === false) { + $this->token_type = $token; + $this->token_value = null; + } else { + $this->token_type = $token[0]; + + $this->token_value = match ($token[0]) { + T_CONSTANT_ENCAPSED_STRING => stripcslashes(substr($token[1], 1, -1)), + T_DNUMBER => (double) $token[1], + T_LNUMBER => (int) $token[1], + default => $token[1], + }; + } + + return $this->token_type; + } + + /** + * Returns the current token type. The valid token types are + * those defined for token_get_all() in the PHP documentation. + */ + public function tokenType(): mixed + { + return $this->token_type; + } + + /** + * Returns the current token value if the token type supports + * a value (T_STRING, T_LNUMBER etc.). Returns null otherwise. + */ + public function tokenValue(): mixed + { + return $this->token_value; + } +} diff --git a/lib/exTpl/SymbolExpression.php b/lib/exTpl/SymbolExpression.php new file mode 100644 index 0000000..178a51f --- /dev/null +++ b/lib/exTpl/SymbolExpression.php @@ -0,0 +1,39 @@ +<?php + +namespace exTpl; + +/** + * SymbolExpression represents a symbol (template variable). + */ +class SymbolExpression implements Expression +{ + protected string $name; + + /** + * Initializes a new Expression instance. + * + * @param string $name symbol name + */ + public function __construct(string $name) + { + $this->name = $name; + } + + /** + * Returns the name of this symbol. + */ + public function name(): string + { + return $this->name; + } + + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value(Context $context): mixed + { + return $context->lookup($this->name); + } +} diff --git a/lib/exTpl/Template.php b/lib/exTpl/Template.php new file mode 100644 index 0000000..274e115 --- /dev/null +++ b/lib/exTpl/Template.php @@ -0,0 +1,547 @@ +<?php +/* + * Template.php - expression template parser + * + * Copyright (c) 2013 Elmar Ludwig + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + */ + +namespace exTpl; + +use Closure; +use InvalidArgumentException; + + +/** + * The Template class is the only externally visible API of this + * template implementation. It can be used to create and render + * template objects. + */ +class Template +{ + private static string $tag_start = '{'; + private static string $tag_end = '}'; + private Closure|string|null $escape = null; + private array $functions; + private ArrayNode $template; + + /** + * Sets the delimiter strings used for the template tags, the + * default delimiters are: $tag_start = '{', $tag_end = '}'. + * + * @param string $tag_start tag start marker + * @param string $tag_end tag end marker + */ + public static function setTagMarkers(string $tag_start, string $tag_end): void + { + self::$tag_start = $tag_start; + self::$tag_end = $tag_end; + } + + /** + * Initializes a new Template instance from the given string. + * + * @param string $string template text + * + * @throws TemplateParserException + */ + public function __construct(string $string) + { + $this->template = new ArrayNode(); + $this->functions = [ + 'count' => fn($a) => count($a), + 'strlen' => fn($a) => mb_strlen($a), + ]; + self::parseTemplate($this->template, $string, 0); + } + + /** + * Enables or disables automatic escaping for template values. + * Currently supported strategies: NULL, 'html', 'json', 'xml' + * + * @param callable|string|null $escape escape strategy or callback + */ + public function autoescape(callable|string|null $escape): void + { + if ($escape === 'html' || $escape === 'xml') { + $this->escape = 'htmlspecialchars'; + } else if ($escape === 'json') { + $this->escape = 'json_encode'; + } else if (is_callable($escape)) { + $this->escape = $escape; + } else if ($escape === null) { + $this->escape = null; + } else { + throw new InvalidArgumentException("invalid escape strategy: $escape"); + } + } + + /** + * Renders the template to a string using the given array of + * bindings to resolve symbol references inside the template. + * + * @param array $bindings symbol table + * + * @return string string representation of the template + */ + public function render(array $bindings): string + { + $context = new Context($bindings + $this->functions); + $context->autoescape($this->escape); + + return $this->template->render($context); + } + + /** + * Skips tokens until the end of the current tag is reached. + * + * @param string $string template text + * @param int $pos offset in string + * + * @return int new offset in the string + */ + private static function skipTokens(string $string, int $pos): int + { + for ($len = strlen($string); $pos < $len && substr_compare($string, self::$tag_end, $pos, strlen(self::$tag_end)); ++$pos) { + $chr = $string[$pos]; + if ($chr === '"' || $chr === "'") { + while (++$pos < $len && $string[$pos] !== $chr) { + if ($string[$pos] === '\\') { + ++$pos; + } + } + } + } + + return $pos; + } + + /** + * Parses a template string into a template node tree, starting + * at the specified offset. All created nodes are added to the + * given sequence node. + * + * @param ArrayNode $node template node to build + * @param string $string string to parse + * @param int $pos offset in string + * + * @return int new offset in the string + * @throws TemplateParserException + */ + private static function parseTemplate(ArrayNode $node, string $string, int $pos): int + { + $len = strlen($string); + + while ($pos < $len) { + $next_pos = strpos($string, self::$tag_start, $pos); + + if ($next_pos === false) { + $child = new TextNode(substr($string, $pos)); + $node->addChild($child); + break; + } + + if ($next_pos > $pos) { + $child = new TextNode(substr($string, $pos, $next_pos - $pos)); + $node->addChild($child); + } + + $pos = $next_pos + strlen(self::$tag_start); + $next_pos = self::skipTokens($string, $pos); + $scanner = new Scanner(substr($string, $pos, $next_pos - $pos)); + $pos = $next_pos + strlen(self::$tag_end); + + switch ($scanner->nextToken()) { + case T_FOREACH: + $scanner->nextToken(); + $expr = self::parseExpr($scanner); + $key_name = 'index'; + $val_name = 'this'; + + if ($scanner->tokenType() === T_AS) { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $val_name = $scanner->tokenValue(); + $scanner->nextToken(); + + if ($scanner->tokenType() === T_DOUBLE_ARROW) { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $key_name = $val_name; + $val_name = $scanner->tokenValue(); + $scanner->nextToken(); + } + } + + $child = new IteratorNode($expr, $key_name, $val_name); + $pos = self::parseTemplate($child, $string, $pos); + $node->addChild($child); + break; + case T_ENDIF: + case T_ENDFOREACH: + return $pos; + case T_IF: + $scanner->nextToken(); + $child = new ConditionNode(self::parseExpr($scanner)); + $pos = self::parseTemplate($child, $string, $pos); + $node->addChild($child); + break; + case T_ELSEIF: + $scanner->nextToken(); + $child = new ConditionNode(self::parseExpr($scanner)); + $node->addElse(); + $node->addChild($child); + return self::parseTemplate($child, $string, $pos); + case T_ELSE: + $scanner->nextToken(); + $node->addElse(); + break; + default: + $child = new ExpressionNode(self::parseExpr($scanner)); + $node->addChild($child); + } + + if ($scanner->tokenType() !== false) { + throw new TemplateParserException('syntax error', $scanner); + } + } + + return $pos; + } + + /** + * value: NUMBER | STRING | SYMBOL | '(' expr ')' + * + * @throws TemplateParserException + */ + private static function parseValue(Scanner $scanner): mixed + { + switch ($scanner->tokenType()) { + case T_CONSTANT_ENCAPSED_STRING: + case T_DNUMBER: + case T_LNUMBER: + $result = new ConstantExpression($scanner->tokenValue()); + break; + case T_STRING: + $result = new SymbolExpression($scanner->tokenValue()); + break; + case '(': + $scanner->nextToken(); + $result = self::parseExpr($scanner); + + if ($scanner->tokenType() !== ')') { + throw new TemplateParserException('missing ")"', $scanner); + } + break; + default: + throw new TemplateParserException('syntax error', $scanner); + } + + $scanner->nextToken(); + return $result; + } + + /** + * function: value | function '(' ')' | function '(' expr { ',' expr } ')' + * + * @throws TemplateParserException + */ + private static function parseFunction(Scanner $scanner): mixed + { + $result = self::parseValue($scanner); + $type = $scanner->tokenType(); + + while ($type === '(') { + $scanner->nextToken(); + $arguments = []; + + if ($scanner->tokenType() !== ')') { + $arguments[] = self::parseExpr($scanner); + + while ($scanner->tokenType() === ',') { + $scanner->nextToken(); + $arguments[] = self::parseExpr($scanner); + } + + if ($scanner->tokenType() !== ')') { + throw new TemplateParserException('missing ")"', $scanner); + } + } + + $scanner->nextToken(); + $result = new FunctionExpression($result, $arguments); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * index: function | index '[' expr ']' | index '.' SYMBOL + * + * @throws TemplateParserException + */ + private static function parseIndex(Scanner $scanner): mixed + { + $result = self::parseFunction($scanner); + $type = $scanner->tokenType(); + + while ($type === '[' || $type === '.') { + $scanner->nextToken(); + + if ($type === '[') { + $expr = self::parseExpr($scanner); + + if ($scanner->tokenType() !== ']') { + throw new TemplateParserException('missing "]"', $scanner); + } + } else if ($scanner->tokenType() === T_STRING) { + $expr = new ConstantExpression($scanner->tokenValue()); + } else { + throw new TemplateParserException('symbol expected', $scanner); + } + + $scanner->nextToken(); + $result = new IndexExpression($result, $expr, $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * filter: index | filter '|' SYMBOL | filter '|' SYMBOL '(' expr { ',' expr } ')' + * + * @throws TemplateParserException + */ + private static function parseFilter(Scanner $scanner): mixed + { + $result = self::parseIndex($scanner); + $type = $scanner->tokenType(); + + while ($type === '|') { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $arguments = [$result]; + $symbol = new SymbolExpression($scanner->tokenValue()); + $scanner->nextToken(); + + if ($scanner->tokenType() === '(') { + $scanner->nextToken(); + + if ($scanner->tokenType() !== ')') { + $arguments[] = self::parseExpr($scanner); + + while ($scanner->tokenType() === ',') { + $scanner->nextToken(); + $arguments[] = self::parseExpr($scanner); + } + + if ($scanner->tokenType() !== ')') { + throw new TemplateParserException('missing ")"', $scanner); + } + } + + $scanner->nextToken(); + } + + if ($symbol->name() === 'raw') { + $result = new RawExpression($result); + } else { + $result = new FunctionExpression($symbol, $arguments); + } + + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * sign: '!' sign | '+' sign | '-' sign | filter + * + * @throws TemplateParserException + */ + private static function parseSign(Scanner $scanner): mixed + { + switch ($scanner->tokenType()) { + case '!': + $scanner->nextToken(); + $result = new NotExpression(self::parseSign($scanner)); + break; + case '+': + $scanner->nextToken(); + $result = self::parseSign($scanner); + break; + case '-': + $scanner->nextToken(); + $result = new MinusExpression(self::parseSign($scanner)); + break; + default: + $result = self::parseFilter($scanner); + } + + return $result; + } + + /** + * product: sign | product '*' sign | product '/' sign | product '%' sign + * + * @throws TemplateParserException + */ + private static function parseProduct(Scanner $scanner): mixed + { + $result = self::parseSign($scanner); + $type = $scanner->tokenType(); + + while ($type === '*' || $type === '/' || $type === '%') { + $scanner->nextToken(); + $result = new ArithExpression($result, self::parseSign($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * sum: product | sum '+' product | sum '-' product | sum '~' product + * + * @throws TemplateParserException + */ + private static function parseSum(Scanner $scanner): mixed + { + $result = self::parseProduct($scanner); + $type = $scanner->tokenType(); + + while ($type === '+' || $type === '-' || $type === '~') { + $scanner->nextToken(); + $result = new ArithExpression($result, self::parseProduct($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * lt_gt: sum | lt_gt '<' concat | lt_gt IS_SMALLER_OR_EQUAL concat + * | lt_gt '>' concat | lt_gt IS_GREATER_OR_EQUAL concat + * + * @throws TemplateParserException + */ + private static function parseLtGt(Scanner $scanner): mixed + { + $result = self::parseSum($scanner); + $type = $scanner->tokenType(); + + while ($type === '<' || $type === T_IS_SMALLER_OR_EQUAL || + $type === '>' || $type === T_IS_GREATER_OR_EQUAL) { + $scanner->nextToken(); + $result = new BooleanExpression($result, self::parseSum($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * cmp: lt_gt | cmp IS_EQUAL lt_gt | cmp IS_NOT_EQUAL lt_gt + * + * @throws TemplateParserException + */ + private static function parseCmp(Scanner $scanner): mixed + { + $result = self::parseLtGt($scanner); + $type = $scanner->tokenType(); + + while ($type === T_IS_EQUAL || $type === T_IS_NOT_EQUAL) { + $scanner->nextToken(); + $result = new BooleanExpression($result, self::parseLtGt($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * and: cmp | and BOOLEAN_AND cmp + * + * @throws TemplateParserException + */ + private static function parseAnd(Scanner $scanner): mixed + { + $result = self::parseCmp($scanner); + $type = $scanner->tokenType(); + + while ($type === T_BOOLEAN_AND) { + $scanner->nextToken(); + $result = new BooleanExpression($result, self::parseCmp($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * or: and | or BOOLEAN_OR and + * + * @throws TemplateParserException + */ + private static function parseOr(Scanner $scanner): mixed + { + $result = self::parseAnd($scanner); + $type = $scanner->tokenType(); + + while ($type === T_BOOLEAN_OR) { + $scanner->nextToken(); + $result = new BooleanExpression($result, self::parseAnd($scanner), $type); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * expr: or | or '?' expr ':' expr | or '?' ':' expr + * + * @throws TemplateParserException + */ + private static function parseExpr(Scanner $scanner): mixed + { + $result = self::parseOr($scanner); + + if ($scanner->tokenType() === '?') { + $scanner->nextToken(); + + if ($scanner->tokenType() !== ':') { + $expr = self::parseExpr($scanner); + } else { + $expr = $result; + } + + if ($scanner->tokenType() !== ':') { + throw new TemplateParserException('missing ":"', $scanner); + } + + $scanner->nextToken(); + $result = new ConditionExpression($result, $expr, self::parseExpr($scanner)); + } + + return $result; + } +} diff --git a/lib/exTpl/TemplateParserException.php b/lib/exTpl/TemplateParserException.php new file mode 100644 index 0000000..2c7abf8 --- /dev/null +++ b/lib/exTpl/TemplateParserException.php @@ -0,0 +1,19 @@ +<?php + +namespace exTpl; + +use Exception; + +/** + * Exception class used to report template parse errors. + */ +class TemplateParserException extends Exception +{ + public function __construct(string $message, Scanner $scanner) + { + $type = $scanner->tokenType(); + $value = is_int($type) ? $scanner->tokenValue() : $type; + + return parent::__construct("$message at \"$value\""); + } +} diff --git a/lib/exTpl/TextNode.php b/lib/exTpl/TextNode.php new file mode 100644 index 0000000..053cafc --- /dev/null +++ b/lib/exTpl/TextNode.php @@ -0,0 +1,31 @@ +<?php + +namespace exTpl; + +/** + * TextNode represents a verbatim text segment. + */ +class TextNode implements Node +{ + protected string $text; + + /** + * Initializes a new Node instance with the given text. + * + * @param string $text verbatim text + */ + public function __construct(string $text) + { + $this->text = $text; + } + + /** + * Returns a string representation of this node. + * + * @param Context $context symbol table + */ + public function render(Context $context): string + { + return $this->text; + } +} diff --git a/lib/exTpl/UnaryExpression.php b/lib/exTpl/UnaryExpression.php new file mode 100644 index 0000000..9979311 --- /dev/null +++ b/lib/exTpl/UnaryExpression.php @@ -0,0 +1,21 @@ +<?php + +namespace exTpl; + +/** + * UnaryExpression represents a unary operator. + */ +abstract class UnaryExpression implements Expression +{ + protected Expression $expr; + + /** + * Initializes a new Expression instance. + * + * @param Expression $expr expression object + */ + public function __construct(Expression $expr) + { + $this->expr = $expr; + } +} |
