diff --git a/src/textual/app.py b/src/textual/app.py index 13ab075ee..d11726bf2 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -28,7 +28,7 @@ import rich.repr from rich.console import Console, RenderableType from rich.measure import Measurement from rich.protocol import is_renderable -from rich.segment import Segments +from rich.segment import Segment, Segments from rich.traceback import Traceback from . import ( @@ -1016,10 +1016,11 @@ class App(Generic[ReturnType], DOMNode): is_renderable(renderable) for renderable in renderables ), "Can only call panic with strings or Rich renderables" - pre_rendered = [ - Segments(self.console.render(renderable, self.console.options)) - for renderable in renderables - ] + def render(renderable: RenderableType) -> list[Segment]: + segments = list(self.console.render(renderable, self.console.options)) + return segments + + pre_rendered = [Segments(render(renderable)) for renderable in renderables] self._exit_renderables.extend(pre_rendered) self._close_messages_no_wait() diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index eeb80ce1b..d2d4123fe 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -53,7 +53,7 @@ class BorderApp(App): def on_button_pressed(self, event: Button.Pressed) -> None: self.text.styles.border = ( event.button.id, - self.stylesheet.variables["secondary"], + self.stylesheet._variables["secondary"], ) self.bell() diff --git a/src/textual/css/_help_renderables.py b/src/textual/css/_help_renderables.py index b1ba7d24b..ca2307f22 100644 --- a/src/textual/css/_help_renderables.py +++ b/src/textual/css/_help_renderables.py @@ -2,8 +2,8 @@ from __future__ import annotations from typing import Iterable +import rich.repr from rich.console import Console, ConsoleOptions, RenderResult - from rich.highlighter import ReprHighlighter from rich.markup import render from rich.text import Text @@ -42,6 +42,7 @@ class Example: yield _markup_and_highlight(f" [dim]e.g. [/][i]{self.markup}[/]") +@rich.repr.auto class Bullet: """Renderable for a single 'bullet point' containing information and optionally some examples pertaining to that information. @@ -59,10 +60,11 @@ class Bullet: def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: - yield _markup_and_highlight(f"{self.markup}") + yield _markup_and_highlight(self.markup) yield from self.examples +@rich.repr.auto class HelpText: """Renderable for help text - the user is shown this when they encounter a style-related error (e.g. setting a style property to an invalid diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 284d70cc7..b379e85d4 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -1,10 +1,16 @@ from __future__ import annotations from functools import lru_cache -from typing import cast, Iterable, NoReturn, Sequence +from typing import Iterable, NoReturn, Sequence, cast import rich.repr +from .._border import BorderValue, normalize_border_value +from .._duration import _duration_as_seconds +from .._easing import EASING +from ..color import Color, ColorParseError +from ..geometry import Spacing, SpacingDimensions, clamp +from ..suggestions import get_suggestion from ._error_tools import friendly_list from ._help_renderables import HelpText from ._help_text import ( @@ -33,34 +39,28 @@ from .constants import ( VALID_ALIGN_VERTICAL, VALID_BORDER, VALID_BOX_SIZING, - VALID_EDGE, VALID_DISPLAY, + VALID_EDGE, VALID_OVERFLOW, - VALID_VISIBILITY, - VALID_STYLE_FLAGS, VALID_SCROLLBAR_GUTTER, + VALID_STYLE_FLAGS, VALID_TEXT_ALIGN, + VALID_VISIBILITY, ) from .errors import DeclarationError, StyleValueError from .model import Declaration from .scalar import ( Scalar, - ScalarOffset, - Unit, ScalarError, + ScalarOffset, ScalarParseError, + Unit, percentage_string_to_float, ) -from .styles import DockGroup, Styles +from .styles import Styles from .tokenize import Token from .transition import Transition -from .types import BoxSizing, Edge, Display, Overflow, Visibility, EdgeType -from .._border import normalize_border_value, BorderValue -from ..color import Color, ColorParseError -from .._duration import _duration_as_seconds -from .._easing import EASING -from ..geometry import Spacing, SpacingDimensions, clamp -from ..suggestions import get_suggestion +from .types import BoxSizing, Display, Edge, EdgeType, Overflow, Visibility def _join_tokens(tokens: Iterable[Token], joiner: str = "") -> str: @@ -434,6 +434,7 @@ class StylesBuilder: process_padding_left = _process_space_partial def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue: + border_type: EdgeType = "solid" border_color = Color(0, 255, 0) @@ -553,7 +554,7 @@ class StylesBuilder: self.styles._rules["offset"] = ScalarOffset(x, y) def process_layout(self, name: str, tokens: list[Token]) -> None: - from ..layouts.factory import get_layout, MissingLayout + from ..layouts.factory import MissingLayout, get_layout if tokens: if len(tokens) != 1: @@ -602,7 +603,6 @@ class StylesBuilder: if color is not None or alpha is not None: if alpha is not None: - color = (color or Color(255, 255, 255)).with_alpha(alpha) self.styles._rules[name] = color diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index 7ea876355..c9ba19fd2 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -301,9 +301,7 @@ def substitute_references( for _token in reference_tokens: yield _token.with_reference( ReferencedBy( - name=ref_name, - location=ref_location, - length=ref_length, + ref_name, ref_location, ref_length, token.code ) ) else: @@ -318,13 +316,10 @@ def substitute_references( variable_tokens = variables[variable_name] ref_location = token.location ref_length = len(token.value) - for token in variable_tokens: - yield token.with_reference( - ReferencedBy( - name=variable_name, - location=ref_location, - length=ref_length, - ) + ref_code = token.code + for _token in variable_tokens: + yield _token.with_reference( + ReferencedBy(variable_name, ref_location, ref_length, ref_code) ) else: _unresolved(variable_name, variables.keys(), token) @@ -336,6 +331,7 @@ def parse( css: str, path: str | PurePath, variables: dict[str, str] | None = None, + variable_tokens: dict[str, list[Token]] | None = None, is_default_rules: bool = False, tie_breaker: int = 0, ) -> Iterable[RuleSet]: @@ -349,7 +345,11 @@ def parse( is_default_rules (bool): True if the rules we're extracting are default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS. """ - variable_tokens = tokenize_values(variables or {}) + + reference_tokens = tokenize_values(variables) if variables is not None else {} + if variable_tokens: + reference_tokens.update(variable_tokens) + tokens = iter(substitute_references(tokenize(css, path), variable_tokens)) while True: token = next(tokens, None) diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index d696525a8..12258fe10 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -2,10 +2,9 @@ from __future__ import annotations import os from collections import defaultdict -from functools import partial from operator import itemgetter from pathlib import Path, PurePath -from typing import Iterable, NamedTuple, cast +from typing import Iterable, NamedTuple, Sequence, cast import rich.repr from rich.console import Console, ConsoleOptions, RenderableType, RenderResult @@ -39,20 +38,15 @@ class StylesheetParseError(StylesheetError): class StylesheetErrors: - def __init__( - self, rules: list[RuleSet], variables: dict[str, str] | None = None - ) -> None: + def __init__(self, rules: list[RuleSet]) -> None: self.rules = rules self.variables: dict[str, str] = {} - self._css_variables: dict[str, list[Token]] = {} - if variables: - self.set_variables(variables) @classmethod def _get_snippet(cls, code: str, line_no: int) -> RenderableType: syntax = Syntax( code, - lexer="scss", + lexer="sass", theme="ansi_light", line_numbers=True, indent_guides=True, @@ -61,11 +55,6 @@ class StylesheetErrors: ) return syntax - def set_variables(self, variable_map: dict[str, str]) -> None: - """Pre-populate CSS variables.""" - self.variables.update(variable_map) - self._css_variables = tokenize_values(self.variables) - def __rich_console__( self, console: Console, options: ConsoleOptions ) -> RenderResult: @@ -105,7 +94,10 @@ class StylesheetErrors: title = Text.assemble(Text("Error at ", style="bold red"), path_text) yield "" yield Panel( - self._get_snippet(token.code, line_no), + self._get_snippet( + token.referenced_by.code if token.referenced_by else token.code, + line_no, + ), title=title, title_align="left", border_style="red", @@ -138,13 +130,20 @@ class Stylesheet: def __init__(self, *, variables: dict[str, str] | None = None) -> None: self._rules: list[RuleSet] = [] self._rules_map: dict[str, list[RuleSet]] | None = None - self.variables = variables or {} + self._variables = variables or {} + self.__variable_tokens: dict[str, list[Token]] | None = None self.source: dict[str, CssSource] = {} self._require_parse = False def __rich_repr__(self) -> rich.repr.Result: yield list(self.source.keys()) + @property + def _variable_tokens(self) -> dict[str, list[Token]]: + if self.__variable_tokens is None: + self.__variable_tokens = tokenize_values(self._variables) + return self.__variable_tokens + @property def rules(self) -> list[RuleSet]: """List of rule sets. @@ -183,7 +182,7 @@ class Stylesheet: Returns: Stylesheet: New stylesheet. """ - stylesheet = Stylesheet(variables=self.variables.copy()) + stylesheet = Stylesheet(variables=self._variables.copy()) stylesheet.source = self.source.copy() return stylesheet @@ -193,7 +192,8 @@ class Stylesheet: Args: variables (dict[str, str]): A mapping of name to variable. """ - self.variables = variables + self._variables = variables + self._variables_tokens = None def _parse_rules( self, @@ -222,7 +222,7 @@ class Stylesheet: parse( css, path, - variables=self.variables, + variable_tokens=self._variable_tokens, is_default_rules=is_default_rules, tie_breaker=tie_breaker, ) @@ -317,7 +317,7 @@ class Stylesheet: """ # Do this in a fresh Stylesheet so if there are errors we don't break self. - stylesheet = Stylesheet(variables=self.variables) + stylesheet = Stylesheet(variables=self._variables) for path, (css, is_defaults, tie_breaker) in self.source.items(): stylesheet.add_source( css, path, is_default_css=is_defaults, tie_breaker=tie_breaker diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index c7fb9183b..15dc90508 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -118,6 +118,7 @@ class ReferencedBy(NamedTuple): name: str location: tuple[int, int] length: int + code: str @rich.repr.auto @@ -209,6 +210,7 @@ class Tokenizer: message, ) iter_groups = iter(match.groups()) + next(iter_groups) for name, value in zip(expect.names, iter_groups): diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py index 606cdf371..f13c5294f 100644 --- a/src/textual/widgets/_button.py +++ b/src/textual/widgets/_button.py @@ -36,7 +36,7 @@ class Button(Widget, can_focus=True): height: 3; background: $panel; color: $text; - border: none; + border: none; border-top: tall $panel-lighten-2; border-bottom: tall $panel-darken-3; content-align: center middle;