From b7d7bc31708a968b230b9af68dbddcf20cb0472e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?=
<5621605+rodrigogiraoserrao@users.noreply.github.com>
Date: Mon, 19 Feb 2024 17:49:36 +0000
Subject: [PATCH] Markdown reloads when component classes change.
---
CHANGELOG.md | 6 +
src/textual/widgets/_markdown.py | 152 +++++++++-------
.../__snapshots__/test_snapshots.ambr | 169 ++++++++++++++++++
.../markdown_component_classes_reloading.py | 50 ++++++
.../markdown_component_classes_reloading.tcss | 1 +
tests/snapshot_tests/test_snapshots.py | 25 ++-
6 files changed, 341 insertions(+), 62 deletions(-)
create mode 100644 tests/snapshot_tests/snapshot_apps/markdown_component_classes_reloading.py
create mode 100644 tests/snapshot_tests/snapshot_apps/markdown_component_classes_reloading.tcss
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62ff923eb5..9d82d533ce 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## Unreleased
+
+### Fixed
+
+- Markdown component classes weren't refreshed when watching for CSS https://github.com/Textualize/textual/issues/3464
+
## [0.52.0] - 2023-02-19
### Changed
diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py
index 24cde3dd91..4dc1f0e353 100644
--- a/src/textual/widgets/_markdown.py
+++ b/src/textual/widgets/_markdown.py
@@ -103,6 +103,7 @@ def __init__(self, markdown: Markdown, *args, **kwargs) -> None:
self._markdown: Markdown = markdown
"""A reference to the Markdown document that contains this block."""
self._text = Text()
+ self._token: Token | None = None
self._blocks: list[MarkdownBlock] = []
super().__init__(*args, **kwargs)
@@ -118,6 +119,95 @@ async def action_link(self, href: str) -> None:
"""Called on link click."""
self.post_message(Markdown.LinkClicked(self._markdown, href))
+ def notify_style_update(self) -> None:
+ """If CSS was reloaded, try to rebuild this block from its token."""
+ super().notify_style_update()
+ self.rebuild()
+
+ def rebuild(self) -> None:
+ """Rebuild the content of the block if we have a source token."""
+ if self._token is not None:
+ self.build_from_token(self._token)
+
+ def build_from_token(self, token: Token) -> None:
+ """Build the block content from its source token.
+
+ This method allows the block to be rebuilt on demand, which is useful
+ when the styles assigned to the
+ [Markdown.COMPONENT_CLASSES][textual.widgets.Markdown.COMPONENT_CLASSES]
+ change.
+
+ See https://github.com/Textualize/textual/issues/3464 for more information.
+
+ Args:
+ token: The token from which this block is built.
+ """
+
+ self._token = token
+ style_stack: list[Style] = [Style()]
+ content = Text()
+ if token.children:
+ for child in token.children:
+ if child.type == "text":
+ content.append(child.content, style_stack[-1])
+ if child.type == "hardbreak":
+ content.append("\n")
+ if child.type == "softbreak":
+ content.append(" ", style_stack[-1])
+ elif child.type == "code_inline":
+ content.append(
+ child.content,
+ style_stack[-1]
+ + self._markdown.get_component_rich_style(
+ "code_inline", partial=True
+ ),
+ )
+ elif child.type == "em_open":
+ style_stack.append(
+ style_stack[-1]
+ + self._markdown.get_component_rich_style("em", partial=True)
+ )
+ elif child.type == "strong_open":
+ style_stack.append(
+ style_stack[-1]
+ + self._markdown.get_component_rich_style(
+ "strong", partial=True
+ )
+ )
+ elif child.type == "s_open":
+ style_stack.append(
+ style_stack[-1]
+ + self._markdown.get_component_rich_style("s", partial=True)
+ )
+ elif child.type == "link_open":
+ href = child.attrs.get("href", "")
+ action = f"link({href!r})"
+ style_stack.append(
+ style_stack[-1] + Style.from_meta({"@click": action})
+ )
+ elif child.type == "image":
+ href = child.attrs.get("src", "")
+ alt = child.attrs.get("alt", "")
+
+ action = f"link({href!r})"
+ style_stack.append(
+ style_stack[-1] + Style.from_meta({"@click": action})
+ )
+
+ content.append("🖼 ", style_stack[-1])
+ if alt:
+ content.append(f"({alt})", style_stack[-1])
+ if child.children is not None:
+ for grandchild in child.children:
+ content.append(grandchild.content, style_stack[-1])
+
+ style_stack.pop()
+
+ elif child.type.endswith("_close"):
+ style_stack.pop()
+
+ self.set_content(content)
+
class MarkdownHeader(MarkdownBlock):
"""Base class for a Markdown header."""
@@ -833,67 +923,7 @@ def update(self, markdown: str) -> AwaitComplete:
else:
output.append(block)
elif token.type == "inline":
- style_stack: list[Style] = [Style()]
- content = Text()
- if token.children:
- for child in token.children:
- if child.type == "text":
- content.append(child.content, style_stack[-1])
- if child.type == "hardbreak":
- content.append("\n")
- if child.type == "softbreak":
- content.append(" ", style_stack[-1])
- elif child.type == "code_inline":
- content.append(
- child.content,
- style_stack[-1]
- + self.get_component_rich_style(
- "code_inline", partial=True
- ),
- )
- elif child.type == "em_open":
- style_stack.append(
- style_stack[-1]
- + self.get_component_rich_style("em", partial=True)
- )
- elif child.type == "strong_open":
- style_stack.append(
- style_stack[-1]
- + self.get_component_rich_style("strong", partial=True)
- )
- elif child.type == "s_open":
- style_stack.append(
- style_stack[-1]
- + self.get_component_rich_style("s", partial=True)
- )
- elif child.type == "link_open":
- href = child.attrs.get("href", "")
- action = f"link({href!r})"
- style_stack.append(
- style_stack[-1] + Style.from_meta({"@click": action})
- )
- elif child.type == "image":
- href = child.attrs.get("src", "")
- alt = child.attrs.get("alt", "")
-
- action = f"link({href!r})"
- style_stack.append(
- style_stack[-1] + Style.from_meta({"@click": action})
- )
-
- content.append("🖼 ", style_stack[-1])
- if alt:
- content.append(f"({alt})", style_stack[-1])
- if child.children is not None:
- for grandchild in child.children:
- content.append(grandchild.content, style_stack[-1])
-
- style_stack.pop()
-
- elif child.type.endswith("_close"):
- style_stack.pop()
-
- stack[-1].set_content(content)
+ stack[-1].build_from_token(token)
elif token.type in ("fence", "code_block"):
(stack[-1]._blocks if stack else output).append(
MarkdownFence(self, token.content.rstrip(), token.info)
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index f31002578f..e6ead4ca43 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -22170,6 +22170,175 @@
'''
# ---
+# name: test_markdown_component_classes_reloading
+ '''
+
+
+ '''
+# ---
# name: test_markdown_dark_theme_override
'''