Skip to content

Commit 0c01bd4

Browse files
[6.x] Adds Component Tag Syntax Support to Antlers (#11799)
1 parent 6202028 commit 0c01bd4

3 files changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Statamic\View\Antlers\Language\Parser;
4+
5+
use Illuminate\Support\Str;
6+
use Stillat\BladeParser\Nodes\Components\ComponentNode;
7+
use Stillat\BladeParser\Parser\DocumentParser;
8+
9+
class ComponentCompiler
10+
{
11+
protected array $statamicTags = ['statamic', 's'];
12+
13+
public function compile($template)
14+
{
15+
if (! Str::contains($template, ['<s-', '<s:', '<statamic-', '<statamic:'])) {
16+
return $template;
17+
}
18+
19+
return (new DocumentParser())
20+
->registerCustomComponentTags($this->statamicTags)
21+
->onlyParseComponents()
22+
->parseTemplate($template)
23+
->toDocument()
24+
->getRootNodes()
25+
->pipe(fn ($nodes) => $this->compileNodes($nodes));
26+
}
27+
28+
protected function compileNodes($nodes)
29+
{
30+
return $nodes
31+
->map(function ($node) {
32+
if (! $node instanceof ComponentNode) {
33+
return $node->unescapedContent;
34+
}
35+
36+
if (! in_array(mb_strtolower($node->componentPrefix), $this->statamicTags)) {
37+
return $node->outerDocumentContent;
38+
}
39+
40+
if ($node->isClosingTag && ! $node->isSelfClosing) {
41+
return '';
42+
}
43+
44+
return $this->compileComponent($node);
45+
})
46+
->join('');
47+
}
48+
49+
protected function compileComponent(ComponentNode $component)
50+
{
51+
if ($component->isSelfClosing) {
52+
return "{{ %$component->innerContent /}}";
53+
}
54+
55+
$innerContent = $this->compileNodes($component->getNodes());
56+
57+
return "{{ %$component->innerContent }}$innerContent{{ /%$component->innerContent }}";
58+
}
59+
}

src/View/Antlers/Language/Runtime/RuntimeParser.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Statamic\View\Antlers\Language\Nodes\AbstractNode;
2424
use Statamic\View\Antlers\Language\Nodes\AntlersNode;
2525
use Statamic\View\Antlers\Language\Nodes\Position;
26+
use Statamic\View\Antlers\Language\Parser\ComponentCompiler;
2627
use Statamic\View\Antlers\Language\Parser\DocumentParser;
2728
use Statamic\View\Antlers\Language\Parser\LanguageKeywords;
2829
use Statamic\View\Antlers\Language\Parser\LanguageParser;
@@ -117,12 +118,16 @@ class RuntimeParser implements Parser
117118
*/
118119
private $isolateRuntimes = false;
119120

121+
protected $componentCompiler;
122+
120123
public function __construct(DocumentParser $documentParser, NodeProcessor $nodeProcessor, AntlersLexer $lexer, LanguageParser $antlersParser)
121124
{
122125
$this->documentParser = $documentParser;
123126
$this->nodeProcessor = $nodeProcessor;
124127
$this->antlersLexer = $lexer;
125128
$this->antlersParser = $antlersParser;
129+
130+
$this->componentCompiler = new ComponentCompiler();
126131
}
127132

128133
/**
@@ -333,6 +338,8 @@ protected function shouldCacheRenderNodes($text)
333338
*/
334339
protected function renderText($text, $data = [])
335340
{
341+
$text = $this->componentCompiler->compile($text);
342+
336343
$this->parseStack += 1;
337344
$text = $this->runPreParserCallbacks($text);
338345

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace Tests\Antlers\Parser;
4+
5+
use Facades\Tests\Factories\EntryFactory;
6+
use Statamic\Facades\Collection;
7+
use Statamic\Tags\Tags;
8+
use Statamic\Testing\Concerns\PreventsSavingStacheItemsToDisk;
9+
use Tests\Antlers\ParserTestCase;
10+
use Tests\FakesViews;
11+
12+
class ComponentTagsTest extends ParserTestCase
13+
{
14+
use FakesViews,
15+
PreventsSavingStacheItemsToDisk;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
Collection::make('pages')
22+
->routes(['en' => '{slug}'])
23+
->save();
24+
25+
for ($i = 1; $i <= 5; $i++) {
26+
EntryFactory::collection('pages')
27+
->id("page-$i")
28+
->data([
29+
'title' => "Page: $i",
30+
])
31+
->slug('page-'.$i)
32+
->create();
33+
}
34+
}
35+
36+
public function test_component_tags_are_parsed()
37+
{
38+
$template = <<<'ANTLERS'
39+
<s:collection:pages>
40+
{{ title }}.
41+
</s:collection:pages>
42+
ANTLERS;
43+
44+
$this->assertSame(
45+
'Page: 1. Page: 2. Page: 3. Page: 4. Page: 5.',
46+
(string) str($this->renderString($template))->squish(),
47+
);
48+
}
49+
50+
public function test_component_tags_with_dynamic_parameters_are_parsed()
51+
{
52+
$template = <<<'ANTLERS'
53+
<s:collection :from="collection_name">{{ title }}.</s:collection>
54+
ANTLERS;
55+
56+
$result = $this->renderString($template, [
57+
'collection_name' => 'pages',
58+
]);
59+
60+
$this->assertSame(
61+
'Page: 1.Page: 2.Page: 3.Page: 4.Page: 5.',
62+
$result,
63+
);
64+
}
65+
66+
public function test_component_tags_with_parameters_are_parsed()
67+
{
68+
$template = <<<'ANTLERS'
69+
<s:collection:pages limit="1">{{ title }}</s:collection:pages>
70+
ANTLERS;
71+
72+
$this->assertSame(
73+
'Page: 1',
74+
$this->renderString($template),
75+
);
76+
}
77+
78+
public function test_self_closing_component_tags_are_parsed()
79+
{
80+
(new class extends Tags
81+
{
82+
protected static $handle = 'the_tag';
83+
84+
public function index()
85+
{
86+
if ($this->isPair) {
87+
return 'Paired!';
88+
}
89+
90+
return 'Self-Closing!';
91+
}
92+
})::register();
93+
94+
$this->assertSame('Self-Closing!', $this->renderString('<s:the_tag />'));
95+
$this->assertSame('Paired!', $this->renderString('<s:the_tag> </s:the_tag>'));
96+
}
97+
98+
public function test_nested_component_tags_are_parsed()
99+
{
100+
$template = <<<'ANTLERS'
101+
<s:collection:pages sort="title:asc">
102+
Before: {{ title }}
103+
104+
<nested><s:collection:pages sort="title:desc" limit="2">{{ title }}</s:collection:pages></nested>
105+
106+
After: {{ title }}
107+
</s:collection:pages>
108+
ANTLERS;
109+
110+
$expected = <<<'RESULT'
111+
Before: Page: 1 <nested>Page: 5Page: 4</nested> After: Page: 1 Before: Page: 2 <nested>Page: 5Page: 4</nested> After: Page: 2 Before: Page: 3 <nested>Page: 5Page: 4</nested> After: Page: 3 Before: Page: 4 <nested>Page: 5Page: 4</nested> After: Page: 4 Before: Page: 5 <nested>Page: 5Page: 4</nested> After: Page: 5
112+
RESULT;
113+
114+
$this->assertSame(
115+
$expected,
116+
(string) str($this->renderString($template))->squish(),
117+
);
118+
}
119+
120+
public function test_component_syntax_works_with_render_text_calls()
121+
{
122+
$this->withFakeViews();
123+
$template = <<<'ANTLERS'
124+
Page Title: {{ title }}
125+
<s:collection:pages>{{ title }}.</s:collection:pages>
126+
ANTLERS;
127+
128+
$this->viewShouldReturnRaw('default', $template);
129+
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
130+
131+
$this->get('/page-1')
132+
->assertOk()
133+
->assertSee('Page Title: Page: 1')
134+
->assertSee('Page: 1.Page: 2.Page: 3.Page: 4.Page: 5.');
135+
}
136+
}

0 commit comments

Comments
 (0)