Skip to content

Commit df5422d

Browse files
committed
Introduce a Short Tags system for TwigComponents
1 parent e653f48 commit df5422d

File tree

9 files changed

+411
-6
lines changed

9 files changed

+411
-6
lines changed

src/TwigComponent/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 2.24.0
4+
5+
- Introduce an experimental Short tags system for TwigComponents, making `twig:` prefix optional #2662
6+
37
## 2.20.0
48

59
- Add Anonymous Component support for 3rd-party bundles #2019

src/TwigComponent/doc/index.rst

+30
Original file line numberDiff line numberDiff line change
@@ -1719,6 +1719,36 @@ Pass the name of some component as an argument to print its details:
17191719
| | int $min = 10 |
17201720
+---------------------------------------------------+-----------------------------------+
17211721
1722+
Short Tags
1723+
----------
1724+
1725+
An experimental new short tag system, allowing the omission of the 'twig:' prefix in HTML tags, was introduced in version 2.24.
1726+
1727+
This mode allows you to omit the `twig:` prefix and reference components directly by their name,
1728+
with the first letter capitalized.
1729+
1730+
.. code-block:: html+twig
1731+
1732+
<Acme:Button type="primary">
1733+
Click me
1734+
</Acme:Button>
1735+
1736+
This is equivalent to:
1737+
1738+
.. code-block:: html+twig
1739+
1740+
<twig:Acme:Button type="primary">
1741+
Click me
1742+
</twig:Acme:Button>
1743+
1744+
To enable this feature, add the following configuration:
1745+
1746+
.. code-block:: yaml
1747+
1748+
# config/packages/twig_component.yaml
1749+
twig_component:
1750+
short_tags: true
1751+
17221752
Contributing
17231753
------------
17241754

src/TwigComponent/src/Command/TwigComponentDebugCommand.php

+16
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public function __construct(
3939
private Environment $twig,
4040
private readonly array $componentClassMap,
4141
?string $anonymousDirectory = null,
42+
private readonly ?bool $withShortSyntax = false,
43+
private readonly ?bool $withProfiler = false,
4244
) {
4345
parent::__construct();
4446
$this->anonymousDirectory = $anonymousDirectory ?? 'components';
@@ -85,6 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8587

8688
$components = $this->findComponents();
8789
$this->displayComponentsTable($io, $components);
90+
$this->displayDetailsAboutConfiguration($io);
8891

8992
return Command::SUCCESS;
9093
}
@@ -356,4 +359,17 @@ private function getAnonymousComponentProperties(ComponentMetadata $metadata): a
356359

357360
return $properties;
358361
}
362+
363+
private function displayDetailsAboutConfiguration(SymfonyStyle $io): void
364+
{
365+
$io->section('Configuration of TwigComponent');
366+
$io->table(
367+
['Configuration', 'Current value'],
368+
[
369+
['anonymous_template_directory', $this->anonymousDirectory],
370+
['short_syntax', $this->withShortSyntax ? 'enabled' : 'disabled'],
371+
['profiler', $this->withProfiler ? 'enabled' : 'disabled'],
372+
]
373+
);
374+
}
359375
}

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

+20
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,34 @@ static function (ChildDefinition $definition, AsTwigComponent $attribute) {
129129
;
130130

131131
$container->register('ux.twig_component.twig.lexer', ComponentLexer::class);
132+
if (true === $config['short_tags']) {
133+
$container->getDefinition('ux.twig_component.twig.lexer')
134+
->addMethodCall('enableShortTags');
135+
}
132136

133137
$container->register('ux.twig_component.twig.environment_configurator', TwigEnvironmentConfigurator::class)
134138
->setDecoratedService(new Reference('twig.configurator.environment'))
135139
->setArguments([new Reference('ux.twig_component.twig.environment_configurator.inner')]);
136140

141+
// Currently, the ComponentLexer is not injected into the TwigEnvironmentConfigurator, but built directly in the
142+
// code (with a new ComponentLexer($environment)).
143+
// We cannot change this behavior without a major refactoring : environment is currently configured at runtime.
144+
// So we add setters for our required options
145+
// This should be improved in the future: currently, parameters of the ComponentLexer are not injectables.
146+
if (true === $config['short_tags']) {
147+
$container->getDefinition('ux.twig_component.twig.environment_configurator')
148+
->addMethodCall('enabledShortTags');
149+
}
150+
137151
$container->register('ux.twig_component.command.debug', TwigComponentDebugCommand::class)
138152
->setArguments([
139153
new Parameter('twig.default_path'),
140154
new Reference('ux.twig_component.component_factory'),
141155
new Reference('twig'),
142156
new AbstractArgument(\sprintf('Added in %s.', TwigComponentPass::class)),
143157
$config['anonymous_template_directory'],
158+
$config['short_tags'],
159+
$config['profiler'],
144160
])
145161
->addTag('console.command')
146162
;
@@ -217,6 +233,10 @@ public function getConfigTreeBuilder(): TreeBuilder
217233
->info('Enables the profiler for Twig Component (in debug mode)')
218234
->defaultValue('%kernel.debug%')
219235
->end()
236+
->booleanNode('short_tags')
237+
->info('Enables the short syntax for Twig Components (the <twig: prefix is optional)')
238+
->defaultValue(false)
239+
->end()
220240
->scalarNode('controllers_json')
221241
->setDeprecated('symfony/ux-twig-component', '2.18', 'The "twig_component.controllers_json" config option is deprecated, and will be removed in 3.0.')
222242
->defaultNull()

src/TwigComponent/src/Twig/ComponentLexer.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
*/
2727
class ComponentLexer extends Lexer
2828
{
29+
private bool $withShortTags = false;
30+
2931
public function tokenize(Source $source): TokenStream
3032
{
31-
$preLexer = new TwigPreLexer();
33+
$preLexer = new TwigPreLexer(withShortTags: $this->withShortTags);
3234
$preparsed = $preLexer->preLexComponents($source->getCode());
3335

3436
return parent::tokenize(
@@ -39,4 +41,9 @@ public function tokenize(Source $source): TokenStream
3941
)
4042
);
4143
}
44+
45+
public function enabledShortTags(): void
46+
{
47+
$this->withShortTags = true;
48+
}
4249
}

src/TwigComponent/src/Twig/TwigEnvironmentConfigurator.php

+17-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
*/
2323
class TwigEnvironmentConfigurator
2424
{
25+
private bool $withShortTags = false;
26+
2527
public function __construct(
2628
private readonly EnvironmentConfigurator $decorated,
2729
) {
@@ -31,12 +33,26 @@ public function configure(Environment $environment): void
3133
{
3234
$this->decorated->configure($environment);
3335

34-
$environment->setLexer(new ComponentLexer($environment));
36+
$componentLexer = new ComponentLexer($environment);
37+
38+
if ($this->withShortTags) {
39+
$componentLexer->enabledShortTags();
40+
}
41+
42+
$environment->setLexer($componentLexer);
3543

3644
if (class_exists(EscaperRuntime::class)) {
3745
$environment->getRuntime(EscaperRuntime::class)->addSafeClass(ComponentAttributes::class, ['html']);
3846
} elseif ($environment->hasExtension(EscaperExtension::class)) {
3947
$environment->getExtension(EscaperExtension::class)->addSafeClass(ComponentAttributes::class, ['html']);
4048
}
4149
}
50+
51+
/**
52+
* This method should be replaced by a proper autowiring configuration.
53+
*/
54+
public function enabledShortTags(): void
55+
{
56+
$this->withShortTags = true;
57+
}
4258
}

src/TwigComponent/src/Twig/TwigPreLexer.php

+21-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use Twig\Lexer;
1616

1717
/**
18-
* Rewrites <twig:component> syntaxes to {% component %} syntaxes.
18+
* Rewrites <twig:component> or <Component> syntaxes to {% component %} syntaxes.
1919
*/
2020
class TwigPreLexer
2121
{
@@ -28,17 +28,34 @@ class TwigPreLexer
2828
*/
2929
private array $currentComponents = [];
3030

31-
public function __construct(int $startingLine = 1)
31+
public function __construct(int $startingLine = 1, private readonly bool $withShortTags = false)
3232
{
3333
$this->line = $startingLine;
3434
}
3535

3636
public function preLexComponents(string $input): string
3737
{
38-
if (!str_contains($input, '<twig:')) {
38+
// tag may be:
39+
// - prefixed: <twig:componentName>
40+
// - short (jsx like): <ComponentName> (with a capital letter)
41+
42+
$isPrefixedTags = str_contains($input, '<twig:');
43+
$isShortTags = $this->withShortTags && preg_match_all('/<([A-Z][a-zA-Z0-9_:-]+)([^>]*)>/', $input, $matches, \PREG_SET_ORDER);
44+
45+
if (!$isPrefixedTags && !$isShortTags) {
3946
return $input;
4047
}
4148

49+
if ($isShortTags) {
50+
$componentNames = array_map(fn ($match) => $match[1], $matches);
51+
$componentNames = array_unique(array_filter($componentNames));
52+
53+
// To simplify things in the rest of the class, we replace the component name with twig:<componentName>
54+
foreach ($componentNames as $componentName) {
55+
$input = preg_replace('!<(/?)'.preg_quote($componentName).'!', '<$1twig:'.lcfirst($componentName), $input);
56+
}
57+
}
58+
4259
$this->input = $input = str_replace(["\r\n", "\r"], "\n", $input);
4360
$this->length = \strlen($input);
4461
$output = '';
@@ -394,7 +411,7 @@ private function consumeBlock(string $componentName): string
394411
}
395412
$blockContents = $this->consumeUntilEndBlock();
396413

397-
$subLexer = new self($this->line);
414+
$subLexer = new self($this->line, $this->withShortTags);
398415
$output .= $subLexer->preLexComponents($blockContents);
399416

400417
$this->consume($closingTag);

src/TwigComponent/tests/Integration/Command/TwigComponentDebugCommandTest.php

+13
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,17 @@ private function tableDisplayCheck(string $display): void
241241
$this->assertStringContainsString('Template', $display);
242242
$this->assertStringContainsString('Properties', $display);
243243
}
244+
245+
public function testDisplayDetailsAboutConfiguration(): void
246+
{
247+
$commandTester = $this->createCommandTester();
248+
$commandTester->execute([]);
249+
250+
$commandTester->assertCommandIsSuccessful();
251+
252+
$display = $commandTester->getDisplay();
253+
254+
$this->assertStringContainsString('anonymous_template_directory components', $display);
255+
$this->assertStringContainsString('short_syntax disabled', $display);
256+
}
244257
}

0 commit comments

Comments
 (0)