summaryrefslogtreecommitdiff
path: root/inc/lib/Twig/ExpressionParser.php
diff options
context:
space:
mode:
authorMichael Foster <savetheinternet@tinyboard.org>2013-08-01 15:20:12 -0400
committerMichael Foster <savetheinternet@tinyboard.org>2013-08-01 15:20:12 -0400
commit0fe552857468aa12097a74e08f51f5aaa983be07 (patch)
tree22dbca1be90d7086b20a077752552296abf23325 /inc/lib/Twig/ExpressionParser.php
parent22f3a95e0e421de05a11633eac846df96a2a4768 (diff)
Upgrade Twig library
Diffstat (limited to 'inc/lib/Twig/ExpressionParser.php')
-rw-r--r--inc/lib/Twig/ExpressionParser.php304
1 files changed, 261 insertions, 43 deletions
diff --git a/inc/lib/Twig/ExpressionParser.php b/inc/lib/Twig/ExpressionParser.php
index b4fc5d5d..9cf19344 100644
--- a/inc/lib/Twig/ExpressionParser.php
+++ b/inc/lib/Twig/ExpressionParser.php
@@ -18,8 +18,7 @@
* @see http://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
* @see http://en.wikipedia.org/wiki/Operator-precedence_parser
*
- * @package twig
- * @author Fabien Potencier <fabien@symfony.com>
+ * @author Fabien Potencier <fabien@symfony.com>
*/
class Twig_ExpressionParser
{
@@ -89,9 +88,19 @@ class Twig_ExpressionParser
{
while ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '?')) {
$this->parser->getStream()->next();
- $expr2 = $this->parseExpression();
- $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'The ternary operator must have a default value');
- $expr3 = $this->parseExpression();
+ if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+ $expr2 = $this->parseExpression();
+ if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+ $this->parser->getStream()->next();
+ $expr3 = $this->parseExpression();
+ } else {
+ $expr3 = new Twig_Node_Expression_Constant('', $this->parser->getCurrentToken()->getLine());
+ }
+ } else {
+ $this->parser->getStream()->next();
+ $expr2 = $expr;
+ $expr3 = $this->parseExpression();
+ }
$expr = new Twig_Node_Expression_Conditional($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
}
@@ -143,31 +152,67 @@ class Twig_ExpressionParser
break;
case Twig_Token::NUMBER_TYPE:
- case Twig_Token::STRING_TYPE:
$this->parser->getStream()->next();
$node = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
break;
+ case Twig_Token::STRING_TYPE:
+ case Twig_Token::INTERPOLATION_START_TYPE:
+ $node = $this->parseStringExpression();
+ break;
+
default:
if ($token->test(Twig_Token::PUNCTUATION_TYPE, '[')) {
$node = $this->parseArrayExpression();
} elseif ($token->test(Twig_Token::PUNCTUATION_TYPE, '{')) {
$node = $this->parseHashExpression();
} else {
- throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine());
+ throw new Twig_Error_Syntax(sprintf('Unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($token->getType(), $token->getLine()), $token->getValue()), $token->getLine(), $this->parser->getFilename());
}
}
return $this->parsePostfixExpression($node);
}
+ public function parseStringExpression()
+ {
+ $stream = $this->parser->getStream();
+
+ $nodes = array();
+ // a string cannot be followed by another string in a single expression
+ $nextCanBeString = true;
+ while (true) {
+ if ($stream->test(Twig_Token::STRING_TYPE) && $nextCanBeString) {
+ $token = $stream->next();
+ $nodes[] = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
+ $nextCanBeString = false;
+ } elseif ($stream->test(Twig_Token::INTERPOLATION_START_TYPE)) {
+ $stream->next();
+ $nodes[] = $this->parseExpression();
+ $stream->expect(Twig_Token::INTERPOLATION_END_TYPE);
+ $nextCanBeString = true;
+ } else {
+ break;
+ }
+ }
+
+ $expr = array_shift($nodes);
+ foreach ($nodes as $node) {
+ $expr = new Twig_Node_Expression_Binary_Concat($expr, $node, $node->getLine());
+ }
+
+ return $expr;
+ }
+
public function parseArrayExpression()
{
$stream = $this->parser->getStream();
$stream->expect(Twig_Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
- $elements = array();
+
+ $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
+ $first = true;
while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
- if (!empty($elements)) {
+ if (!$first) {
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
// trailing ,?
@@ -175,21 +220,24 @@ class Twig_ExpressionParser
break;
}
}
+ $first = false;
- $elements[] = $this->parseExpression();
+ $node->addElement($this->parseExpression());
}
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
- return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
+ return $node;
}
public function parseHashExpression()
{
$stream = $this->parser->getStream();
$stream->expect(Twig_Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
- $elements = array();
+
+ $node = new Twig_Node_Expression_Array(array(), $stream->getCurrent()->getLine());
+ $first = true;
while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, '}')) {
- if (!empty($elements)) {
+ if (!$first) {
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
// trailing ,?
@@ -197,19 +245,33 @@ class Twig_ExpressionParser
break;
}
}
+ $first = false;
- if (!$stream->test(Twig_Token::STRING_TYPE) && !$stream->test(Twig_Token::NUMBER_TYPE)) {
+ // a hash key can be:
+ //
+ // * a number -- 12
+ // * a string -- 'a'
+ // * a name, which is equivalent to a string -- a
+ // * an expression, which must be enclosed in parentheses -- (1 + 2)
+ if ($stream->test(Twig_Token::STRING_TYPE) || $stream->test(Twig_Token::NAME_TYPE) || $stream->test(Twig_Token::NUMBER_TYPE)) {
+ $token = $stream->next();
+ $key = new Twig_Node_Expression_Constant($token->getValue(), $token->getLine());
+ } elseif ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
+ $key = $this->parseExpression();
+ } else {
$current = $stream->getCurrent();
- throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string or a number (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine());
+
+ throw new Twig_Error_Syntax(sprintf('A hash key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s"', Twig_Token::typeToEnglish($current->getType(), $current->getLine()), $current->getValue()), $current->getLine(), $this->parser->getFilename());
}
- $key = $stream->next()->getValue();
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
- $elements[$key] = $this->parseExpression();
+ $value = $this->parseExpression();
+
+ $node->addElement($value, $key);
}
$stream->expect(Twig_Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
- return new Twig_Node_Expression_Array($elements, $stream->getCurrent()->getLine());
+ return $node;
}
public function parsePostfixExpression($node)
@@ -234,43 +296,56 @@ class Twig_ExpressionParser
public function getFunctionNode($name, $line)
{
- $args = $this->parseArguments();
switch ($name) {
case 'parent':
+ $args = $this->parseArguments();
if (!count($this->parser->getBlockStack())) {
- throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line);
+ throw new Twig_Error_Syntax('Calling "parent" outside a block is forbidden', $line, $this->parser->getFilename());
}
- if (!$this->parser->getParent()) {
- throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend another one is forbidden', $line);
+ if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
+ throw new Twig_Error_Syntax('Calling "parent" on a template that does not extend nor "use" another template is forbidden', $line, $this->parser->getFilename());
}
return new Twig_Node_Expression_Parent($this->parser->peekBlockStack(), $line);
case 'block':
- return new Twig_Node_Expression_BlockReference($args->getNode(0), false, $line);
+ return new Twig_Node_Expression_BlockReference($this->parseArguments()->getNode(0), false, $line);
case 'attribute':
+ $args = $this->parseArguments();
if (count($args) < 2) {
- throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attribute)', $line);
+ throw new Twig_Error_Syntax('The "attribute" function takes at least two arguments (the variable and the attributes)', $line, $this->parser->getFilename());
}
return new Twig_Node_Expression_GetAttr($args->getNode(0), $args->getNode(1), count($args) > 2 ? $args->getNode(2) : new Twig_Node_Expression_Array(array(), $line), Twig_TemplateInterface::ANY_CALL, $line);
default:
- if (null !== $alias = $this->parser->getImportedFunction($name)) {
- return new Twig_Node_Expression_GetAttr($alias['node'], new Twig_Node_Expression_Constant($alias['name'], $line), $args, Twig_TemplateInterface::METHOD_CALL, $line);
+ if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
+ $arguments = new Twig_Node_Expression_Array(array(), $line);
+ foreach ($this->parseArguments() as $n) {
+ $arguments->addElement($n);
+ }
+
+ $node = new Twig_Node_Expression_MethodCall($alias['node'], $alias['name'], $arguments, $line);
+ $node->setAttribute('safe', true);
+
+ return $node;
}
- return new Twig_Node_Expression_Function($name, $args, $line);
+ $args = $this->parseArguments(true);
+ $class = $this->getFunctionNodeClass($name, $line);
+
+ return new $class($name, $args, $line);
}
}
public function parseSubscriptExpression($node)
{
- $token = $this->parser->getStream()->next();
+ $stream = $this->parser->getStream();
+ $token = $stream->next();
$lineno = $token->getLine();
- $arguments = new Twig_Node();
+ $arguments = new Twig_Node_Expression_Array(array(), $lineno);
$type = Twig_TemplateInterface::ANY_CALL;
if ($token->getValue() == '.') {
- $token = $this->parser->getStream()->next();
+ $token = $stream->next();
if (
$token->getType() == Twig_Token::NAME_TYPE
||
@@ -280,20 +355,60 @@ class Twig_ExpressionParser
) {
$arg = new Twig_Node_Expression_Constant($token->getValue(), $lineno);
- if ($this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
+ if ($stream->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
$type = Twig_TemplateInterface::METHOD_CALL;
- $arguments = $this->parseArguments();
- } else {
- $arguments = new Twig_Node();
+ foreach ($this->parseArguments() as $n) {
+ $arguments->addElement($n);
+ }
}
} else {
- throw new Twig_Error_Syntax('Expected name or number', $lineno);
+ throw new Twig_Error_Syntax('Expected name or number', $lineno, $this->parser->getFilename());
+ }
+
+ if ($node instanceof Twig_Node_Expression_Name && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
+ if (!$arg instanceof Twig_Node_Expression_Constant) {
+ throw new Twig_Error_Syntax(sprintf('Dynamic macro names are not supported (called on "%s")', $node->getAttribute('name')), $token->getLine(), $this->parser->getFilename());
+ }
+
+ $node = new Twig_Node_Expression_MethodCall($node, 'get'.$arg->getAttribute('value'), $arguments, $lineno);
+ $node->setAttribute('safe', true);
+
+ return $node;
}
} else {
$type = Twig_TemplateInterface::ARRAY_CALL;
- $arg = $this->parseExpression();
- $this->parser->getStream()->expect(Twig_Token::PUNCTUATION_TYPE, ']');
+ // slice?
+ $slice = false;
+ if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+ $slice = true;
+ $arg = new Twig_Node_Expression_Constant(0, $token->getLine());
+ } else {
+ $arg = $this->parseExpression();
+ }
+
+ if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ':')) {
+ $slice = true;
+ $stream->next();
+ }
+
+ if ($slice) {
+ if ($stream->test(Twig_Token::PUNCTUATION_TYPE, ']')) {
+ $length = new Twig_Node_Expression_Constant(null, $token->getLine());
+ } else {
+ $length = $this->parseExpression();
+ }
+
+ $class = $this->getFilterNodeClass('slice', $token->getLine());
+ $arguments = new Twig_Node(array($arg, $length));
+ $filter = new $class($node, new Twig_Node_Expression_Constant('slice', $token->getLine()), $arguments, $token->getLine());
+
+ $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
+
+ return $filter;
+ }
+
+ $stream->expect(Twig_Token::PUNCTUATION_TYPE, ']');
}
return new Twig_Node_Expression_GetAttr($node, $arg, $arguments, $type, $lineno);
@@ -315,10 +430,12 @@ class Twig_ExpressionParser
if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '(')) {
$arguments = new Twig_Node();
} else {
- $arguments = $this->parseArguments();
+ $arguments = $this->parseArguments(true);
}
- $node = new Twig_Node_Expression_Filter($node, $name, $arguments, $token->getLine(), $tag);
+ $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
+
+ $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
if (!$this->parser->getStream()->test(Twig_Token::PUNCTUATION_TYPE, '|')) {
break;
@@ -330,17 +447,62 @@ class Twig_ExpressionParser
return $node;
}
- public function parseArguments()
+ /**
+ * Parses arguments.
+ *
+ * @param Boolean $namedArguments Whether to allow named arguments or not
+ * @param Boolean $definition Whether we are parsing arguments for a function definition
+ */
+ public function parseArguments($namedArguments = false, $definition = false)
{
$args = array();
$stream = $this->parser->getStream();
- $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must be opened by a parenthesis');
+ $stream->expect(Twig_Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
while (!$stream->test(Twig_Token::PUNCTUATION_TYPE, ')')) {
if (!empty($args)) {
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
}
- $args[] = $this->parseExpression();
+
+ if ($definition) {
+ $token = $stream->expect(Twig_Token::NAME_TYPE, null, 'An argument must be a name');
+ $value = new Twig_Node_Expression_Name($token->getValue(), $this->parser->getCurrentToken()->getLine());
+ } else {
+ $value = $this->parseExpression();
+ }
+
+ $name = null;
+ if ($namedArguments && $stream->test(Twig_Token::OPERATOR_TYPE, '=')) {
+ $token = $stream->next();
+ if (!$value instanceof Twig_Node_Expression_Name) {
+ throw new Twig_Error_Syntax(sprintf('A parameter name must be a string, "%s" given', get_class($value)), $token->getLine(), $this->parser->getFilename());
+ }
+ $name = $value->getAttribute('name');
+
+ if ($definition) {
+ $value = $this->parsePrimaryExpression();
+
+ if (!$this->checkConstantExpression($value)) {
+ throw new Twig_Error_Syntax(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $this->parser->getFilename());
+ }
+ } else {
+ $value = $this->parseExpression();
+ }
+ }
+
+ if ($definition) {
+ if (null === $name) {
+ $name = $value->getAttribute('name');
+ $value = new Twig_Node_Expression_Constant(null, $this->parser->getCurrentToken()->getLine());
+ }
+ $args[$name] = $value;
+ } else {
+ if (null === $name) {
+ $args[] = $value;
+ } else {
+ $args[$name] = $value;
+ }
+ }
}
$stream->expect(Twig_Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
@@ -353,7 +515,7 @@ class Twig_ExpressionParser
while (true) {
$token = $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, null, 'Only variables can be assigned to');
if (in_array($token->getValue(), array('true', 'false', 'none'))) {
- throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine());
+ throw new Twig_Error_Syntax(sprintf('You cannot assign a value to "%s"', $token->getValue()), $token->getLine(), $this->parser->getFilename());
}
$targets[] = new Twig_Node_Expression_AssignName($token->getValue(), $token->getLine());
@@ -379,4 +541,60 @@ class Twig_ExpressionParser
return new Twig_Node($targets);
}
+
+ protected function getFunctionNodeClass($name, $line)
+ {
+ $env = $this->parser->getEnvironment();
+
+ if (false === $function = $env->getFunction($name)) {
+ $message = sprintf('The function "%s" does not exist', $name);
+ if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFunctions()))) {
+ $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
+ }
+
+ throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
+ }
+
+ if ($function instanceof Twig_SimpleFunction) {
+ return $function->getNodeClass();
+ }
+
+ return $function instanceof Twig_Function_Node ? $function->getClass() : 'Twig_Node_Expression_Function';
+ }
+
+ protected function getFilterNodeClass($name, $line)
+ {
+ $env = $this->parser->getEnvironment();
+
+ if (false === $filter = $env->getFilter($name)) {
+ $message = sprintf('The filter "%s" does not exist', $name);
+ if ($alternatives = $env->computeAlternatives($name, array_keys($env->getFilters()))) {
+ $message = sprintf('%s. Did you mean "%s"', $message, implode('", "', $alternatives));
+ }
+
+ throw new Twig_Error_Syntax($message, $line, $this->parser->getFilename());
+ }
+
+ if ($filter instanceof Twig_SimpleFilter) {
+ return $filter->getNodeClass();
+ }
+
+ return $filter instanceof Twig_Filter_Node ? $filter->getClass() : 'Twig_Node_Expression_Filter';
+ }
+
+ // checks that the node only contains "constant" elements
+ protected function checkConstantExpression(Twig_NodeInterface $node)
+ {
+ if (!($node instanceof Twig_Node_Expression_Constant || $node instanceof Twig_Node_Expression_Array)) {
+ return false;
+ }
+
+ foreach ($node as $n) {
+ if (!$this->checkConstantExpression($n)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}