Repository: Textualize/textual

Issue:
Allow for empty CSS variable definitions
Consider the following CSS snippets:

 1.
```sass
$x: 
* {
    background: red;
}
```

2.
```sass
$x:
* {
    background: $x;
}
```

3.
```sass
* {
    background: red;
}
$x:
```

All three should work with the following behaviour:
 - an empty variable definition is allowed; and
 - setting a style to an empty variable should unset that style.

For example, 2. should unset the background of all widgets.

As of now, 2. and 3. can't be parsed.
2. raises an error regarding an undefined variable and 3. raises an `AttributeError` inside `substitute_references` in `parse.py`.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf9a3b79b..ec1c24b51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 - Fixed a crash when a `SelectionList` had a prompt wider than itself https://github.com/Textualize/textual/issues/2900
 - Fixed a bug where `Click` events were bubbling up from `Switch` widgets https://github.com/Textualize/textual/issues/2366
+- Fixed a crash when using empty CSS variables https://github.com/Textualize/textual/issues/1849
 
 ## [0.30.0] - 2023-07-17
 
diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py
index 13aeea488..1b31e8b66 100644
--- a/src/textual/css/parse.py
+++ b/src/textual/css/parse.py
@@ -269,6 +269,7 @@ def substitute_references(
             break
         if token.name == "variable_name":
             variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. "$x:" -> "x"
+            variable_tokens = variables.setdefault(variable_name, [])
             yield token
 
             while True:
@@ -284,7 +285,7 @@ def substitute_references(
                 if not token:
                     break
                 elif token.name == "whitespace":
-                    variables.setdefault(variable_name, []).append(token)
+                    variable_tokens.append(token)
                     yield token
                 elif token.name == "variable_value_end":
                     yield token
@@ -293,7 +294,6 @@ def substitute_references(
                 elif token.name == "variable_ref":
                     ref_name = token.value[1:]
                     if ref_name in variables:
-                        variable_tokens = variables.setdefault(variable_name, [])
                         reference_tokens = variables[ref_name]
                         variable_tokens.extend(reference_tokens)
                         ref_location = token.location
@@ -307,7 +307,7 @@ def substitute_references(
                     else:
                         _unresolved(ref_name, variables.keys(), token)
                 else:
-                    variables.setdefault(variable_name, []).append(token)
+                    variable_tokens.append(token)
                     yield token
                 token = next(iter_tokens, None)
         elif token.name == "variable_ref":
diff --git a/src/textual/events.py b/src/textual/events.py
index 4e2523535..3f6015dfb 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -4,7 +4,7 @@ Builtin events sent by Textual.
 
 Events may be marked as "Bubbles" and "Verbose".
 See the [events guide](/guide/events/#bubbling) for an explanation of bubbling.
-Verbose events are excluded from the textual console, unless you explicit request them with the `-v` switch as follows:
+Verbose events are excluded from the textual console, unless you explicitly request them with the `-v` switch as follows:
 
 ```
 textual console -v


Test patch:
diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py
index fea7b3dad..cb6e6aad0 100644
--- a/tests/css/test_parse.py
+++ b/tests/css/test_parse.py
@@ -220,6 +220,22 @@ class TestVariableReferenceSubstitution:
         with pytest.raises(UnresolvedVariableError):
             list(substitute_references(tokenize(css, "")))
 
+    def test_empty_variable(self):
+        css = "$x:\n* { background:$x; }"
+        result = list(substitute_references(tokenize(css, "")))
+        assert [(t.name, t.value) for t in result] == [
+            ("variable_name", "$x:"),
+            ("variable_value_end", "\n"),
+            ("selector_start_universal", "*"),
+            ("whitespace", " "),
+            ("declaration_set_start", "{"),
+            ("whitespace", " "),
+            ("declaration_name", "background:"),
+            ("declaration_end", ";"),
+            ("whitespace", " "),
+            ("declaration_set_end", "}"),
+        ]
+
     def test_transitive_reference(self):
         css = "$x: 1\n$y: $x\n.thing { border: $y }"
         assert list(substitute_references(tokenize(css, ""))) == [