tokenize(new Source($template, 'index')); $stream->expect(Token::BLOCK_START_TYPE); $this->assertSame('§', $stream->expect(Token::NAME_TYPE)->getValue()); } public function testNameLabelForFunction() { $template = '{{ §() }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $this->assertSame('§', $stream->expect(Token::NAME_TYPE)->getValue()); } public function testBracketsNesting() { $template = '{{ {"a":{"b":"c"}} }}'; $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '{')); $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}')); } public function testSpreadOperator() { $template = '{{ { a: "a", ...{ b: "b" } } }}'; $this->assertEquals(1, $this->countToken($template, Token::SPREAD_TYPE, '...')); // sanity check on lexing after spread $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '{')); $this->assertEquals(2, $this->countToken($template, Token::PUNCTUATION_TYPE, '}')); } protected function countToken($template, $type, $value = null) { $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $count = 0; while (!$stream->isEOF()) { $token = $stream->next(); if ($type === $token->getType()) { if (null === $value || $value === $token->getValue()) { ++$count; } } } return $count; } public function testLineDirective() { $template = "foo\n" ."bar\n" ."{% line 10 %}\n" ."{{\n" ."baz\n" ."}}\n"; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); // foo\nbar\n $this->assertSame(1, $stream->expect(Token::TEXT_TYPE)->getLine()); // \n (after {% line %}) $this->assertSame(10, $stream->expect(Token::TEXT_TYPE)->getLine()); // {{ $this->assertSame(11, $stream->expect(Token::VAR_START_TYPE)->getLine()); // baz $this->assertSame(12, $stream->expect(Token::NAME_TYPE)->getLine()); } public function testLineDirectiveInline() { $template = "foo\n" ."bar{% line 10 %}{{\n" ."baz\n" ."}}\n"; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); // foo\nbar $this->assertSame(1, $stream->expect(Token::TEXT_TYPE)->getLine()); // {{ $this->assertSame(10, $stream->expect(Token::VAR_START_TYPE)->getLine()); // baz $this->assertSame(11, $stream->expect(Token::NAME_TYPE)->getLine()); } public function testLongComments() { $template = '{# '.str_repeat('*', 100000).' #}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testLongVerbatim() { $template = '{% verbatim %}'.str_repeat('*', 100000).'{% endverbatim %}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testLongVar() { $template = '{{ '.str_repeat('x', 100000).' }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testLongBlock() { $template = '{% '.str_repeat('x', 100000).' %}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $lexer->tokenize(new Source($template, 'index')); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testBigNumbers() { $template = '{{ 922337203685477580700 }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->next(); $node = $stream->next(); $this->assertEquals('922337203685477580700', $node->getValue()); } /** * @dataProvider getStringWithEscapedDelimiter */ public function testStringWithEscapedDelimiter(string $template, string $expected) { $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $token = $stream->expect(Token::STRING_TYPE); $this->assertSame($expected, $token->getValue()); } public static function getStringWithEscapedDelimiter() { yield '{{ \'\x6\' }} => \x6' => [ '{{ \'\x6\' }}', "\x6", ]; yield '{{ \'\065\x64\' }} => \065\x64' => [ '{{ \'\065\x64\' }}', "\065\x64", ]; yield '{{ \'App\\\\Test\' }} => App\Test' => [ '{{ \'App\\\\Test\' }}', 'App\\Test', ]; yield '{{ "App\#{var}" }} => App#{var}' => [ '{{ "App\#{var}" }}', 'App#{var}', ]; yield '{{ \'foo \\\' bar\' }} => foo \' bar' => [ '{{ \'foo \\\' bar\' }}', 'foo \' bar', ]; yield '{{ "foo \" bar" }} => foo " bar' => [ '{{ "foo \\" bar" }}', 'foo " bar', ]; yield '{{ \'\f\n\r\t\v\' }} => \f\n\r\t\v' => [ '{{ \'\\f\\n\\r\\t\\v\' }}', "\f\n\r\t\v", ]; yield '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }} => \\f\\n\\r\\t\\v' => [ '{{ \'\\\\f\\\\n\\\\r\\\\t\\\\v\' }}', '\\f\\n\\r\\t\\v', ]; } /** * @group legacy * @dataProvider getStringWithEscapedDelimiterProducingDeprecation */ public function testStringWithEscapedDelimiterProducingDeprecation(string $template, string $expected, string $expectedDeprecation) { $this->expectDeprecation($expectedDeprecation); $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, $expected); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public static function getStringWithEscapedDelimiterProducingDeprecation() { yield '{{ \'App\Test\' }} => AppTest' => [ '{{ \'App\\Test\' }}', 'AppTest', 'Since twig/twig 3.12: Character "T" at position 5 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; yield '{{ "foo \\\' bar" }} => foo \' bar' => [ '{{ "foo \\\' bar" }}', 'foo \' bar', 'Since twig/twig 3.12: Character "\'" at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; yield '{{ \'foo \" bar\' }} => foo " bar' => [ '{{ \'foo \\" bar\' }}', 'foo " bar', 'Since twig/twig 3.12: Character """ at position 6 should not be escaped; the "\" character is ignored in Twig v3 but will not be in v4. Please remove the extra "\" character.', ]; } public function testStringWithInterpolation() { $template = 'foo {{ "bar #{ baz + 1 }" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::TEXT_TYPE, 'foo '); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar '); $stream->expect(Token::INTERPOLATION_START_TYPE); $stream->expect(Token::NAME_TYPE, 'baz'); $stream->expect(Token::OPERATOR_TYPE, '+'); $stream->expect(Token::NUMBER_TYPE, '1'); $stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(Token::VAR_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testStringWithEscapedInterpolation() { $template = '{{ "bar \#{baz+1}" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar #{baz+1}'); $stream->expect(Token::VAR_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testStringWithHash() { $template = '{{ "bar # baz" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar # baz'); $stream->expect(Token::VAR_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testStringWithUnterminatedInterpolation() { $template = '{{ "bar #{x" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unclosed """'); $lexer->tokenize(new Source($template, 'index')); } public function testStringWithNestedInterpolations() { $template = '{{ "bar #{ "foo#{bar}" }" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, 'bar '); $stream->expect(Token::INTERPOLATION_START_TYPE); $stream->expect(Token::STRING_TYPE, 'foo'); $stream->expect(Token::INTERPOLATION_START_TYPE); $stream->expect(Token::NAME_TYPE, 'bar'); $stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(Token::VAR_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testStringWithNestedInterpolationsInBlock() { $template = '{% foo "bar #{ "foo#{bar}" }" %}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::BLOCK_START_TYPE); $stream->expect(Token::NAME_TYPE, 'foo'); $stream->expect(Token::STRING_TYPE, 'bar '); $stream->expect(Token::INTERPOLATION_START_TYPE); $stream->expect(Token::STRING_TYPE, 'foo'); $stream->expect(Token::INTERPOLATION_START_TYPE); $stream->expect(Token::NAME_TYPE, 'bar'); $stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(Token::INTERPOLATION_END_TYPE); $stream->expect(Token::BLOCK_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testOperatorEndingWithALetterAtTheEndOfALine() { $template = "{{ 1 and\n0}}"; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::NUMBER_TYPE, 1); $stream->expect(Token::OPERATOR_TYPE, 'and'); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public function testUnterminatedVariable() { $template = ' {{ bar '; $lexer = new Lexer(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unclosed "variable" in "index" at line 3'); $lexer->tokenize(new Source($template, 'index')); } public function testUnterminatedBlock() { $template = ' {% bar '; $lexer = new Lexer(new Environment(new ArrayLoader())); $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Unclosed "block" in "index" at line 3'); $lexer->tokenize(new Source($template, 'index')); } public function testOverridingSyntax() { $template = '[# comment #]{# variable #}/# if true #/true/# endif #/'; $lexer = new Lexer(new Environment(new ArrayLoader()), [ 'tag_comment' => ['[#', '#]'], 'tag_block' => ['/#', '#/'], 'tag_variable' => ['{#', '#}'], ]); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::NAME_TYPE, 'variable'); $stream->expect(Token::VAR_END_TYPE); $stream->expect(Token::BLOCK_START_TYPE); $stream->expect(Token::NAME_TYPE, 'if'); $stream->expect(Token::NAME_TYPE, 'true'); $stream->expect(Token::BLOCK_END_TYPE); $stream->expect(Token::TEXT_TYPE, 'true'); $stream->expect(Token::BLOCK_START_TYPE); $stream->expect(Token::NAME_TYPE, 'endif'); $stream->expect(Token::BLOCK_END_TYPE); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } /** * @dataProvider getTemplateForErrorsAtTheEndOfTheStream */ public function testErrorsAtTheEndOfTheStream(string $template) { $lexer = new Lexer(new Environment(new ArrayLoader())); set_error_handler(function () { $this->fail('Lexer should not emit warnings.'); }); try { $lexer->tokenize(new Source($template, 'index')); $this->addToAssertionCount(1); } finally { restore_error_handler(); } } public static function getTemplateForErrorsAtTheEndOfTheStream() { yield ['{{ =']; yield ['{{ ..']; } /** * @dataProvider getTemplateForStrings */ public function testStrings(string $expected) { $template = '{{ "'.$expected.'" }}'; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, $expected); $template = "{{ '".$expected."' }}"; $lexer = new Lexer(new Environment(new ArrayLoader())); $stream = $lexer->tokenize(new Source($template, 'index')); $stream->expect(Token::VAR_START_TYPE); $stream->expect(Token::STRING_TYPE, $expected); // add a dummy assertion here to satisfy PHPUnit, the only thing we want to test is that the code above // can be executed without throwing any exceptions $this->addToAssertionCount(1); } public static function getTemplateForStrings() { yield ['日本では、春になると桜の花が咲きます。多くの人々は、公園や川の近くに集まり、お花見を楽しみます。桜の花びらが風に舞い、まるで雪のように見える瞬間は、とても美しいです。']; yield ['في العالم العربي، يُعتبر الخط العربي أحد أجمل أشكال الفن. يُستخدم الخط في تزيين المساجد والكتب والمخطوطات القديمة. يتميز الخط العربي بجماله وتناسقه، ويُعتبر رمزًا للثقافة الإسلامية.']; } }