aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElmar Ludwig <elmar.ludwig@uni-osnabrueck.de>2022-05-11 09:09:14 +0000
committerDavid Siegfried <david.siegfried@uni-vechta.de>2022-05-11 09:09:14 +0000
commit475e030c243e153c6267ecb59d5a92a98f051041 (patch)
tree409f4997d4499d76f4e261f3ce10cebe76861d23
parent044134b190155bae9dffb9b137cb950c9fe59e67 (diff)
extend exTpl syntax, closes #896
Merge request studip/studip!507
-rw-r--r--vendor/exTpl/Context.php27
-rw-r--r--vendor/exTpl/Expression.php66
-rw-r--r--vendor/exTpl/Node.php20
-rw-r--r--vendor/exTpl/Scanner.php4
-rw-r--r--vendor/exTpl/Template.php158
-rw-r--r--vendor/exTpl/template_test.php67
6 files changed, 327 insertions, 15 deletions
diff --git a/vendor/exTpl/Context.php b/vendor/exTpl/Context.php
index 222c17f..ea94b2f 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 0885f4e..ced08b5 100644
--- a/vendor/exTpl/Expression.php
+++ b/vendor/exTpl/Expression.php
@@ -72,6 +72,14 @@ class SymbolExpression implements Expression
}
/**
+ * Returns the name of this symbol.
+ */
+ public function name()
+ {
+ return $this->name;
+ }
+
+ /**
* Returns the value of this expression.
*
* @param Context $context symbol table
@@ -133,6 +141,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.
*/
abstract class BinaryExpression implements Expression
@@ -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 5fdada2..0dbd946 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 d6ec2e6..b2464fb 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 28db8fe..0be46bf 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,10 +49,32 @@ 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 5adbb13..b794a82 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));
+ }
}