diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index 70d731eee..5fd4ec9b3 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -10,6 +10,7 @@ from textual.css.constants import ( VALID_EDGE, VALID_ALIGN_HORIZONTAL, VALID_ALIGN_VERTICAL, + VALID_STYLE_FLAGS, ) if sys.version_info >= (3, 8): @@ -384,7 +385,7 @@ def border_property_help_text( ], ), Bullet( - f"Valid values for are:\n {friendly_list(VALID_BORDER)}" + f"Valid values for are:\n{friendly_list(VALID_BORDER)}" ), Bullet( f"Colors can be specified using hex, RGB, or ANSI color names" @@ -626,3 +627,36 @@ def offset_single_axis_help_text(property_name: str) -> HelpText: Bullet(f"Valid scalar units are {friendly_list(SYMBOL_UNIT)}"), ], ) + + +def style_flags_property_help_text( + property_name: str, value: str, context: StylingContext | None +) -> HelpText: + property_name = _contextualize_property_name(property_name, context) + return HelpText( + summary=f"Invalid value '{value}' in [i]{property_name}[/] property", + bullets=[ + Bullet( + f"Style flag values such as [i]{property_name}[/] expect space-separated values" + ), + Bullet(f"Permitted values are {friendly_list(VALID_STYLE_FLAGS)}"), + *ContextSpecificBullets( + inline=[ + Bullet( + markup="In Python, you can supply a string or Style object", + examples=[ + Example( + f'widget.styles.{property_name} = "bold italic underline"' + ) + ], + ), + ], + css=[ + Bullet( + markup="In Textual CSS, you can supply style flags separated by spaces", + examples=[Example(f"{property_name}: bold italic underline;")], + ) + ], + ).get_by_context(context), + ], + ) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index b2de6de4f..1b30dddef 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -19,6 +19,7 @@ from ._help_text import ( layout_property_help_text, fractional_property_help_text, offset_property_help_text, + style_flags_property_help_text, ) from ._help_text import ( spacing_wrong_number_of_values, @@ -28,7 +29,7 @@ from ._help_text import ( ) from ..color import Color, ColorPair, ColorParseError from ._error_tools import friendly_list -from .constants import NULL_SPACING +from .constants import NULL_SPACING, VALID_STYLE_FLAGS from .errors import StyleTypeError, StyleValueError from .scalar import ( get_symbols, @@ -790,20 +791,6 @@ class ColorProperty: class StyleFlagsProperty: """Descriptor for getting and set style flag properties (e.g. ``bold italic underline``).""" - _VALID_PROPERTIES = { - "none", - "not", - "bold", - "italic", - "underline", - "overline", - "strike", - "b", - "i", - "u", - "o", - } - def __set_name__(self, owner: Styles, name: str) -> None: self.name = name @@ -840,12 +827,14 @@ class StyleFlagsProperty: obj.refresh() else: words = [word.strip() for word in style_flags.split(" ")] - valid_word = self._VALID_PROPERTIES.__contains__ + valid_word = VALID_STYLE_FLAGS.__contains__ for word in words: if not valid_word(word): raise StyleValueError( - f"unknown word {word!r} in style flags, " - f"valid values are {friendly_list(self._VALID_PROPERTIES)}" + f"unknown word {word!r} in style flags", + help_text=style_flags_property_help_text( + self.name, word, context="inline" + ), ) style = Style.parse(style_flags) if obj.set_rule(self.name, style): diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 589ee4e46..ac1e96eb1 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -20,6 +20,7 @@ from ._help_text import ( align_help_text, offset_property_help_text, offset_single_axis_help_text, + style_flags_property_help_text, ) from .constants import ( VALID_ALIGN_HORIZONTAL, @@ -30,6 +31,7 @@ from .constants import ( VALID_DISPLAY, VALID_OVERFLOW, VALID_VISIBILITY, + VALID_STYLE_FLAGS, ) from .errors import DeclarationError, StyleValueError from .model import Declaration @@ -542,6 +544,15 @@ class StylesBuilder: process_scrollbar_background_active = process_color def process_text_style(self, name: str, tokens: list[Token]) -> None: + for token in tokens: + value = token.value + if value not in VALID_STYLE_FLAGS: + self.error( + name, + token, + style_flags_property_help_text(name, value, context="css"), + ) + style_definition = " ".join(token.value for token in tokens) self.styles.text_style = style_definition diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 39cca7137..6acf98cd3 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -31,5 +31,18 @@ VALID_BOX_SIZING: Final = {"border-box", "content-box"} VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"} VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"} VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"} +VALID_STYLE_FLAGS: Final = { + "none", + "not", + "bold", + "italic", + "underline", + "overline", + "strike", + "b", + "i", + "u", + "o", +} NULL_SPACING: Final = Spacing.all(0) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 1768ba576..dc6c9adad 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -219,7 +219,7 @@ class StylesBase(ABC): align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") def __eq__(self, styles: object) -> bool: - """Check that Styles containts the same rules.""" + """Check that Styles contains the same rules.""" if not isinstance(styles, StylesBase): return NotImplemented return self.get_rules() == styles.get_rules() diff --git a/tests/css/test_styles.py b/tests/css/test_styles.py index b05a44684..055ac5932 100644 --- a/tests/css/test_styles.py +++ b/tests/css/test_styles.py @@ -3,7 +3,7 @@ import pytest from rich.style import Style from textual.color import Color -from textual.css.errors import StyleTypeError, StyleValueError +from textual.css.errors import StyleValueError from textual.css.styles import Styles, RenderStyles from textual.dom import DOMNode