mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
remove blank styles, refresh table correctly
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user