Skip to content

Commit 07ff0a3

Browse files
author
matheo
committed
introduce a new syntax for twig component
1 parent 6535b56 commit 07ff0a3

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

src/TwigComponent/src/DependencyInjection/TwigComponentExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
use Symfony\UX\TwigComponent\ComponentStack;
2727
use Symfony\UX\TwigComponent\DependencyInjection\Compiler\TwigComponentPass;
2828
use Symfony\UX\TwigComponent\Twig\ComponentExtension;
29+
use Symfony\UX\TwigComponent\Twig\ComponentLexer;
30+
use Symfony\UX\TwigComponent\Twig\TwigEnvironmentConfigurator;
2931

3032
/**
3133
* @author Kevin Bond <[email protected]>
@@ -75,5 +77,11 @@ class_exists(AbstractArgument::class) ? new AbstractArgument(sprintf('Added in %
7577
->addTag('container.service_subscriber', ['key' => ComponentRenderer::class, 'id' => 'ux.twig_component.component_renderer'])
7678
->addTag('container.service_subscriber', ['key' => ComponentFactory::class, 'id' => 'ux.twig_component.component_factory'])
7779
;
80+
81+
$container->register('ux.twig_component.twig.lexer', ComponentLexer::class);
82+
83+
$container->register('ux.twig_component.twig.environment_configurator', TwigEnvironmentConfigurator::class)
84+
->setDecoratedService(new Reference('twig.configurator.environment'))
85+
->setArguments([new Reference('ux.twig_component.twig.environment_configurator.inner')]);
7886
}
7987
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Twig;
4+
5+
use Twig\Lexer;
6+
use Twig\Source;
7+
use Twig\TokenStream;
8+
9+
class ComponentLexer extends Lexer
10+
{
11+
const ATTRIBUTES_REGEX = '(?<attributes>(?:\s+[\w\-:.@]+(=(?:\\\"[^\\\"]*\\\"|\'[^\']*\'|[^\'\\\"=<>]+))?)*\s*)';
12+
const COMPONENTS_REGEX = [
13+
'open_tags' => '/<\s*([A-Z][\w\-\:\.]+)\s*' . self::ATTRIBUTES_REGEX . '(\s?)+>/',
14+
'close_tags' => '/<\/\s*([A-Z][\w\-\:\.]+)\s*>/',
15+
'self_close_tags' => '/<\s*([A-Z][\w\-\:\.]+)\s*' . self::ATTRIBUTES_REGEX . '*(\s?)+\/>/',
16+
];
17+
18+
public function tokenize(Source $source): TokenStream
19+
{
20+
$preparsed = $this->preparsed($source->getCode());
21+
22+
return parent::tokenize(
23+
new Source(
24+
$preparsed,
25+
$source->getName(),
26+
$source->getPath()
27+
)
28+
);
29+
}
30+
31+
private function preparsed(string $value)
32+
{
33+
$value = $this->lexSelfCloseTag($value);
34+
$value = $this->lexOpeningTags($value);
35+
$value = $this->lexClosingTag($value);
36+
37+
return $value;
38+
}
39+
40+
private function lexOpeningTags(string $value)
41+
{
42+
return preg_replace_callback(
43+
self::COMPONENTS_REGEX['open_tags'],
44+
function (array $matches) {
45+
$name = lcfirst($matches[1]);
46+
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
47+
48+
return "{% component " . $name . " with " . $attributes . "%}";
49+
},
50+
$value
51+
52+
);
53+
}
54+
55+
private function lexClosingTag(string $value)
56+
{
57+
return preg_replace(self::COMPONENTS_REGEX['close_tags'], '{% endcomponent %}', $value);
58+
}
59+
60+
private function lexSelfCloseTag(string $value)
61+
{
62+
return preg_replace_callback(
63+
self::COMPONENTS_REGEX['self_close_tags'],
64+
function (array $matches) {
65+
$name = lcfirst($matches[1]);
66+
$attributes = $this->getAttributesFromAttributeString($matches['attributes']);
67+
68+
return "{{ component('" . $name . "', " . $attributes . ") }}";
69+
},
70+
$value
71+
);
72+
}
73+
74+
protected function getAttributesFromAttributeString(string $attributeString)
75+
{
76+
$attributeString = $this->parseAttributeBag($attributeString);
77+
78+
$pattern = '/
79+
(?<attribute>[\w\-:.@]+)
80+
(
81+
=
82+
(?<value>
83+
(
84+
\"[^\"]+\"
85+
|
86+
\\\'[^\\\']+\\\'
87+
|
88+
[^\s>]+
89+
)
90+
)
91+
)?
92+
/x';
93+
94+
if (! preg_match_all($pattern, $attributeString, $matches, PREG_SET_ORDER)) {
95+
return '{}';
96+
}
97+
98+
99+
$attributes = [];
100+
101+
foreach ($matches as $match) {
102+
$attribute = $match['attribute'];
103+
$value = $match['value'] ?? null;
104+
105+
if (is_null($value)) {
106+
$value = 'true';
107+
}
108+
109+
110+
if (strpos($attribute, ":") === 0) {
111+
$attribute = str_replace(":", "", $attribute);
112+
$value = $this->stripQuotes($value);
113+
}
114+
115+
$valueWithoutQuotes = $this->stripQuotes($value);
116+
117+
if ((strpos($valueWithoutQuotes, '{{') === 0) && (strpos($valueWithoutQuotes, '}}') === strlen($valueWithoutQuotes) - 2)) {
118+
$value = substr($valueWithoutQuotes, 2, -2);
119+
} else {
120+
$value = $value;
121+
}
122+
123+
$attributes[$attribute] = $value;
124+
}
125+
126+
$out = "{";
127+
foreach ($attributes as $key => $value) {
128+
$key = "'$key'";
129+
$out .= "$key: $value,";
130+
};
131+
132+
return rtrim($out, ',') . "}";
133+
}
134+
135+
public function stripQuotes(string $value)
136+
{
137+
return strpos($value, '"') === 0 || strpos($value, '\'') === 0
138+
? substr($value, 1, -1)
139+
: $value;
140+
}
141+
142+
protected function parseAttributeBag(string $attributeString)
143+
{
144+
$pattern = "/
145+
(?:^|\s+) # start of the string or whitespace between attributes
146+
\{\{\s*(attributes(?:.+?(?<!\s))?)\s*\}\} # exact match of attributes variable being echoed
147+
/x";
148+
149+
return preg_replace($pattern, ' :attributes="$1"', $attributeString);
150+
}
151+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Symfony\UX\TwigComponent\Twig;
4+
5+
use Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator;
6+
use Twig\Environment;
7+
8+
class TwigEnvironmentConfigurator
9+
{
10+
public function __construct(
11+
private readonly EnvironmentConfigurator $decorated
12+
) {}
13+
14+
public function configure(Environment $environment): void
15+
{
16+
$this->decorated->configure($environment);
17+
18+
$environment->setLexer(new ComponentLexer($environment));
19+
}
20+
}

0 commit comments

Comments
 (0)