Skip to content
Snippets Groups Projects
Commit 15c29abc authored by David Siegfried's avatar David Siegfried Committed by Jan-Hendrik Willms
Browse files

move exTpl to lib, re #4119

Merge request studip/studip!2963
parent 2c4c41a7
No related branches found
No related tags found
No related merge requests found
Showing
with 768 additions and 110 deletions
<?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,
};
}
}
<?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;
}
}
<?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;
}
}
<?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,
};
}
}
<?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);
}
}
<?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) : '';
}
}
<?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;
}
}
<?php <?php
/* /**
* Context.php - template parser symbol table * Context.php - template parser symbol table
* *
* Copyright (c) 2013 Elmar Ludwig * 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.
* *
* This program is free software; you can redistribute it and/or * @copyright 2013 Elmar Ludwig
* modify it under the terms of the GNU General Public License as * @license GPL2 or any later version
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*/ */
namespace exTpl; namespace exTpl;
/** use Closure;
* 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.
*/
class Context class Context
{ {
private $bindings; private array $bindings;
private $escape; private Closure|null $escape;
private $parent; private Context|null $parent;
/** /**
* Initializes a new Context instance with the given bindings. * Initializes a new Context instance with the given bindings.
* *
* @param array $bindings symbol table * @param array $bindings symbol table
* @param Context $parent parent context (or NULL) * @param Context|null $parent parent context (or NULL)
*/ */
public function __construct($bindings, Context $parent = NULL) public function __construct(array $bindings, Context $parent = null)
{ {
$this->bindings = $bindings; $this->bindings = $bindings;
$this->parent = $parent; $this->parent = $parent;
} }
/** /**
* Looks up the value of a symbol in this context and returns it. * Looks up the value of a symbol in this context and returns it.
* The reserved symbol "this" is an alias for the current context. * The reserved symbol "this" is an alias for the current context.
* *
* @param string $key symbol name * @param string $key symbol name
*/ */
public function lookup($key) public function lookup(string $key): mixed
{ {
if (isset($this->bindings[$key])) { if (isset($this->bindings[$key])) {
return $this->bindings[$key]; return $this->bindings[$key];
...@@ -49,25 +46,25 @@ class Context ...@@ -49,25 +46,25 @@ class Context
return $this->parent->lookup($key); return $this->parent->lookup($key);
} }
return NULL; return null;
} }
/** /**
* Enables or disables automatic escaping for template values. * Enables or disables automatic escaping for template values.
* *
* @param callable $escape escape callback or NULL * @param callable|null $escape escape callback or null
*/ */
public function autoescape($escape) public function autoescape(?callable $escape): void
{ {
$this->escape = $escape; $this->escape = $escape ? $escape(...) : null;
} }
/** /**
* Escapes the given value using the configured strategy. * Escapes the given value using the configured strategy.
* *
* @param mixed $value expression value * @param mixed $value expression value
*/ */
public function escape($value) public function escape(mixed $value): mixed
{ {
if (isset($this->escape)) { if (isset($this->escape)) {
$value = call_user_func($this->escape, $value); $value = call_user_func($this->escape, $value);
......
<?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;
}
<?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;
}
}
<?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;
}
}
<?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];
}
}
<?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;
}
}
<?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);
}
}
<?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;
}
<?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);
}
}
<?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);
}
}
<?php <?php
/* /**
* Scanner.php - template parser lexical scanner * Scanner.php - template parser lexical scanner
* *
* Copyright (c) 2013 Elmar Ludwig * Simple wrapper class around the Zend engine's lexical scanner. It
* automatically skips whitespace.
* *
* This program is free software; you can redistribute it and/or * @copyright 2013 Elmar Ludwig
* modify it under the terms of the GNU General Public License as * @license GPL2 or any later version
* published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*/ */
namespace exTpl; namespace exTpl;
/**
* Simple wrapper class around the Zend engine's lexical scanner. It
* automatically skips whitespace and offers an interator interface.
*/
class Scanner class Scanner
{ {
private $tokens; private array $tokens;
private $token_type; private mixed $token_type;
private $token_value; private mixed $token_value;
/** /**
* Initializes a new Scanner instance for the given text. * Initializes a new Scanner instance for the given text.
* *
* @param string $text string to parse * @param string $text string to parse
*/ */
public function __construct($text) public function __construct(string $text)
{ {
$this->tokens = token_get_all('<?php ' . $text); $this->tokens = token_get_all('<?php ' . $text);
} }
...@@ -37,17 +31,19 @@ class Scanner ...@@ -37,17 +31,19 @@ class Scanner
* The valid token types are those defined for token_get_all() in the * The valid token types are those defined for token_get_all() in the
* PHP documentation. Returns false when the end of input is reached. * PHP documentation. Returns false when the end of input is reached.
*/ */
public function nextToken() public function nextToken(): mixed
{ {
do { do {
$token = next($this->tokens); $token = next($this->tokens);
$key = key($this->tokens); $key = key($this->tokens);
// FIXME this workaround should be dropped // FIXME this workaround should be dropped
while ($token && $token[0] === T_STRING && while (
isset($this->tokens[$key + 2]) && $token && $token[0] === T_STRING
$this->tokens[++$key] === '-' && && isset($this->tokens[$key + 2])
$this->tokens[++$key][0] === T_STRING) { && $this->tokens[++$key] === '-'
&& $this->tokens[++$key][0] === T_STRING
) {
$token[1] .= '-' . $this->tokens[$key][1]; $token[1] .= '-' . $this->tokens[$key][1];
next($this->tokens); next($this->tokens);
next($this->tokens); next($this->tokens);
...@@ -55,24 +51,17 @@ class Scanner ...@@ -55,24 +51,17 @@ class Scanner
} while (is_array($token) && $token[0] === T_WHITESPACE); } while (is_array($token) && $token[0] === T_WHITESPACE);
if (is_string($token) || $token === false) { if (is_string($token) || $token === false) {
$this->token_type = $token; $this->token_type = $token;
$this->token_value = NULL; $this->token_value = null;
} else { } else {
$this->token_type = $token[0]; $this->token_type = $token[0];
switch ($token[0]) { $this->token_value = match ($token[0]) {
case T_CONSTANT_ENCAPSED_STRING: T_CONSTANT_ENCAPSED_STRING => stripcslashes(substr($token[1], 1, -1)),
$this->token_value = stripcslashes(substr($token[1], 1, -1)); T_DNUMBER => (double) $token[1],
break; T_LNUMBER => (int) $token[1],
case T_DNUMBER: default => $token[1],
$this->token_value = (double) $token[1]; };
break;
case T_LNUMBER:
$this->token_value = (int) $token[1];
break;
default:
$this->token_value = $token[1];
}
} }
return $this->token_type; return $this->token_type;
...@@ -82,16 +71,16 @@ class Scanner ...@@ -82,16 +71,16 @@ class Scanner
* Returns the current token type. The valid token types are * Returns the current token type. The valid token types are
* those defined for token_get_all() in the PHP documentation. * those defined for token_get_all() in the PHP documentation.
*/ */
public function tokenType() public function tokenType(): mixed
{ {
return $this->token_type; return $this->token_type;
} }
/** /**
* Returns the current token value if the token type supports * Returns the current token value if the token type supports
* a value (T_STRING, T_LNUMBER etc.). Returns NULL otherwise. * a value (T_STRING, T_LNUMBER etc.). Returns null otherwise.
*/ */
public function tokenValue() public function tokenValue(): mixed
{ {
return $this->token_value; return $this->token_value;
} }
......
<?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);
}
}
...@@ -12,8 +12,9 @@ ...@@ -12,8 +12,9 @@
namespace exTpl; namespace exTpl;
require_once 'Scanner.php'; use Closure;
require_once 'Node.php'; use InvalidArgumentException;
/** /**
* The Template class is the only externally visible API of this * The Template class is the only externally visible API of this
...@@ -22,37 +23,39 @@ require_once 'Node.php'; ...@@ -22,37 +23,39 @@ require_once 'Node.php';
*/ */
class Template class Template
{ {
private static $tag_start = '{'; private static string $tag_start = '{';
private static $tag_end = '}'; private static string $tag_end = '}';
private $escape; private Closure|string|null $escape = null;
private $functions; private array $functions;
private $template; private ArrayNode $template;
/** /**
* Sets the delimiter strings used for the template tags, the * Sets the delimiter strings used for the template tags, the
* default delimiters are: $tag_start = '{', $tag_end = '}'. * default delimiters are: $tag_start = '{', $tag_end = '}'.
* *
* @param string $tag_start tag start marker * @param string $tag_start tag start marker
* @param string $tag_end tag end marker * @param string $tag_end tag end marker
*/ */
public static function setTagMarkers($tag_start, $tag_end) public static function setTagMarkers(string $tag_start, string $tag_end): void
{ {
self::$tag_start = $tag_start; self::$tag_start = $tag_start;
self::$tag_end = $tag_end; self::$tag_end = $tag_end;
} }
/** /**
* Initializes a new Template instance from the given string. * Initializes a new Template instance from the given string.
* *
* @param string $string template text * @param string $string template text
*
* @throws TemplateParserException
*/ */
public function __construct($string) public function __construct(string $string)
{ {
$this->template = new ArrayNode(); $this->template = new ArrayNode();
$this->functions = array( $this->functions = [
'count' => function($a) { return count($a); }, 'count' => fn($a) => count($a),
'strlen' => function($a) { return mb_strlen($a); } 'strlen' => fn($a) => mb_strlen($a),
); ];
self::parseTemplate($this->template, $string, 0); self::parseTemplate($this->template, $string, 0);
} }
...@@ -60,9 +63,9 @@ class Template ...@@ -60,9 +63,9 @@ class Template
* Enables or disables automatic escaping for template values. * Enables or disables automatic escaping for template values.
* Currently supported strategies: NULL, 'html', 'json', 'xml' * Currently supported strategies: NULL, 'html', 'json', 'xml'
* *
* @param string|callable $escape escape strategy or callback * @param callable|string|null $escape escape strategy or callback
*/ */
public function autoescape($escape) public function autoescape(callable|string|null $escape): void
{ {
if ($escape === 'html' || $escape === 'xml') { if ($escape === 'html' || $escape === 'xml') {
$this->escape = 'htmlspecialchars'; $this->escape = 'htmlspecialchars';
...@@ -70,8 +73,8 @@ class Template ...@@ -70,8 +73,8 @@ class Template
$this->escape = 'json_encode'; $this->escape = 'json_encode';
} else if (is_callable($escape)) { } else if (is_callable($escape)) {
$this->escape = $escape; $this->escape = $escape;
} else if ($escape === NULL) { } else if ($escape === null) {
$this->escape = NULL; $this->escape = null;
} else { } else {
throw new InvalidArgumentException("invalid escape strategy: $escape"); throw new InvalidArgumentException("invalid escape strategy: $escape");
} }
...@@ -81,11 +84,11 @@ class Template ...@@ -81,11 +84,11 @@ class Template
* Renders the template to a string using the given array of * Renders the template to a string using the given array of
* bindings to resolve symbol references inside the template. * bindings to resolve symbol references inside the template.
* *
* @param array $bindings symbol table * @param array $bindings symbol table
* *
* @return string string representation of the template * @return string string representation of the template
*/ */
public function render(array $bindings) public function render(array $bindings): string
{ {
$context = new Context($bindings + $this->functions); $context = new Context($bindings + $this->functions);
$context->autoescape($this->escape); $context->autoescape($this->escape);
...@@ -96,15 +99,14 @@ class Template ...@@ -96,15 +99,14 @@ class Template
/** /**
* Skips tokens until the end of the current tag is reached. * Skips tokens until the end of the current tag is reached.
* *
* @param string $string template text * @param string $string template text
* @param int $pos offset in string * @param int $pos offset in string
* *
* @return int new offset in the string * @return int new offset in the string
*/ */
private static function skipTokens($string, $pos) private static function skipTokens(string $string, int $pos): int
{ {
for ($len = strlen($string); $pos < $len && for ($len = strlen($string); $pos < $len && substr_compare($string, self::$tag_end, $pos, strlen(self::$tag_end)); ++$pos) {
substr_compare($string, self::$tag_end, $pos, strlen(self::$tag_end)); ++$pos) {
$chr = $string[$pos]; $chr = $string[$pos];
if ($chr === '"' || $chr === "'") { if ($chr === '"' || $chr === "'") {
while (++$pos < $len && $string[$pos] !== $chr) { while (++$pos < $len && $string[$pos] !== $chr) {
...@@ -128,8 +130,9 @@ class Template ...@@ -128,8 +130,9 @@ class Template
* @param int $pos offset in string * @param int $pos offset in string
* *
* @return int new offset in the string * @return int new offset in the string
* @throws TemplateParserException
*/ */
private static function parseTemplate(ArrayNode $node, $string, $pos) private static function parseTemplate(ArrayNode $node, string $string, int $pos): int
{ {
$len = strlen($string); $len = strlen($string);
...@@ -147,15 +150,15 @@ class Template ...@@ -147,15 +150,15 @@ class Template
$node->addChild($child); $node->addChild($child);
} }
$pos = $next_pos + strlen(self::$tag_start); $pos = $next_pos + strlen(self::$tag_start);
$next_pos = self::skipTokens($string, $pos); $next_pos = self::skipTokens($string, $pos);
$scanner = new Scanner(substr($string, $pos, $next_pos - $pos)); $scanner = new Scanner(substr($string, $pos, $next_pos - $pos));
$pos = $next_pos + strlen(self::$tag_end); $pos = $next_pos + strlen(self::$tag_end);
switch ($scanner->nextToken()) { switch ($scanner->nextToken()) {
case T_FOREACH: case T_FOREACH:
$scanner->nextToken(); $scanner->nextToken();
$expr = self::parseExpr($scanner); $expr = self::parseExpr($scanner);
$key_name = 'index'; $key_name = 'index';
$val_name = 'this'; $val_name = 'this';
...@@ -183,15 +186,16 @@ class Template ...@@ -183,15 +186,16 @@ class Template
} }
$child = new IteratorNode($expr, $key_name, $val_name); $child = new IteratorNode($expr, $key_name, $val_name);
$pos = self::parseTemplate($child, $string, $pos); $pos = self::parseTemplate($child, $string, $pos);
$node->addChild($child); $node->addChild($child);
break; break;
case T_ENDIF:
case T_ENDFOREACH: case T_ENDFOREACH:
return $pos; return $pos;
case T_IF: case T_IF:
$scanner->nextToken(); $scanner->nextToken();
$child = new ConditionNode(self::parseExpr($scanner)); $child = new ConditionNode(self::parseExpr($scanner));
$pos = self::parseTemplate($child, $string, $pos); $pos = self::parseTemplate($child, $string, $pos);
$node->addChild($child); $node->addChild($child);
break; break;
case T_ELSEIF: case T_ELSEIF:
...@@ -204,8 +208,6 @@ class Template ...@@ -204,8 +208,6 @@ class Template
$scanner->nextToken(); $scanner->nextToken();
$node->addElse(); $node->addElse();
break; break;
case T_ENDIF:
return $pos;
default: default:
$child = new ExpressionNode(self::parseExpr($scanner)); $child = new ExpressionNode(self::parseExpr($scanner));
$node->addChild($child); $node->addChild($child);
...@@ -221,8 +223,10 @@ class Template ...@@ -221,8 +223,10 @@ class Template
/** /**
* value: NUMBER | STRING | SYMBOL | '(' expr ')' * value: NUMBER | STRING | SYMBOL | '(' expr ')'
*
* @throws TemplateParserException
*/ */
private static function parseValue(Scanner $scanner) private static function parseValue(Scanner $scanner): mixed
{ {
switch ($scanner->tokenType()) { switch ($scanner->tokenType()) {
case T_CONSTANT_ENCAPSED_STRING: case T_CONSTANT_ENCAPSED_STRING:
...@@ -251,15 +255,17 @@ class Template ...@@ -251,15 +255,17 @@ class Template
/** /**
* function: value | function '(' ')' | function '(' expr { ',' expr } ')' * function: value | function '(' ')' | function '(' expr { ',' expr } ')'
*
* @throws TemplateParserException
*/ */
private static function parseFunction(Scanner $scanner) private static function parseFunction(Scanner $scanner): mixed
{ {
$result = self::parseValue($scanner); $result = self::parseValue($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '(') { while ($type === '(') {
$scanner->nextToken(); $scanner->nextToken();
$arguments = array(); $arguments = [];
if ($scanner->tokenType() !== ')') { if ($scanner->tokenType() !== ')') {
$arguments[] = self::parseExpr($scanner); $arguments[] = self::parseExpr($scanner);
...@@ -276,7 +282,7 @@ class Template ...@@ -276,7 +282,7 @@ class Template
$scanner->nextToken(); $scanner->nextToken();
$result = new FunctionExpression($result, $arguments); $result = new FunctionExpression($result, $arguments);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -284,11 +290,13 @@ class Template ...@@ -284,11 +290,13 @@ class Template
/** /**
* index: function | index '[' expr ']' | index '.' SYMBOL * index: function | index '[' expr ']' | index '.' SYMBOL
*
* @throws TemplateParserException
*/ */
private static function parseIndex(Scanner $scanner) private static function parseIndex(Scanner $scanner): mixed
{ {
$result = self::parseFunction($scanner); $result = self::parseFunction($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '[' || $type === '.') { while ($type === '[' || $type === '.') {
$scanner->nextToken(); $scanner->nextToken();
...@@ -315,11 +323,13 @@ class Template ...@@ -315,11 +323,13 @@ class Template
/** /**
* filter: index | filter '|' SYMBOL | filter '|' SYMBOL '(' expr { ',' expr } ')' * filter: index | filter '|' SYMBOL | filter '|' SYMBOL '(' expr { ',' expr } ')'
*
* @throws TemplateParserException
*/ */
private static function parseFilter(Scanner $scanner) private static function parseFilter(Scanner $scanner): mixed
{ {
$result = self::parseIndex($scanner); $result = self::parseIndex($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '|') { while ($type === '|') {
$scanner->nextToken(); $scanner->nextToken();
...@@ -328,8 +338,8 @@ class Template ...@@ -328,8 +338,8 @@ class Template
throw new TemplateParserException('symbol expected', $scanner); throw new TemplateParserException('symbol expected', $scanner);
} }
$arguments = array($result); $arguments = [$result];
$symbol = new SymbolExpression($scanner->tokenValue()); $symbol = new SymbolExpression($scanner->tokenValue());
$scanner->nextToken(); $scanner->nextToken();
if ($scanner->tokenType() === '(') { if ($scanner->tokenType() === '(') {
...@@ -365,8 +375,10 @@ class Template ...@@ -365,8 +375,10 @@ class Template
/** /**
* sign: '!' sign | '+' sign | '-' sign | filter * sign: '!' sign | '+' sign | '-' sign | filter
*
* @throws TemplateParserException
*/ */
private static function parseSign(Scanner $scanner) private static function parseSign(Scanner $scanner): mixed
{ {
switch ($scanner->tokenType()) { switch ($scanner->tokenType()) {
case '!': case '!':
...@@ -390,16 +402,18 @@ class Template ...@@ -390,16 +402,18 @@ class Template
/** /**
* product: sign | product '*' sign | product '/' sign | product '%' sign * product: sign | product '*' sign | product '/' sign | product '%' sign
*
* @throws TemplateParserException
*/ */
private static function parseProduct(Scanner $scanner) private static function parseProduct(Scanner $scanner): mixed
{ {
$result = self::parseSign($scanner); $result = self::parseSign($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '*' || $type === '/' || $type === '%') { while ($type === '*' || $type === '/' || $type === '%') {
$scanner->nextToken(); $scanner->nextToken();
$result = new ArithExpression($result, self::parseSign($scanner), $type); $result = new ArithExpression($result, self::parseSign($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -407,16 +421,18 @@ class Template ...@@ -407,16 +421,18 @@ class Template
/** /**
* sum: product | sum '+' product | sum '-' product | sum '~' product * sum: product | sum '+' product | sum '-' product | sum '~' product
*
* @throws TemplateParserException
*/ */
private static function parseSum(Scanner $scanner) private static function parseSum(Scanner $scanner): mixed
{ {
$result = self::parseProduct($scanner); $result = self::parseProduct($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '+' || $type === '-' || $type === '~') { while ($type === '+' || $type === '-' || $type === '~') {
$scanner->nextToken(); $scanner->nextToken();
$result = new ArithExpression($result, self::parseProduct($scanner), $type); $result = new ArithExpression($result, self::parseProduct($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -425,17 +441,19 @@ class Template ...@@ -425,17 +441,19 @@ class Template
/** /**
* lt_gt: sum | lt_gt '<' concat | lt_gt IS_SMALLER_OR_EQUAL concat * lt_gt: sum | lt_gt '<' concat | lt_gt IS_SMALLER_OR_EQUAL concat
* | lt_gt '>' concat | lt_gt IS_GREATER_OR_EQUAL concat * | lt_gt '>' concat | lt_gt IS_GREATER_OR_EQUAL concat
*
* @throws TemplateParserException
*/ */
private static function parseLtGt(Scanner $scanner) private static function parseLtGt(Scanner $scanner): mixed
{ {
$result = self::parseSum($scanner); $result = self::parseSum($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === '<' || $type === T_IS_SMALLER_OR_EQUAL || while ($type === '<' || $type === T_IS_SMALLER_OR_EQUAL ||
$type === '>' || $type === T_IS_GREATER_OR_EQUAL) { $type === '>' || $type === T_IS_GREATER_OR_EQUAL) {
$scanner->nextToken(); $scanner->nextToken();
$result = new BooleanExpression($result, self::parseSum($scanner), $type); $result = new BooleanExpression($result, self::parseSum($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -443,16 +461,18 @@ class Template ...@@ -443,16 +461,18 @@ class Template
/** /**
* cmp: lt_gt | cmp IS_EQUAL lt_gt | cmp IS_NOT_EQUAL lt_gt * cmp: lt_gt | cmp IS_EQUAL lt_gt | cmp IS_NOT_EQUAL lt_gt
*
* @throws TemplateParserException
*/ */
private static function parseCmp(Scanner $scanner) private static function parseCmp(Scanner $scanner): mixed
{ {
$result = self::parseLtGt($scanner); $result = self::parseLtGt($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === T_IS_EQUAL || $type === T_IS_NOT_EQUAL) { while ($type === T_IS_EQUAL || $type === T_IS_NOT_EQUAL) {
$scanner->nextToken(); $scanner->nextToken();
$result = new BooleanExpression($result, self::parseLtGt($scanner), $type); $result = new BooleanExpression($result, self::parseLtGt($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -460,16 +480,18 @@ class Template ...@@ -460,16 +480,18 @@ class Template
/** /**
* and: cmp | and BOOLEAN_AND cmp * and: cmp | and BOOLEAN_AND cmp
*
* @throws TemplateParserException
*/ */
private static function parseAnd(Scanner $scanner) private static function parseAnd(Scanner $scanner): mixed
{ {
$result = self::parseCmp($scanner); $result = self::parseCmp($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === T_BOOLEAN_AND) { while ($type === T_BOOLEAN_AND) {
$scanner->nextToken(); $scanner->nextToken();
$result = new BooleanExpression($result, self::parseCmp($scanner), $type); $result = new BooleanExpression($result, self::parseCmp($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -477,16 +499,18 @@ class Template ...@@ -477,16 +499,18 @@ class Template
/** /**
* or: and | or BOOLEAN_OR and * or: and | or BOOLEAN_OR and
*
* @throws TemplateParserException
*/ */
private static function parseOr(Scanner $scanner) private static function parseOr(Scanner $scanner): mixed
{ {
$result = self::parseAnd($scanner); $result = self::parseAnd($scanner);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
while ($type === T_BOOLEAN_OR) { while ($type === T_BOOLEAN_OR) {
$scanner->nextToken(); $scanner->nextToken();
$result = new BooleanExpression($result, self::parseAnd($scanner), $type); $result = new BooleanExpression($result, self::parseAnd($scanner), $type);
$type = $scanner->tokenType(); $type = $scanner->tokenType();
} }
return $result; return $result;
...@@ -494,8 +518,10 @@ class Template ...@@ -494,8 +518,10 @@ class Template
/** /**
* expr: or | or '?' expr ':' expr | or '?' ':' expr * expr: or | or '?' expr ':' expr | or '?' ':' expr
*
* @throws TemplateParserException
*/ */
private static function parseExpr(Scanner $scanner) private static function parseExpr(Scanner $scanner): mixed
{ {
$result = self::parseOr($scanner); $result = self::parseOr($scanner);
...@@ -519,17 +545,3 @@ class Template ...@@ -519,17 +545,3 @@ class Template
return $result; return $result;
} }
} }
/**
* Exception class used to report template parse errors.
*/
class TemplateParserException extends \Exception
{
public function __construct($message, $scanner)
{
$type = $scanner->tokenType();
$value = is_int($type) ? $scanner->tokenValue() : $type;
return parent::__construct("$message at \"$value\"");
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment