mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
[css] Add a "did you mean" suggestion when an unknown CSS property is spotted
So using "bckgroundu: red" in a CSS file will report _"unknown declaration 'bckgroundu'; did you mean 'background'?"_
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from difflib import get_close_matches
|
||||||
|
from functools import lru_cache
|
||||||
from typing import cast, Iterable, NoReturn
|
from typing import cast, Iterable, NoReturn
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
@@ -84,24 +86,35 @@ class StylesBuilder:
|
|||||||
process_method = getattr(self, f"process_{rule_name}", None)
|
process_method = getattr(self, f"process_{rule_name}", None)
|
||||||
|
|
||||||
if process_method is None:
|
if process_method is None:
|
||||||
|
error_message = f"unknown declaration {declaration.name!r}"
|
||||||
|
did_you_mean_rule_name = self._did_you_mean_for_rule_name(declaration.name)
|
||||||
|
if did_you_mean_rule_name:
|
||||||
|
error_message += f"; did you mean {did_you_mean_rule_name!r}?"
|
||||||
self.error(
|
self.error(
|
||||||
declaration.name,
|
declaration.name,
|
||||||
declaration.token,
|
declaration.token,
|
||||||
f"unknown declaration {declaration.name!r}",
|
error_message,
|
||||||
)
|
)
|
||||||
else:
|
return
|
||||||
tokens = declaration.tokens
|
|
||||||
|
|
||||||
important = tokens[-1].name == "important"
|
tokens = declaration.tokens
|
||||||
if important:
|
|
||||||
tokens = tokens[:-1]
|
important = tokens[-1].name == "important"
|
||||||
self.styles.important.add(rule_name)
|
if important:
|
||||||
try:
|
tokens = tokens[:-1]
|
||||||
process_method(declaration.name, tokens)
|
self.styles.important.add(rule_name)
|
||||||
except DeclarationError:
|
try:
|
||||||
raise
|
process_method(declaration.name, tokens)
|
||||||
except Exception as error:
|
except DeclarationError:
|
||||||
self.error(declaration.name, declaration.token, str(error))
|
raise
|
||||||
|
except Exception as error:
|
||||||
|
self.error(declaration.name, declaration.token, str(error))
|
||||||
|
|
||||||
|
@lru_cache(maxsize=None)
|
||||||
|
def _processable_rule_names(self) -> frozenset[str]:
|
||||||
|
return frozenset(
|
||||||
|
[attr[8:] for attr in dir(self) if attr.startswith("process_")]
|
||||||
|
)
|
||||||
|
|
||||||
def _process_enum_multiple(
|
def _process_enum_multiple(
|
||||||
self, name: str, tokens: list[Token], valid_values: set[str], count: int
|
self, name: str, tokens: list[Token], valid_values: set[str], count: int
|
||||||
@@ -717,3 +730,9 @@ class StylesBuilder:
|
|||||||
process_content_align = process_align
|
process_content_align = process_align
|
||||||
process_content_align_horizontal = process_align_horizontal
|
process_content_align_horizontal = process_align_horizontal
|
||||||
process_content_align_vertical = process_align_vertical
|
process_content_align_vertical = process_align_vertical
|
||||||
|
|
||||||
|
def _did_you_mean_for_rule_name(self, rule_name: str) -> str | None:
|
||||||
|
possible_matches = get_close_matches(
|
||||||
|
rule_name, self._processable_rule_names(), n=1
|
||||||
|
)
|
||||||
|
return None if not possible_matches else possible_matches[0]
|
||||||
|
|||||||
@@ -53,3 +53,40 @@ def test_color_property_parsing(css_value, expectation, expected_color):
|
|||||||
if expected_color:
|
if expected_color:
|
||||||
css_rule = stylesheet.rules[0]
|
css_rule = stylesheet.rules[0]
|
||||||
assert css_rule.styles.background == expected_color
|
assert css_rule.styles.background == expected_color
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"css_property_name,expected_property_name_suggestion",
|
||||||
|
[
|
||||||
|
["backgroundu", "background"],
|
||||||
|
["bckgroundu", "background"],
|
||||||
|
["colr", "color"],
|
||||||
|
["colour", "color"],
|
||||||
|
["wdth", "width"],
|
||||||
|
["wth", "width"],
|
||||||
|
["wh", None],
|
||||||
|
["xkcd", None],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_did_you_mean_for_css_property_names(
|
||||||
|
css_property_name, expected_property_name_suggestion
|
||||||
|
):
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
css = """
|
||||||
|
* {
|
||||||
|
border: blue;
|
||||||
|
${PROPERTY}: red;
|
||||||
|
}
|
||||||
|
""".replace(
|
||||||
|
"${PROPERTY}", css_property_name
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(StylesheetParseError) as err:
|
||||||
|
stylesheet.parse(css)
|
||||||
|
|
||||||
|
error_token, error_message = err.value.errors.stylesheet.rules[0].errors[0]
|
||||||
|
if expected_property_name_suggestion is None:
|
||||||
|
assert "did you mean" not in error_message
|
||||||
|
else:
|
||||||
|
expected_did_you_mean_error_message = f"unknown declaration '{css_property_name}'; did you mean '{expected_property_name_suggestion}'?"
|
||||||
|
assert expected_did_you_mean_error_message == error_message
|
||||||
|
|||||||
Reference in New Issue
Block a user