From 15c29abc3dc12e40c40a9331f6b9478386d2bf7d Mon Sep 17 00:00:00 2001 From: David Siegfried Date: Wed, 8 May 2024 07:38:22 +0000 Subject: move exTpl to lib, re #4119 Merge request studip/studip!2963 --- lib/exTpl/ArithExpression.php | 29 ++ lib/exTpl/ArrayNode.php | 37 ++ lib/exTpl/BinaryExpression.php | 27 ++ lib/exTpl/BooleanExpression.php | 31 ++ lib/exTpl/ConditionExpression.php | 38 ++ lib/exTpl/ConditionNode.php | 59 +++ lib/exTpl/ConstantExpression.php | 31 ++ lib/exTpl/Context.php | 77 ++++ lib/exTpl/Expression.php | 27 ++ lib/exTpl/ExpressionNode.php | 37 ++ lib/exTpl/FunctionExpression.php | 45 +++ lib/exTpl/IndexExpression.php | 22 ++ lib/exTpl/IteratorNode.php | 55 +++ lib/exTpl/MinusExpression.php | 19 + lib/exTpl/Node.php | 28 ++ lib/exTpl/NotExpression.php | 19 + lib/exTpl/RawExpression.php | 19 + lib/exTpl/Scanner.php | 87 ++++ lib/exTpl/SymbolExpression.php | 39 ++ lib/exTpl/Template.php | 547 ++++++++++++++++++++++++++ lib/exTpl/TemplateParserException.php | 19 + lib/exTpl/TextNode.php | 31 ++ lib/exTpl/UnaryExpression.php | 21 + lib/extern/ExternPage.php | 2 - tests/unit/_bootstrap.php | 1 + tests/unit/lib/classes/extTPLTemplateTest.php | 213 ++++++++++ vendor/exTpl/Context.php | 80 ---- vendor/exTpl/Expression.php | 329 ---------------- vendor/exTpl/Node.php | 234 ----------- vendor/exTpl/Scanner.php | 98 ----- vendor/exTpl/Template.php | 535 ------------------------- vendor/exTpl/template_test.php | 215 ---------- 32 files changed, 1558 insertions(+), 1493 deletions(-) create mode 100644 lib/exTpl/ArithExpression.php create mode 100644 lib/exTpl/ArrayNode.php create mode 100644 lib/exTpl/BinaryExpression.php create mode 100644 lib/exTpl/BooleanExpression.php create mode 100644 lib/exTpl/ConditionExpression.php create mode 100644 lib/exTpl/ConditionNode.php create mode 100644 lib/exTpl/ConstantExpression.php create mode 100644 lib/exTpl/Context.php create mode 100644 lib/exTpl/Expression.php create mode 100644 lib/exTpl/ExpressionNode.php create mode 100644 lib/exTpl/FunctionExpression.php create mode 100644 lib/exTpl/IndexExpression.php create mode 100644 lib/exTpl/IteratorNode.php create mode 100644 lib/exTpl/MinusExpression.php create mode 100644 lib/exTpl/Node.php create mode 100644 lib/exTpl/NotExpression.php create mode 100644 lib/exTpl/RawExpression.php create mode 100644 lib/exTpl/Scanner.php create mode 100644 lib/exTpl/SymbolExpression.php create mode 100644 lib/exTpl/Template.php create mode 100644 lib/exTpl/TemplateParserException.php create mode 100644 lib/exTpl/TextNode.php create mode 100644 lib/exTpl/UnaryExpression.php create mode 100644 tests/unit/lib/classes/extTPLTemplateTest.php delete mode 100644 vendor/exTpl/Context.php delete mode 100644 vendor/exTpl/Expression.php delete mode 100644 vendor/exTpl/Node.php delete mode 100644 vendor/exTpl/Scanner.php delete mode 100644 vendor/exTpl/Template.php delete mode 100644 vendor/exTpl/template_test.php 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +] 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 @@ +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 @@ +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 @@ +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 @@ +tokens = token_get_all('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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +expr = $expr; + } +} diff --git a/lib/extern/ExternPage.php b/lib/extern/ExternPage.php index d775bc4..308caeb 100644 --- a/lib/extern/ExternPage.php +++ b/lib/extern/ExternPage.php @@ -13,8 +13,6 @@ * @since 5.4 */ -require_once 'vendor/exTpl/Template.php'; - abstract class ExternPage { /** diff --git a/tests/unit/_bootstrap.php b/tests/unit/_bootstrap.php index e5299a6..3aa1144 100644 --- a/tests/unit/_bootstrap.php +++ b/tests/unit/_bootstrap.php @@ -53,6 +53,7 @@ StudipAutoloader::addAutoloadPath('lib/activities', 'Studip\\Activity'); StudipAutoloader::addAutoloadPath('lib/models'); StudipAutoloader::addAutoloadPath('lib/classes'); StudipAutoloader::addAutoloadPath('lib/classes', 'Studip'); +StudipAutoloader::addAutoloadPath('lib/exTpl', 'exTpl'); StudipAutoloader::addAutoloadPath('lib/exceptions'); StudipAutoloader::addAutoloadPath('lib/classes/sidebar'); StudipAutoloader::addAutoloadPath('lib/classes/helpbar'); diff --git a/tests/unit/lib/classes/extTPLTemplateTest.php b/tests/unit/lib/classes/extTPLTemplateTest.php new file mode 100644 index 0000000..43a9629 --- /dev/null +++ b/tests/unit/lib/classes/extTPLTemplateTest.php @@ -0,0 +1,213 @@ +assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testConstantExpression() + { + $bindings = array(); + $template = '17 + 4 = {"foo" != "bar" ? 17 + 4 : 42.0}'; + $expected = '17 + 4 = 21'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testConditionExpression() + { + $bindings = array('a' => 0, 'b' => 42); + $template = 'answer is {"" ?: a ?: b}'; + $expected = 'answer is 42'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testStringEscapes() + { + $bindings = array(); + $template = '"{"\\tfoo\'\\"\\n"}{\'{"bar"}\'}"'; + $expected = "\"\tfoo'\"\n{\"bar\"}\""; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testOperatorPrecedence() + { + $bindings = array('val' => array(array(42))); + $template = '{-val[0][0] / (17+4) + 8 > 6 && "foo" == "f"~"o"~"o" ? 1 : 2}'; + $expected = '2'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testSimpleBindings() + { + $bindings = array('foo' => 'bar', 'val' => array(17, 4), 'pi' => 3.14159); + $template = 'foo = "{foo}", sum = {val[0] + val[1]}, pi^2 = {pi * pi}, x = {x}'; + $expected = 'foo = "bar", sum = 21, pi^2 = 9.8695877281, x = '; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testConditional() + { + $bindings = array('foo' => 'bar', 'pi' => 3.14159); + $template = '{if foo == "foo"}NO{elseif foo == "bar"}pi = {pi}{else}NO{endif}'; + $expected = 'pi = 3.14159'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testConditionalIteration() + { + $bindings = array('foo' => 'bar', 'pi' => 3.14159); + $template = '{foreach foo}{if foo}{foo}{endif}{endforeach}'; + $expected = 'bar'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testIteration() + { + $bindings = array('persons' => array( + 1 => array('user' => 'jane', 'phone' => '555-81281'), + 2 => array('user' => 'mike', 'phone' => '230-28382'), + 3 => array('user' => 'john', 'phone' => '911-19212') + )); + $template = ''; + $expected = ''; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testEmptyIteration() + { + $bindings = array('foo' => array(), 'bar' => false); + $template = '{foreach foo}foo{endforeach}:{foreach bar}bar{endforeach}'; + $expected = ':'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testVariableScope() + { + $bindings = array('value' => 42, 'test' => array( + array(), + array('value' => 17), + array('test' => array( + array(), + array('value' => 4) + )) + )); + $template = '{foreach test}{value}:{foreach test}{value}~{endforeach}{endforeach}'; + $expected = '42:42~17~42~17:17~17~17~42:42~4~'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testNestedStatements() + { + foreach (range(0, 9) as $i) { + $bindings['loop'][$i]['i'] = "$i"; + } + $template = '{foreach loop}' . + '{if i+1>4 && i<(1+10/2)}{i==4*1 ? \'foo\'~i : "bar"}' . + '{elseif !(i<=+4)}+{elseif i==""}..{else}{"-"}{endif}' . + '{endforeach}'; + $expected = '----foo4bar++++'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testFunctionCall() + { + $bindings = array('val' => array(0, 1, 2, 3)); + $template = '{strlen("foobar") + count(val)}'; + $expected = '10'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testFilters() + { + $bindings = array( + 'pi' => 3.14159, + 'format' => function($a, $b) { return number_format($a, $b); }, + 'upper' => function($a) { return strtoupper($a); } + ); + $template = '{pi|format(3) ~ ":" ~ "foobar"|upper}'; + $expected = '3.142:FOOBAR'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testRawFilter() + { + $bindings = array('foo' => '', 'upper' => function($a) { return strtoupper($a); }); + $template = '{foo}:{foo|upper|raw}'; + $expected = '<img>:'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('html'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testHtmlAutoEscape() + { + $bindings = array('foo' => '', 'pi' => 3.14159); + $template = '{foo}:{pi}'; + $expected = '<img>:3.14159'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('html'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testJsonAutoEscape() + { + $bindings = array('foo' => '', 'pi' => 3.14159); + $template = '{foo}:{pi}'; + $expected = '"":3.14159'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('json'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } +} diff --git a/vendor/exTpl/Context.php b/vendor/exTpl/Context.php deleted file mode 100644 index ea94b2f..0000000 --- a/vendor/exTpl/Context.php +++ /dev/null @@ -1,80 +0,0 @@ -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($key) - { - 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 $escape escape callback or NULL - */ - public function autoescape($escape) - { - $this->escape = $escape; - } - - /** - * Escapes the given value using the configured strategy. - * - * @param mixed $value expression value - */ - public function escape($value) - { - 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/vendor/exTpl/Expression.php b/vendor/exTpl/Expression.php deleted file mode 100644 index 35ea458..0000000 --- a/vendor/exTpl/Expression.php +++ /dev/null @@ -1,329 +0,0 @@ -value = $value; - } - - /** - * Returns the value of this expression. - * - * @param Context $context symbol table - */ - public function value($context) - { - return $this->value; - } -} - -/** - * SymbolExpression represents a symbol (template variable). - */ -class SymbolExpression implements Expression -{ - protected $name; - - /** - * Initializes a new Expression instance. - * - * @param string $name symbol name - */ - public function __construct($name) - { - $this->name = $name; - } - - /** - * Returns the name of this symbol. - */ - public function name() - { - return $this->name; - } - - /** - * Returns the value of this expression. - * - * @param Context $context symbol table - */ - public function value($context) - { - return $context->lookup($this->name); - } -} - -/** - * UnaryExpression represents a unary operator. - */ -abstract class UnaryExpression implements Expression -{ - protected $expr; - - /** - * Initializes a new Expression instance. - * - * @param Expression $expr expression object - */ - public function __construct(Expression $expr) - { - $this->expr = $expr; - } -} - -/** - * 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) - { - return -$this->expr->value($context); - } -} - -/** - * 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) - { - return !$this->expr->value($context); - } -} - -/** - * 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) - { - return $this->expr->value($context); - } -} - -/** - * BinaryExpression represents a binary operator. - */ -abstract class BinaryExpression implements Expression -{ - protected $left, $right; - protected $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, $operator) - { - $this->left = $left; - $this->right = $right; - $this->operator = $operator; - } -} - -/** - * ArithExpression represents an arithmetic operator. - */ -class ArithExpression extends BinaryExpression -{ - /** - * Returns the value of this expression. - * - * @param Context $context symbol table - */ - public function value($context) - { - $left = $this->left->value($context); - $right = $this->right->value($context); - - switch ($this->operator) { - case '+': return $left + $right; - case '-': return $left - $right; - case '*': return $left * $right; - case '/': return $left / $right; - case '%': return $left % $right; - case '~': return $left . $right; - } - } -} - -/** - * 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) - { - $left = $this->left->value($context); - $right = $this->right->value($context); - - return $left[$right]; - } -} - -/** - * BooleanExpression represents a boolean operator. - */ -class BooleanExpression extends BinaryExpression -{ - /** - * Returns the value of this expression. - * - * @param Context $context symbol table - */ - public function value($context) - { - $left = $this->left->value($context); - $right = $this->right->value($context); - - switch ($this->operator) { - case T_IS_EQUAL : return $left == $right; - case T_IS_NOT_EQUAL : return $left != $right; - case '<' : return $left < $right; - case T_IS_SMALLER_OR_EQUAL: return $left <= $right; - case '>' : return $left > $right; - case T_IS_GREATER_OR_EQUAL: return $left >= $right; - case T_BOOLEAN_AND : return $left && $right; - case T_BOOLEAN_OR : return $left || $right; - } - } -} - -/** - * ConditionExpression represents the conditional operator ('?:'). - */ -class ConditionExpression implements Expression -{ - protected $condition; - protected $left, $right; - - /** - * Initializes a new Expression instance. - * - * @param Expression $condition expression - * @param Expression $left left alternative - * @param Expression $right right alternative - */ - public function __construct($condition, $left, $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) - { - return $this->condition->value($context) ? - $this->left->value($context) : $this->right->value($context); - } -} - -/** - * FunctionExpression represents a function call. - */ -class FunctionExpression implements Expression -{ - protected $name; - protected $arguments; - - /** - * Initializes a new Expression instance. - * - * @param Expression $name function name - * @param array $arguments function arguments - */ - public function __construct(Expression $name, $arguments) - { - $this->name = $name; - $this->arguments = $arguments; - } - - /** - * Returns the value of this expression. - * - * @param Context $context symbol table - */ - public function value($context) - { - $callable = $this->name->value($context); - $arguments = array(); - - foreach ($this->arguments as $expr) { - $arguments[] = $expr->value($context); - } - - if ($callable instanceof \Closure) { - return call_user_func_array($callable, $arguments); - } - - return NULL; - } -} diff --git a/vendor/exTpl/Node.php b/vendor/exTpl/Node.php deleted file mode 100644 index 0dbd946..0000000 --- a/vendor/exTpl/Node.php +++ /dev/null @@ -1,234 +0,0 @@ -text = $text; - } - - /** - * Returns a string representation of this node. - * - * @param Context $context symbol table - */ - public function render($context) - { - return $this->text; - } -} - -/** - * ExpressionNode represents an expression tag: "{...}". - */ -class ExpressionNode implements Node -{ - protected $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) - { - $value = $this->expr->value($context); - - if (!($this->expr instanceof RawExpression)) { - $value = $context->escape($value); - } - - return $value; - } -} - -/** - * ArrayNode represents a sequence of arbitrary nodes. - */ -class ArrayNode implements Node -{ - protected $nodes = array(); - - /** - * Adds a child node to this sequence node. - * - * @param Node $node child node to add - */ - public function addChild(Node $node) - { - $this->nodes[] = $node; - } - - /** - * Returns a string representation of this node. - * - * @param Context $context symbol table - */ - public function render($context) - { - $result = ''; - - foreach ($this->nodes as $node) { - $result .= $node->render($context); - } - - return $result; - } -} - -/** - * IteratorNode represents a single iterator tag: - * "{foreach ARRAY [as [KEY =>] VALUE]}...{endforeach}". - */ -class IteratorNode extends ArrayNode -{ - protected $expr; - protected $key_name; - protected $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, $key_name, $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) - { - $values = $this->expr->value($context); - $result = ''; - - if (is_array($values) && is_int(key($values))) { - $bindings = array($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; - } -} - -/** - * ConditionNode represents a single condition tag: - * "{if CONDITION}...{else}...{endif}". - */ -class ConditionNode extends ArrayNode -{ - protected $condition; - protected $else_node; - - /** - * 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() - { - $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) - { - 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) - { - if ($this->condition->value($context)) { - return parent::render($context); - } - - return $this->else_node ? $this->else_node->render($context) : ''; - } -} diff --git a/vendor/exTpl/Scanner.php b/vendor/exTpl/Scanner.php deleted file mode 100644 index b2464fb..0000000 --- a/vendor/exTpl/Scanner.php +++ /dev/null @@ -1,98 +0,0 @@ -tokens = token_get_all('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]; - - switch ($token[0]) { - case T_CONSTANT_ENCAPSED_STRING: - $this->token_value = stripcslashes(substr($token[1], 1, -1)); - break; - case T_DNUMBER: - $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; - } - - /** - * Returns the current token type. The valid token types are - * those defined for token_get_all() in the PHP documentation. - */ - public function tokenType() - { - 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() - { - return $this->token_value; - } -} diff --git a/vendor/exTpl/Template.php b/vendor/exTpl/Template.php deleted file mode 100644 index 9d5be04..0000000 --- a/vendor/exTpl/Template.php +++ /dev/null @@ -1,535 +0,0 @@ -template = new ArrayNode(); - $this->functions = array( - 'count' => function($a) { return count($a); }, - 'strlen' => function($a) { return 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 string|callable $escape escape strategy or callback - */ - public function autoescape($escape) - { - 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) - { - $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, $pos) - { - 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 - */ - private static function parseTemplate(ArrayNode $node, $string, $pos) - { - $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_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; - case T_ENDIF: - return $pos; - 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 ')' - */ - private static function parseValue(Scanner $scanner) - { - 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 } ')' - */ - private static function parseFunction(Scanner $scanner) - { - $result = self::parseValue($scanner); - $type = $scanner->tokenType(); - - while ($type === '(') { - $scanner->nextToken(); - $arguments = array(); - - 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 - */ - private static function parseIndex(Scanner $scanner) - { - $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 } ')' - */ - private static function parseFilter(Scanner $scanner) - { - $result = self::parseIndex($scanner); - $type = $scanner->tokenType(); - - while ($type === '|') { - $scanner->nextToken(); - - if ($scanner->tokenType() !== T_STRING) { - throw new TemplateParserException('symbol expected', $scanner); - } - - $arguments = array($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 - */ - private static function parseSign(Scanner $scanner) - { - 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 - */ - private static function parseProduct(Scanner $scanner) - { - $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 - */ - private static function parseSum(Scanner $scanner) - { - $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 - */ - private static function parseLtGt(Scanner $scanner) - { - $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 - */ - private static function parseCmp(Scanner $scanner) - { - $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 - */ - private static function parseAnd(Scanner $scanner) - { - $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 - */ - private static function parseOr(Scanner $scanner) - { - $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 - */ - private static function parseExpr(Scanner $scanner) - { - $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; - } -} - -/** - * 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\""); - } -} diff --git a/vendor/exTpl/template_test.php b/vendor/exTpl/template_test.php deleted file mode 100644 index 62aee1c..0000000 --- a/vendor/exTpl/template_test.php +++ /dev/null @@ -1,215 +0,0 @@ -assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testConstantExpression() - { - $bindings = array(); - $template = '17 + 4 = {"foo" != "bar" ? 17 + 4 : 42.0}'; - $expected = '17 + 4 = 21'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testConditionExpression() - { - $bindings = array('a' => 0, 'b' => 42); - $template = 'answer is {"" ?: a ?: b}'; - $expected = 'answer is 42'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testStringEscapes() - { - $bindings = array(); - $template = '"{"\\tfoo\'\\"\\n"}{\'{"bar"}\'}"'; - $expected = "\"\tfoo'\"\n{\"bar\"}\""; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testOperatorPrecedence() - { - $bindings = array('val' => array(array(42))); - $template = '{-val[0][0] / (17+4) + 8 > 6 && "foo" == "f"~"o"~"o" ? 1 : 2}'; - $expected = '2'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testSimpleBindings() - { - $bindings = array('foo' => 'bar', 'val' => array(17, 4), 'pi' => 3.14159); - $template = 'foo = "{foo}", sum = {val[0] + val[1]}, pi^2 = {pi * pi}, x = {x}'; - $expected = 'foo = "bar", sum = 21, pi^2 = 9.8695877281, x = '; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testConditional() - { - $bindings = array('foo' => 'bar', 'pi' => 3.14159); - $template = '{if foo == "foo"}NO{elseif foo == "bar"}pi = {pi}{else}NO{endif}'; - $expected = 'pi = 3.14159'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testConditionalIteration() - { - $bindings = array('foo' => 'bar', 'pi' => 3.14159); - $template = '{foreach foo}{if foo}{foo}{endif}{endforeach}'; - $expected = 'bar'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testIteration() - { - $bindings = array('persons' => array( - 1 => array('user' => 'jane', 'phone' => '555-81281'), - 2 => array('user' => 'mike', 'phone' => '230-28382'), - 3 => array('user' => 'john', 'phone' => '911-19212') - )); - $template = ''; - $expected = ''; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testEmptyIteration() - { - $bindings = array('foo' => array(), 'bar' => false); - $template = '{foreach foo}foo{endforeach}:{foreach bar}bar{endforeach}'; - $expected = ':'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testVariableScope() - { - $bindings = array('value' => 42, 'test' => array( - array(), - array('value' => 17), - array('test' => array( - array(), - array('value' => 4) - )) - )); - $template = '{foreach test}{value}:{foreach test}{value}~{endforeach}{endforeach}'; - $expected = '42:42~17~42~17:17~17~17~42:42~4~'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testNestedStatements() - { - foreach (range(0, 9) as $i) { - $bindings['loop'][$i]['i'] = "$i"; - } - $template = '{foreach loop}' . - '{if i+1>4 && i<(1+10/2)}{i==4*1 ? \'foo\'~i : "bar"}' . - '{elseif !(i<=+4)}+{elseif i==""}..{else}{"-"}{endif}' . - '{endforeach}'; - $expected = '----foo4bar++++'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testFunctionCall() - { - $bindings = array('val' => array(0, 1, 2, 3)); - $template = '{strlen("foobar") + count(val)}'; - $expected = '10'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testFilters() - { - $bindings = array( - 'pi' => 3.14159, - 'format' => function($a, $b) { return number_format($a, $b); }, - 'upper' => function($a) { return strtoupper($a); } - ); - $template = '{pi|format(3) ~ ":" ~ "foobar"|upper}'; - $expected = '3.142:FOOBAR'; - $tmpl_obj = new Template($template); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testRawFilter() - { - $bindings = array('foo' => '', 'upper' => function($a) { return strtoupper($a); }); - $template = '{foo}:{foo|upper|raw}'; - $expected = '<img>:'; - $tmpl_obj = new Template($template); - $tmpl_obj->autoescape('html'); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testHtmlAutoEscape() - { - $bindings = array('foo' => '', 'pi' => 3.14159); - $template = '{foo}:{pi}'; - $expected = '<img>:3.14159'; - $tmpl_obj = new Template($template); - $tmpl_obj->autoescape('html'); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } - - public function testJsonAutoEscape() - { - $bindings = array('foo' => '', 'pi' => 3.14159); - $template = '{foo}:{pi}'; - $expected = '"":3.14159'; - $tmpl_obj = new Template($template); - $tmpl_obj->autoescape('json'); - - $this->assertEquals($expected, $tmpl_obj->render($bindings)); - } -} -- cgit v1.0