Skip to content

Commit 593b90c

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

File tree

7 files changed

+380
-6
lines changed

7 files changed

+380
-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/DependencyInjection/TwigComponentExtension.php

+18
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,25 @@ 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'),
@@ -217,6 +231,10 @@ public function getConfigTreeBuilder(): TreeBuilder
217231
->info('Enables the profiler for Twig Component (in debug mode)')
218232
->defaultValue('%kernel.debug%')
219233
->end()
234+
->booleanNode('short_tags')
235+
->info('Enables the short syntax for Twig Components (the <twig: prefix is optional)')
236+
->defaultValue(false)
237+
->end()
220238
->scalarNode('controllers_json')
221239
->setDeprecated('symfony/ux-twig-component', '2.18', 'The "twig_component.controllers_json" config option is deprecated, and will be removed in 3.0.')
222240
->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);

0 commit comments

Comments
 (0)