diff --git a/lib/exTpl/ArithExpression.php b/lib/exTpl/ArithExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..4888b73125b14268ac22e7a0ea86847629882efa
--- /dev/null
+++ b/lib/exTpl/ArithExpression.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ArithExpression represents an arithmetic operator.
+ */
+class ArithExpression extends BinaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        $left  = $this->left->value($context);
+        $right = $this->right->value($context);
+
+        return match ($this->operator) {
+            '+' => $left + $right,
+            '-' => $left - $right,
+            '*' => $left * $right,
+            '/' => $left / $right,
+            '%' => $left % $right,
+            '~' => $left . $right,
+        };
+    }
+}
diff --git a/lib/exTpl/ArrayNode.php b/lib/exTpl/ArrayNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..3981598a0c21cb3900ed653694a704e526cc468f
--- /dev/null
+++ b/lib/exTpl/ArrayNode.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ArrayNode represents a sequence of arbitrary nodes.
+ */
+class ArrayNode implements Node
+{
+    protected array $nodes = [];
+
+    /**
+     * Adds a child node to this sequence node.
+     *
+     * @param Node $node child node to add
+     */
+    public function addChild(Node $node): void
+    {
+        $this->nodes[] = $node;
+    }
+
+    /**
+     * Returns a string representation of this node.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): string
+    {
+        $result = '';
+
+        foreach ($this->nodes as $node) {
+            $result .= $node->render($context);
+        }
+
+        return $result;
+    }
+}
diff --git a/lib/exTpl/BinaryExpression.php b/lib/exTpl/BinaryExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..1c82730e4c06755112f37c2934b3aaa7fab37ed8
--- /dev/null
+++ b/lib/exTpl/BinaryExpression.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * BinaryExpression represents a binary operator.
+ */
+abstract class BinaryExpression implements Expression
+{
+    protected Expression $left;
+    protected Expression $right;
+    protected mixed $operator;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param Expression $left left operand
+     * @param Expression $right right operand
+     * @param mixed $operator operator token
+     */
+    public function __construct(Expression $left, Expression $right, mixed $operator)
+    {
+        $this->left     = $left;
+        $this->right    = $right;
+        $this->operator = $operator;
+    }
+}
diff --git a/lib/exTpl/BooleanExpression.php b/lib/exTpl/BooleanExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e18df02594dbaf4f80bf6b90cd4fae6f4e06e91
--- /dev/null
+++ b/lib/exTpl/BooleanExpression.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * BooleanExpression represents a boolean operator.
+ */
+class BooleanExpression extends BinaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): bool
+    {
+        $left  = $this->left->value($context);
+        $right = $this->right->value($context);
+
+        return match ($this->operator) {
+            T_IS_EQUAL            => $left == $right,
+            T_IS_NOT_EQUAL        => $left != $right,
+            '<'                   => $left < $right,
+            T_IS_SMALLER_OR_EQUAL => $left <= $right,
+            '>'                   => $left > $right,
+            T_IS_GREATER_OR_EQUAL => $left >= $right,
+            T_BOOLEAN_AND         => $left && $right,
+            T_BOOLEAN_OR          => $left || $right,
+        };
+    }
+}
diff --git a/lib/exTpl/ConditionExpression.php b/lib/exTpl/ConditionExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..657ee3193fc0cd2eb2324bb5300df2e0817d43c6
--- /dev/null
+++ b/lib/exTpl/ConditionExpression.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ConditionExpression represents the conditional operator ('?:').
+ */
+class ConditionExpression implements Expression
+{
+    protected Expression $condition;
+    protected Expression $left;
+    protected Expression $right;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param Expression $condition expression
+     * @param Expression $left left alternative
+     * @param Expression $right right alternative
+     */
+    public function __construct(Expression $condition, Expression $left, Expression $right)
+    {
+        $this->condition = $condition;
+        $this->left      = $left;
+        $this->right     = $right;
+    }
+
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        return $this->condition->value($context) ?
+            $this->left->value($context) : $this->right->value($context);
+    }
+}
diff --git a/lib/exTpl/ConditionNode.php b/lib/exTpl/ConditionNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..bab212141f8b73f2872847a127c8dbb5b19f55d4
--- /dev/null
+++ b/lib/exTpl/ConditionNode.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ConditionNode represents a single condition tag:
+ * "{if CONDITION}...{else}...{endif}".
+ */
+class ConditionNode extends ArrayNode
+{
+    protected Expression $condition;
+    protected ArrayNode|null $else_node = null;
+
+    /**
+     * Initializes a new Node instance with the given expression.
+     *
+     * @param Expression $condition expression object
+     */
+    public function __construct(Expression $condition)
+    {
+        $this->condition = $condition;
+    }
+
+    /**
+     * Adds an else block to this condition node.
+     */
+    public function addElse(): void
+    {
+        $this->else_node = new ArrayNode();
+    }
+
+    /**
+     * Adds a child node to this condition node.
+     *
+     * @param Node $node child node to add
+     */
+    public function addChild(Node $node): void
+    {
+        if ($this->else_node) {
+            $this->else_node->addChild($node);
+        } else {
+            parent::addChild($node);
+        }
+    }
+
+    /**
+     * Returns a string representation of this node.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): string
+    {
+        if ($this->condition->value($context)) {
+            return parent::render($context);
+        }
+
+        return $this->else_node ? $this->else_node->render($context) : '';
+    }
+}
diff --git a/lib/exTpl/ConstantExpression.php b/lib/exTpl/ConstantExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..72356473a6e8ef481f4f33208f58e4632f1a31a0
--- /dev/null
+++ b/lib/exTpl/ConstantExpression.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ConstantExpression represents a literal value.
+ */
+class ConstantExpression implements Expression
+{
+    protected mixed $value;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param mixed $value expression value
+     */
+    public function __construct(mixed $value)
+    {
+        $this->value = $value;
+    }
+
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        return $this->value;
+    }
+}
diff --git a/vendor/exTpl/Context.php b/lib/exTpl/Context.php
similarity index 52%
rename from vendor/exTpl/Context.php
rename to lib/exTpl/Context.php
index ea94b2fd25f8b917bf1330814147829ed97d8e75..309174fe02a6d806a68d3413301404cc51373d46 100644
--- a/vendor/exTpl/Context.php
+++ b/lib/exTpl/Context.php
@@ -1,47 +1,44 @@
 <?php
-/*
+/**
  * 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
- * 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.
+ * @copyright 2013  Elmar Ludwig
+ * @license GPL2 or any later version
  */
 
 namespace exTpl;
 
-/**
- * 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.
- */
+use Closure;
+
 class Context
 {
-    private $bindings;
-    private $escape;
-    private $parent;
+    private array $bindings;
+    private Closure|null $escape;
+    private Context|null $parent;
 
     /**
      * Initializes a new Context instance with the given bindings.
      *
-     * @param array $bindings   symbol table
-     * @param Context $parent   parent context (or NULL)
+     * @param array $bindings symbol table
+     * @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->parent = $parent;
+        $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
+     * @param string $key symbol name
      */
-    public function lookup($key)
+    public function lookup(string $key): mixed
     {
         if (isset($this->bindings[$key])) {
             return $this->bindings[$key];
@@ -49,25 +46,25 @@ class Context
             return $this->parent->lookup($key);
         }
 
-        return NULL;
+        return null;
     }
 
     /**
      * 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.
      *
-     * @param mixed $value      expression value
+     * @param mixed $value expression value
      */
-    public function escape($value)
+    public function escape(mixed $value): mixed
     {
         if (isset($this->escape)) {
             $value = call_user_func($this->escape, $value);
diff --git a/lib/exTpl/Expression.php b/lib/exTpl/Expression.php
new file mode 100644
index 0000000000000000000000000000000000000000..15a485ddffb684a8674df46a6ab58d1380796497
--- /dev/null
+++ b/lib/exTpl/Expression.php
@@ -0,0 +1,27 @@
+<?php
+/*
+ * Expression.php - template parser expression interface and classes
+ *
+ * Copyright (c) 2013  Elmar Ludwig
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+namespace exTpl;
+
+/**
+ * Basic interface for expressions in the template parse tree. The
+ * only required method is "value" to get the expression's value.
+ */
+interface Expression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context  symbol table
+     */
+    public function value(Context $context): mixed;
+}
diff --git a/lib/exTpl/ExpressionNode.php b/lib/exTpl/ExpressionNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..061edce2497e3ae1ce7882aa3adc0633dd7a826c
--- /dev/null
+++ b/lib/exTpl/ExpressionNode.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * ExpressionNode represents an expression tag: "{...}".
+ */
+class ExpressionNode implements Node
+{
+    protected Expression $expr;
+
+    /**
+     * Initializes a new Node instance with the given expression.
+     *
+     * @param Expression $expr expression object
+     */
+    public function __construct(Expression $expr)
+    {
+        $this->expr = $expr;
+    }
+
+    /**
+     * Returns a string representation of this node.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): ?string
+    {
+        $value = $this->expr->value($context);
+
+        if (!($this->expr instanceof RawExpression)) {
+            $value = $context->escape($value);
+        }
+
+        return $value;
+    }
+}
diff --git a/lib/exTpl/FunctionExpression.php b/lib/exTpl/FunctionExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..48d4413e6e71e1eea4498d31ff121ab35ab16806
--- /dev/null
+++ b/lib/exTpl/FunctionExpression.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * FunctionExpression represents a function call.
+ */
+class FunctionExpression implements Expression
+{
+    protected Expression $name;
+    protected array $arguments;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param Expression $name function name
+     * @param array $arguments function arguments
+     */
+    public function __construct(Expression $name, array $arguments)
+    {
+        $this->name      = $name;
+        $this->arguments = $arguments;
+    }
+
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        $callable  = $this->name->value($context);
+        $arguments = [];
+
+        foreach ($this->arguments as $expr) {
+            $arguments[] = $expr->value($context);
+        }
+
+        if (is_callable($callable)) {
+            return $callable(...$arguments);
+        }
+
+        return null;
+    }
+}
diff --git a/lib/exTpl/IndexExpression.php b/lib/exTpl/IndexExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..ecb3241bee275c7633ea322188efbb9e1215dbff
--- /dev/null
+++ b/lib/exTpl/IndexExpression.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * IndexExpression represents the array index operator.
+ */
+class IndexExpression extends BinaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        $left  = $this->left->value($context);
+        $right = $this->right->value($context);
+
+        return $left[$right];
+    }
+}
diff --git a/lib/exTpl/IteratorNode.php b/lib/exTpl/IteratorNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..58dd593599a84e9907f3129f452591f5219229d0
--- /dev/null
+++ b/lib/exTpl/IteratorNode.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * IteratorNode represents a single iterator tag:
+ * "{foreach ARRAY [as [KEY =>] VALUE]}...{endforeach}".
+ */
+class IteratorNode extends ArrayNode
+{
+    protected Expression $expr;
+    protected string $key_name;
+    protected string $val_name;
+
+    /**
+     * Initializes a new Node instance with the given expression.
+     *
+     * @param Expression $expr expression object
+     * @param string $key_name name of variable on each iteration
+     * @param string $val_name name of variable on each iteration
+     */
+    public function __construct(Expression $expr, string $key_name, string $val_name)
+    {
+        $this->expr     = $expr;
+        $this->key_name = $key_name;
+        $this->val_name = $val_name;
+    }
+
+    /**
+     * Returns a string representation of this node. The IteratorNode
+     * renders the node sequence for each value in the expression list.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): string
+    {
+        $values = $this->expr->value($context);
+        $result = '';
+
+        if (is_array($values) && is_int(key($values))) {
+            $bindings = [$this->key_name => &$key, $this->val_name => &$value];
+            $context  = new Context($bindings, $context);
+
+            foreach ($values as $key => $value) {
+                $result .= parent::render(new Context($value, $context));
+            }
+        } else if (is_array($values) && count($values)) {
+            return parent::render(new Context($values, $context));
+        } else if ($values) {
+            return parent::render($context);
+        }
+
+        return $result;
+    }
+}
diff --git a/lib/exTpl/MinusExpression.php b/lib/exTpl/MinusExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b81e91dd1e3c1c82424deb6cc79cb5ca6600828
--- /dev/null
+++ b/lib/exTpl/MinusExpression.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * MinusExpression represents the unary minus operator ('-').
+ */
+class MinusExpression extends UnaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context  symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        return -$this->expr->value($context);
+    }
+}
diff --git a/lib/exTpl/Node.php b/lib/exTpl/Node.php
new file mode 100644
index 0000000000000000000000000000000000000000..5c24cab123aff80e199da198f362c2aca4109f2e
--- /dev/null
+++ b/lib/exTpl/Node.php
@@ -0,0 +1,28 @@
+<?php
+/*
+ * Node.php - template parser node interface and classes
+ *
+ * Copyright (c) 2013  Elmar Ludwig
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ */
+
+namespace exTpl;
+
+
+/**
+ * Basic interface for nodes in the template parse tree. The only
+ * required method is "render" to render a node and its children.
+ */
+interface Node
+{
+    /**
+     * Returns a string representation of this node.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): ?string;
+}
diff --git a/lib/exTpl/NotExpression.php b/lib/exTpl/NotExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf7472d605137b815fd629f5146304b22dfa84cb
--- /dev/null
+++ b/lib/exTpl/NotExpression.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * NotExpression represents the logical negation operator ('!').
+ */
+class NotExpression extends UnaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): bool
+    {
+        return !$this->expr->value($context);
+    }
+}
diff --git a/lib/exTpl/RawExpression.php b/lib/exTpl/RawExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..275f239a6519a6b6053ae5ffc422751b28f69a27
--- /dev/null
+++ b/lib/exTpl/RawExpression.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * RawExpression represents the "raw" filter function.
+ */
+class RawExpression extends UnaryExpression
+{
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        return $this->expr->value($context);
+    }
+}
diff --git a/lib/exTpl/Scanner.php b/lib/exTpl/Scanner.php
new file mode 100644
index 0000000000000000000000000000000000000000..76cbc01d41429cf28e391f69cd905a2d3545b41f
--- /dev/null
+++ b/lib/exTpl/Scanner.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Scanner.php - template parser lexical scanner
+ *
+ * Simple wrapper class around the Zend engine's lexical scanner. It
+ *  automatically skips whitespace.
+ *
+ * @copyright 2013  Elmar Ludwig
+ * @license GPL2 or any later version
+ */
+namespace exTpl;
+
+class Scanner
+{
+    private array $tokens;
+    private mixed $token_type;
+    private mixed $token_value;
+
+    /**
+     * Initializes a new Scanner instance for the given text.
+     *
+     * @param string $text string to parse
+     */
+    public function __construct(string $text)
+    {
+        $this->tokens = token_get_all('<?php ' . $text);
+    }
+
+    /**
+     * Advances the scanner to the next token and returns its token type.
+     * The valid token types are those defined for token_get_all() in the
+     * PHP documentation. Returns false when the end of input is reached.
+     */
+    public function nextToken(): mixed
+    {
+        do {
+            $token = next($this->tokens);
+            $key   = key($this->tokens);
+
+            // FIXME this workaround should be dropped
+            while (
+                $token && $token[0] === T_STRING
+                && isset($this->tokens[$key + 2])
+                && $this->tokens[++$key] === '-'
+                && $this->tokens[++$key][0] === T_STRING
+            ) {
+                $token[1] .= '-' . $this->tokens[$key][1];
+                next($this->tokens);
+                next($this->tokens);
+            }
+        } while (is_array($token) && $token[0] === T_WHITESPACE);
+
+        if (is_string($token) || $token === false) {
+            $this->token_type  = $token;
+            $this->token_value = null;
+        } else {
+            $this->token_type = $token[0];
+
+            $this->token_value = match ($token[0]) {
+                T_CONSTANT_ENCAPSED_STRING => stripcslashes(substr($token[1], 1, -1)),
+                T_DNUMBER => (double) $token[1],
+                T_LNUMBER => (int) $token[1],
+                default => $token[1],
+            };
+        }
+
+        return $this->token_type;
+    }
+
+    /**
+     * Returns the current token type. The valid token types are
+     * those defined for token_get_all() in the PHP documentation.
+     */
+    public function tokenType(): mixed
+    {
+        return $this->token_type;
+    }
+
+    /**
+     * Returns the current token value if the token type supports
+     * a value (T_STRING, T_LNUMBER etc.). Returns null otherwise.
+     */
+    public function tokenValue(): mixed
+    {
+        return $this->token_value;
+    }
+}
diff --git a/lib/exTpl/SymbolExpression.php b/lib/exTpl/SymbolExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..178a51f0a3c20e891e9a5b41c368bd5ea0251a4f
--- /dev/null
+++ b/lib/exTpl/SymbolExpression.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * SymbolExpression represents a symbol (template variable).
+ */
+class SymbolExpression implements Expression
+{
+    protected string $name;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param string $name      symbol name
+     */
+    public function __construct(string $name)
+    {
+        $this->name = $name;
+    }
+
+    /**
+     * Returns the name of this symbol.
+     */
+    public function name(): string
+    {
+        return $this->name;
+    }
+
+    /**
+     * Returns the value of this expression.
+     *
+     * @param Context $context  symbol table
+     */
+    public function value(Context $context): mixed
+    {
+        return $context->lookup($this->name);
+    }
+}
diff --git a/vendor/exTpl/Template.php b/lib/exTpl/Template.php
similarity index 76%
rename from vendor/exTpl/Template.php
rename to lib/exTpl/Template.php
index 9d5be04be159aa9614d743c2a586ba4963be3c7b..274e1156e34e352d38251e4dbd974137ecf54cb0 100644
--- a/vendor/exTpl/Template.php
+++ b/lib/exTpl/Template.php
@@ -12,8 +12,9 @@
 
 namespace exTpl;
 
-require_once 'Scanner.php';
-require_once 'Node.php';
+use Closure;
+use InvalidArgumentException;
+
 
 /**
  * The Template class is the only externally visible API of this
@@ -22,37 +23,39 @@ require_once 'Node.php';
  */
 class Template
 {
-    private static $tag_start = '{';
-    private static $tag_end = '}';
-    private $escape;
-    private $functions;
-    private $template;
+    private static string $tag_start = '{';
+    private static string $tag_end   = '}';
+    private Closure|string|null $escape = null;
+    private array $functions;
+    private ArrayNode $template;
 
     /**
      * Sets the delimiter strings used for the template tags, the
      * default delimiters are: $tag_start = '{', $tag_end = '}'.
      *
      * @param string $tag_start tag start marker
-     * @param string $tag_end   tag end marker
+     * @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_end = $tag_end;
+        self::$tag_end   = $tag_end;
     }
 
     /**
      * 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->functions = array(
-            'count' => function($a) { return count($a); },
-            'strlen' => function($a) { return mb_strlen($a); }
-        );
+        $this->template  = new ArrayNode();
+        $this->functions = [
+            'count'  => fn($a) => count($a),
+            'strlen' => fn($a) => mb_strlen($a),
+        ];
         self::parseTemplate($this->template, $string, 0);
     }
 
@@ -60,9 +63,9 @@ class Template
      * Enables or disables automatic escaping for template values.
      * 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') {
             $this->escape = 'htmlspecialchars';
@@ -70,8 +73,8 @@ class Template
             $this->escape = 'json_encode';
         } else if (is_callable($escape)) {
             $this->escape = $escape;
-        } else if ($escape === NULL) {
-            $this->escape = NULL;
+        } else if ($escape === null) {
+            $this->escape = null;
         } else {
             throw new InvalidArgumentException("invalid escape strategy: $escape");
         }
@@ -81,11 +84,11 @@ class Template
      * Renders the template to a string using the given array of
      * 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
      */
-    public function render(array $bindings)
+    public function render(array $bindings): string
     {
         $context = new Context($bindings + $this->functions);
         $context->autoescape($this->escape);
@@ -96,15 +99,14 @@ class Template
     /**
      * Skips tokens until the end of the current tag is reached.
      *
-     * @param string $string    template text
-     * @param int    $pos       offset in string
+     * @param string $string template text
+     * @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 &&
-                substr_compare($string, self::$tag_end, $pos, strlen(self::$tag_end)); ++$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) {
@@ -128,8 +130,9 @@ class Template
      * @param int       $pos    offset in 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);
 
@@ -147,15 +150,15 @@ class Template
                 $node->addChild($child);
             }
 
-            $pos = $next_pos + strlen(self::$tag_start);
+            $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);
+            $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);
+                    $expr     = self::parseExpr($scanner);
                     $key_name = 'index';
                     $val_name = 'this';
 
@@ -183,15 +186,16 @@ class Template
                     }
 
                     $child = new IteratorNode($expr, $key_name, $val_name);
-                    $pos = self::parseTemplate($child, $string, $pos);
+                    $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);
+                    $pos   = self::parseTemplate($child, $string, $pos);
                     $node->addChild($child);
                     break;
                 case T_ELSEIF:
@@ -204,8 +208,6 @@ class Template
                     $scanner->nextToken();
                     $node->addElse();
                     break;
-                case T_ENDIF:
-                    return $pos;
                 default:
                     $child = new ExpressionNode(self::parseExpr($scanner));
                     $node->addChild($child);
@@ -221,8 +223,10 @@ class Template
 
     /**
      * value: NUMBER | STRING | SYMBOL | '(' expr ')'
+     *
+     * @throws TemplateParserException
      */
-    private static function parseValue(Scanner $scanner)
+    private static function parseValue(Scanner $scanner): mixed
     {
         switch ($scanner->tokenType()) {
             case T_CONSTANT_ENCAPSED_STRING:
@@ -251,15 +255,17 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '(') {
             $scanner->nextToken();
-            $arguments = array();
+            $arguments = [];
 
             if ($scanner->tokenType() !== ')') {
                 $arguments[] = self::parseExpr($scanner);
@@ -276,7 +282,7 @@ class Template
 
             $scanner->nextToken();
             $result = new FunctionExpression($result, $arguments);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -284,11 +290,13 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '[' || $type === '.') {
             $scanner->nextToken();
@@ -315,11 +323,13 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '|') {
             $scanner->nextToken();
@@ -328,8 +338,8 @@ class Template
                 throw new TemplateParserException('symbol expected', $scanner);
             }
 
-            $arguments = array($result);
-            $symbol = new SymbolExpression($scanner->tokenValue());
+            $arguments = [$result];
+            $symbol    = new SymbolExpression($scanner->tokenValue());
             $scanner->nextToken();
 
             if ($scanner->tokenType() === '(') {
@@ -365,8 +375,10 @@ class Template
 
     /**
      * sign: '!' sign | '+' sign | '-' sign | filter
+     *
+     * @throws TemplateParserException
      */
-    private static function parseSign(Scanner $scanner)
+    private static function parseSign(Scanner $scanner): mixed
     {
         switch ($scanner->tokenType()) {
             case '!':
@@ -390,16 +402,18 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '*' || $type === '/' || $type === '%') {
             $scanner->nextToken();
             $result = new ArithExpression($result, self::parseSign($scanner), $type);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -407,16 +421,18 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '+' || $type === '-' || $type === '~') {
             $scanner->nextToken();
             $result = new ArithExpression($result, self::parseProduct($scanner), $type);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -425,17 +441,19 @@ class Template
     /**
      * 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)
+    private static function parseLtGt(Scanner $scanner): mixed
     {
         $result = self::parseSum($scanner);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === '<' || $type === T_IS_SMALLER_OR_EQUAL ||
-               $type === '>' || $type === T_IS_GREATER_OR_EQUAL) {
+            $type === '>' || $type === T_IS_GREATER_OR_EQUAL) {
             $scanner->nextToken();
             $result = new BooleanExpression($result, self::parseSum($scanner), $type);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -443,16 +461,18 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $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();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -460,16 +480,18 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === T_BOOLEAN_AND) {
             $scanner->nextToken();
             $result = new BooleanExpression($result, self::parseCmp($scanner), $type);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -477,16 +499,18 @@ class Template
 
     /**
      * 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);
-        $type = $scanner->tokenType();
+        $type   = $scanner->tokenType();
 
         while ($type === T_BOOLEAN_OR) {
             $scanner->nextToken();
             $result = new BooleanExpression($result, self::parseAnd($scanner), $type);
-            $type = $scanner->tokenType();
+            $type   = $scanner->tokenType();
         }
 
         return $result;
@@ -494,8 +518,10 @@ class Template
 
     /**
      * 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);
 
@@ -519,17 +545,3 @@ class Template
         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/lib/exTpl/TemplateParserException.php b/lib/exTpl/TemplateParserException.php
new file mode 100644
index 0000000000000000000000000000000000000000..2c7abf88f7227ead22c66161d3904c829aa61c89
--- /dev/null
+++ b/lib/exTpl/TemplateParserException.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace exTpl;
+
+use Exception;
+
+/**
+ * Exception class used to report template parse errors.
+ */
+class TemplateParserException extends Exception
+{
+    public function __construct(string $message, Scanner $scanner)
+    {
+        $type  = $scanner->tokenType();
+        $value = is_int($type) ? $scanner->tokenValue() : $type;
+
+        return parent::__construct("$message at \"$value\"");
+    }
+}
diff --git a/lib/exTpl/TextNode.php b/lib/exTpl/TextNode.php
new file mode 100644
index 0000000000000000000000000000000000000000..053cafc04dc28746f96e2b18638e4e897ec34bdb
--- /dev/null
+++ b/lib/exTpl/TextNode.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * TextNode represents a verbatim text segment.
+ */
+class TextNode implements Node
+{
+    protected string $text;
+
+    /**
+     * Initializes a new Node instance with the given text.
+     *
+     * @param string $text verbatim text
+     */
+    public function __construct(string $text)
+    {
+        $this->text = $text;
+    }
+
+    /**
+     * Returns a string representation of this node.
+     *
+     * @param Context $context symbol table
+     */
+    public function render(Context $context): string
+    {
+        return $this->text;
+    }
+}
diff --git a/lib/exTpl/UnaryExpression.php b/lib/exTpl/UnaryExpression.php
new file mode 100644
index 0000000000000000000000000000000000000000..997931160793fccb3fa5ad7fa7fd0d639da6c709
--- /dev/null
+++ b/lib/exTpl/UnaryExpression.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace exTpl;
+
+/**
+ * UnaryExpression represents a unary operator.
+ */
+abstract class UnaryExpression implements Expression
+{
+    protected Expression $expr;
+
+    /**
+     * Initializes a new Expression instance.
+     *
+     * @param Expression $expr  expression object
+     */
+    public function __construct(Expression $expr)
+    {
+        $this->expr = $expr;
+    }
+}
diff --git a/lib/extern/ExternPage.php b/lib/extern/ExternPage.php
index d775bc4ac59658ea8dbeef8de0964f8d50e9fdf8..308caebad8b82624f9af234acc1f508050487f3e 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 e5299a6df17e6a12123e3297f6ea6037780066a5..3aa1144f66872c84576d8bea631eee01ee697c72 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/vendor/exTpl/template_test.php b/tests/unit/lib/classes/extTPLTemplateTest.php
similarity index 98%
rename from vendor/exTpl/template_test.php
rename to tests/unit/lib/classes/extTPLTemplateTest.php
index 62aee1c555bc18a4cdbee81784e2c76b53bda429..43a9629c948135f88e3b5c990681592745ab5d42 100644
--- a/vendor/exTpl/template_test.php
+++ b/tests/unit/lib/classes/extTPLTemplateTest.php
@@ -10,15 +10,13 @@
  * the License, or (at your option) any later version.
  */
 
-require 'Template.php';
-
 use exTpl\Template;
 
-class template_test extends PHPUnit\Framework\TestCase
+class extTplTemplateTest extends \Codeception\Test\Unit
 {
     public function testSimpleString()
     {
-        $bindings = array();
+        $bindings = [];
         $template = 'The quick brown fox jumps over the layz dog.';
         $expected = $template;
         $tmpl_obj = new Template($template);
diff --git a/vendor/exTpl/Expression.php b/vendor/exTpl/Expression.php
deleted file mode 100644
index 35ea4581001f316024ae8f380cd237b3b3c503c0..0000000000000000000000000000000000000000
--- a/vendor/exTpl/Expression.php
+++ /dev/null
@@ -1,329 +0,0 @@
-<?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);
-}
-
-/**
- * ConstantExpression represents a literal value.
- */
-class ConstantExpression implements Expression
-{
-    protected $value;
-
-    /**
-     * Initializes a new Expression instance.
-     *
-     * @param mixed $value      expression value
-     */
-    public function __construct($value)
-    {
-        $this->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 0dbd9463d0ca8ff308b8077efed076c81018ecc1..0000000000000000000000000000000000000000
--- a/vendor/exTpl/Node.php
+++ /dev/null
@@ -1,234 +0,0 @@
-<?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;
-
-require_once 'Context.php';
-require_once 'Expression.php';
-
-/**
- * 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);
-}
-
-/**
- * TextNode represents a verbatim text segment.
- */
-class TextNode implements Node
-{
-    protected $text;
-
-    /**
-     * Initializes a new Node instance with the given text.
-     *
-     * @param string $text      verbatim text
-     */
-    public function __construct($text)
-    {
-        $this->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 b2464fbb4410a31dc82c68e70fa2d7e34fe7f184..0000000000000000000000000000000000000000
--- a/vendor/exTpl/Scanner.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-/*
- * Scanner.php - template parser lexical scanner
- *
- * 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;
-
-/**
- * Simple wrapper class around the Zend engine's lexical scanner. It
- * automatically skips whitespace and offers an interator interface.
- */
-class Scanner
-{
-    private $tokens;
-    private $token_type;
-    private $token_value;
-
-    /**
-     * Initializes a new Scanner instance for the given text.
-     *
-     * @param string $text      string to parse
-     */
-    public function __construct($text)
-    {
-        $this->tokens = token_get_all('<?php ' . $text);
-    }
-
-    /**
-     * Advances the scanner to the next token and returns its token type.
-     * The valid token types are those defined for token_get_all() in the
-     * PHP documentation. Returns false when the end of input is reached.
-     */
-    public function nextToken()
-    {
-        do {
-            $token = next($this->tokens);
-            $key = key($this->tokens);
-
-            // FIXME this workaround should be dropped
-            while ($token && $token[0] === T_STRING &&
-                   isset($this->tokens[$key + 2]) &&
-                   $this->tokens[++$key] === '-' &&
-                   $this->tokens[++$key][0] === T_STRING) {
-                $token[1] .= '-' . $this->tokens[$key][1];
-                next($this->tokens);
-                next($this->tokens);
-            }
-        } while (is_array($token) && $token[0] === T_WHITESPACE);
-
-        if (is_string($token) || $token === false) {
-            $this->token_type = $token;
-            $this->token_value = NULL;
-        } else {
-            $this->token_type = $token[0];
-
-            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;
-    }
-}