From d0b8cacc350744dd1086aecd0bd52a85389ea779 Mon Sep 17 00:00:00 2001 From: Olivier Philippon Date: Thu, 28 Apr 2022 10:25:37 +0100 Subject: [PATCH 1/2] [css][bugfix] CSS colors can now have digits at the end of their names --- src/textual/css/_styles_builder.py | 19 ++++++++++-- tests/css/test_stylesheet.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/css/test_stylesheet.py diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 1e3c07b46..6c849db58 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -461,10 +461,25 @@ class StylesBuilder: def process_color(self, name: str, tokens: list[Token]) -> None: """Processes a simple color declaration.""" name = name.replace("-", "_") - for token in tokens: + token_indexes_to_skip = [] + for i, token in enumerate(tokens): + if i in token_indexes_to_skip: + continue if token.name in ("color", "token"): + value = token.value + # Color names can include digits (e.g. "turquoise4"): if the next token is a number + # we should consider it part of the color name: + next_token = tokens[i + 1] if len(tokens) > (i + 1) else None + if ( + next_token + and next_token.name == "number" + and next_token.location[1] == token.location[1] + len(value) + ): + value = value + next_token.value + # skip next token, as we included it in this one's value: + token_indexes_to_skip.append(i + 1) try: - self.styles._rules[name] = Color.parse(token.value) + self.styles._rules[name] = Color.parse(value) except Exception as error: self.error( name, token, f"failed to parse color {token.value!r}; {error}" diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py new file mode 100644 index 000000000..87f27845d --- /dev/null +++ b/tests/css/test_stylesheet.py @@ -0,0 +1,49 @@ +from contextlib import nullcontext as does_not_raise +import pytest + +from textual.color import Color +from textual.css.stylesheet import Stylesheet, StylesheetParseError +from textual.css.tokenizer import TokenizeError + + +@pytest.mark.parametrize( + "css_value,expectation,expected_color", + [ + # Valid values: + ["red", does_not_raise(), Color(128, 0, 0)], + ["dark_cyan", does_not_raise(), Color(0, 175, 135)], + ["medium_turquoise", does_not_raise(), Color(95, 215, 215)], + ["turquoise4", does_not_raise(), Color(0, 135, 135)], + ["#ffcc00", does_not_raise(), Color(255, 204, 0)], + ["#ffcc0033", does_not_raise(), Color(255, 204, 0, 0.2)], + ["rgb(200,90,30)", does_not_raise(), Color(200, 90, 30)], + ["rgba(200,90,30,0.3)", does_not_raise(), Color(200, 90, 30, 0.3)], + # Some invalid ones: + ["coffee", pytest.raises(StylesheetParseError), None], # invalid color name + ["turquoise10", pytest.raises(StylesheetParseError), None], + ["turquoise 4", pytest.raises(StylesheetParseError), None], # space in it + ["1", pytest.raises(StylesheetParseError), None], # invalid value + ["()", pytest.raises(TokenizeError), None], # invalid tokens + # TODO: implement hex colors with 3 chars? @link https://devdocs.io/css/color_value + ["#09f", pytest.raises(TokenizeError), None], + # TODO: allow spaces in rgb/rgba expressions? + ["rgb(200, 90, 30)", pytest.raises(TokenizeError), None], + ["rgba(200,90,30, 0.4)", pytest.raises(TokenizeError), None], + ], +) +def test_color_property_parsing(css_value, expectation, expected_color): + stylesheet = Stylesheet() + css = """ + * { + background: ${COLOR}; + } + """.replace( + "${COLOR}", css_value + ) + + with expectation: + stylesheet.parse(css) + + if expected_color: + css_rule = stylesheet.rules[0] + assert css_rule.styles.background == expected_color From 3ed17cb406fb48d93d240f1ef7cc9af5f1b37e5e Mon Sep 17 00:00:00 2001 From: Olivier Philippon Date: Thu, 28 Apr 2022 12:18:23 +0100 Subject: [PATCH 2/2] [css][bugfix] CSS tokens can now have digits in their names ...excepted for their first char of course --- src/textual/css/_styles_builder.py | 19 ++----------------- src/textual/css/tokenize.py | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 6c849db58..1e3c07b46 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -461,25 +461,10 @@ class StylesBuilder: def process_color(self, name: str, tokens: list[Token]) -> None: """Processes a simple color declaration.""" name = name.replace("-", "_") - token_indexes_to_skip = [] - for i, token in enumerate(tokens): - if i in token_indexes_to_skip: - continue + for token in tokens: if token.name in ("color", "token"): - value = token.value - # Color names can include digits (e.g. "turquoise4"): if the next token is a number - # we should consider it part of the color name: - next_token = tokens[i + 1] if len(tokens) > (i + 1) else None - if ( - next_token - and next_token.name == "number" - and next_token.location[1] == token.location[1] + len(value) - ): - value = value + next_token.value - # skip next token, as we included it in this one's value: - token_indexes_to_skip.append(i + 1) try: - self.styles._rules[name] = Color.parse(value) + self.styles._rules[name] = Color.parse(token.value) except Exception as error: self.error( name, token, f"failed to parse color {token.value!r}; {error}" diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 3ccc8166f..7d5dbe3a0 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -11,7 +11,7 @@ DURATION = r"\d+\.?\d*(?:ms|s)" NUMBER = r"\-?\d+\.?\d*" COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|rgb\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)|rgba\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)" KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+" -TOKEN = "[a-zA-Z_-]+" +TOKEN = "[a-zA-Z][a-zA-Z0-9_-]*" STRING = r"\".*?\"" VARIABLE_REF = r"\$[a-zA-Z0-9_\-]+"