We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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`.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/textual/css/parse.py`
Content:
```
1 from __future__ import annotations
2 
3 from functools import lru_cache
4 from pathlib import PurePath
5 from typing import Iterable, Iterator, NoReturn
6 
7 from ..suggestions import get_suggestion
8 from ._help_renderables import HelpText
9 from ._styles_builder import DeclarationError, StylesBuilder
10 from .errors import UnresolvedVariableError
11 from .model import (
12     CombinatorType,
13     Declaration,
14     RuleSet,
15     Selector,
16     SelectorSet,
17     SelectorType,
18 )
19 from .styles import Styles
20 from .tokenize import Token, tokenize, tokenize_declarations, tokenize_values
21 from .tokenizer import EOFError, ReferencedBy
22 from .types import Specificity3
23 
24 SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {
25     "selector": (SelectorType.TYPE, (0, 0, 1)),
26     "selector_start": (SelectorType.TYPE, (0, 0, 1)),
27     "selector_class": (SelectorType.CLASS, (0, 1, 0)),
28     "selector_start_class": (SelectorType.CLASS, (0, 1, 0)),
29     "selector_id": (SelectorType.ID, (1, 0, 0)),
30     "selector_start_id": (SelectorType.ID, (1, 0, 0)),
31     "selector_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
32     "selector_start_universal": (SelectorType.UNIVERSAL, (0, 0, 0)),
33 }
34 
35 
36 @lru_cache(maxsize=1024)
37 def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
38     if not css_selectors.strip():
39         return ()
40 
41     tokens = iter(tokenize(css_selectors, ""))
42 
43     get_selector = SELECTOR_MAP.get
44     combinator: CombinatorType | None = CombinatorType.DESCENDENT
45     selectors: list[Selector] = []
46     rule_selectors: list[list[Selector]] = []
47 
48     while True:
49         try:
50             token = next(tokens)
51         except EOFError:
52             break
53         token_name = token.name
54         if token_name == "pseudo_class":
55             selectors[-1]._add_pseudo_class(token.value.lstrip(":"))
56         elif token_name == "whitespace":
57             if combinator is None or combinator == CombinatorType.SAME:
58                 combinator = CombinatorType.DESCENDENT
59         elif token_name == "new_selector":
60             rule_selectors.append(selectors[:])
61             selectors.clear()
62             combinator = None
63         elif token_name == "declaration_set_start":
64             break
65         elif token_name == "combinator_child":
66             combinator = CombinatorType.CHILD
67         else:
68             _selector, specificity = get_selector(
69                 token_name, (SelectorType.TYPE, (0, 0, 0))
70             )
71             selectors.append(
72                 Selector(
73                     name=token.value.lstrip(".#"),
74                     combinator=combinator or CombinatorType.DESCENDENT,
75                     type=_selector,
76                     specificity=specificity,
77                 )
78             )
79             combinator = CombinatorType.SAME
80     if selectors:
81         rule_selectors.append(selectors[:])
82 
83     selector_set = tuple(SelectorSet.from_selectors(rule_selectors))
84     return selector_set
85 
86 
87 def parse_rule_set(
88     tokens: Iterator[Token],
89     token: Token,
90     is_default_rules: bool = False,
91     tie_breaker: int = 0,
92 ) -> Iterable[RuleSet]:
93     get_selector = SELECTOR_MAP.get
94     combinator: CombinatorType | None = CombinatorType.DESCENDENT
95     selectors: list[Selector] = []
96     rule_selectors: list[list[Selector]] = []
97     styles_builder = StylesBuilder()
98 
99     while True:
100         if token.name == "pseudo_class":
101             selectors[-1]._add_pseudo_class(token.value.lstrip(":"))
102         elif token.name == "whitespace":
103             if combinator is None or combinator == CombinatorType.SAME:
104                 combinator = CombinatorType.DESCENDENT
105         elif token.name == "new_selector":
106             rule_selectors.append(selectors[:])
107             selectors.clear()
108             combinator = None
109         elif token.name == "declaration_set_start":
110             break
111         elif token.name == "combinator_child":
112             combinator = CombinatorType.CHILD
113         else:
114             _selector, specificity = get_selector(
115                 token.name, (SelectorType.TYPE, (0, 0, 0))
116             )
117             selectors.append(
118                 Selector(
119                     name=token.value.lstrip(".#"),
120                     combinator=combinator or CombinatorType.DESCENDENT,
121                     type=_selector,
122                     specificity=specificity,
123                 )
124             )
125             combinator = CombinatorType.SAME
126 
127         token = next(tokens)
128 
129     if selectors:
130         rule_selectors.append(selectors[:])
131 
132     declaration = Declaration(token, "")
133 
134     errors: list[tuple[Token, str | HelpText]] = []
135 
136     while True:
137         token = next(tokens)
138         token_name = token.name
139         if token_name in ("whitespace", "declaration_end"):
140             continue
141         if token_name == "declaration_name":
142             if declaration.tokens:
143                 try:
144                     styles_builder.add_declaration(declaration)
145                 except DeclarationError as error:
146                     errors.append((error.token, error.message))
147             declaration = Declaration(token, "")
148             declaration.name = token.value.rstrip(":")
149         elif token_name == "declaration_set_end":
150             break
151         else:
152             declaration.tokens.append(token)
153 
154     if declaration.tokens:
155         try:
156             styles_builder.add_declaration(declaration)
157         except DeclarationError as error:
158             errors.append((error.token, error.message))
159 
160     rule_set = RuleSet(
161         list(SelectorSet.from_selectors(rule_selectors)),
162         styles_builder.styles,
163         errors,
164         is_default_rules=is_default_rules,
165         tie_breaker=tie_breaker,
166     )
167     rule_set._post_parse()
168     yield rule_set
169 
170 
171 def parse_declarations(css: str, path: str) -> Styles:
172     """Parse declarations and return a Styles object.
173 
174     Args:
175         css: String containing CSS.
176         path: Path to the CSS, or something else to identify the location.
177 
178     Returns:
179         A styles object.
180     """
181 
182     tokens = iter(tokenize_declarations(css, path))
183     styles_builder = StylesBuilder()
184 
185     declaration: Declaration | None = None
186     errors: list[tuple[Token, str | HelpText]] = []
187 
188     while True:
189         token = next(tokens, None)
190         if token is None:
191             break
192         token_name = token.name
193         if token_name in ("whitespace", "declaration_end", "eof"):
194             continue
195         if token_name == "declaration_name":
196             if declaration and declaration.tokens:
197                 try:
198                     styles_builder.add_declaration(declaration)
199                 except DeclarationError as error:
200                     errors.append((error.token, error.message))
201                     raise
202             declaration = Declaration(token, "")
203             declaration.name = token.value.rstrip(":")
204         elif token_name == "declaration_set_end":
205             break
206         else:
207             if declaration:
208                 declaration.tokens.append(token)
209 
210     if declaration and declaration.tokens:
211         try:
212             styles_builder.add_declaration(declaration)
213         except DeclarationError as error:
214             errors.append((error.token, error.message))
215             raise
216 
217     return styles_builder.styles
218 
219 
220 def _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> NoReturn:
221     """Raise a TokenError regarding an unresolved variable.
222 
223     Args:
224         variable_name: A variable name.
225         variables: Possible choices used to generate suggestion.
226         token: The Token.
227 
228     Raises:
229         UnresolvedVariableError: Always raises a TokenError.
230     """
231     message = f"reference to undefined variable '${variable_name}'"
232     suggested_variable = get_suggestion(variable_name, list(variables))
233     if suggested_variable:
234         message += f"; did you mean '${suggested_variable}'?"
235 
236     raise UnresolvedVariableError(
237         token.path,
238         token.code,
239         token.start,
240         message,
241         end=token.end,
242     )
243 
244 
245 def substitute_references(
246     tokens: Iterable[Token], css_variables: dict[str, list[Token]] | None = None
247 ) -> Iterable[Token]:
248     """Replace variable references with values by substituting variable reference
249     tokens with the tokens representing their values.
250 
251     Args:
252         tokens: Iterator of Tokens which may contain tokens
253             with the name "variable_ref".
254 
255     Returns:
256         Yields Tokens such that any variable references (tokens where
257             token.name == "variable_ref") have been replaced with the tokens representing
258             the value. In other words, an Iterable of Tokens similar to the original input,
259             but with variables resolved. Substituted tokens will have their referenced_by
260             attribute populated with information about where the tokens are being substituted to.
261     """
262     variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}
263 
264     iter_tokens = iter(tokens)
265 
266     while True:
267         token = next(iter_tokens, None)
268         if token is None:
269             break
270         if token.name == "variable_name":
271             variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. "$x:" -> "x"
272             yield token
273 
274             while True:
275                 token = next(iter_tokens, None)
276                 if token is not None and token.name == "whitespace":
277                     yield token
278                 else:
279                     break
280 
281             # Store the tokens for any variable definitions, and substitute
282             # any variable references we encounter with them.
283             while True:
284                 if not token:
285                     break
286                 elif token.name == "whitespace":
287                     variables.setdefault(variable_name, []).append(token)
288                     yield token
289                 elif token.name == "variable_value_end":
290                     yield token
291                     break
292                 # For variables referring to other variables
293                 elif token.name == "variable_ref":
294                     ref_name = token.value[1:]
295                     if ref_name in variables:
296                         variable_tokens = variables.setdefault(variable_name, [])
297                         reference_tokens = variables[ref_name]
298                         variable_tokens.extend(reference_tokens)
299                         ref_location = token.location
300                         ref_length = len(token.value)
301                         for _token in reference_tokens:
302                             yield _token.with_reference(
303                                 ReferencedBy(
304                                     ref_name, ref_location, ref_length, token.code
305                                 )
306                             )
307                     else:
308                         _unresolved(ref_name, variables.keys(), token)
309                 else:
310                     variables.setdefault(variable_name, []).append(token)
311                     yield token
312                 token = next(iter_tokens, None)
313         elif token.name == "variable_ref":
314             variable_name = token.value[1:]  # Trim the $, so $x -> x
315             if variable_name in variables:
316                 variable_tokens = variables[variable_name]
317                 ref_location = token.location
318                 ref_length = len(token.value)
319                 ref_code = token.code
320                 for _token in variable_tokens:
321                     yield _token.with_reference(
322                         ReferencedBy(variable_name, ref_location, ref_length, ref_code)
323                     )
324             else:
325                 _unresolved(variable_name, variables.keys(), token)
326         else:
327             yield token
328 
329 
330 def parse(
331     css: str,
332     path: str | PurePath,
333     variables: dict[str, str] | None = None,
334     variable_tokens: dict[str, list[Token]] | None = None,
335     is_default_rules: bool = False,
336     tie_breaker: int = 0,
337 ) -> Iterable[RuleSet]:
338     """Parse CSS by tokenizing it, performing variable substitution,
339     and generating rule sets from it.
340 
341     Args:
342         css: The input CSS
343         path: Path to the CSS
344         variables: Substitution variables to substitute tokens for.
345         is_default_rules: True if the rules we're extracting are
346             default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.
347     """
348 
349     reference_tokens = tokenize_values(variables) if variables is not None else {}
350     if variable_tokens:
351         reference_tokens.update(variable_tokens)
352 
353     tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
354     while True:
355         token = next(tokens, None)
356         if token is None:
357             break
358         if token.name.startswith("selector_start"):
359             yield from parse_rule_set(
360                 tokens,
361                 token,
362                 is_default_rules=is_default_rules,
363                 tie_breaker=tie_breaker,
364             )
365
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py
--- a/src/textual/css/parse.py
+++ b/src/textual/css/parse.py
@@ -269,6 +269,7 @@
             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 @@
                 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 @@
                 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 @@
                     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":

{"golden_diff": "diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py\n--- a/src/textual/css/parse.py\n+++ b/src/textual/css/parse.py\n@@ -269,6 +269,7 @@\n             break\n         if token.name == \"variable_name\":\n             variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. \"$x:\" -> \"x\"\n+            variable_tokens = variables.setdefault(variable_name, [])\n             yield token\n \n             while True:\n@@ -284,7 +285,7 @@\n                 if not token:\n                     break\n                 elif token.name == \"whitespace\":\n-                    variables.setdefault(variable_name, []).append(token)\n+                    variable_tokens.append(token)\n                     yield token\n                 elif token.name == \"variable_value_end\":\n                     yield token\n@@ -293,7 +294,6 @@\n                 elif token.name == \"variable_ref\":\n                     ref_name = token.value[1:]\n                     if ref_name in variables:\n-                        variable_tokens = variables.setdefault(variable_name, [])\n                         reference_tokens = variables[ref_name]\n                         variable_tokens.extend(reference_tokens)\n                         ref_location = token.location\n@@ -307,7 +307,7 @@\n                     else:\n                         _unresolved(ref_name, variables.keys(), token)\n                 else:\n-                    variables.setdefault(variable_name, []).append(token)\n+                    variable_tokens.append(token)\n                     yield token\n                 token = next(iter_tokens, None)\n         elif token.name == \"variable_ref\":\n", "issue": "Allow for empty CSS variable definitions\nConsider the following CSS snippets:\r\n\r\n 1.\r\n```sass\r\n$x: \r\n* {\r\n    background: red;\r\n}\r\n```\r\n\r\n2.\r\n```sass\r\n$x:\r\n* {\r\n    background: $x;\r\n}\r\n```\r\n\r\n3.\r\n```sass\r\n* {\r\n    background: red;\r\n}\r\n$x:\r\n```\r\n\r\nAll three should work with the following behaviour:\r\n - an empty variable definition is allowed; and\r\n - setting a style to an empty variable should unset that style.\r\n\r\nFor example, 2. should unset the background of all widgets.\r\n\r\nAs of now, 2. and 3. can't be parsed.\r\n2. raises an error regarding an undefined variable and 3. raises an `AttributeError` inside `substitute_references` in `parse.py`.\n", "before_files": [{"content": "from __future__ import annotations\n\nfrom functools import lru_cache\nfrom pathlib import PurePath\nfrom typing import Iterable, Iterator, NoReturn\n\nfrom ..suggestions import get_suggestion\nfrom ._help_renderables import HelpText\nfrom ._styles_builder import DeclarationError, StylesBuilder\nfrom .errors import UnresolvedVariableError\nfrom .model import (\n    CombinatorType,\n    Declaration,\n    RuleSet,\n    Selector,\n    SelectorSet,\n    SelectorType,\n)\nfrom .styles import Styles\nfrom .tokenize import Token, tokenize, tokenize_declarations, tokenize_values\nfrom .tokenizer import EOFError, ReferencedBy\nfrom .types import Specificity3\n\nSELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {\n    \"selector\": (SelectorType.TYPE, (0, 0, 1)),\n    \"selector_start\": (SelectorType.TYPE, (0, 0, 1)),\n    \"selector_class\": (SelectorType.CLASS, (0, 1, 0)),\n    \"selector_start_class\": (SelectorType.CLASS, (0, 1, 0)),\n    \"selector_id\": (SelectorType.ID, (1, 0, 0)),\n    \"selector_start_id\": (SelectorType.ID, (1, 0, 0)),\n    \"selector_universal\": (SelectorType.UNIVERSAL, (0, 0, 0)),\n    \"selector_start_universal\": (SelectorType.UNIVERSAL, (0, 0, 0)),\n}\n\n\n@lru_cache(maxsize=1024)\ndef parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:\n    if not css_selectors.strip():\n        return ()\n\n    tokens = iter(tokenize(css_selectors, \"\"))\n\n    get_selector = SELECTOR_MAP.get\n    combinator: CombinatorType | None = CombinatorType.DESCENDENT\n    selectors: list[Selector] = []\n    rule_selectors: list[list[Selector]] = []\n\n    while True:\n        try:\n            token = next(tokens)\n        except EOFError:\n            break\n        token_name = token.name\n        if token_name == \"pseudo_class\":\n            selectors[-1]._add_pseudo_class(token.value.lstrip(\":\"))\n        elif token_name == \"whitespace\":\n            if combinator is None or combinator == CombinatorType.SAME:\n                combinator = CombinatorType.DESCENDENT\n        elif token_name == \"new_selector\":\n            rule_selectors.append(selectors[:])\n            selectors.clear()\n            combinator = None\n        elif token_name == \"declaration_set_start\":\n            break\n        elif token_name == \"combinator_child\":\n            combinator = CombinatorType.CHILD\n        else:\n            _selector, specificity = get_selector(\n                token_name, (SelectorType.TYPE, (0, 0, 0))\n            )\n            selectors.append(\n                Selector(\n                    name=token.value.lstrip(\".#\"),\n                    combinator=combinator or CombinatorType.DESCENDENT,\n                    type=_selector,\n                    specificity=specificity,\n                )\n            )\n            combinator = CombinatorType.SAME\n    if selectors:\n        rule_selectors.append(selectors[:])\n\n    selector_set = tuple(SelectorSet.from_selectors(rule_selectors))\n    return selector_set\n\n\ndef parse_rule_set(\n    tokens: Iterator[Token],\n    token: Token,\n    is_default_rules: bool = False,\n    tie_breaker: int = 0,\n) -> Iterable[RuleSet]:\n    get_selector = SELECTOR_MAP.get\n    combinator: CombinatorType | None = CombinatorType.DESCENDENT\n    selectors: list[Selector] = []\n    rule_selectors: list[list[Selector]] = []\n    styles_builder = StylesBuilder()\n\n    while True:\n        if token.name == \"pseudo_class\":\n            selectors[-1]._add_pseudo_class(token.value.lstrip(\":\"))\n        elif token.name == \"whitespace\":\n            if combinator is None or combinator == CombinatorType.SAME:\n                combinator = CombinatorType.DESCENDENT\n        elif token.name == \"new_selector\":\n            rule_selectors.append(selectors[:])\n            selectors.clear()\n            combinator = None\n        elif token.name == \"declaration_set_start\":\n            break\n        elif token.name == \"combinator_child\":\n            combinator = CombinatorType.CHILD\n        else:\n            _selector, specificity = get_selector(\n                token.name, (SelectorType.TYPE, (0, 0, 0))\n            )\n            selectors.append(\n                Selector(\n                    name=token.value.lstrip(\".#\"),\n                    combinator=combinator or CombinatorType.DESCENDENT,\n                    type=_selector,\n                    specificity=specificity,\n                )\n            )\n            combinator = CombinatorType.SAME\n\n        token = next(tokens)\n\n    if selectors:\n        rule_selectors.append(selectors[:])\n\n    declaration = Declaration(token, \"\")\n\n    errors: list[tuple[Token, str | HelpText]] = []\n\n    while True:\n        token = next(tokens)\n        token_name = token.name\n        if token_name in (\"whitespace\", \"declaration_end\"):\n            continue\n        if token_name == \"declaration_name\":\n            if declaration.tokens:\n                try:\n                    styles_builder.add_declaration(declaration)\n                except DeclarationError as error:\n                    errors.append((error.token, error.message))\n            declaration = Declaration(token, \"\")\n            declaration.name = token.value.rstrip(\":\")\n        elif token_name == \"declaration_set_end\":\n            break\n        else:\n            declaration.tokens.append(token)\n\n    if declaration.tokens:\n        try:\n            styles_builder.add_declaration(declaration)\n        except DeclarationError as error:\n            errors.append((error.token, error.message))\n\n    rule_set = RuleSet(\n        list(SelectorSet.from_selectors(rule_selectors)),\n        styles_builder.styles,\n        errors,\n        is_default_rules=is_default_rules,\n        tie_breaker=tie_breaker,\n    )\n    rule_set._post_parse()\n    yield rule_set\n\n\ndef parse_declarations(css: str, path: str) -> Styles:\n    \"\"\"Parse declarations and return a Styles object.\n\n    Args:\n        css: String containing CSS.\n        path: Path to the CSS, or something else to identify the location.\n\n    Returns:\n        A styles object.\n    \"\"\"\n\n    tokens = iter(tokenize_declarations(css, path))\n    styles_builder = StylesBuilder()\n\n    declaration: Declaration | None = None\n    errors: list[tuple[Token, str | HelpText]] = []\n\n    while True:\n        token = next(tokens, None)\n        if token is None:\n            break\n        token_name = token.name\n        if token_name in (\"whitespace\", \"declaration_end\", \"eof\"):\n            continue\n        if token_name == \"declaration_name\":\n            if declaration and declaration.tokens:\n                try:\n                    styles_builder.add_declaration(declaration)\n                except DeclarationError as error:\n                    errors.append((error.token, error.message))\n                    raise\n            declaration = Declaration(token, \"\")\n            declaration.name = token.value.rstrip(\":\")\n        elif token_name == \"declaration_set_end\":\n            break\n        else:\n            if declaration:\n                declaration.tokens.append(token)\n\n    if declaration and declaration.tokens:\n        try:\n            styles_builder.add_declaration(declaration)\n        except DeclarationError as error:\n            errors.append((error.token, error.message))\n            raise\n\n    return styles_builder.styles\n\n\ndef _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> NoReturn:\n    \"\"\"Raise a TokenError regarding an unresolved variable.\n\n    Args:\n        variable_name: A variable name.\n        variables: Possible choices used to generate suggestion.\n        token: The Token.\n\n    Raises:\n        UnresolvedVariableError: Always raises a TokenError.\n    \"\"\"\n    message = f\"reference to undefined variable '${variable_name}'\"\n    suggested_variable = get_suggestion(variable_name, list(variables))\n    if suggested_variable:\n        message += f\"; did you mean '${suggested_variable}'?\"\n\n    raise UnresolvedVariableError(\n        token.path,\n        token.code,\n        token.start,\n        message,\n        end=token.end,\n    )\n\n\ndef substitute_references(\n    tokens: Iterable[Token], css_variables: dict[str, list[Token]] | None = None\n) -> Iterable[Token]:\n    \"\"\"Replace variable references with values by substituting variable reference\n    tokens with the tokens representing their values.\n\n    Args:\n        tokens: Iterator of Tokens which may contain tokens\n            with the name \"variable_ref\".\n\n    Returns:\n        Yields Tokens such that any variable references (tokens where\n            token.name == \"variable_ref\") have been replaced with the tokens representing\n            the value. In other words, an Iterable of Tokens similar to the original input,\n            but with variables resolved. Substituted tokens will have their referenced_by\n            attribute populated with information about where the tokens are being substituted to.\n    \"\"\"\n    variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}\n\n    iter_tokens = iter(tokens)\n\n    while True:\n        token = next(iter_tokens, None)\n        if token is None:\n            break\n        if token.name == \"variable_name\":\n            variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. \"$x:\" -> \"x\"\n            yield token\n\n            while True:\n                token = next(iter_tokens, None)\n                if token is not None and token.name == \"whitespace\":\n                    yield token\n                else:\n                    break\n\n            # Store the tokens for any variable definitions, and substitute\n            # any variable references we encounter with them.\n            while True:\n                if not token:\n                    break\n                elif token.name == \"whitespace\":\n                    variables.setdefault(variable_name, []).append(token)\n                    yield token\n                elif token.name == \"variable_value_end\":\n                    yield token\n                    break\n                # For variables referring to other variables\n                elif token.name == \"variable_ref\":\n                    ref_name = token.value[1:]\n                    if ref_name in variables:\n                        variable_tokens = variables.setdefault(variable_name, [])\n                        reference_tokens = variables[ref_name]\n                        variable_tokens.extend(reference_tokens)\n                        ref_location = token.location\n                        ref_length = len(token.value)\n                        for _token in reference_tokens:\n                            yield _token.with_reference(\n                                ReferencedBy(\n                                    ref_name, ref_location, ref_length, token.code\n                                )\n                            )\n                    else:\n                        _unresolved(ref_name, variables.keys(), token)\n                else:\n                    variables.setdefault(variable_name, []).append(token)\n                    yield token\n                token = next(iter_tokens, None)\n        elif token.name == \"variable_ref\":\n            variable_name = token.value[1:]  # Trim the $, so $x -> x\n            if variable_name in variables:\n                variable_tokens = variables[variable_name]\n                ref_location = token.location\n                ref_length = len(token.value)\n                ref_code = token.code\n                for _token in variable_tokens:\n                    yield _token.with_reference(\n                        ReferencedBy(variable_name, ref_location, ref_length, ref_code)\n                    )\n            else:\n                _unresolved(variable_name, variables.keys(), token)\n        else:\n            yield token\n\n\ndef parse(\n    css: str,\n    path: str | PurePath,\n    variables: dict[str, str] | None = None,\n    variable_tokens: dict[str, list[Token]] | None = None,\n    is_default_rules: bool = False,\n    tie_breaker: int = 0,\n) -> Iterable[RuleSet]:\n    \"\"\"Parse CSS by tokenizing it, performing variable substitution,\n    and generating rule sets from it.\n\n    Args:\n        css: The input CSS\n        path: Path to the CSS\n        variables: Substitution variables to substitute tokens for.\n        is_default_rules: True if the rules we're extracting are\n            default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.\n    \"\"\"\n\n    reference_tokens = tokenize_values(variables) if variables is not None else {}\n    if variable_tokens:\n        reference_tokens.update(variable_tokens)\n\n    tokens = iter(substitute_references(tokenize(css, path), variable_tokens))\n    while True:\n        token = next(tokens, None)\n        if token is None:\n            break\n        if token.name.startswith(\"selector_start\"):\n            yield from parse_rule_set(\n                tokens,\n                token,\n                is_default_rules=is_default_rules,\n                tie_breaker=tie_breaker,\n            )\n", "path": "src/textual/css/parse.py"}], "after_files": [{"content": "from __future__ import annotations\n\nfrom functools import lru_cache\nfrom pathlib import PurePath\nfrom typing import Iterable, Iterator, NoReturn\n\nfrom ..suggestions import get_suggestion\nfrom ._help_renderables import HelpText\nfrom ._styles_builder import DeclarationError, StylesBuilder\nfrom .errors import UnresolvedVariableError\nfrom .model import (\n    CombinatorType,\n    Declaration,\n    RuleSet,\n    Selector,\n    SelectorSet,\n    SelectorType,\n)\nfrom .styles import Styles\nfrom .tokenize import Token, tokenize, tokenize_declarations, tokenize_values\nfrom .tokenizer import EOFError, ReferencedBy\nfrom .types import Specificity3\n\nSELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {\n    \"selector\": (SelectorType.TYPE, (0, 0, 1)),\n    \"selector_start\": (SelectorType.TYPE, (0, 0, 1)),\n    \"selector_class\": (SelectorType.CLASS, (0, 1, 0)),\n    \"selector_start_class\": (SelectorType.CLASS, (0, 1, 0)),\n    \"selector_id\": (SelectorType.ID, (1, 0, 0)),\n    \"selector_start_id\": (SelectorType.ID, (1, 0, 0)),\n    \"selector_universal\": (SelectorType.UNIVERSAL, (0, 0, 0)),\n    \"selector_start_universal\": (SelectorType.UNIVERSAL, (0, 0, 0)),\n}\n\n\n@lru_cache(maxsize=1024)\ndef parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:\n    if not css_selectors.strip():\n        return ()\n\n    tokens = iter(tokenize(css_selectors, \"\"))\n\n    get_selector = SELECTOR_MAP.get\n    combinator: CombinatorType | None = CombinatorType.DESCENDENT\n    selectors: list[Selector] = []\n    rule_selectors: list[list[Selector]] = []\n\n    while True:\n        try:\n            token = next(tokens)\n        except EOFError:\n            break\n        token_name = token.name\n        if token_name == \"pseudo_class\":\n            selectors[-1]._add_pseudo_class(token.value.lstrip(\":\"))\n        elif token_name == \"whitespace\":\n            if combinator is None or combinator == CombinatorType.SAME:\n                combinator = CombinatorType.DESCENDENT\n        elif token_name == \"new_selector\":\n            rule_selectors.append(selectors[:])\n            selectors.clear()\n            combinator = None\n        elif token_name == \"declaration_set_start\":\n            break\n        elif token_name == \"combinator_child\":\n            combinator = CombinatorType.CHILD\n        else:\n            _selector, specificity = get_selector(\n                token_name, (SelectorType.TYPE, (0, 0, 0))\n            )\n            selectors.append(\n                Selector(\n                    name=token.value.lstrip(\".#\"),\n                    combinator=combinator or CombinatorType.DESCENDENT,\n                    type=_selector,\n                    specificity=specificity,\n                )\n            )\n            combinator = CombinatorType.SAME\n    if selectors:\n        rule_selectors.append(selectors[:])\n\n    selector_set = tuple(SelectorSet.from_selectors(rule_selectors))\n    return selector_set\n\n\ndef parse_rule_set(\n    tokens: Iterator[Token],\n    token: Token,\n    is_default_rules: bool = False,\n    tie_breaker: int = 0,\n) -> Iterable[RuleSet]:\n    get_selector = SELECTOR_MAP.get\n    combinator: CombinatorType | None = CombinatorType.DESCENDENT\n    selectors: list[Selector] = []\n    rule_selectors: list[list[Selector]] = []\n    styles_builder = StylesBuilder()\n\n    while True:\n        if token.name == \"pseudo_class\":\n            selectors[-1]._add_pseudo_class(token.value.lstrip(\":\"))\n        elif token.name == \"whitespace\":\n            if combinator is None or combinator == CombinatorType.SAME:\n                combinator = CombinatorType.DESCENDENT\n        elif token.name == \"new_selector\":\n            rule_selectors.append(selectors[:])\n            selectors.clear()\n            combinator = None\n        elif token.name == \"declaration_set_start\":\n            break\n        elif token.name == \"combinator_child\":\n            combinator = CombinatorType.CHILD\n        else:\n            _selector, specificity = get_selector(\n                token.name, (SelectorType.TYPE, (0, 0, 0))\n            )\n            selectors.append(\n                Selector(\n                    name=token.value.lstrip(\".#\"),\n                    combinator=combinator or CombinatorType.DESCENDENT,\n                    type=_selector,\n                    specificity=specificity,\n                )\n            )\n            combinator = CombinatorType.SAME\n\n        token = next(tokens)\n\n    if selectors:\n        rule_selectors.append(selectors[:])\n\n    declaration = Declaration(token, \"\")\n\n    errors: list[tuple[Token, str | HelpText]] = []\n\n    while True:\n        token = next(tokens)\n        token_name = token.name\n        if token_name in (\"whitespace\", \"declaration_end\"):\n            continue\n        if token_name == \"declaration_name\":\n            if declaration.tokens:\n                try:\n                    styles_builder.add_declaration(declaration)\n                except DeclarationError as error:\n                    errors.append((error.token, error.message))\n            declaration = Declaration(token, \"\")\n            declaration.name = token.value.rstrip(\":\")\n        elif token_name == \"declaration_set_end\":\n            break\n        else:\n            declaration.tokens.append(token)\n\n    if declaration.tokens:\n        try:\n            styles_builder.add_declaration(declaration)\n        except DeclarationError as error:\n            errors.append((error.token, error.message))\n\n    rule_set = RuleSet(\n        list(SelectorSet.from_selectors(rule_selectors)),\n        styles_builder.styles,\n        errors,\n        is_default_rules=is_default_rules,\n        tie_breaker=tie_breaker,\n    )\n    rule_set._post_parse()\n    yield rule_set\n\n\ndef parse_declarations(css: str, path: str) -> Styles:\n    \"\"\"Parse declarations and return a Styles object.\n\n    Args:\n        css: String containing CSS.\n        path: Path to the CSS, or something else to identify the location.\n\n    Returns:\n        A styles object.\n    \"\"\"\n\n    tokens = iter(tokenize_declarations(css, path))\n    styles_builder = StylesBuilder()\n\n    declaration: Declaration | None = None\n    errors: list[tuple[Token, str | HelpText]] = []\n\n    while True:\n        token = next(tokens, None)\n        if token is None:\n            break\n        token_name = token.name\n        if token_name in (\"whitespace\", \"declaration_end\", \"eof\"):\n            continue\n        if token_name == \"declaration_name\":\n            if declaration and declaration.tokens:\n                try:\n                    styles_builder.add_declaration(declaration)\n                except DeclarationError as error:\n                    errors.append((error.token, error.message))\n                    raise\n            declaration = Declaration(token, \"\")\n            declaration.name = token.value.rstrip(\":\")\n        elif token_name == \"declaration_set_end\":\n            break\n        else:\n            if declaration:\n                declaration.tokens.append(token)\n\n    if declaration and declaration.tokens:\n        try:\n            styles_builder.add_declaration(declaration)\n        except DeclarationError as error:\n            errors.append((error.token, error.message))\n            raise\n\n    return styles_builder.styles\n\n\ndef _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> NoReturn:\n    \"\"\"Raise a TokenError regarding an unresolved variable.\n\n    Args:\n        variable_name: A variable name.\n        variables: Possible choices used to generate suggestion.\n        token: The Token.\n\n    Raises:\n        UnresolvedVariableError: Always raises a TokenError.\n    \"\"\"\n    message = f\"reference to undefined variable '${variable_name}'\"\n    suggested_variable = get_suggestion(variable_name, list(variables))\n    if suggested_variable:\n        message += f\"; did you mean '${suggested_variable}'?\"\n\n    raise UnresolvedVariableError(\n        token.path,\n        token.code,\n        token.start,\n        message,\n        end=token.end,\n    )\n\n\ndef substitute_references(\n    tokens: Iterable[Token], css_variables: dict[str, list[Token]] | None = None\n) -> Iterable[Token]:\n    \"\"\"Replace variable references with values by substituting variable reference\n    tokens with the tokens representing their values.\n\n    Args:\n        tokens: Iterator of Tokens which may contain tokens\n            with the name \"variable_ref\".\n\n    Returns:\n        Yields Tokens such that any variable references (tokens where\n            token.name == \"variable_ref\") have been replaced with the tokens representing\n            the value. In other words, an Iterable of Tokens similar to the original input,\n            but with variables resolved. Substituted tokens will have their referenced_by\n            attribute populated with information about where the tokens are being substituted to.\n    \"\"\"\n    variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}\n\n    iter_tokens = iter(tokens)\n\n    while True:\n        token = next(iter_tokens, None)\n        if token is None:\n            break\n        if token.name == \"variable_name\":\n            variable_name = token.value[1:-1]  # Trim the $ and the :, i.e. \"$x:\" -> \"x\"\n            variable_tokens = variables.setdefault(variable_name, [])\n            yield token\n\n            while True:\n                token = next(iter_tokens, None)\n                if token is not None and token.name == \"whitespace\":\n                    yield token\n                else:\n                    break\n\n            # Store the tokens for any variable definitions, and substitute\n            # any variable references we encounter with them.\n            while True:\n                if not token:\n                    break\n                elif token.name == \"whitespace\":\n                    variable_tokens.append(token)\n                    yield token\n                elif token.name == \"variable_value_end\":\n                    yield token\n                    break\n                # For variables referring to other variables\n                elif token.name == \"variable_ref\":\n                    ref_name = token.value[1:]\n                    if ref_name in variables:\n                        reference_tokens = variables[ref_name]\n                        variable_tokens.extend(reference_tokens)\n                        ref_location = token.location\n                        ref_length = len(token.value)\n                        for _token in reference_tokens:\n                            yield _token.with_reference(\n                                ReferencedBy(\n                                    ref_name, ref_location, ref_length, token.code\n                                )\n                            )\n                    else:\n                        _unresolved(ref_name, variables.keys(), token)\n                else:\n                    variable_tokens.append(token)\n                    yield token\n                token = next(iter_tokens, None)\n        elif token.name == \"variable_ref\":\n            variable_name = token.value[1:]  # Trim the $, so $x -> x\n            if variable_name in variables:\n                variable_tokens = variables[variable_name]\n                ref_location = token.location\n                ref_length = len(token.value)\n                ref_code = token.code\n                for _token in variable_tokens:\n                    yield _token.with_reference(\n                        ReferencedBy(variable_name, ref_location, ref_length, ref_code)\n                    )\n            else:\n                _unresolved(variable_name, variables.keys(), token)\n        else:\n            yield token\n\n\ndef parse(\n    css: str,\n    path: str | PurePath,\n    variables: dict[str, str] | None = None,\n    variable_tokens: dict[str, list[Token]] | None = None,\n    is_default_rules: bool = False,\n    tie_breaker: int = 0,\n) -> Iterable[RuleSet]:\n    \"\"\"Parse CSS by tokenizing it, performing variable substitution,\n    and generating rule sets from it.\n\n    Args:\n        css: The input CSS\n        path: Path to the CSS\n        variables: Substitution variables to substitute tokens for.\n        is_default_rules: True if the rules we're extracting are\n            default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.\n    \"\"\"\n\n    reference_tokens = tokenize_values(variables) if variables is not None else {}\n    if variable_tokens:\n        reference_tokens.update(variable_tokens)\n\n    tokens = iter(substitute_references(tokenize(css, path), variable_tokens))\n    while True:\n        token = next(tokens, None)\n        if token is None:\n            break\n        if token.name.startswith(\"selector_start\"):\n            yield from parse_rule_set(\n                tokens,\n                token,\n                is_default_rules=is_default_rules,\n                tie_breaker=tie_breaker,\n            )\n", "path": "src/textual/css/parse.py"}]}