diff --git a/lib/Twig/Extensions/Grammar.php b/lib/Twig/Extensions/Grammar.php new file mode 100644 index 00000000..4d031b19 --- /dev/null +++ b/lib/Twig/Extensions/Grammar.php @@ -0,0 +1,30 @@ +name = $name; + } + + public function setParser(Twig_ParserInterface $parser) + { + $this->parser = $parser; + } + + public function getName() + { + return $this->name; + } +} diff --git a/lib/Twig/Extensions/Grammar/Arguments.php b/lib/Twig/Extensions/Grammar/Arguments.php new file mode 100644 index 00000000..158c05ac --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Arguments.php @@ -0,0 +1,22 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + return $this->parser->getExpressionParser()->parseArguments(); + } +} diff --git a/lib/Twig/Extensions/Grammar/Array.php b/lib/Twig/Extensions/Grammar/Array.php new file mode 100644 index 00000000..34aece0f --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Array.php @@ -0,0 +1,22 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + return $this->parser->getExpressionParser()->parseArrayExpression(); + } +} diff --git a/lib/Twig/Extensions/Grammar/Body.php b/lib/Twig/Extensions/Grammar/Body.php new file mode 100644 index 00000000..ae93e40d --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Body.php @@ -0,0 +1,39 @@ +end = null === $end ? 'end'.$name : $end; + } + + public function __toString() + { + return sprintf('<%s:body>', $this->name); + } + + public function parse(Twig_Token $token) + { + $stream = $this->parser->getStream(); + $stream->expect(Twig_Token::BLOCK_END_TYPE); + + return $this->parser->subparse(array($this, 'decideBlockEnd'), true); + } + + public function decideBlockEnd($token) + { + return $token->test($this->end); + } +} diff --git a/lib/Twig/Extensions/Grammar/Boolean.php b/lib/Twig/Extensions/Grammar/Boolean.php new file mode 100644 index 00000000..c0048090 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Boolean.php @@ -0,0 +1,24 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, array('true', 'false')); + + return new Twig_Node_Expression_Constant('true' === $token->getValue() ? true : false, $token->getLine()); + } +} diff --git a/lib/Twig/Extensions/Grammar/Constant.php b/lib/Twig/Extensions/Grammar/Constant.php new file mode 100644 index 00000000..9df60458 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Constant.php @@ -0,0 +1,37 @@ +name = $name; + $this->type = null === $type ? Twig_Token::NAME_TYPE : $type; + } + + public function __toString() + { + return $this->name; + } + + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect($this->type, $this->name); + + return $this->name; + } + + public function getType() + { + return $this->type; + } +} diff --git a/lib/Twig/Extensions/Grammar/Expression.php b/lib/Twig/Extensions/Grammar/Expression.php new file mode 100644 index 00000000..4c33df0e --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Expression.php @@ -0,0 +1,22 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + return $this->parser->getExpressionParser()->parseExpression(); + } +} diff --git a/lib/Twig/Extensions/Grammar/Hash.php b/lib/Twig/Extensions/Grammar/Hash.php new file mode 100644 index 00000000..98b07d20 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Hash.php @@ -0,0 +1,22 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + return $this->parser->getExpressionParser()->parseHashExpression(); + } +} diff --git a/lib/Twig/Extensions/Grammar/Number.php b/lib/Twig/Extensions/Grammar/Number.php new file mode 100644 index 00000000..f0857d20 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Number.php @@ -0,0 +1,24 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect(Twig_Token::NUMBER_TYPE); + + return new Twig_Node_Expression_Constant($token->getValue(), $token->getLine()); + } +} diff --git a/lib/Twig/Extensions/Grammar/Optional.php b/lib/Twig/Extensions/Grammar/Optional.php new file mode 100644 index 00000000..da427485 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Optional.php @@ -0,0 +1,69 @@ +grammar = array(); + foreach (func_get_args() as $grammar) { + $this->addGrammar($grammar); + } + } + + public function __toString() + { + $repr = array(); + foreach ($this->grammar as $grammar) { + $repr[] = (string) $grammar; + } + + return sprintf('[%s]', implode(' ', $repr)); + } + + public function addGrammar(Twig_Extensions_GrammarInterface $grammar) + { + $this->grammar[] = $grammar; + } + + public function parse(Twig_Token $token) + { + // test if we have the optional element before consuming it + if ($this->grammar[0] instanceof Twig_Extensions_Grammar_Constant) { + if (!$this->parser->getStream()->test($this->grammar[0]->getType(), $this->grammar[0]->getName())) { + return array(); + } + } elseif ($this->grammar[0] instanceof Twig_Extensions_Grammar_Name) { + if (!$this->parser->getStream()->test(Twig_Token::NAME_TYPE)) { + return array(); + } + } elseif ($this->parser->getStream()->test(Twig_Token::BLOCK_END_TYPE)) { + // if this is not a Constant or a Name, it must be the last element of the tag + + return array(); + } + + $elements = array(); + foreach ($this->grammar as $grammar) { + $grammar->setParser($this->parser); + + $element = $grammar->parse($token); + if (is_array($element)) { + $elements = array_merge($elements, $element); + } else { + $elements[$grammar->getName()] = $element; + } + } + + return $elements; + } +} diff --git a/lib/Twig/Extensions/Grammar/Switch.php b/lib/Twig/Extensions/Grammar/Switch.php new file mode 100644 index 00000000..4245f2c8 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Switch.php @@ -0,0 +1,24 @@ +', $this->name); + } + + public function parse(Twig_Token $token) + { + $this->parser->getStream()->expect(Twig_Token::NAME_TYPE, $this->name); + + return new Twig_Node_Expression_Constant(true, $token->getLine()); + } +} diff --git a/lib/Twig/Extensions/Grammar/Tag.php b/lib/Twig/Extensions/Grammar/Tag.php new file mode 100644 index 00000000..727f2610 --- /dev/null +++ b/lib/Twig/Extensions/Grammar/Tag.php @@ -0,0 +1,56 @@ +grammar = array(); + foreach (func_get_args() as $grammar) { + $this->addGrammar($grammar); + } + } + + public function __toString() + { + $repr = array(); + foreach ($this->grammar as $grammar) { + $repr[] = (string) $grammar; + } + + return implode(' ', $repr); + } + + public function addGrammar(Twig_Extensions_GrammarInterface $grammar) + { + $this->grammar[] = $grammar; + } + + public function parse(Twig_Token $token) + { + $elements = array(); + foreach ($this->grammar as $grammar) { + $grammar->setParser($this->parser); + + $element = $grammar->parse($token); + if (is_array($element)) { + $elements = array_merge($elements, $element); + } else { + $elements[$grammar->getName()] = $element; + } + } + + $this->parser->getStream()->expect(Twig_Token::BLOCK_END_TYPE); + + return $elements; + } +} diff --git a/lib/Twig/Extensions/GrammarInterface.php b/lib/Twig/Extensions/GrammarInterface.php new file mode 100644 index 00000000..22713bf2 --- /dev/null +++ b/lib/Twig/Extensions/GrammarInterface.php @@ -0,0 +1,18 @@ +getGrammar(); + if (!is_object($grammar)) { + $grammar = self::parseGrammar($grammar); + } + + $grammar->setParser($this->parser); + $values = $grammar->parse($token); + + return $this->getNode($values, $token->getLine()); + } + + /** + * Gets the grammar as an object or as a string. + * + * @return string|Twig_Extensions_Grammar A Twig_Extensions_Grammar instance or a string + */ + abstract protected function getGrammar(); + + /** + * Gets the nodes based on the parsed values. + * + * @param array $values An array of values + * @param integer $line The parser line + */ + abstract protected function getNode(array $values, $line); + + protected function getAttribute($node, $attribute, $arguments = array(), $type = Twig_Node_Expression_GetAttr::TYPE_ANY, $line = -1) + { + return new Twig_Node_Expression_GetAttr( + $node instanceof Twig_NodeInterface ? $node : new Twig_Node_Expression_Name($node, $line), + $attribute instanceof Twig_NodeInterface ? $attribute : new Twig_Node_Expression_Constant($attribute, $line), + $arguments instanceof Twig_NodeInterface ? $arguments : new Twig_Node($arguments), + $type, + $line + ); + } + + protected function call($node, $attribute, $arguments = array(), $line = -1) + { + return $this->getAttribute($node, $attribute, $arguments, Twig_Node_Expression_GetAttr::TYPE_METHOD, $line); + } + + protected function markAsSafe(Twig_NodeInterface $node, $line = -1) + { + return new Twig_Node_Expression_Filter( + $node, + new Twig_Node_Expression_Constant('raw', $line), + new Twig_Node(), + $line + ); + } + + protected function output(Twig_NodeInterface $node, $line = -1) + { + return new Twig_Node_Print($node, $line); + } + + protected function getNodeValues(array $values) + { + $nodes = array(); + foreach ($values as $value) { + if ($value instanceof Twig_NodeInterface) { + $nodes[] = $value; + } + } + + return $nodes; + } + + static public function parseGrammar($str, $main = true) + { + static $cursor; + + if (true === $main) { + $cursor = 0; + $grammar = new Twig_Extensions_Grammar_Tag(); + } else { + $grammar = new Twig_Extensions_Grammar_Optional(); + } + + while ($cursor < strlen($str)) { + if (preg_match('/\s+/A', $str, $match, null, $cursor)) { + $cursor += strlen($match[0]); + } elseif (preg_match('/<(\w+)(?:\:(\w+))?>/A', $str, $match, null, $cursor)) { + $class = sprintf('Twig_Extensions_Grammar_%s', ucfirst(isset($match[2]) ? $match[2] : 'Expression')); + if (!class_exists($class)) { + throw new Twig_Error_Runtime(sprintf('Unable to understand "%s" in grammar (%s class does not exist)', $match[0], $class)); + } + $grammar->addGrammar(new $class($match[1])); + $cursor += strlen($match[0]); + } elseif (preg_match('/\w+/A', $str, $match, null, $cursor)) { + $grammar->addGrammar(new Twig_Extensions_Grammar_Constant($match[0])); + $cursor += strlen($match[0]); + } elseif (preg_match('/,/A', $str, $match, null, $cursor)) { + $grammar->addGrammar(new Twig_Extensions_Grammar_Constant($match[0], Twig_Token::PUNCTUATION_TYPE)); + $cursor += strlen($match[0]); + } elseif (preg_match('/\[/A', $str, $match, null, $cursor)) { + $cursor += strlen($match[0]); + $grammar->addGrammar(self::parseGrammar($str, false)); + } elseif (true !== $main && preg_match('/\]/A', $str, $match, null, $cursor)) { + $cursor += strlen($match[0]); + + return $grammar; + } else { + throw new Twig_Error_Runtime(sprintf('Unable to parse grammar "%s" near "...%s..."', $str, substr($str, $cursor, 10))); + } + } + + return $grammar; + } +} diff --git a/test/Twig/Tests/Grammar/ArgumentsTest.php b/test/Twig/Tests/Grammar/ArgumentsTest.php new file mode 100644 index 00000000..a6ee6121 --- /dev/null +++ b/test/Twig/Tests/Grammar/ArgumentsTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/ArrayTest.php b/test/Twig/Tests/Grammar/ArrayTest.php new file mode 100644 index 00000000..66f7a6ab --- /dev/null +++ b/test/Twig/Tests/Grammar/ArrayTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/BodyTest.php b/test/Twig/Tests/Grammar/BodyTest.php new file mode 100644 index 00000000..6ba5a9c6 --- /dev/null +++ b/test/Twig/Tests/Grammar/BodyTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/BooleanTest.php b/test/Twig/Tests/Grammar/BooleanTest.php new file mode 100644 index 00000000..4894d511 --- /dev/null +++ b/test/Twig/Tests/Grammar/BooleanTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/ConstantTest.php b/test/Twig/Tests/Grammar/ConstantTest.php new file mode 100644 index 00000000..d5d89b19 --- /dev/null +++ b/test/Twig/Tests/Grammar/ConstantTest.php @@ -0,0 +1,19 @@ +assertEquals('foo', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/ExpressionTest.php b/test/Twig/Tests/Grammar/ExpressionTest.php new file mode 100644 index 00000000..5323a21c --- /dev/null +++ b/test/Twig/Tests/Grammar/ExpressionTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/NumberTest.php b/test/Twig/Tests/Grammar/NumberTest.php new file mode 100644 index 00000000..a803904e --- /dev/null +++ b/test/Twig/Tests/Grammar/NumberTest.php @@ -0,0 +1,19 @@ +assertEquals('', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/OptionalTest.php b/test/Twig/Tests/Grammar/OptionalTest.php new file mode 100644 index 00000000..704c294a --- /dev/null +++ b/test/Twig/Tests/Grammar/OptionalTest.php @@ -0,0 +1,19 @@ +assertEquals('[foo ]', (string) $grammar); + } +} diff --git a/test/Twig/Tests/Grammar/TagTest.php b/test/Twig/Tests/Grammar/TagTest.php new file mode 100644 index 00000000..3254d1ba --- /dev/null +++ b/test/Twig/Tests/Grammar/TagTest.php @@ -0,0 +1,23 @@ +assertEquals('foo [foo ]', (string) $grammar); + } +} diff --git a/test/Twig/Tests/SimpleTokenParser.php b/test/Twig/Tests/SimpleTokenParser.php new file mode 100644 index 00000000..245aae10 --- /dev/null +++ b/test/Twig/Tests/SimpleTokenParser.php @@ -0,0 +1,48 @@ +tag = $tag; + $this->grammar = $grammar; + } + + public function getGrammar() + { + return $this->grammar; + } + + public function getTag() + { + return $this->tag; + } + + public function getNode(array $values, $line) + { + $nodes = array(); + $nodes[] = new Twig_Node_Print(new Twig_Node_Expression_Constant('|', $line), $line); + foreach ($values as $value) { + if ($value instanceof Twig_NodeInterface) { + $nodes[] = new Twig_Node_Print($value, $line); + } else { + $nodes[] = new Twig_Node_Print(new Twig_Node_Expression_Constant($value, $line), $line); + } + $nodes[] = new Twig_Node_Print(new Twig_Node_Expression_Constant('|', $line), $line); + } + + return new Twig_Node($nodes); + } +} \ No newline at end of file diff --git a/test/Twig/Tests/SimpleTokenParserTest.php b/test/Twig/Tests/SimpleTokenParserTest.php new file mode 100644 index 00000000..60f8358d --- /dev/null +++ b/test/Twig/Tests/SimpleTokenParserTest.php @@ -0,0 +1,112 @@ +assertEquals($grammar, Twig_Extensions_SimpleTokenParser::parseGrammar($str), '::parseGrammar() parses a grammar'); + } + + public function testParseGrammarExceptions() + { + try { + Twig_Extensions_SimpleTokenParser::parseGrammar(''); + $this->fail(); + } catch (Exception $e) { + $this->assertEquals('Twig_Error_Runtime', get_class($e)); + } + + try { + Twig_Extensions_SimpleTokenParser::parseGrammar('fail(); + } catch (Exception $e) { + $this->assertEquals('Twig_Error_Runtime', get_class($e)); + } + + try { + Twig_Extensions_SimpleTokenParser::parseGrammar(' (with'); + $this->fail(); + } catch (Exception $e) { + $this->assertEquals('Twig_Error_Runtime', get_class($e)); + } + } + + public function getTests() + { + return array( + array('', new Twig_Extensions_Grammar_Tag()), + array('const', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Constant('const') + )), + array(' const ', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Constant('const') + )), + array('', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr') + )), + array('', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr') + )), + array(' ', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr') + )), + array('', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Number('nb') + )), + array('', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Boolean('bool') + )), + array('', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Body('content') + )), + array(' [with ]', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant('with'), + new Twig_Extensions_Grammar_Array('arguments') + ) + )), + array(' [ with ] ', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant('with'), + new Twig_Extensions_Grammar_Array('arguments') + ) + )), + array(' [with [or ]]', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant('with'), + new Twig_Extensions_Grammar_Array('arguments'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant('or'), + new Twig_Extensions_Grammar_Expression('optional') + ) + ) + )), + array(' [with [, ]]', new Twig_Extensions_Grammar_Tag( + new Twig_Extensions_Grammar_Expression('expr'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant('with'), + new Twig_Extensions_Grammar_Array('arguments'), + new Twig_Extensions_Grammar_Optional( + new Twig_Extensions_Grammar_Constant(',', Twig_Token::PUNCTUATION_TYPE), + new Twig_Extensions_Grammar_Expression('optional') + ) + ) + )), + ); + } +} diff --git a/test/Twig/Tests/grammarTest.php b/test/Twig/Tests/grammarTest.php new file mode 100644 index 00000000..0a63d13d --- /dev/null +++ b/test/Twig/Tests/grammarTest.php @@ -0,0 +1,63 @@ + false)); + $twig->addTokenParser(new SimpleTokenParser($tag, $grammar)); + + $ok = true; + try { + $template = $twig->loadTemplate($template); + } catch (Exception $e) { + $ok = false; + + if (false === $exception) { + $this->fail('Exception not expected'); + } else { + $this->assertEquals($exception, get_class($e)); + } + } + + if ($ok) { + if (false !== $exception) { + $this->fail(sprintf('Exception "%s" expected', $exception)); + } + + $actual = $template->render(array()); + $this->assertEquals($output, $actual); + } + } + + public function getTests() + { + return array( + array('foo1', '', '{% foo1 %}', '|', false), + array('foo2', '', '{% foo2 "bar" %}', '|', 'Twig_Error_Syntax'), + array('foo3', '', '{% foo3 "bar" %}', '|bar|', false), + array('foo4', '', '{% foo4 1 + 2 %}', '|3|', false), + array('foo5', '', '{% foo5 1 + 2 %}', '|3|', false), + array('foo6', '', '{% foo6 1 + 2 %}', '|3|', 'Twig_Error_Syntax'), + array('foo7', '', '{% foo7 %}', '|3|', 'Twig_Error_Syntax'), + array('foo8', '', '{% foo8 [1, 2] %}', '|Array|', false), + array('foo9', ' with ', '{% foo9 "bar" with "foobar" %}', '|bar|with|foobar|', false), + array('foo10', ' [with ]', '{% foo10 "bar" with "foobar" %}', '|bar|with|foobar|', false), + array('foo11', ' [with ]', '{% foo11 "bar" %}', '|bar|', false), + ); + } +}