Skip to content

Commit 7ea9cc7

Browse files
committed
bug #3762 [TwigComponent] Fix twig:blockquote is not a block (smnandre)
This PR was merged into the 3.x branch. Discussion ---------- [TwigComponent] Fix twig:blockquote is not a block | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | - | License | MIT A component whose name starts with `block` breaks the block it sits in. ## Reproducer ```twig <twig:Card> <twig:block name="footer"> <twig:blockquote cite="Matt">He ho!</twig:blockquote> </twig:block> </twig:Card> ``` ``` Twig\Error\SyntaxError: Expected closing tag "</twig:Card>" not found at line 5. ``` The error points at `Card`, which is correctly closed, and at the last line of the template. Nothing mentions `blockquote`, so there is little to go on. Same failure with `<twig:blockquote />`, `<twig:block-title />`, `<twig:blocked />`, and inside a traditional `{% block %} … {% endblock %}`. Not affected: `<twig:Blockquote />` (the comparison is case-sensitive), and any of these outside a block. ## Cause `TwigPreLexer::consumeUntilEndBlock()` tracks block nesting with a fixed-length prefix comparison: ```php if (!$inComment && '<twig:block' === substr($this->input, $this->position, 11)) { ++$depth; } ``` `'<twig:block'` is also the first 11 characters of `<twig:blockquote`, so the depth is incremented for a component that is not a block. It is never decremented back: the closing comparison is `'</twig:block>'`, which ends with `>` and therefore does not match `</twig:blockquote>`. The enclosing `<twig:block>` never terminates, scanning runs to the end of the template, and the outer component is reported as unclosed. ## Fix Anchor the match and require that `block` is not followed by another component-name character, using the same character class as `consumeComponentName()`: ```php if (!$inComment && preg_match('/\G<twig:block(?![A-Za-z0-9_:@\-.])/', $this->input, $matches, 0, $this->position)) { ++$depth; } ``` The closing comparison needs no change. Commits ------- 0d2b87f [TwigComponent] Fix twig:blockquote is not a block
2 parents 68e8b50 + 0d2b87f commit 7ea9cc7

2 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/TwigComponent/src/Twig/TwigPreLexer.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ private function check(string $chars): bool
455455
&& 0 === substr_compare($this->input, $chars, $this->position, \strlen($chars));
456456
}
457457

458+
private function isNameChar(string $char): bool
459+
{
460+
return '' !== $char && (ctype_alnum($char) || str_contains('_:@-.', $char));
461+
}
462+
458463
private function consumeBlock(string $componentName): string
459464
{
460465
$attributes = $this->consumeAttributes($componentName);
@@ -542,7 +547,9 @@ private function consumeUntilEndBlock(): string
542547
}
543548

544549
--$depth;
545-
} elseif ('<twig:block' === substr($this->input, $this->position, 11)) {
550+
} elseif ('<twig:block' === substr($this->input, $this->position, 11)
551+
&& !$this->isNameChar($this->input[$this->position + 11] ?? '')
552+
) {
546553
++$depth;
547554
}
548555
break;

src/TwigComponent/tests/Unit/TwigPreLexerTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,52 @@ public static function getLexTests(): iterable
110110
'{% component \'foo\' %}{% block foo_block %}{% component \'bar\' %}{% block content %}{{ component(\'baz\') }}{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}',
111111
];
112112

113+
yield 'block_prefixed_component_name' => [
114+
'<twig:foo><twig:block name="foo_block"><twig:blockquote>Quote</twig:blockquote></twig:block></twig:foo>',
115+
'{% component \'foo\' %}{% block foo_block %}{% component \'blockquote\' %}{% block content %}Quote{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}',
116+
];
117+
118+
yield 'block_prefixed_component_name_self_closing' => [
119+
'<twig:foo><twig:block name="foo_block"><twig:blockquote /></twig:block></twig:foo>',
120+
'{% component \'foo\' %}{% block foo_block %}{{ component(\'blockquote\') }}{% endblock %}{% endcomponent %}',
121+
];
122+
123+
yield 'block_prefixed_component_name_with_dash' => [
124+
'<twig:foo><twig:block name="foo_block"><twig:block-title /></twig:block></twig:foo>',
125+
'{% component \'foo\' %}{% block foo_block %}{{ component(\'block-title\') }}{% endblock %}{% endcomponent %}',
126+
];
127+
128+
yield 'block_prefixed_component_name_with_colon' => [
129+
'<twig:foo><twig:block name="foo_block"><twig:block:title /></twig:block></twig:foo>',
130+
'{% component \'foo\' %}{% block foo_block %}{{ component(\'block:title\') }}{% endblock %}{% endcomponent %}',
131+
];
132+
133+
yield 'block_prefixed_component_name_with_suffix' => [
134+
'<twig:foo><twig:block name="foo_block"><twig:blocked /></twig:block></twig:foo>',
135+
'{% component \'foo\' %}{% block foo_block %}{{ component(\'blocked\') }}{% endblock %}{% endcomponent %}',
136+
];
137+
138+
// a traditional block does not pre-lex the components nested in it, hence the verbatim tag
139+
yield 'block_prefixed_component_name_in_traditional_block' => [
140+
'<twig:foo>{% block foo_block %}<twig:blockquote />{% endblock %}</twig:foo>',
141+
'{% component \'foo\' %}{% block foo_block %}<twig:blockquote />{% endblock %}{% endcomponent %}',
142+
];
143+
144+
yield 'block_prefixed_component_name_capitalized' => [
145+
'<twig:foo><twig:block name="foo_block"><twig:Blockquote /></twig:block></twig:foo>',
146+
'{% component \'foo\' %}{% block foo_block %}{{ component(\'Blockquote\') }}{% endblock %}{% endcomponent %}',
147+
];
148+
149+
yield 'block_prefixed_component_name_outside_block' => [
150+
'<twig:foo><twig:blockquote /></twig:foo>',
151+
'{% component \'foo\' %}{% block content %}{{ component(\'blockquote\') }}{% endblock %}{% endcomponent %}',
152+
];
153+
154+
yield 'block_prefixed_component_name_around_a_real_nested_block' => [
155+
'<twig:foo><twig:block name="a"><twig:blockquote /><twig:bar><twig:block name="b">x</twig:block></twig:bar></twig:block></twig:foo>',
156+
'{% component \'foo\' %}{% block a %}{{ component(\'blockquote\') }}{% component \'bar\' %}{% block b %}x{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}',
157+
];
158+
113159
yield 'component_with_embedded_component' => [
114160
'<twig:foo>foo_content<twig:bar><twig:baz /></twig:bar></twig:foo>',
115161
'{% component \'foo\' %}{% block content %}foo_content{% component \'bar\' %}{% block content %}{{ component(\'baz\') }}{% endblock %}{% endcomponent %}{% endblock %}{% endcomponent %}',

0 commit comments

Comments
 (0)