Substitution variables

This commit is contained in:
Darren Burns
2022-02-02 15:49:16 +00:00
parent ac9e3cdfff
commit 65a6b8d261
5 changed files with 140 additions and 34 deletions

View File

@@ -1,8 +1,10 @@
/* CSS file for basic.py */ /* CSS file for basic.py */
$primary: #20639b;
App > View { App > View {
docks: side=left/1; docks: side=left/1;
text: on #20639b; text: on $primary;
} }
Widget:hover { Widget:hover {
@@ -34,7 +36,7 @@ Widget:hover {
} }
#content { #content {
text: white on #20639b; text: white on $primary;
border-bottom: hkey #0f2b41; border-bottom: hkey #0f2b41;
} }

View File

@@ -9,6 +9,10 @@ class DeclarationError(Exception):
super().__init__(message) super().__init__(message)
class UnresolvedVariableError(NameError):
pass
class StyleTypeError(TypeError): class StyleTypeError(TypeError):
pass pass

View File

@@ -1,17 +1,14 @@
from __future__ import annotations from __future__ import annotations
import itertools
from collections import defaultdict from collections import defaultdict
from functools import lru_cache
from itertools import dropwhile
from typing import Iterator, Iterable
from rich import print from rich import print
from functools import lru_cache from textual.css.errors import UnresolvedVariableError
from typing import Iterator, Iterable from ._styles_builder import StylesBuilder, DeclarationError
from .styles import Styles
from .tokenize import tokenize, tokenize_declarations, Token
from .tokenizer import EOFError
from .model import ( from .model import (
Declaration, Declaration,
RuleSet, RuleSet,
@@ -20,8 +17,9 @@ from .model import (
SelectorSet, SelectorSet,
SelectorType, SelectorType,
) )
from ._styles_builder import StylesBuilder, DeclarationError from .styles import Styles
from .tokenize import tokenize, tokenize_declarations, Token
from .tokenizer import EOFError
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = { SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
"selector": (SelectorType.TYPE, (0, 0, 1)), "selector": (SelectorType.TYPE, (0, 0, 1)),
@@ -37,7 +35,6 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]: def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
tokens = iter(tokenize(css_selectors, "")) tokens = iter(tokenize(css_selectors, ""))
get_selector = SELECTOR_MAP.get get_selector = SELECTOR_MAP.get
@@ -84,7 +81,6 @@ def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]: def parse_rule_set(tokens: Iterator[Token], token: Token) -> Iterable[RuleSet]:
get_selector = SELECTOR_MAP.get get_selector = SELECTOR_MAP.get
combinator: CombinatorType | None = CombinatorType.DESCENDENT combinator: CombinatorType | None = CombinatorType.DESCENDENT
selectors: list[Selector] = [] selectors: list[Selector] = []
@@ -208,21 +204,84 @@ def parse_declarations(css: str, path: str) -> Styles:
return styles_builder.styles return styles_builder.styles
# def _with_resolved_variables(tokens: Iterable[Token]) -> Iterable[Token]: def _is_whitespace(token: Token) -> bool:
# variables: dict[str, list[Token]] = defaultdict(list) return token.name == "whitespace"
# for token in tokens:
# token = next(tokens, None)
# if token is None: def _unresolved(
# break variable_name: str, location: tuple[int, int]
# if token.name == "variable_name": ) -> UnresolvedVariableError:
# variable_name = token.value[1:-1] # Trim the $ and the :, i.e. "$x:" -> "x" return UnresolvedVariableError(
# # At this point, we need to tokenize the variable value, as when we pass f"variable ${variable_name} is not defined. "
# # the Declarations to the style builder, types must be known (e.g. Scalar vs Duration) f"attempted reference at location {location!r}"
# variables[variable_name] = )
def substitute_references(tokens: Iterator[Token]) -> Iterable[Token]:
"""Replace variable references with values by substituting variable reference
tokens with the tokens representing their values.
Args:
tokens (Iterator[Token]): Iterator of Tokens which may contain tokens
with the name "variable_ref".
Returns:
Iterable[Token]: Yields Tokens such that any variable references (tokens where
token.name == "variable_ref") have been replaced with the tokens representing
the value. In other words, an Iterable of Tokens similar to the original input,
but with variables resolved.
"""
variables: dict[str, list[Token]] = defaultdict(list)
while tokens:
token = next(tokens, None)
if token is None:
break
if token.name == "variable_name":
variable_name = token.value[1:-1] # Trim the $ and the :, i.e. "$x:" -> "x"
yield token
# Lookahead for the variable value tokens
while True:
token = next(tokens, None)
if not token:
break
if token.name != "variable_value_end":
# For variables referring to other variables
if token.name == "variable_ref":
ref_name = token.value[1:]
if ref_name in variables:
variables[variable_name].extend(variables[ref_name])
variable_tokens = dropwhile(
_is_whitespace,
variables[ref_name],
)
yield from variable_tokens
else:
raise _unresolved(
variable_name=ref_name, location=token.location
)
else:
variables[variable_name].append(token)
yield token
else:
yield token
break
elif token.name == "variable_ref":
variable_name = token.value[1:] # Trim the $, so $x -> x
if variable_name in variables:
variable_tokens = dropwhile(
_is_whitespace,
variables[variable_name],
)
yield from variable_tokens
else:
raise _unresolved(variable_name=variable_name, location=token.location)
else:
yield token
def parse(css: str, path: str) -> Iterable[RuleSet]: def parse(css: str, path: str) -> Iterable[RuleSet]:
tokens = iter((tokenize(css, path))) tokens = iter(substitute_references(tokenize(css, path)))
while True: while True:
token = next(tokens, None) token = next(tokens, None)
if token is None: if token is None:

View File

@@ -1,9 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import NamedTuple
import re import re
from typing import NamedTuple
from rich import print
import rich.repr import rich.repr

View File

@@ -1,19 +1,61 @@
import pytest import pytest
from rich.color import Color, ColorType from rich.color import Color, ColorType
# from textual.css.parse import _resolve_variables from textual.css.errors import UnresolvedVariableError
from textual.css.parse import substitute_references
from textual.css.scalar import Scalar, Unit from textual.css.scalar import Scalar, Unit
from textual.css.stylesheet import Stylesheet, StylesheetParseError from textual.css.stylesheet import Stylesheet, StylesheetParseError
from textual.css.tokenize import tokenize from textual.css.tokenize import tokenize
from textual.css.tokenizer import Token
from textual.css.transition import Transition from textual.css.transition import Transition
from textual.layouts.dock import DockLayout from textual.layouts.dock import DockLayout
# class TestVariableResolution: class TestVariableReferenceSubstitution:
# def test_resolve_single_variable(self): def test_simple_reference(self):
# css = "$x: 1;" css = "$x: 1; #some-widget{border: $x;}"
# variables = _resolve_variables(tokenize(css, "")) variables = substitute_references(tokenize(css, ""))
# assert variables == {"x": } assert list(variables) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3)),
Token(name='number', value='1', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 5)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 6)),
Token(name='selector_start_id', value='#some-widget', path='', code=css, location=(0, 7)),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 19)),
Token(name='declaration_name', value='border:', path='', code=css, location=(0, 20)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 27)),
Token(name='number', value='1', path='', code=css, location=(0, 4)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 30)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 31))
]
def test_undefined_variable(self):
css = ".thing { border: $not-defined; }"
with pytest.raises(UnresolvedVariableError):
list(substitute_references(tokenize(css, "")))
def test_transitive_reference(self):
css = "$x: 1\n$y: $x\n.thing { border: $y }"
assert list(substitute_references(tokenize(css, ""))) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3)),
Token(name='number', value='1', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 5)),
Token(name='variable_name', value='$y:', path='', code=css, location=(1, 0)),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 3)),
Token(name='number', value='1', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value='\n', path='', code=css, location=(1, 6)),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(2, 0)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 6)),
Token(name='declaration_set_start', value='{', path='', code=css, location=(2, 7)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 8)),
Token(name='declaration_name', value='border:', path='', code=css, location=(2, 9)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 16)),
Token(name='number', value='1', path='', code=css, location=(0, 4)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 19)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(2, 20))
]
class TestParseLayout: class TestParseLayout: