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 */
|
/* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user