Skip to content

Commit 5972df1

Browse files
committed
content simplify
1 parent a38ad12 commit 5972df1

File tree

7 files changed

+96
-8
lines changed

7 files changed

+96
-8
lines changed

src/textual/_compositor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,7 @@ def add_widget(
614614
)
615615
)
616616
widget.set_reactive(Widget.scroll_y, new_scroll_y)
617+
widget.set_reactive(Widget.scroll_target_y, new_scroll_y)
617618
widget.vertical_scrollbar._reactive_position = new_scroll_y
618619

619620
if visible_only:

src/textual/_slug.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,30 @@ def slug(self, text: str) -> str:
114114
if used:
115115
slugged = f"{slugged}-{used}"
116116
return slugged
117+
118+
119+
VALID_ID_CHARACTERS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-")
120+
121+
122+
def slug_for_tcss_id(text: str) -> str:
123+
"""Produce a slug usable as a TCSS id from the given text.
124+
125+
Args:
126+
text: Text.
127+
128+
Returns:
129+
A slugified version of text suitable for use as a TCSS id.
130+
"""
131+
slug = "".join(
132+
(
133+
character
134+
if character in VALID_ID_CHARACTERS
135+
else ord(character).__format__("x")
136+
)
137+
for character in text
138+
)
139+
if not slug:
140+
return "-"
141+
if slug[0].isdecimal():
142+
return f"-{slug}"
143+
return slug

src/textual/content.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,34 @@ def assemble(
395395
text_append(end)
396396
return cls("".join(text), spans)
397397

398+
def simplify(self) -> Content:
399+
"""Simplify spans in place.
400+
401+
This joins contiguous spans together which can produce faster renders.
402+
403+
Note that this is only typically worth it if you have appended a large number of Content instances together,
404+
and it only needs to be done once.
405+
406+
Returns:
407+
Self.
408+
"""
409+
spans = self.spans
410+
if not spans:
411+
return self
412+
last_span = Span(0, 0, Style())
413+
new_spans: list[Span] = []
414+
changed: bool = False
415+
for span in self._spans:
416+
if span.start == last_span.end and span.style == last_span.style:
417+
last_span = new_spans[-1] = Span(last_span.start, span.end, span.style)
418+
changed = True
419+
else:
420+
new_spans.append(span)
421+
last_span = span
422+
if changed:
423+
self._spans[:] = new_spans
424+
return self
425+
398426
def __eq__(self, other: object) -> bool:
399427
"""Compares text only, so that markup doesn't effect sorting."""
400428
if isinstance(other, str):
@@ -528,7 +556,6 @@ def get_span(y: int) -> tuple[int, int] | None:
528556
return None
529557

530558
for y, line in enumerate(self.split(allow_blank=True)):
531-
532559
if post_style is not None:
533560
line = line.stylize(post_style)
534561

@@ -1201,6 +1228,11 @@ def render_segments(
12011228
]
12021229
return segments
12031230

1231+
def __rich__(self):
1232+
from rich.segment import Segments
1233+
1234+
return Segments(self.render_segments(Style(), "\n"))
1235+
12041236
def _divide_spans(self, offsets: tuple[int, ...]) -> list[tuple[Span, int, int]]:
12051237
"""Divide content from a list of offset to cut.
12061238
@@ -1568,7 +1600,6 @@ def to_strip(self, style: Style) -> tuple[list[Segment], int]:
15681600
def _apply_link_style(
15691601
self, link_style: RichStyle, segments: list[Segment]
15701602
) -> list[Segment]:
1571-
15721603
_Segment = Segment
15731604
segments = [
15741605
_Segment(

src/textual/getters.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,37 @@
55

66
from __future__ import annotations
77

8-
from typing import Generic, overload
8+
from typing import Generic, TypeVar, overload
99

1010
from textual.css.query import NoMatches, QueryType, WrongType
1111
from textual.dom import DOMNode
1212
from textual.widget import Widget
1313

14+
AppType = TypeVar("AppType", bound="App")
15+
16+
17+
class app(Generic[AppType]):
18+
"""A typed getter for the app.
19+
20+
Example:
21+
```python
22+
class MyWidget(Widget):
23+
app = getters.app(MyApp)
24+
```
25+
26+
27+
Args:
28+
Generic (_type_): _description_
29+
"""
30+
31+
def __init__(self, app_type: type[AppType]) -> None:
32+
self._app_type = app_type
33+
34+
def __get__(self, obj: DOMNode, obj_type: type[DOMNode]) -> AppType:
35+
app = obj.app
36+
assert isinstance(app, self._app_type)
37+
return app
38+
1439

1540
class query_one(Generic[QueryType]):
1641
"""Create a query one property.

src/textual/visual.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def to_strips(
196196
height: int | None,
197197
style: Style,
198198
*,
199+
apply_selection: bool = True,
199200
pad: bool = False,
200201
post_style: Style | None = None,
201202
) -> list[Strip]:
@@ -207,6 +208,7 @@ def to_strips(
207208
width: Desired width (in cells).
208209
height: Desired height (in lines) or `None` for no limit.
209210
style: A (Visual) Style instance.
211+
apply_selection: Automatically apply selection styles?
210212
pad: Pad to desired width?
211213
post_style: Optional Style to apply to strips after rendering.
212214
@@ -229,7 +231,7 @@ def to_strips(
229231
RenderOptions(
230232
widget._get_style,
231233
widget.styles,
232-
selection,
234+
selection if apply_selection else None,
233235
selection_style,
234236
),
235237
)

src/textual/widgets/_markdown.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from rich.text import Text
1414
from typing_extensions import TypeAlias
1515

16-
from textual._slug import TrackedSlugs, slug
16+
from textual._slug import TrackedSlugs, slug_for_tcss_id
1717
from textual.app import ComposeResult
1818
from textual.await_complete import AwaitComplete
1919
from textual.containers import Horizontal, Vertical, VerticalScroll
@@ -1271,7 +1271,9 @@ def _parse_markdown(self, tokens: Iterable[Token]) -> Iterable[MarkdownBlock]:
12711271
elif token_type.endswith("_close"):
12721272
block = stack.pop()
12731273
if token.type == "heading_close":
1274-
block.id = f"heading-{slug(block._content.plain)}-{id(block)}"
1274+
block.id = (
1275+
f"heading-{slug_for_tcss_id(block._content.plain)}-{id(block)}"
1276+
)
12751277
if stack:
12761278
stack[-1]._blocks.append(block)
12771279
else:

tests/test_slug.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from textual._slug import TrackedSlugs, slug
3+
from textual._slug import TrackedSlugs, slug_for_tcss_id
44

55

66
@pytest.mark.xdist_group("group1")
@@ -31,7 +31,7 @@
3131
)
3232
def test_simple_slug(text: str, expected: str) -> None:
3333
"""The simple slug function should produce the expected slug."""
34-
assert slug(text) == expected
34+
assert slug_for_tcss_id(text) == expected
3535

3636

3737
@pytest.fixture(scope="module")

0 commit comments

Comments
 (0)