Merge branch 'css' into inline-styles-view

This commit is contained in:
Will McGugan
2022-02-07 16:17:10 +00:00
committed by GitHub
13 changed files with 551 additions and 211 deletions

View File

@@ -1,9 +1,11 @@
/* CSS file for basic.py */
$primary: #20639b;
App > View {
layout: dock;
docks: side=left/1;
text: on #20639b;
text: on $primary;
}
#sidebar {
@@ -26,7 +28,7 @@ App > View {
}
#content {
text: white on #20639b;
text: white on $primary;
border-bottom: hkey #0f2b41;
}

View File

@@ -1,44 +0,0 @@
/* CSS file for basic.py */
App > View {
docks: side=left/1;
text: on #20639b;
}
Widget:hover {
outline: heavy;
text: bold !important;
}
#sidebar {
text: #09312e on #3caea3;
dock: side;
width: 30;
offset-x: -100%;
transition: offset 500ms in_out_cubic;
border-right: outer #09312e;
}
#sidebar.-active {
offset-x: 0;
}
#header {
text: white on #173f5f;
height: 3;
border: hkey;
}
#header.-visible {
visibility: hidden;
}
#content {
text: white on #20639b;
border-bottom: hkey #0f2b41;
}
#footer {
text: #3a3009 on #f6d55c;
height: 3;
}

View File

@@ -1,23 +1,32 @@
from rich.console import RenderableType
from rich.panel import Panel
from textual.app import App
from textual.widget import Widget
class PanelWidget(Widget):
def render(self) -> RenderableType:
return Panel("hello world!", title="Title")
class BasicApp(App):
"""A basic app demonstrating CSS"""
"""Sandbox application used for testing/development by Textual developers"""
def on_load(self):
"""Bind keys here."""
self.bind("tab", "toggle_class('#sidebar', '-active')")
self.bind("a", "toggle_class('#header', '-visible')")
self.bind("c", "toggle_class('#content', '-content-visible')")
def on_mount(self):
"""Build layout here."""
self.mount(
header=Widget(),
content=Widget(),
content=PanelWidget(),
footer=Widget(),
sidebar=Widget(),
)
BasicApp.run(css_file="dev_sandbox.css", watch_css=True, log="textual.log")
BasicApp.run(css_file="dev_sandbox.scss", watch_css=True, log="textual.log")

58
examples/dev_sandbox.scss Normal file
View File

@@ -0,0 +1,58 @@
/* CSS file for dev_sandbox.py */
$text: #f0f0f0;
$primary: #021720;
$secondary:#95d52a;
$background: #262626;
$primary-style: $text on $background;
$animation-speed: 500ms;
$animation: offset $animation-speed in_out_cubic;
App > View {
docks: side=left/1;
text: on $background;
}
Widget:hover {
outline: heavy;
text: bold !important;
}
#sidebar {
text: $primary-style;
dock: side;
width: 30;
offset-x: -100%;
transition: $animation;
border-right: outer $secondary;
}
#sidebar.-active {
offset-x: 0;
}
#header {
text: $text on $primary;
height: 3;
border-bottom: hkey $secondary;
}
#header.-visible {
visibility: hidden;
}
#content {
text: $text on $background;
offset-y: -3;
}
#content.-content-visible {
visibility: hidden;
}
#footer {
text: $text on $primary;
height: 3;
border-top: hkey $secondary;
}

View File

@@ -17,6 +17,7 @@ from .transition import Transition
from .types import Edge, Display, Visibility
from .._duration import _duration_as_seconds
from .._easing import EASING
from .._loop import loop_last
from ..geometry import Spacing, SpacingDimensions
@@ -66,7 +67,7 @@ class StylesBuilder:
def process_display(self, name: str, tokens: list[Token], important: bool) -> None:
for token in tokens:
name, value, _, _, location = token
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
@@ -109,7 +110,7 @@ class StylesBuilder:
self, name: str, tokens: list[Token], important: bool
) -> None:
for token in tokens:
name, value, _, _, location = token
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_VISIBILITY:
@@ -127,7 +128,7 @@ class StylesBuilder:
space: list[int] = []
append = space.append
for token in tokens:
(token_name, value, _, _, location) = token
token_name, value, _, _, location, _ = token
if token_name in ("number", "scalar"):
try:
append(int(value))
@@ -153,7 +154,7 @@ class StylesBuilder:
style_tokens: list[str] = []
append = style_tokens.append
for token in tokens:
token_name, value, _, _, _ = token
token_name, value, _, _, _, _ = token
if token_name == "token":
if value in VALID_BORDER:
border_type = value
@@ -299,15 +300,26 @@ class StylesBuilder:
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
style_definition = " ".join(token.value for token in tokens)
# If every token in the value is a referenced by the same variable,
# we can display the variable name before the style definition.
# TODO: Factor this out to apply it to other properties too.
unique_references = {t.referenced_by for t in tokens if t.referenced_by}
if tokens and tokens[0].referenced_by and len(unique_references) == 1:
variable_prefix = f"${tokens[0].referenced_by.name}="
else:
variable_prefix = ""
try:
style = Style.parse(style_definition)
self.styles.text = style
except Exception as error:
self.error(name, tokens[0], f"failed to parse style; {error}")
message = f"property 'text' has invalid value {variable_prefix}{style_definition!r}; {error}"
self.error(name, tokens[0], message)
if important:
self.styles.important.update(
{"text_style", "text_background", "text_color"}
)
self.styles.text = style
def process_text_color(
self, name: str, tokens: list[Token], important: bool

View File

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

View File

@@ -1,14 +1,14 @@
from __future__ import annotations
from rich import print
from collections import defaultdict
from functools import lru_cache
from typing import Iterator, Iterable
from typing import Iterator, Iterable, Optional
from .styles import Styles
from .tokenize import tokenize, tokenize_declarations, Token
from .tokenizer import EOFError
from rich import print
from rich.cells import cell_len
from textual.css.errors import UnresolvedVariableError
from ._styles_builder import StylesBuilder, DeclarationError
from .model import (
Declaration,
RuleSet,
@@ -17,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, ReferencedBy
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
"selector": (SelectorType.TYPE, (0, 0, 1)),
@@ -34,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
@@ -81,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] = []
@@ -205,9 +204,111 @@ def parse_declarations(css: str, path: str) -> Styles:
return styles_builder.styles
def parse(css: str, path: str) -> Iterable[RuleSet]:
def _unresolved(
variable_name: str, location: tuple[int, int]
) -> UnresolvedVariableError:
line_idx, col_idx = location
return UnresolvedVariableError(
f"reference to undefined variable '${variable_name}' at line {line_idx + 1}, column {col_idx + 1}."
)
tokens = iter(tokenize(css, path))
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. Substituted tokens will have their referenced_by
attribute populated with information about where the tokens are being substituted to.
"""
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
while True:
token = next(tokens, None)
if token.name == "whitespace":
yield token
else:
break
# Store the tokens for any variable definitions, and substitute
# any variable references we encounter with them.
while True:
if not token:
break
elif token.name == "whitespace":
variables[variable_name].append(token)
yield token
elif token.name == "variable_value_end":
yield token
break
# For variables referring to other variables
elif token.name == "variable_ref":
ref_name = token.value[1:]
if ref_name in variables:
variable_tokens = variables[variable_name]
reference_tokens = variables[ref_name]
variable_tokens.extend(reference_tokens)
ref_location = token.location
ref_length = len(token.value)
for _token in reference_tokens:
yield _token.with_reference(
ReferencedBy(
name=ref_name,
location=ref_location,
length=ref_length,
)
)
else:
raise _unresolved(
variable_name=ref_name, location=token.location
)
else:
variables[variable_name].append(token)
yield token
token = next(tokens, None)
elif token.name == "variable_ref":
variable_name = token.value[1:] # Trim the $, so $x -> x
if variable_name in variables:
variable_tokens = variables[variable_name]
ref_location = token.location
ref_length = len(token.value)
for token in variable_tokens:
yield token.with_reference(
ReferencedBy(
name=variable_name,
location=ref_location,
length=ref_length,
)
)
else:
raise _unresolved(variable_name=variable_name, location=token.location)
else:
yield token
def parse(css: str, path: str) -> Iterable[RuleSet]:
"""Parse CSS by tokenizing it, performing variable substitution,
and generating rule sets from it.
Args:
css (str): The input CSS
path (str): Path to the CSS
"""
tokens = iter(substitute_references(tokenize(css, path)))
while True:
token = next(tokens, None)
if token is None:
@@ -216,42 +317,6 @@ def parse(css: str, path: str) -> Iterable[RuleSet]:
yield from parse_rule_set(tokens, token)
# if __name__ == "__main__":
# test = """
# App View {
# text: red;
# }
# .foo.bar baz:focus, #egg .foo.baz {
# /* ignore me, I'm a comment */
# display: block;
# visibility: visible;
# border: solid green !important;
# outline: red;
# padding: 1 2;
# margin: 5;
# text: bold red on magenta
# text-color: green;
# text-background: white
# docks: foo bar bar
# dock-group: foo
# dock-edge: top
# offset-x: 4
# offset-y: 5
# }"""
# from .stylesheet import Stylesheet
# print(test)
# print()
# stylesheet = Stylesheet()
# stylesheet.parse(test)
# print(stylesheet)
# print()
# print(stylesheet.css)
if __name__ == "__main__":
print(parse_selectors("Foo > Bar.baz { foo: bar"))

View File

@@ -36,10 +36,7 @@ from .scalar import Scalar, ScalarOffset, Unit
from .scalar_animation import ScalarAnimation
from .transition import Transition
from .types import Display, Edge, Visibility
from .types import Specificity3, Specificity4
from .. import log
from .._animator import Animation, EasingFunction
from ..geometry import Spacing, SpacingDimensions
from .._box import BoxType
@@ -50,7 +47,6 @@ if sys.version_info >= (3, 8):
else:
from typing_extensions import TypedDict
if TYPE_CHECKING:
from ..layout import Layout
from ..dom import DOMNode

View File

@@ -1,17 +1,21 @@
from __future__ import annotations
import os
from collections import defaultdict
from operator import itemgetter
import os
from typing import cast, Iterable
import rich.repr
from rich.highlighter import ReprHighlighter
from rich.panel import Panel
from rich.text import Text
from rich.console import Group, RenderableType
from rich.highlighter import ReprHighlighter
from rich.padding import Padding
from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text
from textual._loop import loop_last
from .errors import StylesheetError
from .match import _check_selectors
from .model import RuleSet
@@ -19,7 +23,6 @@ from .parse import parse
from .styles import RulesMap
from .types import Specificity3, Specificity4
from ..dom import DOMNode
from .. import log
class StylesheetParseError(Exception):
@@ -35,11 +38,17 @@ class StylesheetErrors:
self.stylesheet = stylesheet
@classmethod
def _get_snippet(cls, code: str, line_no: int, col_no: int, length: int) -> Panel:
lines = Text(code, style="dim").split()
lines[line_no].stylize("bold not dim", col_no, col_no + length - 1)
text = Text("\n").join(lines[max(0, line_no - 1) : line_no + 2])
return Panel(text, border_style="red")
def _get_snippet(cls, code: str, line_no: int) -> Panel:
syntax = Syntax(
code,
lexer="scss",
theme="ansi_light",
line_numbers=True,
indent_guides=True,
line_range=(max(0, line_no - 2), line_no + 1),
highlight_lines={line_no},
)
return Panel(syntax, border_style="red")
def __rich__(self) -> RenderableType:
highlighter = ReprHighlighter()
@@ -47,13 +56,30 @@ class StylesheetErrors:
append = errors.append
for rule in self.stylesheet.rules:
for token, message in rule.errors:
line_no, col_no = token.location
append("")
append(Text(" Error in stylesheet:", style="bold red"))
append(highlighter(f"{token.path or '<unknown>'}:{line_no}"))
append(
self._get_snippet(token.code, line_no, col_no, len(token.value) + 1)
)
append(highlighter(Text(message, "red")))
if token.referenced_by:
line_idx, col_idx = token.referenced_by.location
line_no, col_no = line_idx + 1, col_idx + 1
append(
highlighter(f" {token.path or '<unknown>'}:{line_no}:{col_no}")
)
append(self._get_snippet(token.code, line_no))
else:
line_idx, col_idx = token.location
line_no, col_no = line_idx + 1, col_idx + 1
append(
highlighter(f" {token.path or '<unknown>'}:{line_no}:{col_no}")
)
append(self._get_snippet(token.code, line_no))
final_message = ""
for is_last, message_part in loop_last(message.split(";")):
end = "" if is_last else "\n"
final_message += f"{message_part.strip()};{end}"
append(Padding(highlighter(Text(final_message, "red")), pad=(0, 1)))
append("")
return Group(*errors)
@@ -167,7 +193,6 @@ class Stylesheet:
if __name__ == "__main__":
from rich.traceback import install
install(show_locals=True)

View File

@@ -14,9 +14,9 @@ COLOR = r"\#[0-9a-fA-F]{6}|color\([0-9]{1,3}\)|rgb\(\d{1,3}\,\s?\d{1,3}\,\s?\d{1
KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+"
TOKEN = "[a-zA-Z_-]+"
STRING = r"\".*?\""
VARIABLE_REF = r"\$[a-zA-Z0-9_-]+"
VARIABLE_REF = r"\$[a-zA-Z0-9_\-]+"
# Values permitted in declarations.
# Values permitted in variable and rule declarations.
DECLARATION_VALUES = {
"scalar": SCALAR,
"duration": DURATION,
@@ -38,19 +38,16 @@ expect_root_scope = Expect(
selector_start_class=r"\.[a-zA-Z_\-][a-zA-Z0-9_\-]*",
selector_start_universal=r"\*",
selector_start=r"[a-zA-Z_\-]+",
variable_name=f"{VARIABLE_REF}:",
variable_name=rf"{VARIABLE_REF}:",
).expect_eof(True)
# After a variable declaration e.g. "$warning-text: TOKENS;"
# for tokenizing variable value ------^~~~~~~^
expect_variable_value = Expect(
comment_start=COMMENT_START,
whitespace=r"\s+",
variable_value=rf"[^;\n{COMMENT_START}]+",
)
expect_variable_value_end = Expect(
expect_variable_name_continue = Expect(
variable_value_end=r"\n|;",
whitespace=r"\s+",
comment_start=COMMENT_START,
**DECLARATION_VALUES,
).expect_eof(True)
expect_comment_end = Expect(
@@ -72,8 +69,8 @@ expect_selector_continue = Expect(
declaration_set_start=r"\{",
)
# A declaration e.g. "text: red;"
# ^---^
# A rule declaration e.g. "text: red;"
# ^---^
expect_declaration = Expect(
whitespace=r"\s+",
comment_start=COMMENT_START,
@@ -88,8 +85,8 @@ expect_declaration_solo = Expect(
declaration_set_end=r"\}",
).expect_eof(True)
# The value(s)/content from a declaration e.g. "text: red;"
# ^---^
# The value(s)/content from a rule declaration e.g. "text: red;"
# ^---^
expect_declaration_content = Expect(
declaration_end=r"\n|;",
whitespace=r"\s+",
@@ -115,8 +112,7 @@ class TokenizerState:
EXPECT = expect_root_scope
STATE_MAP = {
"variable_name": expect_variable_value,
"variable_value": expect_variable_value_end,
"variable_name": expect_variable_name_continue,
"variable_value_end": expect_root_scope,
"selector_start": expect_selector_continue,
"selector_start_id": expect_selector_continue,

View File

@@ -1,10 +1,10 @@
from __future__ import annotations
from typing import NamedTuple
import re
from typing import NamedTuple
from rich import print
import rich.repr
from rich.cells import cell_len
class EOFError(Exception):
@@ -39,6 +39,12 @@ class Expect:
yield from zip(self.names, self.regexes)
class ReferencedBy(NamedTuple):
name: str
location: tuple[int, int]
length: int
@rich.repr.auto
class Token(NamedTuple):
name: str
@@ -46,6 +52,23 @@ class Token(NamedTuple):
path: str
code: str
location: tuple[int, int]
referenced_by: ReferencedBy | None
def with_reference(self, by: ReferencedBy | None) -> "Token":
"""Return a copy of the Token, with reference information attached.
This is used for variable substitution, where a variable reference
can refer to tokens which were defined elsewhere. With the additional
ReferencedBy data attached, we can track where the token we are referring
to is used.
"""
return Token(
name=self.name,
value=self.value,
path=self.path,
code=self.code,
location=self.location,
referenced_by=by,
)
def __str__(self) -> str:
return self.value
@@ -55,6 +78,7 @@ class Token(NamedTuple):
yield "value", self.value
yield "path", self.path
yield "location", self.location
yield "referenced_by", self.referenced_by
class Tokenizer:
@@ -70,7 +94,7 @@ class Tokenizer:
col_no = self.col_no
if line_no >= len(self.lines):
if expect._expect_eof:
return Token("eof", "", self.path, self.code, (line_no, col_no))
return Token("eof", "", self.path, self.code, (line_no, col_no), None)
else:
raise EOFError()
line = self.lines[line_no]
@@ -88,7 +112,9 @@ class Tokenizer:
if value is not None:
break
token = Token(name, value, self.path, self.code, (line_no, col_no))
token = Token(
name, value, self.path, self.code, (line_no, col_no), referenced_by=None
)
col_no += len(value)
if col_no >= len(line):
line_no += 1

View File

@@ -1,12 +1,178 @@
import pytest
from rich.color import Color, ColorType
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, ReferencedBy
from textual.css.transition import Transition
from textual.layouts.dock import DockLayout
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), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='selector_start_id', value='#some-widget', path='', code=css, location=(0, 7),
referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 19), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(0, 20), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 27), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='x', location=(0, 28), length=2)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 30), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 31), referenced_by=None)
]
def test_simple_reference_no_whitespace(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), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='selector_start_id', value='#some-widget', path='', code=css, location=(0, 6),
referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 18), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(0, 19), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 26), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 3),
referenced_by=ReferencedBy(name='x', location=(0, 27), length=2)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 29), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 30), referenced_by=None)
]
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), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='variable_name', value='$y:', path='', code=css, location=(1, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 3), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='x', location=(1, 4), length=2)),
Token(name='variable_value_end', value='\n', path='', code=css, location=(1, 6), referenced_by=None),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(2, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 6), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(2, 7), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 8), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(2, 9), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 16), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 19), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(2, 20), referenced_by=None)
]
def test_multi_value_variable(self):
css = "$x: 2 4\n$y: 6 $x 2\n.thing { border: $y }"
assert list(substitute_references(tokenize(css, ""))) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='2', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='number', value='4', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 7), referenced_by=None),
Token(name='variable_name', value='$y:', path='', code=css, location=(1, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 3), referenced_by=None),
Token(name='number', value='6', path='', code=css, location=(1, 4), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 5), referenced_by=None),
Token(name='number', value='2', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='x', location=(1, 6), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5),
referenced_by=ReferencedBy(name='x', location=(1, 6), length=2)),
Token(name='number', value='4', path='', code=css, location=(0, 6),
referenced_by=ReferencedBy(name='x', location=(1, 6), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 8), referenced_by=None),
Token(name='number', value='2', path='', code=css, location=(1, 9), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(1, 10), referenced_by=None),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(2, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 6), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(2, 7), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 8), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(2, 9), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 16), referenced_by=None),
Token(name='number', value='6', path='', code=css, location=(1, 4),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 5),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='number', value='2', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='number', value='4', path='', code=css, location=(0, 6),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 8),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='number', value='2', path='', code=css, location=(1, 9),
referenced_by=ReferencedBy(name='y', location=(2, 17), length=2)),
Token(name='whitespace', value=' ', path='', code=css, location=(2, 19), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(2, 20), referenced_by=None)
]
def test_variable_used_inside_property_value(self):
css = "$x: red\n.thing { border: on $x; }"
assert list(substitute_references(tokenize(css, ""))) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 7), referenced_by=None),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(1, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 6), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(1, 7), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 8), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(1, 9), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 16), referenced_by=None),
Token(name='token', value='on', path='', code=css, location=(1, 17), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 19), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 4),
referenced_by=ReferencedBy(name='x', location=(1, 20), length=2)),
Token(name='declaration_end', value=';', path='', code=css, location=(1, 22), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 23), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(1, 24), referenced_by=None)
]
def test_variable_definition_eof(self):
css = "$x: 1"
assert list(substitute_references(tokenize(css, ""))) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4), referenced_by=None)
]
def test_variable_reference_whitespace_trimming(self):
css = "$x: 123;.thing{border: $x}"
assert list(substitute_references(tokenize(css, ""))) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='123', path='', code=css, location=(0, 7), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 10), referenced_by=None),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(0, 11), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 17), referenced_by=None),
Token(name='declaration_name', value='border:', path='', code=css, location=(0, 18), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 25), referenced_by=None),
Token(name='number', value='123', path='', code=css, location=(0, 7),
referenced_by=ReferencedBy(name='x', location=(0, 26), length=2)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 28), referenced_by=None)
]
class TestParseLayout:
def test_valid_layout_name(self):
css = "#some-widget { layout: dock; }"

View File

@@ -1,6 +1,5 @@
import pytest
import textual.css.tokenizer
from textual.css.tokenize import tokenize
from textual.css.tokenizer import Token, TokenizeError
@@ -21,114 +20,140 @@ VALID_VARIABLE_NAMES = [
def test_variable_declaration_valid_names(name):
css = f"${name}: black on red;"
assert list(tokenize(css, "")) == [
Token(
name="variable_name", value=f"${name}:", path="", code=css, location=(0, 0)
),
Token(name="whitespace", value=" ", path="", code=css, location=(0, 14)),
Token(name="variable_value", value="black on red", path="", code=css, location=(0, 15)),
Token(name="variable_value_end", value=";", path="", code=css, location=(0, 27)),
Token(name='variable_name', value=f'${name}:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 14), referenced_by=None),
Token(name='token', value='black', path='', code=css, location=(0, 15), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 20), referenced_by=None),
Token(name='token', value='on', path='', code=css, location=(0, 21), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 23), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 24), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 27), referenced_by=None),
]
def test_variable_declaration_multiple_values():
css = "$x: 2vw\t4% 6s red;"
assert list(tokenize(css, "")) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='scalar', value='2vw', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='whitespace', value='\t', path='', code=css, location=(0, 7), referenced_by=None),
Token(name='scalar', value='4%', path='', code=css, location=(0, 8), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 10), referenced_by=None),
Token(name='duration', value='6s', path='', code=css, location=(0, 11), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 13), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 15), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 18), referenced_by=None),
]
def test_variable_declaration_comment_ignored():
css = "$x: red; /* comment */"
assert list(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='variable_value', value='red', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 7)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 8)),
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 7), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 8), referenced_by=None),
]
def test_variable_declaration_comment_interspersed_raises():
def test_variable_declaration_comment_interspersed_ignored():
css = "$x: re/* comment */d;"
with pytest.raises(TokenizeError):
assert list(tokenize(css, ""))
def test_variable_declaration_invalid_value_eof():
css = "$x:\n"
with pytest.raises(textual.css.tokenizer.EOFError):
list(tokenize(css, ""))
assert list(tokenize(css, "")) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='token', value='re', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='token', value='d', path='', code=css, location=(0, 19), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 20), referenced_by=None),
]
def test_variable_declaration_no_semicolon():
css = "$x: 1\n$y: 2"
assert list(tokenize(css, "")) == [
Token(name="variable_name", value="$x:", code=css, path="", location=(0, 0)),
Token(name="whitespace", value=" ", code=css, path="", location=(0, 3)),
Token(name="variable_value", value="1", code=css, path="", location=(0, 4)),
Token(name="variable_value_end", value="\n", code=css, path="", location=(0, 5)),
Token(name="variable_name", value="$y:", code=css, path="", location=(1, 0)),
Token(name="whitespace", value=" ", code=css, path="", location=(1, 3)),
Token(name="variable_value", value="2", code=css, path="", location=(1, 4)),
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='variable_name', value='$y:', path='', code=css, location=(1, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(1, 3), referenced_by=None),
Token(name='number', value='2', path='', code=css, location=(1, 4), referenced_by=None),
]
def test_variable_declaration_invalid_value():
css = "$x:(@$12x)"
with pytest.raises(TokenizeError):
list(tokenize(css, ""))
def test_variables_declarations_amongst_rulesets():
css = "$x:1; .thing{text:red;} $y:2;"
assert list(tokenize(css, "")) == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0)),
Token(name='variable_value', value='1', path='', code=css, location=(0, 3)),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 4)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5)),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(0, 6)),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 12)),
Token(name='declaration_name', value='text:', path='', code=css, location=(0, 13)),
Token(name='token', value='red', path='', code=css, location=(0, 18)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 21)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 22)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 23)),
Token(name='variable_name', value='$y:', path='', code=css, location=(0, 24)),
Token(name='variable_value', value='2', path='', code=css, location=(0, 27)),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 28)),
tokens = list(tokenize(css, ""))
assert tokens == [
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='number', value='1', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='selector_start_class', value='.thing', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 12), referenced_by=None),
Token(name='declaration_name', value='text:', path='', code=css, location=(0, 13), referenced_by=None),
Token(name='token', value='red', path='', code=css, location=(0, 18), referenced_by=None),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 21), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 22), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 23), referenced_by=None),
Token(name='variable_name', value='$y:', path='', code=css, location=(0, 24), referenced_by=None),
Token(name='number', value='2', path='', code=css, location=(0, 27), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 28), referenced_by=None),
]
def test_variables_reference_in_rule_declaration_value():
css = ".warn{text: $warning;}"
assert list(tokenize(css, "")) == [
Token(name='selector_start_class', value='.warn', path='', code=css, location=(0, 0)),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 5)),
Token(name='declaration_name', value='text:', path='', code=css, location=(0, 6)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 11)),
Token(name='variable_ref', value='$warning', path='', code=css, location=(0, 12)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 20)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 21)),
Token(name='selector_start_class', value='.warn', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='declaration_name', value='text:', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 11), referenced_by=None),
Token(name='variable_ref', value='$warning', path='', code=css, location=(0, 12), referenced_by=None),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 20), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 21), referenced_by=None),
]
def test_variables_reference_in_rule_declaration_value_multiple():
css = ".card{padding: $pad-y $pad-x;}"
assert list(tokenize(css, "")) == [
Token(name='selector_start_class', value='.card', path='', code=css, location=(0, 0)),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 5)),
Token(name='declaration_name', value='padding:', path='', code=css, location=(0, 6)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 14)),
Token(name='variable_ref', value='$pad-y', path='', code=css, location=(0, 15)),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 21)),
Token(name='variable_ref', value='$pad-x', path='', code=css, location=(0, 22)),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 28)),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 29)),
Token(name='selector_start_class', value='.card', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='declaration_set_start', value='{', path='', code=css, location=(0, 5), referenced_by=None),
Token(name='declaration_name', value='padding:', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 14), referenced_by=None),
Token(name='variable_ref', value='$pad-y', path='', code=css, location=(0, 15), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 21), referenced_by=None),
Token(name='variable_ref', value='$pad-x', path='', code=css, location=(0, 22), referenced_by=None),
Token(name='declaration_end', value=';', path='', code=css, location=(0, 28), referenced_by=None),
Token(name='declaration_set_end', value='}', path='', code=css, location=(0, 29), referenced_by=None)
]
def test_variables_reference_in_variable_declaration():
css = "$x: $y;"
assert list(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='variable_value', value='$y', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 6)),
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='variable_ref', value='$y', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='variable_value_end', value=';', path='', code=css, location=(0, 6), referenced_by=None)
]
def test_variable_references_in_variable_declaration_multiple():
css = "$x: $y $z\n"
assert list(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='variable_value', value='$y $z', path='', code=css, location=(0, 4)),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 10)),
Token(name='variable_name', value='$x:', path='', code=css, location=(0, 0), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 3), referenced_by=None),
Token(name='variable_ref', value='$y', path='', code=css, location=(0, 4), referenced_by=None),
Token(name='whitespace', value=' ', path='', code=css, location=(0, 6), referenced_by=None),
Token(name='variable_ref', value='$z', path='', code=css, location=(0, 8), referenced_by=None),
Token(name='variable_value_end', value='\n', path='', code=css, location=(0, 10), referenced_by=None)
]