diff --git a/src/View/Antlers/Language/Parser/ComponentCompiler.php b/src/View/Antlers/Language/Parser/ComponentCompiler.php new file mode 100644 index 00000000000..d692e425f82 --- /dev/null +++ b/src/View/Antlers/Language/Parser/ComponentCompiler.php @@ -0,0 +1,59 @@ +registerCustomComponentTags($this->statamicTags) + ->onlyParseComponents() + ->parseTemplate($template) + ->toDocument() + ->getRootNodes() + ->pipe(fn ($nodes) => $this->compileNodes($nodes)); + } + + protected function compileNodes($nodes) + { + return $nodes + ->map(function ($node) { + if (! $node instanceof ComponentNode) { + return $node->unescapedContent; + } + + if (! in_array(mb_strtolower($node->componentPrefix), $this->statamicTags)) { + return $node->outerDocumentContent; + } + + if ($node->isClosingTag && ! $node->isSelfClosing) { + return ''; + } + + return $this->compileComponent($node); + }) + ->join(''); + } + + protected function compileComponent(ComponentNode $component) + { + if ($component->isSelfClosing) { + return "{{ %$component->innerContent /}}"; + } + + $innerContent = $this->compileNodes($component->getNodes()); + + return "{{ %$component->innerContent }}$innerContent{{ /%$component->innerContent }}"; + } +} diff --git a/src/View/Antlers/Language/Runtime/RuntimeParser.php b/src/View/Antlers/Language/Runtime/RuntimeParser.php index 4283493d4cf..a6902abe7e5 100644 --- a/src/View/Antlers/Language/Runtime/RuntimeParser.php +++ b/src/View/Antlers/Language/Runtime/RuntimeParser.php @@ -23,6 +23,7 @@ use Statamic\View\Antlers\Language\Nodes\AbstractNode; use Statamic\View\Antlers\Language\Nodes\AntlersNode; use Statamic\View\Antlers\Language\Nodes\Position; +use Statamic\View\Antlers\Language\Parser\ComponentCompiler; use Statamic\View\Antlers\Language\Parser\DocumentParser; use Statamic\View\Antlers\Language\Parser\LanguageKeywords; use Statamic\View\Antlers\Language\Parser\LanguageParser; @@ -117,12 +118,16 @@ class RuntimeParser implements Parser */ private $isolateRuntimes = false; + protected $componentCompiler; + public function __construct(DocumentParser $documentParser, NodeProcessor $nodeProcessor, AntlersLexer $lexer, LanguageParser $antlersParser) { $this->documentParser = $documentParser; $this->nodeProcessor = $nodeProcessor; $this->antlersLexer = $lexer; $this->antlersParser = $antlersParser; + + $this->componentCompiler = new ComponentCompiler(); } /** @@ -328,6 +333,8 @@ protected function isIgnitionInstalled() */ protected function renderText($text, $data = []) { + $text = $this->componentCompiler->compile($text); + $this->parseStack += 1; $text = $this->runPreParserCallbacks($text); diff --git a/tests/Antlers/Parser/ComponentTagsTest.php b/tests/Antlers/Parser/ComponentTagsTest.php new file mode 100644 index 00000000000..0d1721e4e2d --- /dev/null +++ b/tests/Antlers/Parser/ComponentTagsTest.php @@ -0,0 +1,136 @@ +routes(['en' => '{slug}']) + ->save(); + + for ($i = 1; $i <= 5; $i++) { + EntryFactory::collection('pages') + ->id("page-$i") + ->data([ + 'title' => "Page: $i", + ]) + ->slug('page-'.$i) + ->create(); + } + } + + public function test_component_tags_are_parsed() + { + $template = <<<'ANTLERS' + + {{ title }}. + +ANTLERS; + + $this->assertSame( + 'Page: 1. Page: 2. Page: 3. Page: 4. Page: 5.', + (string) str($this->renderString($template))->squish(), + ); + } + + public function test_component_tags_with_dynamic_parameters_are_parsed() + { + $template = <<<'ANTLERS' +{{ title }}. +ANTLERS; + + $result = $this->renderString($template, [ + 'collection_name' => 'pages', + ]); + + $this->assertSame( + 'Page: 1.Page: 2.Page: 3.Page: 4.Page: 5.', + $result, + ); + } + + public function test_component_tags_with_parameters_are_parsed() + { + $template = <<<'ANTLERS' +{{ title }} +ANTLERS; + + $this->assertSame( + 'Page: 1', + $this->renderString($template), + ); + } + + public function test_self_closing_component_tags_are_parsed() + { + (new class extends Tags + { + protected static $handle = 'the_tag'; + + public function index() + { + if ($this->isPair) { + return 'Paired!'; + } + + return 'Self-Closing!'; + } + })::register(); + + $this->assertSame('Self-Closing!', $this->renderString('')); + $this->assertSame('Paired!', $this->renderString(' ')); + } + + public function test_nested_component_tags_are_parsed() + { + $template = <<<'ANTLERS' + + Before: {{ title }} + + {{ title }} + + After: {{ title }} + +ANTLERS; + + $expected = <<<'RESULT' +Before: Page: 1 Page: 5Page: 4 After: Page: 1 Before: Page: 2 Page: 5Page: 4 After: Page: 2 Before: Page: 3 Page: 5Page: 4 After: Page: 3 Before: Page: 4 Page: 5Page: 4 After: Page: 4 Before: Page: 5 Page: 5Page: 4 After: Page: 5 +RESULT; + + $this->assertSame( + $expected, + (string) str($this->renderString($template))->squish(), + ); + } + + public function test_component_syntax_works_with_render_text_calls() + { + $this->withFakeViews(); + $template = <<<'ANTLERS' +Page Title: {{ title }} +{{ title }}. +ANTLERS; + + $this->viewShouldReturnRaw('default', $template); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + + $this->get('/page-1') + ->assertOk() + ->assertSee('Page Title: Page: 1') + ->assertSee('Page: 1.Page: 2.Page: 3.Page: 4.Page: 5.'); + } +}