diff --git a/CHANGELOG.md b/CHANGELOG.md index 190c9923fb..5c7debf71d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added experimental opt-in support for https://github.com/willmcgugan/textual-speedups +### Changed + +- Content markup is now more lenient; if a 'tag' doesn't contain a valid style it will be included verbatim. https://github.com/Textualize/textual/pull/5851 + ## [3.3.0] - 2025-06-01 ### Fixed diff --git a/src/textual/markup.py b/src/textual/markup.py index c2a4571bde..60abb9f5cb 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -48,8 +48,9 @@ class MarkupError(Exception): variable_ref=VARIABLE_REF, whitespace=r"\s+", ) - .expect_eof(False) + .expect_eof(True) .expect_semicolon(False) + .extract_text(True) ) expect_markup = Expect( @@ -374,14 +375,36 @@ def process_text(template_text: str, /) -> str: elif token_name == "open_tag": tag_text = [] + eof = False + contains_text = False for token in iter_tokens: if token.name == "end_tag": break + elif token.name == "text": + contains_text = True + elif token.name == "eof": + eof = True tag_text.append(token.value) - opening_tag = "".join(tag_text).strip() - style_stack.append( - (position, opening_tag, normalize_markup_tag(opening_tag)) - ) + if contains_text or eof: + # "tag" was unparsable + text_content = f"[{''.join(tag_text)}" + ("" if eof else "]") + text_append(text_content) + position += len(text_content) + else: + opening_tag = "".join(tag_text) + + if not opening_tag.strip(): + blank_tag = f"[{opening_tag}]" + text_append(blank_tag) + position += len(blank_tag) + else: + style_stack.append( + ( + position, + opening_tag, + normalize_markup_tag(opening_tag.strip()), + ) + ) elif token_name == "open_closing_tag": tag_text = [] diff --git a/tests/test_content.py b/tests/test_content.py index 9de62d21e4..a5c459f3f6 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -229,6 +229,7 @@ def test_assemble(): ("\\[/foo", "[/foo"), ("\\[/foo]", "[/foo]"), ("\\[]", "[]"), + ("\\[0]", "[0]"), ], ) def test_escape(markup: str, plain: str) -> None: @@ -273,14 +274,3 @@ def test_first_line(): first_line = content.first_line assert first_line.plain == "foo" assert first_line.spans == [Span(0, 3, "red")] - - -def test_errors(): - with pytest.raises(Exception): - Content.from_markup("[") - - with pytest.raises(Exception): - Content.from_markup("[:") - - with pytest.raises(Exception): - Content.from_markup("[foo") diff --git a/tests/test_markup.py b/tests/test_markup.py index 93e2a37b36..346b0a903c 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -10,6 +10,15 @@ ["markup", "content"], [ ("", Content("")), + ("[", Content("[")), + ("[]", Content("[]")), + ("[ ", Content("[ ")), + ("[ ", Content("[ ")), + ("[ ]", Content("[ ]")), + ("[0", Content("[0")), + ("[0]", Content("[0]")), + ("[red", Content("[red")), + ("[red]", Content("")), ("foo", Content("foo")), ("foo\n", Content("foo\n")), ("foo\nbar", Content("foo\nbar")), @@ -152,8 +161,6 @@ def test_to_content(markup: str, content: Content): def test_content_parse_fail() -> None: - with pytest.raises(MarkupError): - to_content("[rgb(1,2,3,4)]foo") with pytest.raises(MarkupError): to_content("[foo]foo[/bar]") with pytest.raises(MarkupError):