From 66e8cd392665e90096d552d84cc49ebfa96f8dec Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 20:19:59 -0400 Subject: [PATCH 1/2] Supply inline Markdown content through its document owner --- src/textual/widgets/_markdown.py | 13 +++++++++++-- tests/test_markdown.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 5deb964d4a..b35bcf0411 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -294,10 +294,11 @@ def build_from_token(self, token: Token) -> None: token: The token from which this block is built. """ self._inline_token = token - content = self._token_to_content(token) + content = self._markdown._get_token_content(token) self.set_content(content) - def _token_to_content(self, token: Token) -> Content: + @staticmethod + def _token_to_content(token: Token) -> Content: """Convert an inline token to Textual Content. Args: @@ -1282,6 +1283,14 @@ async def load(self, path: Path | MarkdownLocation) -> None: """ await self.sanitize_location(path).load(self) + def _get_token_content(self, token: Token) -> Content: + """Supply inline content for every block, including headings and tables. + + Prepared documents may return their acquired content here; ordinary + documents use the same native conversion without a separate parser. + """ + return MarkdownBlock._token_to_content(token) + def unhandled_token(self, token: Token) -> MarkdownBlock | None: """Process an unhandled token. diff --git a/tests/test_markdown.py b/tests/test_markdown.py index fda3ba7d24..f2d0d854cc 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -12,6 +12,7 @@ import textual.widgets._markdown as MD from textual import on from textual.app import App, ComposeResult +from textual.content import Content from textual.style import Style from textual.widget import Widget from textual.widgets import Markdown @@ -41,6 +42,29 @@ def compose(self) -> ComposeResult: yield FussyMarkdown(self._markdown) +async def test_document_supplies_inline_content_for_heading_paragraph_and_table(): + class SuppliedMarkdown(Markdown): + def _get_token_content(self, token: Token) -> Content: + return super()._get_token_content(token) + Content(" supplied") + + document = SuppliedMarkdown( + "# Heading\n\nParagraph [link](https://example.com)\n\n" + "| Header |\n| --- |\n| Cell |" + ) + async with App().run_test() as pilot: + await pilot.app.mount(document) + await pilot.pause() + assert document.query_one(MD.MarkdownH1)._content.plain == "Heading supplied" + paragraph = document.query_one(MD.MarkdownParagraph)._content + assert paragraph.plain == "Paragraph link supplied" + assert paragraph.spans == [ + MD.Span(10, 14, Style.from_meta({"@click": "link('https://example.com')"})) + ] + table = document.query_one(MD.MarkdownTable) + assert [content.plain for content in table._headers] == ["Header supplied"] + assert [[content.plain for content in row] for row in table._rows] == [["Cell supplied"]] + + @pytest.mark.parametrize( ["document", "expected_nodes"], [ From 253c02886ab2bbcee83b5d79d5c8448ddc08abba Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Wed, 7 Oct 2026 20:23:45 -0400 Subject: [PATCH 2/2] Preserve custom block conversion through document content supply --- src/textual/widgets/_markdown.py | 6 +++--- tests/test_markdown.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index b35bcf0411..9dd6f23699 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -294,7 +294,7 @@ def build_from_token(self, token: Token) -> None: token: The token from which this block is built. """ self._inline_token = token - content = self._markdown._get_token_content(token) + content = self._markdown._get_token_content(token, block=self) self.set_content(content) @staticmethod @@ -1283,13 +1283,13 @@ async def load(self, path: Path | MarkdownLocation) -> None: """ await self.sanitize_location(path).load(self) - def _get_token_content(self, token: Token) -> Content: + def _get_token_content(self, token: Token, *, block: MarkdownBlock) -> Content: """Supply inline content for every block, including headings and tables. Prepared documents may return their acquired content here; ordinary documents use the same native conversion without a separate parser. """ - return MarkdownBlock._token_to_content(token) + return block._token_to_content(token) def unhandled_token(self, token: Token) -> MarkdownBlock | None: """Process an unhandled token. diff --git a/tests/test_markdown.py b/tests/test_markdown.py index f2d0d854cc..502f3d2098 100644 --- a/tests/test_markdown.py +++ b/tests/test_markdown.py @@ -43,9 +43,15 @@ def compose(self) -> ComposeResult: async def test_document_supplies_inline_content_for_heading_paragraph_and_table(): + class CustomParagraph(MD.MarkdownParagraph): + def _token_to_content(self, token: Token) -> Content: + return super()._token_to_content(token) + Content(" custom") + class SuppliedMarkdown(Markdown): - def _get_token_content(self, token: Token) -> Content: - return super()._get_token_content(token) + Content(" supplied") + BLOCKS = {**Markdown.BLOCKS, "paragraph_open": CustomParagraph} + + def _get_token_content(self, token: Token, *, block: MarkdownBlock) -> Content: + return super()._get_token_content(token, block=block) + Content(" supplied") document = SuppliedMarkdown( "# Heading\n\nParagraph [link](https://example.com)\n\n" @@ -56,7 +62,7 @@ def _get_token_content(self, token: Token) -> Content: await pilot.pause() assert document.query_one(MD.MarkdownH1)._content.plain == "Heading supplied" paragraph = document.query_one(MD.MarkdownParagraph)._content - assert paragraph.plain == "Paragraph link supplied" + assert paragraph.plain == "Paragraph link custom supplied" assert paragraph.spans == [ MD.Span(10, 14, Style.from_meta({"@click": "link('https://example.com')"})) ]