diff --git a/src/textual/_markup_playground.py b/src/textual/_markup_playground.py index 720e2d68d4..5005016111 100644 --- a/src/textual/_markup_playground.py +++ b/src/textual/_markup_playground.py @@ -4,25 +4,21 @@ from textual.app import App, ComposeResult from textual.content import Content from textual.reactive import reactive -from textual.widgets import Static, TextArea +from textual.widgets import Footer, Pretty, Static, TextArea class MarkupPlayground(App): TITLE = "Markup Playground" CSS = """ - Screen { - & > * { - margin: 0 1; - height: 1fr; - } + Screen { layout: vertical; #editor { - width: 2fr; + width: 1fr; height: 1fr; border: tab $foreground 50%; padding: 1; - margin: 1 1 0 0; + margin: 1 0 0 0; &:focus { border: tab $primary; } @@ -48,16 +44,35 @@ class MarkupPlayground(App): } overflow-y: auto; } - #results { - + #results { padding: 1 1; + width: 1fr; + } + #spans-container { + border: tab $success; + overflow-y: auto; + margin: 0 0 0 1; + } + #spans { + padding: 1 1; + width: 1fr; + } + HorizontalGroup { + height: 1fr; } } """ AUTO_FOCUS = "#editor" + BINDINGS = [ + ("f1", "toggle('show_variables')", "Variables"), + ("f2", "toggle('show_spans')", "Spans"), + ] variables: reactive[dict[str, object]] = reactive({}) + show_variables = reactive(False) + show_spans = reactive(False) + def compose(self) -> ComposeResult: with containers.HorizontalGroup(): yield (editor := TextArea(id="editor")) @@ -65,11 +80,21 @@ def compose(self) -> ComposeResult: editor.border_title = "Markup" variables.border_title = "Variables (JSON)" - with containers.VerticalScroll( - id="results-container", can_focus=False - ) as container: - yield Static(id="results") - container.border_title = "Output" + with containers.HorizontalGroup(): + with containers.VerticalScroll(id="results-container") as container: + yield Static(id="results") + container.border_title = "Output" + with containers.VerticalScroll(id="spans-container") as container: + yield Pretty([], id="spans") + container.border_title = "Spans" + + yield Footer() + + def watch_show_variables(self, show_variables: bool) -> None: + self.query_one("#variables").display = show_variables + + def watch_show_spans(self, show_spans: bool) -> None: + self.query_one("#spans-container").display = show_spans @on(TextArea.Changed, "#editor") def on_markup_changed(self, event: TextArea.Changed) -> None: @@ -78,13 +103,16 @@ def on_markup_changed(self, event: TextArea.Changed) -> None: def update_markup(self) -> None: results = self.query_one("#results", Static) editor = self.query_one("#editor", TextArea) + spans = self.query_one("#spans", Pretty) try: content = Content.from_markup(editor.text, **self.variables) results.update(content) - except Exception as error: + spans.update(content.spans) + except Exception: from rich.traceback import Traceback results.update(Traceback()) + spans.update([]) self.query_one("#results-container").add_class("-error").scroll_end( animate=False diff --git a/src/textual/markup.py b/src/textual/markup.py index 2d92ae7353..06a0effd98 100644 --- a/src/textual/markup.py +++ b/src/textual/markup.py @@ -411,14 +411,13 @@ def process_text(template_text: str, /) -> str: content_text = "".join(text) text_length = len(content_text) - while style_stack: - position, tag_body, _ = style_stack.pop() + for position, tag_body, _ in style_stack: spans.append(Span(position, text_length, tag_body)) - if style: - content = Content(content_text, [Span(0, len(content_text), style), *spans]) - else: - content = Content(content_text, spans) + content = Content( + content_text, + [Span(0, len(content_text), style), *spans] if style else spans, + ) return content diff --git a/tests/test_markup.py b/tests/test_markup.py index 32032bf5bd..718c61752c 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -77,6 +77,13 @@ ], ), ), + ( + "[#ff0000]Hello, [#ffffff]world!\nMy work here is done.", + Content( + "Hello, world!\nMy work here is done.", + spans=[Span(0, 35, style="#ff0000"), Span(7, 35, style="#ffffff")], + ), + ), ], ) def test_to_content(markup: str, content: Content):