diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 84f874813..1c7fa9f51 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -61,6 +61,14 @@ VALID_STYLE_FLAGS: Final = { "underline", "uu", } +VALID_PSEUDO_CLASSES: Final = { + "blur", + "disabled", + "enabled", + "focus-within", + "focus", + "hover", +} NULL_SPACING: Final = Spacing.all(0) diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index 2f35d760f..276b22b54 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -12,7 +12,9 @@ from rich.panel import Panel from rich.syntax import Syntax from rich.text import Text +from ..suggestions import get_suggestion from ._error_tools import friendly_list +from .constants import VALID_PSEUDO_CLASSES class TokenError(Exception): @@ -56,7 +58,7 @@ class TokenError(Exception): line_numbers=True, indent_guides=True, line_range=(max(0, line_no - 2), line_no + 2), - highlight_lines={line_no}, + highlight_lines={line_no + 1}, ) syntax.stylize_range("reverse bold", self.start, self.end) return Panel(syntax, border_style="red") @@ -227,6 +229,29 @@ class Tokenizer: (line_no, col_no), referenced_by=None, ) + + if ( + token.name == "pseudo_class" + and token.value.strip(":") not in VALID_PSEUDO_CLASSES + ): + pseudo_class = token.value.strip(":") + suggestion = get_suggestion(pseudo_class, list(VALID_PSEUDO_CLASSES)) + all_valid = f"must be one of {friendly_list(VALID_PSEUDO_CLASSES)}" + if suggestion: + raise TokenError( + self.path, + self.code, + (line_no, col_no), + f"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}", + ) + else: + raise TokenError( + self.path, + self.code, + (line_no, col_no), + f"unknown pseudo-class {pseudo_class!r}; {all_valid}", + ) + col_no += len(value) if col_no >= len(line): line_no += 1 diff --git a/tests/css/test_mega_stylesheet.css b/tests/css/test_mega_stylesheet.css index 13005ead9..49fe8176c 100644 --- a/tests/css/test_mega_stylesheet.css +++ b/tests/css/test_mega_stylesheet.css @@ -140,52 +140,52 @@ A1 /**********************************************************************/ -A:foo {} -A:foo:bar {} +A:focus {} +A:focus:hover {} A -:foo {} +:focus {} A -:foo:bar {} +:focus:hover {} A -:foo -:bar {} -A:foo-bar {} +:focus +:hover {} +A:enabled {} A -:foo-bar {} +:enabled {} -A :foo {} -A :foo :bar {} -A :foo-bar {} +A :focus {} +A :focus :hover {} +A :enabled {} -.A:foo {} -.A:foo:bar {} +.A:focus {} +.A:focus:hover {} .A -:foo {} +:focus {} .A -:foo:bar {} +:focus:hover {} .A -:foo -:bar {} -.A:foo-bar {} +:focus +:hover {} +.A:enabled {} .A -:foo-bar {} +:enabled {} -#A:foo {} -#A:foo:bar {} +#A:focus {} +#A:focus:hover {} #A -:foo {} +:focus {} #A -:foo:bar {} +:focus:hover {} #A -:foo -:bar {} -#A:foo-bar {} +:focus +:hover {} +#A:enabled {} #A -:foo-bar {} +:enabled {} -A1.A1.A1:foo {} -A1.A1#A1:foo {} -A1:foo.A1:foo#A1:foo {} +A1.A1.A1:focus {} +A1.A1#A1:focus {} +A1:focus.A1:focus#A1:focus {} /**********************************************************************/ diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index 49d3368d6..fea7b3dad 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -1226,3 +1226,21 @@ class TestTypeNames: stylesheet.add_source(f"StartType {separator} 1TestType {{}}") with pytest.raises(TokenError): stylesheet.parse() + + +def test_parse_bad_psuedo_selector(): + """Check unknown selector raises a token error.""" + + bad_selector = """\ +Widget:foo{ + border: red; +} + """ + + stylesheet = Stylesheet() + stylesheet.add_source(bad_selector, "foo") + + with pytest.raises(TokenError) as error: + stylesheet.parse() + + assert error.value.start == (0, 6)