From 631653ab2bddb537e7eea316bc93dcea92c681c8 Mon Sep 17 00:00:00 2001 From: TomJGooding <101601846+TomJGooding@users.noreply.github.com> Date: Sun, 5 Oct 2025 11:29:51 +0100 Subject: [PATCH] fix(markup): fix invalid closing tag parsing Fix the markup parsing of invalid closing tags missing the closing square bracket. Currently `[/` is interpreted as an auto-closing tag, which will crash with a `MarkupError` if there's nothing to close. Fixes #6155 --- src/textual/markup.py | 56 +++++++++++++++++++++++++++---------------- tests/test_content.py | 1 + tests/test_markup.py | 2 ++ 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/textual/markup.py b/src/textual/markup.py index 60abb9f5cb..79c00df714 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -408,32 +408,46 @@ def process_text(template_text: str, /) -> str: elif token_name == "open_closing_tag": tag_text = [] + + eof = False for token in iter_tokens: if token.name == "end_tag": break + elif token.name == "eof": + eof = True tag_text.append(token.value) - closing_tag = "".join(tag_text).strip() - normalized_closing_tag = normalize_markup_tag(closing_tag) - if normalized_closing_tag: - for index, (tag_position, tag_body, normalized_tag_body) in enumerate( - reversed(style_stack), 1 - ): - if normalized_tag_body == normalized_closing_tag: - style_stack.pop(-index) - if tag_position != position: - spans.append(Span(tag_position, position, tag_body)) - break - else: - raise MarkupError( - f"closing tag '[/{closing_tag}]' does not match any open tag" - ) - + if eof: + # "tag" was unparsable + text_content = f"[/{''.join(tag_text)}" + text_append(text_content) + position += len(text_content) else: - if not style_stack: - raise MarkupError("auto closing tag ('[/]') has nothing to close") - open_position, tag_body, _ = style_stack.pop() - if open_position != position: - spans.append(Span(open_position, position, tag_body)) + closing_tag = "".join(tag_text).strip() + normalized_closing_tag = normalize_markup_tag(closing_tag) + if normalized_closing_tag: + for index, ( + tag_position, + tag_body, + normalized_tag_body, + ) in enumerate(reversed(style_stack), 1): + if normalized_tag_body == normalized_closing_tag: + style_stack.pop(-index) + if tag_position != position: + spans.append(Span(tag_position, position, tag_body)) + break + else: + raise MarkupError( + f"closing tag '[/{closing_tag}]' does not match any open tag" + ) + + else: + if not style_stack: + raise MarkupError( + "auto closing tag ('[/]') has nothing to close" + ) + open_position, tag_body, _ = style_stack.pop() + if open_position != position: + spans.append(Span(open_position, position, tag_body)) content_text = "".join(text) text_length = len(content_text) diff --git a/tests/test_content.py b/tests/test_content.py index 5020db8f90..6799ab0eb8 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -232,6 +232,7 @@ def test_assemble(): ("\\[/foo]", "[/foo]"), ("\\[]", "[]"), ("\\[0]", "[0]"), + ("\\[/", "[/"), ], ) def test_escape(markup: str, plain: str) -> None: diff --git a/tests/test_markup.py b/tests/test_markup.py index 346b0a903c..b2ae74680e 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -19,6 +19,8 @@ ("[0]", Content("[0]")), ("[red", Content("[red")), ("[red]", Content("")), + ("[/", Content("[/")), + ("[/red", Content("[/red")), ("foo", Content("foo")), ("foo\n", Content("foo\n")), ("foo\nbar", Content("foo\nbar")),