Skip to content

Commit a9e3d2e

Browse files
committed
[Twig] More test cases and fixes for edge case HTML syntax
1 parent a5fb0bc commit a9e3d2e

File tree

2 files changed

+66
-32
lines changed

2 files changed

+66
-32
lines changed

src/TwigComponent/src/Twig/TwigPreLexer.php

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ private function consumeAttributes(string $componentName): string
180180

181181
// <twig:component someProp> -> someProp: true
182182
if (!$this->check('=')) {
183+
// don't allow "<twig:component :someProp>"
184+
if ($isAttributeDynamic) {
185+
throw new SyntaxError(sprintf('Expected "=" after ":%s" when parsing the "<twig:%s" syntax.', $key, $componentName), $this->line);
186+
}
187+
183188
$attributes[] = sprintf('%s: true', $key);
184189
$this->consumeWhitespace();
185190
continue;
@@ -188,24 +193,15 @@ private function consumeAttributes(string $componentName): string
188193
$this->expectAndConsumeChar('=');
189194
$quote = $this->consumeChar(["'", '"']);
190195

191-
// someProp="{{ dynamicVar }}"
192-
if ($this->consume('{{')) {
193-
$this->consumeWhitespace();
194-
$attributeValue = rtrim($this->consumeUntil('}'));
195-
$this->expectAndConsumeChar('}');
196-
$this->expectAndConsumeChar('}');
197-
$this->consumeUntil($quote);
198-
199-
$attributes[] = sprintf('%s: %s', $key, $attributeValue);
196+
if ($isAttributeDynamic) {
197+
// :someProp="dynamicVar"
198+
$attributeValue = $this->consumeUntil($quote);
200199
} else {
201200
$attributeValue = $this->consumeAttributeValue($quote);
202-
203-
if ($isAttributeDynamic) {
204-
$attributes[] = sprintf('%s: %s', $key, $attributeValue);
205-
} else {
206-
$attributes[] = sprintf("%s: '%s'", $key, $attributeValue);
207-
}
208201
}
202+
203+
$attributes[] = sprintf('%s: %s', $key, $attributeValue);
204+
209205
$this->expectAndConsumeChar($quote);
210206
$this->consumeWhitespace();
211207
}
@@ -246,6 +242,12 @@ private function consumeChar($validChars = null): string
246242
return $char;
247243
}
248244

245+
/**
246+
* Moves the position forward until it finds $endString.
247+
*
248+
* Any string consumed *before* finding that string is returned.
249+
* The position is moved forward to just *before* $endString.
250+
*/
249251
private function consumeUntil(string $endString): string
250252
{
251253
$start = $this->position;
@@ -284,9 +286,14 @@ private function expectAndConsumeChar(string $char): void
284286
throw new \InvalidArgumentException('Expected a single character');
285287
}
286288

287-
if ($this->position >= $this->length || $this->input[$this->position] !== $char) {
289+
if ($this->position >= $this->length) {
290+
throw new SyntaxError("Expected '{$char}' but reached the end of the file.", $this->line);
291+
}
292+
293+
if ($this->input[$this->position] !== $char) {
288294
throw new SyntaxError("Expected '{$char}' but found '{$this->input[$this->position]}'.", $this->line);
289295
}
296+
290297
++$this->position;
291298
}
292299

@@ -370,39 +377,43 @@ private function consumeUntilEndBlock(): string
370377

371378
private function consumeAttributeValue(string $quote): string
372379
{
373-
$attributeValue = '';
380+
$parts = [];
381+
$currentPart = '';
374382
while ($this->position < $this->length) {
375-
if (substr($this->input, $this->position, 1) === $quote) {
383+
if ($this->check($quote)) {
376384
break;
377385
}
378386

379387
if ("\n" === $this->input[$this->position]) {
380388
++$this->line;
381389
}
382390

383-
if ('\'' === $this->input[$this->position]) {
384-
$attributeValue .= "\'";
385-
++$this->position;
386-
387-
continue;
388-
}
391+
if ($this->check('{{')) {
392+
// mark any previous static text as complete: push into parts
393+
if ('' !== $currentPart) {
394+
$parts[] = sprintf("'%s'", str_replace("'", "\'", $currentPart));
395+
$currentPart = '';
396+
}
389397

390-
if ('{{' === substr($this->input, $this->position, 2)) {
398+
// consume the entire {{ }} block
391399
$this->consume('{{');
392-
$attributeValue .= "'~(";
393400
$this->consumeWhitespace();
394-
$value = rtrim($this->consumeUntil('}}'));
401+
$parts[] = sprintf('(%s)', rtrim($this->consumeUntil('}}')));
395402
$this->expectAndConsumeChar('}');
396403
$this->expectAndConsumeChar('}');
397-
$attributeValue .= $value;
398-
$attributeValue .= ")~'";
404+
405+
continue;
399406
}
400407

401-
$attributeValue .= $this->input[$this->position];
408+
$currentPart .= $this->input[$this->position];
402409
++$this->position;
403410
}
404411

405-
return $attributeValue;
412+
if ('' !== $currentPart) {
413+
$parts[] = sprintf("'%s'", str_replace("'", "\'", $currentPart));
414+
}
415+
416+
return implode('~', $parts);
406417
}
407418

408419
private function doesStringEventuallyExist(string $needle): bool

src/TwigComponent/tests/Unit/TwigPreLexerTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function getLexTests(): iterable
3939

4040
yield 'component_with_dynamic_attributes' => [
4141
'<twig:foo dynamic="{{ dynamicVar }}" :otherDynamic="anotherVar" />',
42-
'{{ component(\'foo\', { dynamic: dynamicVar, otherDynamic: anotherVar }) }}',
42+
'{{ component(\'foo\', { dynamic: (dynamicVar), otherDynamic: anotherVar }) }}',
4343
];
4444

4545
yield 'component_with_closing_tag' => [
@@ -100,6 +100,14 @@ public function getLexTests(): iterable
100100
'<twig:foo text="Hello {{ name }}!"/>',
101101
"{{ component('foo', { text: 'Hello '~(name)~'!' }) }}",
102102
];
103+
yield 'component_with_mixture_of_dynamic_twig_from_start' => [
104+
'<twig:foo text="{{ name }} is my name{{ ending~\'!!\' }}"/>',
105+
"{{ component('foo', { text: (name)~' is my name'~(ending~'!!') }) }}",
106+
];
107+
yield 'dynamic_attribute_with_quotation_included' => [
108+
'<twig:foo text="{{ "hello!" }}"/>',
109+
"{{ component('foo', { text: (\"hello!\") }) }}",
110+
];
103111
yield 'component_with_mixture_of_string_and_twig_with_quote_in_argument' => [
104112
'<twig:foo text="Hello {{ name }}, I\'m Theo!"/>',
105113
"{{ component('foo', { text: 'Hello '~(name)~', I\'m Theo!' }) }}",
@@ -128,5 +136,20 @@ public function getLexTests(): iterable
128136
{% endblock %}{% endcomponent %}
129137
EOF
130138
];
139+
140+
yield 'string_inside_of_twig_code_not_escaped' => [
141+
<<<EOF
142+
<twig:TabbedCodeBlocks :files="[
143+
'src/Twig/MealPlanner.php',
144+
'templates/components/MealPlanner.html.twig',
145+
]" />
146+
EOF,
147+
<<<EOF
148+
{{ component('TabbedCodeBlocks', { files: [
149+
'src/Twig/MealPlanner.php',
150+
'templates/components/MealPlanner.html.twig',
151+
] }) }}
152+
EOF
153+
];
131154
}
132155
}

0 commit comments

Comments
 (0)