content simplify

This commit is contained in:
Will McGugan
2025-08-06 16:17:39 +01:00
parent a38ad12e6b
commit 5972df1f4f
7 changed files with 96 additions and 8 deletions

View File

@@ -614,6 +614,7 @@ class Compositor:
)
)
widget.set_reactive(Widget.scroll_y, new_scroll_y)
widget.set_reactive(Widget.scroll_target_y, new_scroll_y)
widget.vertical_scrollbar._reactive_position = new_scroll_y
if visible_only:

View File

@@ -114,3 +114,30 @@ class TrackedSlugs:
if used:
slugged = f"{slugged}-{used}"
return slugged
VALID_ID_CHARACTERS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-")
def slug_for_tcss_id(text: str) -> str:
"""Produce a slug usable as a TCSS id from the given text.
Args:
text: Text.
Returns:
A slugified version of text suitable for use as a TCSS id.
"""
slug = "".join(
(
character
if character in VALID_ID_CHARACTERS
else ord(character).__format__("x")
)
for character in text
)
if not slug:
return "-"
if slug[0].isdecimal():
return f"-{slug}"
return slug

View File

@@ -395,6 +395,34 @@ class Content(Visual):
text_append(end)
return cls("".join(text), spans)
def simplify(self) -> Content:
"""Simplify spans in place.
This joins contiguous spans together which can produce faster renders.
Note that this is only typically worth it if you have appended a large number of Content instances together,
and it only needs to be done once.
Returns:
Self.
"""
spans = self.spans
if not spans:
return self
last_span = Span(0, 0, Style())
new_spans: list[Span] = []
changed: bool = False
for span in self._spans:
if span.start == last_span.end and span.style == last_span.style:
last_span = new_spans[-1] = Span(last_span.start, span.end, span.style)
changed = True
else:
new_spans.append(span)
last_span = span
if changed:
self._spans[:] = new_spans
return self
def __eq__(self, other: object) -> bool:
"""Compares text only, so that markup doesn't effect sorting."""
if isinstance(other, str):
@@ -528,7 +556,6 @@ class Content(Visual):
return None
for y, line in enumerate(self.split(allow_blank=True)):
if post_style is not None:
line = line.stylize(post_style)
@@ -1201,6 +1228,11 @@ class Content(Visual):
]
return segments
def __rich__(self):
from rich.segment import Segments
return Segments(self.render_segments(Style(), "\n"))
def _divide_spans(self, offsets: tuple[int, ...]) -> list[tuple[Span, int, int]]:
"""Divide content from a list of offset to cut.
@@ -1568,7 +1600,6 @@ class _FormattedLine:
def _apply_link_style(
self, link_style: RichStyle, segments: list[Segment]
) -> list[Segment]:
_Segment = Segment
segments = [
_Segment(

View File

@@ -5,12 +5,37 @@ Descriptors to define properties on your widget, screen, or App.
from __future__ import annotations
from typing import Generic, overload
from typing import Generic, TypeVar, overload
from textual.css.query import NoMatches, QueryType, WrongType
from textual.dom import DOMNode
from textual.widget import Widget
AppType = TypeVar("AppType", bound="App")
class app(Generic[AppType]):
"""A typed getter for the app.
Example:
```python
class MyWidget(Widget):
app = getters.app(MyApp)
```
Args:
Generic (_type_): _description_
"""
def __init__(self, app_type: type[AppType]) -> None:
self._app_type = app_type
def __get__(self, obj: DOMNode, obj_type: type[DOMNode]) -> AppType:
app = obj.app
assert isinstance(app, self._app_type)
return app
class query_one(Generic[QueryType]):
"""Create a query one property.

View File

@@ -196,6 +196,7 @@ class Visual(ABC):
height: int | None,
style: Style,
*,
apply_selection: bool = True,
pad: bool = False,
post_style: Style | None = None,
) -> list[Strip]:
@@ -207,6 +208,7 @@ class Visual(ABC):
width: Desired width (in cells).
height: Desired height (in lines) or `None` for no limit.
style: A (Visual) Style instance.
apply_selection: Automatically apply selection styles?
pad: Pad to desired width?
post_style: Optional Style to apply to strips after rendering.
@@ -229,7 +231,7 @@ class Visual(ABC):
RenderOptions(
widget._get_style,
widget.styles,
selection,
selection if apply_selection else None,
selection_style,
),
)

View File

@@ -13,7 +13,7 @@ from markdown_it.token import Token
from rich.text import Text
from typing_extensions import TypeAlias
from textual._slug import TrackedSlugs, slug
from textual._slug import TrackedSlugs, slug_for_tcss_id
from textual.app import ComposeResult
from textual.await_complete import AwaitComplete
from textual.containers import Horizontal, Vertical, VerticalScroll
@@ -1271,7 +1271,9 @@ class Markdown(Widget):
elif token_type.endswith("_close"):
block = stack.pop()
if token.type == "heading_close":
block.id = f"heading-{slug(block._content.plain)}-{id(block)}"
block.id = (
f"heading-{slug_for_tcss_id(block._content.plain)}-{id(block)}"
)
if stack:
stack[-1]._blocks.append(block)
else:

View File

@@ -1,6 +1,6 @@
import pytest
from textual._slug import TrackedSlugs, slug
from textual._slug import TrackedSlugs, slug_for_tcss_id
@pytest.mark.xdist_group("group1")
@@ -31,7 +31,7 @@ from textual._slug import TrackedSlugs, slug
)
def test_simple_slug(text: str, expected: str) -> None:
"""The simple slug function should produce the expected slug."""
assert slug(text) == expected
assert slug_for_tcss_id(text) == expected
@pytest.fixture(scope="module")