expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "foo" tag. Did you mean "for" at line 1?'); $parser->parse($stream); } public function testUnknownTagWithoutSuggestions() { $stream = new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foobar', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), ], new Source('', '')); $parser = new Parser(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unknown "foobar" tag at line 1.'); $parser->parse($stream); } public function testParseIsReentrant() { $twig = new Environment(new ArrayLoader(), [ 'autoescape' => false, 'optimizations' => 0, ]); $twig->addTokenParser(new TestTokenParser()); $parser = new Parser($twig); $parser->parse(new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'test', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::VAR_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'foo', 1), new Token(Token::VAR_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), ], new Source('', ''))); $p = new \ReflectionProperty($parser, 'parent'); $this->assertNull($p->getValue($parser)); } public function testGetVarName() { $twig = new Environment(new ArrayLoader(), [ 'autoescape' => false, 'optimizations' => 0, ]); $twig->parse($twig->tokenize(new Source(<<addToAssertionCount(1); } public function testImplicitMacroArgumentDefaultValues() { $template = '{% macro marco (po, lo = true) %}{% endmacro %}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $argumentNodes = $this->getParser() ->parse($stream) ->getNode('macros') ->getNode('marco') ->getNode('arguments') ; $this->assertTrue($argumentNodes->getNode(1)->hasAttribute('is_implicit')); $this->assertNull($argumentNodes->getNode(1)->getAttribute('value')); $this->assertFalse($argumentNodes->getNode(3)->hasAttribute('is_implicit')); $this->assertTrue($argumentNodes->getNode(3)->getAttribute('value')); } public function testEmbeddedTemplatesHaveSequentialIndices(): void { $template = new Source('{% embed "first" %}{% endembed %}{% embed "second" %}{% endembed %}', 'index'); $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize($template); $embeds = $this->getParser() ->parse($stream) ->getAttribute('embedded_templates'); $this->assertSame(1, $embeds->getNode(0)->getAttribute('index')); $this->assertSame(2, $embeds->getNode(1)->getAttribute('index')); } public function testBodyForChildTemplates() { $twig = new Environment(new ArrayLoader()); $node = $twig->parse($twig->tokenize(new Source(<<getNode('body')->getNode('0'); $this->assertCount(2, $body); $this->assertSame('extends', $body->getNode('0')->getNodeTag()); $this->assertSame('set', $body->getNode('4')->getNodeTag()); } public function testCleanupBodyForChildTemplatesWithASingleNodeBody() { $twig = new Environment(new ArrayLoader()); $twig->addTokenParser(new ParentSettingTokenParser()); $node = $twig->parse($twig->tokenize(new Source('{% set_parent %}', 'index'))); $body = $node->getNode('body')->getNode('0'); $this->assertInstanceOf(EmptyNode::class, $body); } public function testBodyForParentTemplates() { $twig = new Environment(new ArrayLoader()); $node = $twig->parse($twig->tokenize(new Source(<<getNode('body')->getNode('0'); $this->assertCount(5, $body); $this->assertSame('block', $body->getNode('0')->getNodeTag()); $this->assertInstanceOf(TextNode::class, $body->getNode('1')); $this->assertSame('set', $body->getNode('2')->getNodeTag()); $this->assertInstanceOf(TextNode::class, $body->getNode('3')); $this->assertSame('block', $body->getNode('4')->getNodeTag()); } protected function getParser() { $parser = new Parser(new Environment(new ArrayLoader())); $parser->setParent(new ConstantExpression('base.html', 1)); $p = new \ReflectionProperty($parser, 'stream'); $p->setValue($parser, new TokenStream([], new Source('', ''))); return $parser; } } class TestTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { // simulate the parsing of another template right in the middle of the parsing of the current template $this->parser->parse(new TokenStream([ new Token(Token::BLOCK_START_TYPE, '', 1), new Token(Token::NAME_TYPE, 'extends', 1), new Token(Token::STRING_TYPE, 'base', 1), new Token(Token::BLOCK_END_TYPE, '', 1), new Token(Token::EOF_TYPE, '', 1), ], new Source('', ''))); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); return new EmptyNode(1); } public function getTag(): string { return 'test'; } } class ParentSettingTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { $this->parser->setParent(new ConstantExpression('base', $token->getLine()), false); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); // returns a blank text node so the whole child body is a single removable node return new TextNode(' ', $token->getLine()); } public function getTag(): string { return 'set_parent'; } }