[css] Add a "Did you mean" suggestion when the value of a color is wrong but we can find a close one

This commit is contained in:
Olivier Philippon
2022-05-10 11:09:29 +01:00
parent 8fd7703fc5
commit e37036c816
6 changed files with 86 additions and 8 deletions

View File

@@ -224,4 +224,4 @@ Success {
.horizontal {
layout: horizontal
}
}

View File

@@ -23,7 +23,7 @@ from rich.color import Color as RichColor
from rich.style import Style
from rich.text import Text
from textual.suggestions import get_suggestion
from ._color_constants import COLOR_NAME_TO_RGB
from .geometry import clamp
@@ -77,6 +77,17 @@ split_pairs4: Callable[[str], tuple[str, str, str, str]] = itemgetter(
class ColorParseError(Exception):
"""A color failed to parse"""
def __init__(self, message: str, suggested_color: str | None = None):
"""
Creates a new ColorParseError
Args:
message (str): the error message
suggested_color (str | None): a close color we can suggest. Defaults to None.
"""
super().__init__(*[message])
self.suggested_color = suggested_color
@rich.repr.auto
class Color(NamedTuple):
@@ -271,7 +282,14 @@ class Color(NamedTuple):
return cls(*color_from_name)
color_match = RE_COLOR.match(color_text)
if color_match is None:
raise ColorParseError(f"failed to parse {color_text!r} as a color")
error_message = f"failed to parse {color_text!r} as a color"
suggested_color = None
if not color_text.startswith("#") and not color_text.startswith("rgb"):
# Seems like we tried to use a color name: let's try to find one that is close enough:
suggested_color = get_suggestion(color_text, COLOR_NAME_TO_RGB.keys())
if suggested_color:
error_message += f"; did you mean '{suggested_color}'?"
raise ColorParseError(error_message, suggested_color)
(
rgb_hex_triple,
rgb_hex_quad,

View File

@@ -4,6 +4,7 @@ import sys
from dataclasses import dataclass
from typing import Iterable
from textual.color import ColorParseError
from textual.css._help_renderables import Example, Bullet, HelpText
from textual.css.constants import (
VALID_BORDER,
@@ -303,6 +304,8 @@ def string_enum_help_text(
def color_property_help_text(
property_name: str,
context: StylingContext,
*,
error: Exception = None,
) -> HelpText:
"""Help text to show when the user supplies an invalid value for a color
property. For example, an unparseable color string.
@@ -310,14 +313,23 @@ def color_property_help_text(
Args:
property_name (str): The name of the property
context (StylingContext | None): The context the property is being used in.
error (ColorParseError | None): The error that caused this help text to be displayed. Defaults to None.
Returns:
HelpText: Renderable for displaying the help text for this property
"""
property_name = _contextualize_property_name(property_name, context)
suggested_color = (
error.suggested_color if error and isinstance(error, ColorParseError) else None
)
return HelpText(
summary=f"Invalid value for the [i]{property_name}[/] property",
bullets=[
*(
[Bullet(f'Did you mean "{suggested_color}"?')]
if suggested_color
else []
),
Bullet(
f"The [i]{property_name}[/] property can only be set to a valid color"
),

View File

@@ -782,11 +782,13 @@ class ColorProperty:
elif isinstance(color, str):
try:
parsed_color = Color.parse(color)
except ColorParseError:
except ColorParseError as error:
raise StyleValueError(
f"Invalid color value '{color}'",
help_text=color_property_help_text(self.name, context="inline"),
)
help_text=color_property_help_text(
self.name, context="inline", error=error
),
) from error
if obj.set_rule(self.name, parsed_color):
obj.refresh()
else:

View File

@@ -572,9 +572,11 @@ class StylesBuilder:
elif token.name in ("color", "token"):
try:
color = Color.parse(token.value)
except Exception:
except Exception as error:
self.error(
name, token, color_property_help_text(name, context="css")
name,
token,
color_property_help_text(name, context="css", error=error),
)
else:
self.error(name, token, color_property_help_text(name, context="css"))

View File

@@ -101,3 +101,47 @@ def test_did_you_mean_for_css_property_names(
f'Did you mean "{expected_property_name_suggestion}"?'
)
assert help_text.bullets[0].markup == expected_suggestion_message
@pytest.mark.parametrize(
"css_property_name,css_property_value,expected_color_suggestion",
[
["color", "blu", "blue"],
["background", "chartruse", "chartreuse"],
["tint", "ansi_whi", "ansi_white"],
["scrollbar-color", "transprnt", "transparent"],
["color", "xkcd", None],
],
)
def test_did_you_mean_for_color_names(
css_property_name: str, css_property_value: str, expected_color_suggestion
):
stylesheet = Stylesheet()
css = """
* {
border: blue;
${PROPERTY}: ${VALUE};
}
""".replace(
"${PROPERTY}", css_property_name
).replace(
"${VALUE}", css_property_value
)
stylesheet.add_source(css)
with pytest.raises(StylesheetParseError) as err:
stylesheet.parse()
_, help_text = err.value.errors.rules[0].errors[0] # type: Any, HelpText
displayed_css_property_name = css_property_name.replace("_", "-")
assert (
help_text.summary
== f"Invalid value for the [i]{displayed_css_property_name}[/] property"
)
first_bullet = help_text.bullets[0]
if expected_color_suggestion is not None:
expected_suggestion_message = f'Did you mean "{expected_color_suggestion}"?'
assert first_bullet.markup == expected_suggestion_message
else:
assert "Did you mean" not in first_bullet.markup