Repository: Textualize/textual

Issue:
[textual][bug] CSS rule parsing fails when the name of the colour we pass contains a digit
So while this is working correctly:
```css
#my_widget {
  background: dark_cyan;
}
```

...this fails:
```css
#my_widget {
  background: turquoise4;
}
```
...with the following error:
```
 • failed to parse color 'turquoise';                                                                                                                                         
 • failed to parse 'turquoise' as a color;  
```
(maybe just a regex that doesn't take into account the fact that colour names can include numbers?)

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#441](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (663e3ed) into [css](https://codecov.io/gh/Textualize/textual/commit/386aa6dacc885880b8b2bfcecbc1c41beb331337?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (386aa6d) will **increase** coverage by `8.70%`.
> The diff coverage is `50.87%`.

```diff
@@            Coverage Diff             @@
##              css     #441      +/-   ##
==========================================
+ Coverage   38.06%   46.77%   +8.70%     
==========================================
  Files          85       96      +11     
  Lines        6526     7992    +1466     
==========================================
+ Hits         2484     3738    +1254     
- Misses       4042     4254     +212     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `46.77% <50.87%> (+8.70%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/textual/\_event\_broker.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvX2V2ZW50X2Jyb2tlci5weQ==) | `0.00% <ø> (ø)` | |
| [src/textual/\_xterm\_parser.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvX3h0ZXJtX3BhcnNlci5weQ==) | `0.00% <0.00%> (ø)` | |
| [src/textual/app.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvYXBwLnB5) | `0.00% <0.00%> (ø)` | |
| [src/textual/design.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvZGVzaWduLnB5) | `0.00% <0.00%> (ø)` | |
| [src/textual/drivers/linux\_driver.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvZHJpdmVycy9saW51eF9kcml2ZXIucHk=) | `0.00% <0.00%> (ø)` | |
| [src/textual/drivers/win32.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvZHJpdmVycy93aW4zMi5weQ==) | `0.00% <0.00%> (ø)` | |
| [src/textual/layouts/grid.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvbGF5b3V0cy9ncmlkLnB5) | `20.98% <ø> (-8.49%)` | :arrow_down: |
| [src/textual/renderables/blank.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvcmVuZGVyYWJsZXMvYmxhbmsucHk=) | `0.00% <0.00%> (ø)` | |
| [src/textual/scrollbar.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvc2Nyb2xsYmFyLnB5) | `0.00% <0.00%> (ø)` | |
| [src/textual/views/\_dock\_view.py](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3RleHR1YWwvdmlld3MvX2RvY2tfdmlldy5weQ==) | `0.00% <0.00%> (ø)` | |
| ... and [86 more](https://codecov.io/gh/Textualize/textual/pull/441/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [efafbdf...663e3ed](https://codecov.io/gh/Textualize/textual/pull/441?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
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_\-]+"
 


Test patch:
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