diff --git a/vendor/exTpl/Context.php b/vendor/exTpl/Context.php
index 222c17f96b1093b982d8887352a64e7fa2d6285d..ea94b2fd25f8b917bf1330814147829ed97d8e75 100644
--- a/vendor/exTpl/Context.php
+++ b/vendor/exTpl/Context.php
@@ -20,6 +20,7 @@ namespace exTpl;
 class Context
 {
     private $bindings;
+    private $escape;
     private $parent;
 
     /**
@@ -50,4 +51,30 @@ class Context
 
         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
index 0885f4eca110aadd66111b7ebfbecd33d5a78e3e..ced08b59a6875c562905f4b64216d427496b1e20 100644
--- a/vendor/exTpl/Expression.php
+++ b/vendor/exTpl/Expression.php
@@ -71,6 +71,14 @@ class SymbolExpression implements Expression
         $this->name = $name;
     }
 
+    /**
+     * Returns the name of this symbol.
+     */
+    public function name()
+    {
+        return $this->name;
+    }
+
     /**
      * Returns the value of this expression.
      *
@@ -132,6 +140,22 @@ class NotExpression extends UnaryExpression
     }
 }
 
+/**
+ * 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.
  */
@@ -261,3 +285,45 @@ class ConditionExpression implements Expression
                     $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 (is_callable($callable)) {
+            return call_user_func_array($callable, $arguments);
+        }
+
+        return NULL;
+    }
+}
diff --git a/vendor/exTpl/Node.php b/vendor/exTpl/Node.php
index 5fdada2619bec819427bb1e98ffe91c2f1a3a222..0dbd9463d0ca8ff308b8077efed076c81018ecc1 100644
--- a/vendor/exTpl/Node.php
+++ b/vendor/exTpl/Node.php
@@ -81,7 +81,13 @@ class ExpressionNode implements Node
      */
     public function render($context)
     {
-        return $this->expr->value($context);
+        $value = $this->expr->value($context);
+
+        if (!($this->expr instanceof RawExpression)) {
+            $value = $context->escape($value);
+        }
+
+        return $value;
     }
 }
 
@@ -121,20 +127,26 @@ class ArrayNode implements Node
 
 /**
  * IteratorNode represents a single iterator tag:
- * "{foreach ARRAY}...{endforeach}".
+ * "{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)
+    public function __construct(Expression $expr, $key_name, $val_name)
     {
         $this->expr = $expr;
+        $this->key_name = $key_name;
+        $this->val_name = $val_name;
     }
 
     /**
@@ -149,7 +161,7 @@ class IteratorNode extends ArrayNode
         $result = '';
 
         if (is_array($values) && is_int(key($values))) {
-            $bindings = array('index' => &$key, 'this' => &$value);
+            $bindings = array($this->key_name => &$key, $this->val_name => &$value);
             $context = new Context($bindings, $context);
 
             foreach ($values as $key => $value) {
diff --git a/vendor/exTpl/Scanner.php b/vendor/exTpl/Scanner.php
index d6ec2e696f4a2abb0aa8f9b08842a676b0e2ff82..b2464fbb4410a31dc82c68e70fa2d7e34fe7f184 100644
--- a/vendor/exTpl/Scanner.php
+++ b/vendor/exTpl/Scanner.php
@@ -43,7 +43,9 @@ class Scanner
             $token = next($this->tokens);
             $key = key($this->tokens);
 
-            while ($token[0] === T_STRING &&
+            // 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];
diff --git a/vendor/exTpl/Template.php b/vendor/exTpl/Template.php
index 28db8fe6fe730ab897fd1feed68fc46ef418f224..0be46bf85fff6eda33c39e91d687ba16fec49ef2 100644
--- a/vendor/exTpl/Template.php
+++ b/vendor/exTpl/Template.php
@@ -24,6 +24,8 @@ class Template
 {
     private static $tag_start = '{';
     private static $tag_end = '}';
+    private $escape;
+    private $functions;
     private $template;
 
     /**
@@ -47,9 +49,31 @@ class Template
     public function __construct($string)
     {
         $this->template = new ArrayNode();
+        $this->functions = array('count' => 'count', 'strlen' => 'mb_strlen');
         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.
@@ -60,7 +84,10 @@ class Template
      */
     public function render(array $bindings)
     {
-        return $this->template->render(new Context($bindings));
+        $context = new Context($bindings + $this->functions);
+        $context->autoescape($this->escape);
+
+        return $this->template->render($context);
     }
 
     /**
@@ -125,7 +152,34 @@ class Template
             switch ($scanner->nextToken()) {
                 case T_FOREACH:
                     $scanner->nextToken();
-                    $child = new IteratorNode(self::parseExpr($scanner));
+                    $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;
@@ -193,13 +247,46 @@ class Template
     }
 
     /**
-     * index: value | index '[' expr ']' | index '.' SYMBOL
+     * function: value | function '(' ')' | function '(' expr { ',' expr } ')'
      */
-    private static function parseIndex(Scanner $scanner)
+    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();
 
@@ -224,7 +311,57 @@ class Template
     }
 
     /**
-     * sign: '!' sign | '+' sign | '-' sign | index
+     * 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)
     {
@@ -242,7 +379,7 @@ class Template
                 $result = new MinusExpression(self::parseSign($scanner));
                 break;
             default:
-                $result = self::parseIndex($scanner);
+                $result = self::parseFilter($scanner);
         }
 
         return $result;
@@ -353,7 +490,7 @@ class Template
     }
 
     /**
-     * expr: or | or '?' expr ':' expr
+     * expr: or | or '?' expr ':' expr | or '?' ':' expr
      */
     private static function parseExpr(Scanner $scanner)
     {
@@ -361,7 +498,12 @@ class Template
 
         if ($scanner->tokenType() === '?') {
             $scanner->nextToken();
-            $expr = self::parseExpr($scanner);
+
+            if ($scanner->tokenType() !== ':') {
+                $expr = self::parseExpr($scanner);
+            } else {
+                $expr = $result;
+            }
 
             if ($scanner->tokenType() !== ':') {
                 throw new TemplateParserException('missing ":"', $scanner);
diff --git a/vendor/exTpl/template_test.php b/vendor/exTpl/template_test.php
index 5adbb136a34233c752e39a8e9d466a4f53f66b01..b794a820a1b032acb8b5a38ba2c43ef5754b60d9 100644
--- a/vendor/exTpl/template_test.php
+++ b/vendor/exTpl/template_test.php
@@ -14,7 +14,7 @@ require 'Template.php';
 
 use exTpl\Template;
 
-class TemplateTest extends PHPUnit_Framework_TestCase
+class template_test extends PHPUnit\Framework\TestCase
 {
     public function testSimpleString()
     {
@@ -36,6 +36,16 @@ class TemplateTest extends PHPUnit_Framework_TestCase
         $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();
@@ -93,7 +103,7 @@ class TemplateTest extends PHPUnit_Framework_TestCase
                         2 => array('user' => 'mike', 'phone' => '230-28382'),
                         3 => array('user' => 'john', 'phone' => '911-19212')
                     ));
-        $template = '<ul>{foreach persons}<li>{index~":"~this.user~":"~phone}</li>{endforeach}</ul>';
+        $template = '<ul>{foreach persons as person}<li>{index~":"~person.user~":"~phone}</li>{endforeach}</ul>';
         $expected = '<ul>' .
                     '<li>1:jane:555-81281</li>' .
                     '<li>2:mike:230-28382</li>' .
@@ -145,4 +155,57 @@ class TemplateTest extends PHPUnit_Framework_TestCase
 
         $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_number' => 'number_format', 'upper' => 'strtoupper');
+        $template = '{pi|format_number(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' => '<img>', 'upper' => 'strtoupper');
+        $template = '{foo}:{foo|upper|raw}';
+        $expected = '&lt;img&gt;:<IMG>';
+        $tmpl_obj = new Template($template);
+        $tmpl_obj->autoescape('html');
+
+        $this->assertEquals($expected, $tmpl_obj->render($bindings));
+    }
+
+    public function testHtmlAutoEscape()
+    {
+        $bindings = array('foo' => '<img>', 'pi' => 3.14159);
+        $template = '{foo}:{pi}';
+        $expected = '&lt;img&gt;:3.14159';
+        $tmpl_obj = new Template($template);
+        $tmpl_obj->autoescape('html');
+
+        $this->assertEquals($expected, $tmpl_obj->render($bindings));
+    }
+
+    public function testJsonAutoEscape()
+    {
+        $bindings = array('foo' => '<img>', 'pi' => 3.14159);
+        $template = '{foo}:{pi}';
+        $expected = '"<img>":3.14159';
+        $tmpl_obj = new Template($template);
+        $tmpl_obj->autoescape('json');
+
+        $this->assertEquals($expected, $tmpl_obj->render($bindings));
+    }
 }