From ec3259737f7ede2181f03735afab68712cac59bf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Jun 2025 10:30:56 +0100 Subject: [PATCH 1/5] Fix for overlapping tags --- src/textual/markup.py | 12 ++++++++-- tests/test_markup.py | 54 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/textual/markup.py b/src/textual/markup.py index 06a0effd98..79837c6a35 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -5,6 +5,8 @@ from __future__ import annotations +from operator import attrgetter + from textual.css.parse import substitute_references from textual.css.tokenizer import UnexpectedEnd @@ -411,8 +413,14 @@ def process_text(template_text: str, /) -> str: content_text = "".join(text) text_length = len(content_text) - for position, tag_body, _ in style_stack: - spans.append(Span(position, text_length, tag_body)) + spans.extend( + [ + Span(position, text_length, tag_body) + for position, tag_body, _ in reversed(style_stack) + ] + ) + spans.reverse() + spans.sort(key=attrgetter("start")) content = Content( content_text, diff --git a/tests/test_markup.py b/tests/test_markup.py index 718c61752c..443190bcec 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -35,9 +35,9 @@ Content( "What is up with you?", spans=[ + Span(0, 20, style="b"), Span(0, 10, style="on red"), Span(5, 20, style="i"), - Span(0, 20, style="b"), ], ), ), @@ -84,6 +84,58 @@ spans=[Span(0, 35, style="#ff0000"), Span(7, 35, style="#ffffff")], ), ), + ( + "[blue][green][red]R[/red]G[/green]B[/blue]", + Content( + "RGB", + spans=[ + Span(0, 3, "blue"), + Span(0, 2, "green"), + Span(0, 1, "red"), + ], + ), + ), + ( + "[red][blue]X[/blue][/red]", + Content( + "X", + spans=[ + Span(0, 1, "red"), + Span(0, 1, "blue"), + ], + ), + ), + # Non-nested tags + ( + "[red][blue]X[/red][/blue]", + Content( + "X", + spans=[ + Span(0, 1, "blue"), + Span(0, 1, "red"), + ], + ), + ), + ( + "[red][blue]X[/red]", + Content( + "X", + spans=[ + Span(0, 1, "blue"), + Span(0, 1, "red"), + ], + ), + ), + ( + "[red][blue]X", + Content( + "X", + spans=[ + Span(0, 1, "red"), + Span(0, 1, "blue"), + ], + ), + ), ], ) def test_to_content(markup: str, content: Content): From 2e46624f7342bad69a09cc1a4f92bea3303e490a Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Jun 2025 10:35:53 +0100 Subject: [PATCH 2/5] possible optimization --- src/textual/markup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/markup.py b/src/textual/markup.py index 79837c6a35..0a62289332 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -5,7 +5,7 @@ from __future__ import annotations -from operator import attrgetter +from operator import itemgetter from textual.css.parse import substitute_references from textual.css.tokenizer import UnexpectedEnd @@ -420,7 +420,7 @@ def process_text(template_text: str, /) -> str: ] ) spans.reverse() - spans.sort(key=attrgetter("start")) + spans.sort(key=itemgetter(0)) # Zeroth item of Span is 'start' attribute content = Content( content_text, From aa19a51c000680a0c780b192112b56f112d24b2e Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Jun 2025 10:38:27 +0100 Subject: [PATCH 3/5] small potatoes --- src/textual/markup.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/textual/markup.py b/src/textual/markup.py index 0a62289332..75b0de452e 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -413,18 +413,19 @@ def process_text(template_text: str, /) -> str: content_text = "".join(text) text_length = len(content_text) - spans.extend( - [ - Span(position, text_length, tag_body) - for position, tag_body, _ in reversed(style_stack) - ] - ) + if style_stack: + spans.extend( + [ + Span(position, text_length, tag_body) + for position, tag_body, _ in reversed(style_stack) + ] + ) spans.reverse() spans.sort(key=itemgetter(0)) # Zeroth item of Span is 'start' attribute content = Content( content_text, - [Span(0, len(content_text), style), *spans] if style else spans, + [Span(0, text_length, style), *spans] if style else spans, ) return content From f2b0da65f8a54dc9cc48142e5c0063417b730f40 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Jun 2025 10:44:15 +0100 Subject: [PATCH 4/5] edge cases --- src/textual/markup.py | 7 ++++--- tests/test_markup.py | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/textual/markup.py b/src/textual/markup.py index 75b0de452e..13aea5e0aa 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -409,11 +409,12 @@ def process_text(template_text: str, /) -> str: if not style_stack: raise MarkupError("auto closing tag ('[/]') has nothing to close") open_position, tag_body, _ = style_stack.pop() - spans.append(Span(open_position, position, tag_body)) + if open_position != position: + spans.append(Span(open_position, position, tag_body)) content_text = "".join(text) text_length = len(content_text) - if style_stack: + if style_stack and text_length: spans.extend( [ Span(position, text_length, tag_body) @@ -425,7 +426,7 @@ def process_text(template_text: str, /) -> str: content = Content( content_text, - [Span(0, text_length, style), *spans] if style else spans, + [Span(0, text_length, style), *spans] if (style and text_length) else spans, ) return content diff --git a/tests/test_markup.py b/tests/test_markup.py index 443190bcec..5bde3755a2 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -136,6 +136,11 @@ ], ), ), + # Edge cases + ("[bold][/bold]", Content("")), + ("[bold][/]", Content("")), + ("[bold]", Content("")), + ("", Content("")), ], ) def test_to_content(markup: str, content: Content): From 93738b631461a8e8e76c93d78b7541a2ab6ec130 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 1 Jun 2025 10:53:19 +0100 Subject: [PATCH 5/5] another edge case --- src/textual/markup.py | 1 + tests/test_markup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/textual/markup.py b/src/textual/markup.py index 13aea5e0aa..c2a4571bde 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -419,6 +419,7 @@ def process_text(template_text: str, /) -> str: [ Span(position, text_length, tag_body) for position, tag_body, _ in reversed(style_stack) + if position != text_length ] ) spans.reverse() diff --git a/tests/test_markup.py b/tests/test_markup.py index 5bde3755a2..93e2a37b36 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -141,6 +141,7 @@ ("[bold][/]", Content("")), ("[bold]", Content("")), ("", Content("")), + ("[red][green][/red]", Content("")), ], ) def test_to_content(markup: str, content: Content):