remove blank styles, refresh table correctly

This commit is contained in:
Will McGugan
2025-07-21 12:35:49 +01:00
parent 01aa3d33af
commit 4525d7e3a2
6 changed files with 149 additions and 46 deletions

View File

@@ -1375,7 +1375,7 @@ class App(Generic[ReturnType], DOMNode):
self.set_class(not dark, "-light-mode", update=False)
self._refresh_truecolor_filter(self.ansi_theme)
self._invalidate_css()
self.call_next(self.refresh_css)
self.call_next(partial(self.refresh_css, animate=False))
self.call_next(self.theme_changed_signal.publish, theme)
def _invalidate_css(self) -> None:

View File

@@ -126,6 +126,7 @@ class Content(Visual):
text: str = "",
spans: list[Span] | None = None,
cell_length: int | None = None,
get_style: Callable[[str], Style | None] | None = None,
) -> None:
"""
Initialize a Content object.
@@ -138,6 +139,7 @@ class Content(Visual):
self._text: str = _strip_control_codes(text)
self._spans: list[Span] = [] if spans is None else spans
self._cell_length = cell_length
self._get_style = get_style
self._optimal_width_cache: int | None = None
self._minimal_width_cache: int | None = None
self._height_cache: tuple[tuple[int, str, bool] | None, int] = (None, 0)
@@ -335,7 +337,9 @@ class Content(Visual):
if not text:
return Content("")
span_length = cell_len(text) if cell_length is None else cell_length
new_content = cls(text, [Span(0, span_length, style)], span_length)
new_content = cls(
text, [Span(0, span_length, style)] if style else None, span_length
)
return new_content
@classmethod
@@ -822,6 +826,7 @@ class Content(Visual):
extend_spans(
_Span(offset + start, offset + end, style)
for start, end, style in content._spans
if style
)
offset += len(content._text)
if total_cell_length is not None:
@@ -1122,16 +1127,21 @@ class Content(Visual):
get_style: Callable[[str | Style], Style]
if parse_style is None:
def get_style(style: str | Style) -> Style:
def _get_style(style: str | Style) -> Style:
"""The default get_style method."""
if isinstance(style, Style):
return style
try:
visual_style = Style.parse(style)
except Exception:
visual_style = Style.null()
if self._get_style is not None:
visual_style = self._get_style(style) or Style.null()
else:
visual_style = Style.null()
return visual_style
get_style = _get_style
else:
get_style = parse_style

View File

@@ -1164,6 +1164,19 @@ class Widget(DOMNode):
return visual_style
def _get_style(self, style: str) -> VisualStyle | None:
"""A get_style method for use in Content.
Args:
style: A style prefixed with a dot.
Returns:
A visual style if one is fund, otherwise `None`.
"""
if style.startswith("."):
return self.get_visual_style(style[1:])
return None
@overload
def render_str(self, text_content: str) -> Content: ...

View File

@@ -199,6 +199,7 @@ class MarkdownBlock(Static):
self._content: Content = Content()
self._token: Token = token
self._blocks: list[MarkdownBlock] = []
self._inline_token: Token | None = None
self.source_range: tuple[int, int] = source_range or (
(token.map[0], token.map[1]) if token.map is not None else (0, 0)
)
@@ -236,29 +237,36 @@ class MarkdownBlock(Static):
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."""
# # self.refresh(layout=True)
# # """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."""
# return
# if self._token is not None:
# self.build_from_token(self._token)
def rebuild(self) -> None:
"""Rebuild the content of the block if we have a source token."""
if self._inline_token is not None:
self.build_from_token(self._inline_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.
"""Build inline block content from its source token.
Args:
token: The token from which this block is built.
"""
self._inline_token = token
content = self._token_to_content(token)
self.set_content(content)
def _token_to_content(self, token: Token) -> Content:
"""Convert an inline token to Textual Content.
Args:
token: A markdown token.
Returns:
Content instance.
"""
null_style = Style.null()
style_stack: list[Style] = [Style()]
@@ -274,16 +282,17 @@ class MarkdownBlock(Static):
if pending_content:
top_text, top_style = pending_content[-1]
if top_style == style:
# Combine contiguous styles
pending_content[-1] = (top_text + text, style)
else:
pending_content.append((text, style))
else:
pending_content.append((text, style))
get_visual_style = self._markdown.get_visual_style
if token.children is None:
self.set_content(Content(""))
return
return Content("")
get_visual_style = self._markdown.get_visual_style
for child in token.children:
child_type = child.type
if child_type == "text":
@@ -334,7 +343,7 @@ class MarkdownBlock(Static):
style_stack.pop()
content = Content("").join(starmap(Content.styled, pending_content))
self.set_content(content)
return content
class MarkdownHeader(MarkdownBlock):
@@ -450,6 +459,14 @@ class MarkdownParagraph(MarkdownBlock):
}
"""
async def _update_from_block(self, block: MarkdownBlock):
if isinstance(block, MarkdownParagraph):
self.set_content(block._content)
self._token = block._token
self._inline_token = block._inline_token
else:
await super()._update_from_block(block)
class MarkdownBlockQuote(MarkdownBlock):
"""A block quote Markdown block."""
@@ -631,6 +648,17 @@ class MarkdownTableContent(Widget):
).with_tooltip(cell.plain)
self.last_row = row_index
def _update_content(self, headers: list[Content], rows: list[list[Content]]):
"""Update cell contents."""
self.headers = headers
self.rows = rows
cells: list[Content] = [
*self.headers,
*[cell for row in self.rows for cell in row],
]
for child, updated_cell in zip(self.query(MarkdownTableCellContents), cells):
child.update(updated_cell, layout=False)
async def _update_rows(self, updated_rows: list[list[Content]]) -> None:
self.styles.grid_size_columns = len(self.headers)
await self.query_children(f".cell.row{self.last_row}").remove()
@@ -702,6 +730,34 @@ class MarkdownTable(MarkdownBlock):
rows.pop()
return headers, rows
# def notify_style_update(self) -> None:
# self.call_after_refresh(self.rebuild)
def rebuild(self) -> None:
self._rebuild()
def _rebuild(self) -> None:
try:
table_content = self.query_one(MarkdownTableContent)
except NoMatches:
return
def flatten(block: MarkdownBlock) -> Iterable[MarkdownBlock]:
for block in block._blocks:
if block._blocks:
yield from flatten(block)
yield block
for block in flatten(self):
if block._inline_token is not None:
self.log(block._inline_token)
block.rebuild()
headers, rows = self._get_headers_and_rows()
self._headers = headers
self._rows = rows
table_content._update_content(headers, rows)
async def _update_from_block(self, block: MarkdownBlock) -> None:
"""Special case to update a Markdown table.
@@ -832,6 +888,12 @@ class MarkdownFence(MarkdownBlock):
def highlight(cls, code: str, language: str) -> Content:
return highlight(code, language=language)
async def _update_from_block(self, block: MarkdownBlock):
if isinstance(block, MarkdownFence):
self.set_content(block._highlighted_code)
else:
await super()._update_from_block(block)
def on_mount(self):
self.set_content(self._highlighted_code)
@@ -853,31 +915,32 @@ class Markdown(Widget):
height: auto;
padding: 0 2 0 2;
layout: vertical;
color: $foreground;
# background: $surface;
color: $foreground;
overflow-y: hidden;
&:focus {
background-tint: $foreground 5%;
# &:focus {
# background-tint: $foreground 5%;
# }
&:dark > .code_inline {
background: $warning 10%;
color: $text-warning 95%;
}
&:dark .code_inline {
background: $warning-muted 30%;
color: $text-warning;
&:light > .code_inline {
background: $error 5%;
color: $text-error 95%;
}
&:light .code_inline {
background: $error-muted 30%;
color: $text-error;
& > .em {
text-style: italic;
}
& > .strong {
text-style: bold;
}
& > .s {
text-style: strike;
}
}
.em {
text-style: italic;
}
.strong {
text-style: bold;
}
.s {
text-style: strike;
}
"""
@@ -1027,10 +1090,18 @@ class Markdown(Widget):
return self.BLOCKS[block_name]
def notify_style_update(self) -> None:
if self.app.theme != self._theme or self.app.debug:
self.update(self.source)
super().notify_style_update()
if self.app.theme == self._theme:
return
self._theme = self.app.theme
def rebuild_all() -> None:
for child in self.query_children(MarkdownBlock):
child.rebuild()
self.call_after_refresh(rebuild_all)
async def _on_mount(self, _: Mount) -> None:
initial_markdown = self._initial_markdown
self._initial_markdown = None
@@ -1252,6 +1323,15 @@ class Markdown(Widget):
else:
yield external
def _build_from_source(self, markdown: str) -> list[MarkdownBlock]:
parser = (
MarkdownIt("gfm-like")
if self._parser_factory is None
else self._parser_factory()
)
tokens = parser.parse(markdown)
return list(self._parse_markdown(tokens, []))
def update(self, markdown: str) -> AwaitComplete:
"""Update the document with new Markdown.

View File

@@ -76,13 +76,14 @@ class Static(Widget, inherit_bindings=False):
"""
return self.visual
def update(self, content: VisualType = "") -> None:
def update(self, content: VisualType = "", *, layout: bool = True) -> None:
"""Update the widget's content area with new text or Rich renderable.
Args:
content: New content.
layout: Also perform a layout operation (set to `False` if you are certain the size won't change.)
"""
self._content = content
self._visual = visualize(self, content, markup=self._render_markup)
self.refresh(layout=True)
self.refresh(layout=layout)

View File

@@ -112,7 +112,6 @@ URL](https://example.com)\
print(paragraph._content.spans)
expected_spans = [
Span(0, 8, Style()),
Span(8, 20, Style.from_meta({"@click": "link('https://example.com')"})),
]
print(expected_spans)