mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Substitution variables
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
/* CSS file for basic.py */
|
||||
|
||||
$primary: #20639b;
|
||||
|
||||
App > View {
|
||||
docks: side=left/1;
|
||||
text: on #20639b;
|
||||
text: on $primary;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
@@ -34,7 +36,7 @@ Widget:hover {
|
||||
}
|
||||
|
||||
#content {
|
||||
text: white on #20639b;
|
||||
text: white on $primary;
|
||||
border-bottom: hkey #0f2b41;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ class DeclarationError(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UnresolvedVariableError(NameError):
|
||||
pass
|
||||
|
||||
|
||||
class StyleTypeError(TypeError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from itertools import dropwhile
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
from rich import print
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Iterator, Iterable
|
||||
|
||||
from .styles import Styles
|
||||
from .tokenize import tokenize, tokenize_declarations, Token
|
||||
from .tokenizer import EOFError
|
||||
|
||||
from textual.css.errors import UnresolvedVariableError
|
||||
from ._styles_builder import StylesBuilder, DeclarationError
|
||||
from .model import (
|
||||
Declaration,
|
||||
RuleSet,
|
||||
@@ -20,8 +17,9 @@ from .model import (
|
||||
SelectorSet,
|
||||
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": (SelectorType.TYPE, (0, 0, 1)),
|
||||
@@ -37,7 +35,6 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
|
||||
|
||||
tokens = iter(tokenize(css_selectors, ""))
|
||||
|
||||
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]:
|
||||
|
||||
get_selector = SELECTOR_MAP.get
|
||||
combinator: CombinatorType | None = CombinatorType.DESCENDENT
|
||||
selectors: list[Selector] = []
|
||||
@@ -208,21 +204,84 @@ def parse_declarations(css: str, path: str) -> Styles:
|
||||
return styles_builder.styles
|
||||
|
||||
|
||||
# def _with_resolved_variables(tokens: Iterable[Token]) -> Iterable[Token]:
|
||||
# variables: dict[str, list[Token]] = defaultdict(list)
|
||||
# for token in 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"
|
||||
# # At this point, we need to tokenize the variable value, as when we pass
|
||||
# # the Declarations to the style builder, types must be known (e.g. Scalar vs Duration)
|
||||
# variables[variable_name] =
|
||||
def _is_whitespace(token: Token) -> bool:
|
||||
return token.name == "whitespace"
|
||||
|
||||
|
||||
def _unresolved(
|
||||
variable_name: str, location: tuple[int, int]
|
||||
) -> UnresolvedVariableError:
|
||||
return UnresolvedVariableError(
|
||||
f"variable ${variable_name} is not defined. "
|
||||
f"attempted reference at location {location!r}"
|
||||
)
|
||||
|
||||
|
||||
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]:
|
||||
tokens = iter((tokenize(css, path)))
|
||||
tokens = iter(substitute_references(tokenize(css, path)))
|
||||
while True:
|
||||
token = next(tokens, None)
|
||||
if token is None:
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
import re
|
||||
from typing import NamedTuple
|
||||
|
||||
from rich import print
|
||||
import rich.repr
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,61 @@
|
||||
import pytest
|
||||
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.stylesheet import Stylesheet, StylesheetParseError
|
||||
from textual.css.tokenize import tokenize
|
||||
from textual.css.tokenizer import Token
|
||||
from textual.css.transition import Transition
|
||||
from textual.layouts.dock import DockLayout
|
||||
|
||||
|
||||
# class TestVariableResolution:
|
||||
# def test_resolve_single_variable(self):
|
||||
# css = "$x: 1;"
|
||||
# variables = _resolve_variables(tokenize(css, ""))
|
||||
# assert variables == {"x": }
|
||||
class TestVariableReferenceSubstitution:
|
||||
def test_simple_reference(self):
|
||||
css = "$x: 1; #some-widget{border: $x;}"
|
||||
variables = substitute_references(tokenize(css, ""))
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user