* removed the sandboxed attribute of the include tag (use the new sandbox tag instead)
* refactored the Node system (if you have custom nodes, you will have to update them to use the new API)
+ * added a grammar sub-framework to ease the creation of custom tags
* fixed the for tag for large arrays (some loop variables are now only available for arrays and objects that implement the Countable interface)
* removed the Twig_Resource::resolveMissingFilter() method
* fixed the filter tag which did not apply filtering to included files
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_Grammar implements Twig_GrammarInterface
+{
+ protected $name;
+ protected $parser;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function setParser(Twig_ParserInterface $parser)
+ {
+ $this->parser = $parser;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Arguments extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return sprintf('<%s:arguments>', $this->name);
+ }
+
+ public function parse(Twig_Token $token)
+ {
+ return $this->parser->getExpressionParser()->parseArguments();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Array extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return sprintf('<%s:array>', $this->name);
+ }
+
+ public function parse(Twig_Token $token)
+ {
+ return $this->parser->getExpressionParser()->parseArrayExpression();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Body extends Twig_Grammar
+{
+ protected $end;
+
+ public function __construct($name, $end = null)
+ {
+ parent::__construct($name);
+
+ $this->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);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Boolean extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return sprintf('<%s:boolean>', $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());
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Constant extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return $this->name;
+ }
+
+ public function parse(Twig_Token $token)
+ {
+ $this->parser->getStream()->expect($this->name);
+
+ return $this->name;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Expression extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return sprintf('<%s>', $this->name);
+ }
+
+ public function parse(Twig_Token $token)
+ {
+ return $this->parser->getExpressionParser()->parseExpression();
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Number extends Twig_Grammar
+{
+ public function __toString()
+ {
+ return sprintf('<%s:number>', $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());
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Optional extends Twig_Grammar
+{
+ protected $grammar;
+
+ public function __construct()
+ {
+ $this->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_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_Grammar_Constant) {
+ if (!$this->parser->getStream()->test($this->grammar[0]->getName())) {
+ return array();
+ }
+ } elseif ($this->grammar[0] instanceof Twig_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);
+
+ $elements[$grammar->getName()] = $grammar->parse($token);
+ }
+
+ return $elements;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+class Twig_Grammar_Tag extends Twig_Grammar
+{
+ protected $grammar;
+
+ public function __construct()
+ {
+ $this->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_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;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+interface Twig_GrammarInterface
+{
+ public function setParser(Twig_ParserInterface $parser);
+
+ public function parse(Twig_Token $token);
+
+ public function getName();
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) 2010 Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+abstract class Twig_SimpleTokenParser extends Twig_TokenParser
+{
+ /**
+ * Parses a token and returns a node.
+ *
+ * @param Twig_Token $token A Twig_Token instance
+ *
+ * @return Twig_NodeInterface A Twig_NodeInterface instance
+ */
+ public function parse(Twig_Token $token)
+ {
+ $grammar = $this->getGrammar();
+ if (!is_object($grammar)) {
+ $grammar = static::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_Grammar A Twig_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);
+
+ static public function parseGrammar($str, $main = true)
+ {
+ static $cursor;
+
+ if (true === $main) {
+ $cursor = 0;
+ $grammar = new Twig_Grammar_Tag();
+ } else {
+ $grammar = new Twig_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_Grammar_%s', ucfirst(isset($match[2]) ? $match[2] : 'Expression'));
+ if (!class_exists($class)) {
+ throw new InvalidArgumentException(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_Grammar_Constant($match[1]));
+ $cursor += strlen($match[0]);
+ } elseif (preg_match('/\[/A', $str, $match, null, $cursor)) {
+ $cursor += strlen($match[0]);
+ $grammar->addGrammar(static::parseGrammar($str, false));
+ } elseif (true !== $main && preg_match('/\]/A', $str, $match, null, $cursor)) {
+ $cursor += strlen($match[0]);
+
+ return $grammar;
+ } else {
+ throw new InvalidArgumentException(sprintf('Unable to parse grammar "%s" near "...%s..."', $str, substr($str, $cursor, 10)));
+ }
+ }
+
+ return $grammar;
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_ArgumentsTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Arguments('foo');
+ $this->assertEquals('<foo:arguments>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_ArrayTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Array('foo');
+ $this->assertEquals('<foo:array>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_BodyTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Body('foo');
+ $this->assertEquals('<foo:body>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_BooleanTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Boolean('foo');
+ $this->assertEquals('<foo:boolean>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_ConstantTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Constant('foo');
+ $this->assertEquals('foo', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_ExpressionTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Expression('foo');
+ $this->assertEquals('<foo>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_NumberTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Number('foo');
+ $this->assertEquals('<foo:number>', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_OptionalTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Optional(new Twig_Grammar_Constant('foo'), new Twig_Grammar_Number('bar'));
+ $this->assertEquals('[foo <bar:number>]', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_Grammar_TagTest extends PHPUnit_Framework_TestCase
+{
+ public function testMagicToString()
+ {
+ $grammar = new Twig_Grammar_Tag(
+ new Twig_Grammar_Constant('foo'),
+ new Twig_Grammar_Number('bar'),
+ new Twig_Grammar_Optional(new Twig_Grammar_Constant('foo'), new Twig_Grammar_Number('bar'))
+ );
+ $this->assertEquals('foo <bar:number> [foo <bar:number>]', (string) $grammar);
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class SimpleTokenParser extends Twig_SimpleTokenParser
+{
+ protected $tag;
+ protected $grammar;
+
+ public function __construct($tag, $grammar)
+ {
+ $this->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
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+class Twig_Tests_SimpleTokenParserTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getTests
+ */
+ public function testParseGrammar($str, $grammar)
+ {
+ $this->assertEquals($grammar, Twig_SimpleTokenParser::parseGrammar($str), '::parseGrammar() parses a grammar');
+ }
+
+ public function testParseGrammarExceptions()
+ {
+ try {
+ Twig_SimpleTokenParser::parseGrammar('<foo:foo>');
+ $this->fail();
+ } catch (Exception $e) {
+ $this->assertEquals('InvalidArgumentException', get_class($e));
+ }
+
+ try {
+ Twig_SimpleTokenParser::parseGrammar('<foo:foo');
+ $this->fail();
+ } catch (Exception $e) {
+ $this->assertEquals('InvalidArgumentException', get_class($e));
+ }
+
+ try {
+ Twig_SimpleTokenParser::parseGrammar('<foo:foo> (with');
+ $this->fail();
+ } catch (Exception $e) {
+ $this->assertEquals('InvalidArgumentException', get_class($e));
+ }
+ }
+
+ public function getTests()
+ {
+ return array(
+ array('', new Twig_Grammar_Tag()),
+ array('const', new Twig_Grammar_Tag(
+ new Twig_Grammar_Constant('const')
+ )),
+ array(' const ', new Twig_Grammar_Tag(
+ new Twig_Grammar_Constant('const')
+ )),
+ array('<expr>', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr')
+ )),
+ array('<expr:expression>', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr')
+ )),
+ array(' <expr:expression> ', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr')
+ )),
+ array('<nb:number>', new Twig_Grammar_Tag(
+ new Twig_Grammar_Number('nb')
+ )),
+ array('<bool:boolean>', new Twig_Grammar_Tag(
+ new Twig_Grammar_Boolean('bool')
+ )),
+ array('<content:body>', new Twig_Grammar_Tag(
+ new Twig_Grammar_Body('content')
+ )),
+ array('<expr:expression> [with <arguments:array>]', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr'),
+ new Twig_Grammar_Optional(
+ new Twig_Grammar_Constant('with'),
+ new Twig_Grammar_Array('arguments')
+ )
+ )),
+ array(' <expr:expression> [ with <arguments:array> ] ', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr'),
+ new Twig_Grammar_Optional(
+ new Twig_Grammar_Constant('with'),
+ new Twig_Grammar_Array('arguments')
+ )
+ )),
+ array('<expr:expression> [with <arguments:array> [or <optional:expression>]]', new Twig_Grammar_Tag(
+ new Twig_Grammar_Expression('expr'),
+ new Twig_Grammar_Optional(
+ new Twig_Grammar_Constant('with'),
+ new Twig_Grammar_Array('arguments'),
+ new Twig_Grammar_Optional(
+ new Twig_Grammar_Constant('or'),
+ new Twig_Grammar_Expression('optional')
+ )
+ )
+ )),
+ );
+ }
+}
--- /dev/null
+<?php
+
+/*
+ * This file is part of Twig.
+ *
+ * (c) Fabien Potencier
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+require_once dirname(__FILE__).'/SimpleTokenParser.php';
+
+class grammarTest extends PHPUnit_Framework_TestCase
+{
+ /**
+ * @dataProvider getTests
+ */
+ public function testGrammar($tag, $grammar, $template, $output, $exception)
+ {
+ $twig = new Twig_Environment(new Twig_Loader_String(), array('cache' => 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_SyntaxError'),
+ array('foo3', '<foo>', '{% foo3 "bar" %}', '|bar|', false),
+ array('foo4', '<foo>', '{% foo4 1 + 2 %}', '|3|', false),
+ array('foo5', '<foo:expression>', '{% foo5 1 + 2 %}', '|3|', false),
+ array('foo6', '<foo:array>', '{% foo6 1 + 2 %}', '|3|', 'Twig_SyntaxError'),
+ array('foo7', '<foo>', '{% foo7 %}', '|3|', 'Twig_SyntaxError'),
+ array('foo8', '<foo:array>', '{% foo8 [1, 2] %}', '|Array|', false),
+ array('foo9', '<foo> with <bar>', '{% foo9 "bar" with "foobar" %}', '|bar|with|foobar|', false),
+ array('foo10', '<foo> [with <bar>]', '{% foo10 "bar" with "foobar" %}', '|bar|with|foobar|', false),
+ array('foo11', '<foo> [with <bar>]', '{% foo11 "bar" %}', '|bar|', false),
+ );
+ }
+}