Repository: Textualize/textual

Issue:
Nested colors in markup are ignored
While investigating #5820, I found other cases where nested colors in markup seem to be ignored. 

Here's a simple example:

```
[blue][green][red]R[/red]G[/green]B[/blue]
```

![Image](https://github.com/user-attachments/assets/31313a9d-550a-4962-ac92-5eda91dac814)


Discussion:
We found the following entries in the [FAQ](https://textual.textualize.io/FAQ/) which you may find helpful:


- [Why doesn't Textual look good on macOS?](https://textual.textualize.io/FAQ/#why-doesnt-textual-look-good-on-macos)
- [Why doesn't Textual support ANSI themes?](https://textual.textualize.io/FAQ/#why-doesnt-textual-support-ansi-themes)
- [How do I center a widget in a screen?](https://textual.textualize.io/FAQ/#how-do-i-center-a-widget-in-a-screen)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
Don't forget to [star](https://github.com/Textualize/textual) the repository!

Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates.



Patch:
diff --git a/src/textual/markup.py b/src/textual/markup.py
index 06a0effd98..c2a4571bde 100644
--- a/src/textual/markup.py
+++ b/src/textual/markup.py
@@ -5,6 +5,8 @@
 
 from __future__ import annotations
 
+from operator import itemgetter
+
 from textual.css.parse import substitute_references
 from textual.css.tokenizer import UnexpectedEnd
 
@@ -407,16 +409,25 @@ 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)
-    for position, tag_body, _ in style_stack:
-        spans.append(Span(position, text_length, tag_body))
+    if style_stack and text_length:
+        spans.extend(
+            [
+                Span(position, text_length, tag_body)
+                for position, tag_body, _ in reversed(style_stack)
+                if position != text_length
+            ]
+        )
+    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 and text_length) else spans,
     )
 
     return content


Test patch:
diff --git a/tests/test_markup.py b/tests/test_markup.py
index 718c61752c..93e2a37b36 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,64 @@
                 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"),
+                ],
+            ),
+        ),
+        # Edge cases
+        ("[bold][/bold]", Content("")),
+        ("[bold][/]", Content("")),
+        ("[bold]", Content("")),
+        ("", Content("")),
+        ("[red][green][/red]", Content("")),
     ],
 )
 def test_to_content(markup: str, content: Content):