ai-station/.venv/lib/python3.12/site-packages/textual/css/_styles_builder.py

1298 lines
48 KiB
Python

from __future__ import annotations
from typing import Iterable, NoReturn, cast
import rich.repr
from textual._border import BorderValue, normalize_border_value
from textual._cells import cell_len
from textual._duration import _duration_as_seconds
from textual._easing import EASING
from textual.color import TRANSPARENT, Color, ColorParseError
from textual.css._error_tools import friendly_list
from textual.css._help_renderables import HelpText
from textual.css._help_text import (
align_help_text,
border_property_help_text,
color_property_help_text,
dock_property_help_text,
expand_help_text,
fractional_property_help_text,
integer_help_text,
keyline_help_text,
layout_property_help_text,
offset_property_help_text,
offset_single_axis_help_text,
position_help_text,
property_invalid_value_help_text,
scalar_help_text,
scrollbar_size_property_help_text,
scrollbar_size_single_axis_help_text,
spacing_invalid_value_help_text,
spacing_wrong_number_of_values_help_text,
split_property_help_text,
string_enum_help_text,
style_flags_property_help_text,
table_rows_or_columns_help_text,
text_align_help_text,
)
from textual.css.constants import (
HATCHES,
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_BORDER,
VALID_BOX_SIZING,
VALID_CONSTRAIN,
VALID_DISPLAY,
VALID_EDGE,
VALID_EXPAND,
VALID_HATCH,
VALID_KEYLINE,
VALID_OVERFLOW,
VALID_OVERLAY,
VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_SCROLLBAR_VISIBILITY,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
VALID_TEXT_OVERFLOW,
VALID_TEXT_WRAP,
VALID_VISIBILITY,
)
from textual.css.errors import DeclarationError, StyleValueError
from textual.css.model import Declaration
from textual.css.scalar import (
Scalar,
ScalarError,
ScalarOffset,
ScalarParseError,
Unit,
percentage_string_to_float,
)
from textual.css.styles import Styles
from textual.css.tokenize import Token
from textual.css.transition import Transition
from textual.css.types import (
BoxSizing,
Display,
EdgeType,
Overflow,
ScrollbarVisibility,
TextOverflow,
TextWrap,
Visibility,
)
from textual.geometry import Spacing, SpacingDimensions, clamp
from textual.suggestions import get_suggestion
class StylesBuilder:
"""
The StylesBuilder object takes tokens parsed from the CSS and converts
to the appropriate internal types.
"""
def __init__(self) -> None:
self.styles = Styles()
def __rich_repr__(self) -> rich.repr.Result:
yield "styles", self.styles
def __repr__(self) -> str:
return "StylesBuilder()"
def error(self, name: str, token: Token, message: str | HelpText) -> NoReturn:
raise DeclarationError(name, token, message)
def add_declaration(self, declaration: Declaration) -> None:
if not declaration.name:
return
rule_name = declaration.name.replace("-", "_")
if not declaration.tokens:
self.error(
rule_name,
declaration.token,
f"Missing property value for '{declaration.name}:'",
)
process_method = getattr(self, f"process_{rule_name}", None)
if process_method is None:
suggested_property_name = self._get_suggested_property_name_for_rule(
declaration.name
)
self.error(
declaration.name,
declaration.token,
property_invalid_value_help_text(
declaration.name,
"css",
suggested_property_name=suggested_property_name,
),
)
tokens = declaration.tokens
important = tokens[-1].name == "important"
if important:
tokens = tokens[:-1]
self.styles.important.add(rule_name)
# Check for special token(s)
if tokens[0].name == "token":
value = tokens[0].value
if value == "initial":
self.styles._rules[rule_name] = None
return
try:
process_method(declaration.name, tokens)
except DeclarationError:
raise
except Exception as error:
self.error(declaration.name, declaration.token, str(error))
def _process_enum_multiple(
self, name: str, tokens: list[Token], valid_values: set[str], count: int
) -> tuple[str, ...]:
"""Generic code to process a declaration with two enumerations, like overflow: auto auto"""
if len(tokens) > count or not tokens:
self.error(name, tokens[0], f"expected 1 to {count} tokens here")
results: list[str] = []
append = results.append
for token in tokens:
token_name, value, _, _, location, _ = token
if token_name != "token":
self.error(
name,
token,
f"invalid token {value!r}; expected {friendly_list(valid_values)}",
)
append(value)
short_results = results[:]
while len(results) < count:
results.extend(short_results)
results = results[:count]
return tuple(results)
def _process_enum(
self, name: str, tokens: list[Token], valid_values: set[str]
) -> str:
"""Process a declaration that expects an enum.
Args:
name: Name of declaration.
tokens: Tokens from parser.
valid_values: A set of valid values.
Returns:
True if the value is valid or False if it is invalid (also generates an error)
"""
if len(tokens) != 1:
self.error(
name,
tokens[0],
string_enum_help_text(
name, valid_values=list(valid_values), context="css"
),
)
token = tokens[0]
token_name, value, _, _, location, _ = token
if token_name != "token":
self.error(
name,
token,
string_enum_help_text(
name, valid_values=list(valid_values), context="css"
),
)
if value not in valid_values:
self.error(
name,
token,
string_enum_help_text(
name, valid_values=list(valid_values), context="css"
),
)
return value
def process_display(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_DISPLAY:
self.styles._rules["display"] = cast(Display, value)
else:
self.error(
name,
token,
string_enum_help_text(
"display", valid_values=list(VALID_DISPLAY), context="css"
),
)
else:
self.error(
name,
token,
string_enum_help_text(
"display", valid_values=list(VALID_DISPLAY), context="css"
),
)
def _process_scalar(self, name: str, tokens: list[Token]) -> None:
def scalar_error():
self.error(
name, tokens[0], scalar_help_text(property_name=name, context="css")
)
if not tokens:
return
if len(tokens) == 1:
try:
self.styles._rules[name.replace("-", "_")] = Scalar.parse( # type: ignore
tokens[0].value
)
except ScalarParseError:
scalar_error()
else:
scalar_error()
def _distribute_importance(self, prefix: str, suffixes: tuple[str, ...]) -> None:
"""Distribute importance amongst all aspects of the given style.
Args:
prefix: The prefix of the style.
suffixes: The suffixes to distribute amongst.
A number of styles can be set with the 'prefix' of the style,
providing the values as a series of parameters; or they can be set
with specific suffixes. Think `border` vs `border-left`, etc. This
method is used to ensure that if the former is set, `!important` is
distributed amongst all the suffixes.
"""
if prefix in self.styles.important:
self.styles.important.remove(prefix)
self.styles.important.update(f"{prefix}_{suffix}" for suffix in suffixes)
def process_box_sizing(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_BOX_SIZING:
self.styles._rules["box_sizing"] = cast(BoxSizing, value)
else:
self.error(
name,
token,
string_enum_help_text(
"box-sizing",
valid_values=list(VALID_BOX_SIZING),
context="css",
),
)
else:
self.error(
name,
token,
string_enum_help_text(
"box-sizing", valid_values=list(VALID_BOX_SIZING), context="css"
),
)
def process_width(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_height(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_min_width(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_min_height(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_max_width(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_max_height(self, name: str, tokens: list[Token]) -> None:
self._process_scalar(name, tokens)
def process_overflow(self, name: str, tokens: list[Token]) -> None:
rules = self.styles._rules
overflow_x, overflow_y = self._process_enum_multiple(
name, tokens, VALID_OVERFLOW, 2
)
rules["overflow_x"] = cast(Overflow, overflow_x)
rules["overflow_y"] = cast(Overflow, overflow_y)
self._distribute_importance("overflow", ("x", "y"))
def process_overflow_x(self, name: str, tokens: list[Token]) -> None:
self.styles._rules["overflow_x"] = cast(
Overflow, self._process_enum(name, tokens, VALID_OVERFLOW)
)
def process_overflow_y(self, name: str, tokens: list[Token]) -> None:
self.styles._rules["overflow_y"] = cast(
Overflow, self._process_enum(name, tokens, VALID_OVERFLOW)
)
def process_visibility(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_VISIBILITY:
self.styles._rules["visibility"] = cast(Visibility, value)
else:
self.error(
name,
token,
string_enum_help_text(
"visibility",
valid_values=list(VALID_VISIBILITY),
context="css",
),
)
else:
string_enum_help_text(
"visibility", valid_values=list(VALID_VISIBILITY), context="css"
)
def process_text_wrap(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_TEXT_WRAP:
self.styles._rules["text_wrap"] = cast(TextWrap, value)
else:
self.error(
name,
token,
string_enum_help_text(
"text-wrap",
valid_values=list(VALID_TEXT_WRAP),
context="css",
),
)
else:
string_enum_help_text(
"text-wrap", valid_values=list(VALID_TEXT_WRAP), context="css"
)
def process_text_overflow(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
name, value, _, _, location, _ = token
if name == "token":
value = value.lower()
if value in VALID_TEXT_OVERFLOW:
self.styles._rules["text_overflow"] = cast(TextOverflow, value)
else:
self.error(
name,
token,
string_enum_help_text(
"text-overflow",
valid_values=list(VALID_TEXT_OVERFLOW),
context="css",
),
)
else:
string_enum_help_text(
"text-overflow",
valid_values=list(VALID_TEXT_OVERFLOW),
context="css",
)
def _process_fractional(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
token = tokens[0]
error = False
if len(tokens) != 1:
error = True
else:
token_name = token.name
value = token.value
rule_name = name.replace("-", "_")
if token_name == "scalar" and value.endswith("%"):
try:
text_opacity = percentage_string_to_float(value)
self.styles.set_rule(rule_name, text_opacity)
except ValueError:
error = True
elif token_name == "number":
try:
text_opacity = clamp(float(value), 0, 1)
self.styles.set_rule(rule_name, text_opacity)
except ValueError:
error = True
else:
error = True
if error:
self.error(name, token, fractional_property_help_text(name, context="css"))
process_opacity = _process_fractional
process_text_opacity = _process_fractional
def _process_space(self, name: str, tokens: list[Token]) -> None:
space: list[int] = []
append = space.append
for token in tokens:
token_name, value, _, _, _, _ = token
if token_name == "number":
try:
append(int(value))
except ValueError:
self.error(
name,
token,
spacing_invalid_value_help_text(name, context="css"),
)
else:
self.error(
name, token, spacing_invalid_value_help_text(name, context="css")
)
if len(space) not in (1, 2, 4):
self.error(
name,
tokens[0],
spacing_wrong_number_of_values_help_text(
name, num_values_supplied=len(space), context="css"
),
)
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space))) # type: ignore
def _process_space_partial(self, name: str, tokens: list[Token]) -> None:
"""Process granular margin / padding declarations."""
if len(tokens) != 1:
self.error(
name, tokens[0], spacing_invalid_value_help_text(name, context="css")
)
_EDGE_SPACING_MAP = {"top": 0, "right": 1, "bottom": 2, "left": 3}
token = tokens[0]
token_name, value, _, _, _, _ = token
if token_name == "number":
space = int(value)
else:
self.error(
name, token, spacing_invalid_value_help_text(name, context="css")
)
style_name, _, edge = name.replace("-", "_").partition("_")
current_spacing = cast(
"tuple[int, int, int, int]",
self.styles._rules.get(style_name, (0, 0, 0, 0)),
)
spacing_list = list(current_spacing)
spacing_list[_EDGE_SPACING_MAP[edge]] = space
self.styles._rules[style_name] = Spacing(*spacing_list) # type: ignore
process_padding = _process_space
process_margin = _process_space
process_margin_top = _process_space_partial
process_margin_right = _process_space_partial
process_margin_bottom = _process_space_partial
process_margin_left = _process_space_partial
process_padding_top = _process_space_partial
process_padding_right = _process_space_partial
process_padding_bottom = _process_space_partial
process_padding_left = _process_space_partial
def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue:
border_type: EdgeType = "solid"
border_color = Color(0, 255, 0)
border_alpha: float | None = None
def border_value_error():
self.error(name, token, border_property_help_text(name, context="css"))
for token in tokens:
token_name, value, _, _, _, _ = token
if token_name == "token":
if value in VALID_BORDER:
border_type = value # type: ignore
else:
try:
border_color = Color.parse(value)
except ColorParseError:
border_value_error()
elif token_name == "color":
try:
border_color = Color.parse(value)
except ColorParseError:
border_value_error()
elif token_name == "scalar":
alpha_scalar = Scalar.parse(token.value)
if alpha_scalar.unit != Unit.PERCENT:
self.error(name, token, "alpha must be given as a percentage.")
border_alpha = alpha_scalar.value / 100.0
else:
border_value_error()
if border_alpha is not None:
border_color = border_color.multiply_alpha(border_alpha)
return normalize_border_value((border_type, border_color))
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border(name, tokens)
self.styles._rules[f"border_{edge}"] = border # type: ignore
def process_border(self, name: str, tokens: list[Token]) -> None:
border = self._parse_border(name, tokens)
rules = self.styles._rules
rules["border_top"] = rules["border_right"] = border
rules["border_bottom"] = rules["border_left"] = border
self._distribute_importance("border", ("top", "left", "bottom", "right"))
def process_border_top(self, name: str, tokens: list[Token]) -> None:
self._process_border_edge("top", name, tokens)
def process_border_right(self, name: str, tokens: list[Token]) -> None:
self._process_border_edge("right", name, tokens)
def process_border_bottom(self, name: str, tokens: list[Token]) -> None:
self._process_border_edge("bottom", name, tokens)
def process_border_left(self, name: str, tokens: list[Token]) -> None:
self._process_border_edge("left", name, tokens)
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
border = self._parse_border(name, tokens)
self.styles._rules[f"outline_{edge}"] = border # type: ignore
def process_outline(self, name: str, tokens: list[Token]) -> None:
border = self._parse_border(name, tokens)
rules = self.styles._rules
rules["outline_top"] = rules["outline_right"] = border
rules["outline_bottom"] = rules["outline_left"] = border
self._distribute_importance("outline", ("top", "left", "bottom", "right"))
def process_outline_top(self, name: str, tokens: list[Token]) -> None:
self._process_outline("top", name, tokens)
def process_outline_right(self, name: str, tokens: list[Token]) -> None:
self._process_outline("right", name, tokens)
def process_outline_bottom(self, name: str, tokens: list[Token]) -> None:
self._process_outline("bottom", name, tokens)
def process_outline_left(self, name: str, tokens: list[Token]) -> None:
self._process_outline("left", name, tokens)
def process_keyline(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) > 3:
self.error(name, tokens[0], keyline_help_text())
keyline_style = "none"
keyline_color = Color.parse("green")
keyline_alpha = 1.0
for token in tokens:
if token.name == "color":
try:
keyline_color = Color.parse(token.value)
except Exception as error:
self.error(
name,
token,
color_property_help_text(
name, context="css", error=error, value=token.value
),
)
elif token.name == "token":
try:
keyline_color = Color.parse(token.value)
except Exception:
keyline_style = token.value
if keyline_style not in VALID_KEYLINE:
self.error(name, token, keyline_help_text())
elif token.name == "scalar":
alpha_scalar = Scalar.parse(token.value)
if alpha_scalar.unit != Unit.PERCENT:
self.error(name, token, "alpha must be given as a percentage.")
keyline_alpha = alpha_scalar.value / 100.0
self.styles._rules["keyline"] = (
keyline_style,
keyline_color.multiply_alpha(keyline_alpha),
)
def process_offset(self, name: str, tokens: list[Token]) -> None:
def offset_error(name: str, token: Token) -> None:
self.error(name, token, offset_property_help_text(context="css"))
if not tokens:
return
if len(tokens) != 2:
offset_error(name, tokens[0])
else:
token1, token2 = tokens
if token1.name not in ("scalar", "number"):
offset_error(name, token1)
if token2.name not in ("scalar", "number"):
offset_error(name, token2)
scalar_x = Scalar.parse(token1.value, Unit.WIDTH)
scalar_y = Scalar.parse(token2.value, Unit.HEIGHT)
self.styles._rules["offset"] = ScalarOffset(scalar_x, scalar_y)
def process_offset_x(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.name not in ("scalar", "number"):
self.error(name, token, offset_single_axis_help_text(name))
x = Scalar.parse(token.value, Unit.WIDTH)
y = self.styles.offset.y
self.styles._rules["offset"] = ScalarOffset(x, y)
def process_offset_y(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.name not in ("scalar", "number"):
self.error(name, token, offset_single_axis_help_text(name))
y = Scalar.parse(token.value, Unit.HEIGHT)
x = self.styles.offset.x
self.styles._rules["offset"] = ScalarOffset(x, y)
def process_position(self, name: str, tokens: list[Token]):
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.value not in VALID_POSITION:
self.error(name, tokens[0], position_help_text(name))
self.styles._rules["position"] = token.value
def process_layout(self, name: str, tokens: list[Token]) -> None:
from textual.layouts.factory import MissingLayout, get_layout
if tokens:
if len(tokens) != 1:
self.error(
name, tokens[0], layout_property_help_text(name, context="css")
)
else:
value = tokens[0].value
layout_name = value
try:
self.styles._rules["layout"] = get_layout(layout_name)
except MissingLayout:
self.error(
name,
tokens[0],
layout_property_help_text(name, context="css"),
)
def process_color(self, name: str, tokens: list[Token]) -> None:
"""Processes a simple color declaration."""
name = name.replace("-", "_")
color: Color | None = None
alpha: float | None = None
self.styles._rules[f"auto_{name}"] = False # type: ignore
for token in tokens:
if (
"background" not in name
and token.name == "token"
and token.value == "auto"
):
self.styles._rules[f"auto_{name}"] = True # type: ignore
elif token.name == "scalar":
alpha_scalar = Scalar.parse(token.value)
if alpha_scalar.unit != Unit.PERCENT:
self.error(name, token, "alpha must be given as a percentage.")
alpha = alpha_scalar.value / 100.0
elif token.name in ("color", "token"):
try:
color = Color.parse(token.value)
except Exception as error:
self.error(
name,
token,
color_property_help_text(
name, context="css", error=error, value=token.value
),
)
else:
self.error(
name,
token,
color_property_help_text(name, context="css", value=token.value),
)
if color is not None or alpha is not None:
if alpha is not None:
color = (color or Color(255, 255, 255)).multiply_alpha(alpha)
self.styles._rules[name] = color # type: ignore
process_tint = process_color
process_background = process_color
process_background_tint = process_color
process_scrollbar_color = process_color
process_scrollbar_color_hover = process_color
process_scrollbar_color_active = process_color
process_scrollbar_corner_color = process_color
process_scrollbar_background = process_color
process_scrollbar_background_hover = process_color
process_scrollbar_background_active = process_color
def process_scrollbar_visibility(self, name: str, tokens: list[Token]) -> None:
"""Process scrollbar visibility rules."""
self.styles._rules["scrollbar_visibility"] = cast(
ScrollbarVisibility,
self._process_enum(name, tokens, VALID_SCROLLBAR_VISIBILITY),
)
process_link_color = process_color
process_link_background = process_color
process_link_color_hover = process_color
process_link_background_hover = process_color
process_border_title_color = process_color
process_border_title_background = process_color
process_border_subtitle_color = process_color
process_border_subtitle_background = process_color
def process_text_style(self, name: str, tokens: list[Token]) -> None:
for token in tokens:
value = token.value
if value not in VALID_STYLE_FLAGS:
self.error(
name,
token,
style_flags_property_help_text(name, value, context="css"),
)
style_definition = " ".join(token.value for token in tokens)
self.styles._rules[name.replace("-", "_")] = style_definition # type: ignore
process_link_style = process_text_style
process_link_style_hover = process_text_style
process_border_title_style = process_text_style
process_border_subtitle_style = process_text_style
def process_text_align(self, name: str, tokens: list[Token]) -> None:
"""Process a text-align declaration"""
if not tokens:
return
if len(tokens) > 1 or tokens[0].value not in VALID_TEXT_ALIGN:
self.error(
name,
tokens[0],
text_align_help_text(),
)
self.styles._rules["text_align"] = tokens[0].value # type: ignore
def process_dock(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) > 1 or tokens[0].value not in VALID_EDGE:
self.error(
name,
tokens[0],
dock_property_help_text(name, context="css"),
)
dock_value = tokens[0].value
self.styles._rules["dock"] = dock_value
def process_split(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) > 1 or tokens[0].value not in VALID_EDGE:
self.error(
name,
tokens[0],
split_property_help_text(name, context="css"),
)
split_value = tokens[0].value
self.styles._rules["split"] = split_value
def process_layer(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1:
self.error(name, tokens[1], "unexpected tokens in dock-edge declaration")
self.styles._rules["layer"] = tokens[0].value
def process_layers(self, name: str, tokens: list[Token]) -> None:
layers: list[str] = []
for token in tokens:
if token.name not in {"token", "string"}:
self.error(name, token, f"{token.name} not expected here")
layers.append(token.value)
self.styles._rules["layers"] = tuple(layers)
def process_transition(self, name: str, tokens: list[Token]) -> None:
transitions: dict[str, Transition] = {}
def make_groups() -> Iterable[list[Token]]:
"""Batch tokens into comma-separated groups."""
group: list[Token] = []
for token in tokens:
if token.name == "comma":
if group:
yield group
group = []
else:
group.append(token)
if group:
yield group
valid_duration_token_names = ("duration", "number")
for tokens in make_groups():
css_property = ""
duration = 1.0
easing = "linear"
delay = 0.0
try:
iter_tokens = iter(tokens)
token = next(iter_tokens)
if token.name != "token":
self.error(name, token, "expected property")
css_property = token.value
token = next(iter_tokens)
if token.name not in valid_duration_token_names:
self.error(name, token, "expected duration or number")
try:
duration = _duration_as_seconds(token.value)
except ScalarError as error:
self.error(name, token, str(error))
token = next(iter_tokens)
if token.name != "token":
self.error(name, token, "easing function expected")
if token.value not in EASING:
self.error(
name,
token,
f"expected easing function; found {token.value!r}",
)
easing = token.value
token = next(iter_tokens)
if token.name not in valid_duration_token_names:
self.error(name, token, "expected duration or number")
try:
delay = _duration_as_seconds(token.value)
except ScalarError as error:
self.error(name, token, str(error))
except StopIteration:
pass
transitions[css_property] = Transition(duration, easing, delay)
self.styles._rules["transitions"] = transitions
def process_align(self, name: str, tokens: list[Token]) -> None:
def align_error(name, token):
self.error(name, token, align_help_text())
if len(tokens) != 2:
self.error(name, tokens[0], align_help_text())
token_horizontal = tokens[0]
token_vertical = tokens[1]
if token_horizontal.name != "token":
align_error(name, token_horizontal)
elif token_horizontal.value not in VALID_ALIGN_HORIZONTAL:
align_error(name, token_horizontal)
if token_vertical.name != "token":
align_error(name, token_vertical)
elif token_vertical.value not in VALID_ALIGN_VERTICAL:
align_error(name, token_horizontal)
name = name.replace("-", "_")
self.styles._rules[f"{name}_horizontal"] = token_horizontal.value # type: ignore
self.styles._rules[f"{name}_vertical"] = token_vertical.value # type: ignore
self._distribute_importance(name, ("horizontal", "vertical"))
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_ALIGN_HORIZONTAL, context="css"),
)
else:
self.styles._rules[name.replace("-", "_")] = value # type: ignore
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_ALIGN_VERTICAL)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_ALIGN_VERTICAL, context="css"),
)
else:
self.styles._rules[name.replace("-", "_")] = value # type: ignore
process_content_align = process_align
process_content_align_horizontal = process_align_horizontal
process_content_align_vertical = process_align_vertical
process_border_title_align = process_align_horizontal
process_border_subtitle_align = process_align_horizontal
def process_scrollbar_gutter(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_SCROLLBAR_GUTTER)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_SCROLLBAR_GUTTER, context="css"),
)
else:
self.styles._rules[name.replace("-", "_")] = value # type: ignore
def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None:
def scrollbar_size_error(name: str, token: Token) -> None:
self.error(name, token, scrollbar_size_property_help_text(context="css"))
if not tokens:
return
if len(tokens) != 2:
scrollbar_size_error(name, tokens[0])
else:
token1, token2 = tokens
if token1.name != "number" or not token1.value.isdigit():
scrollbar_size_error(name, token1)
if token2.name != "number" or not token2.value.isdigit():
scrollbar_size_error(name, token2)
horizontal = int(token1.value)
vertical = int(token2.value)
self.styles._rules["scrollbar_size_horizontal"] = horizontal
self.styles._rules["scrollbar_size_vertical"] = vertical
self._distribute_importance("scrollbar_size", ("horizontal", "vertical"))
def process_scrollbar_size_vertical(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], scrollbar_size_single_axis_help_text(name))
else:
token = tokens[0]
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
self.styles._rules["scrollbar_size_vertical"] = value
def process_scrollbar_size_horizontal(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], scrollbar_size_single_axis_help_text(name))
else:
token = tokens[0]
if token.name != "number" or not token.value.isdigit():
self.error(name, token, scrollbar_size_single_axis_help_text(name))
value = int(token.value)
self.styles._rules["scrollbar_size_horizontal"] = value
def _process_grid_rows_or_columns(self, name: str, tokens: list[Token]) -> None:
scalars: list[Scalar] = []
percent_unit = Unit.WIDTH if name == "grid-columns" else Unit.HEIGHT
for token in tokens:
if token.name == "number":
scalars.append(Scalar.from_number(float(token.value)))
elif token.name == "scalar":
scalars.append(Scalar.parse(token.value, percent_unit=percent_unit))
elif token.name == "token" and token.value == "auto":
scalars.append(Scalar.parse("auto"))
else:
self.error(
name,
token,
table_rows_or_columns_help_text(name, token.value, context="css"),
)
self.styles._rules[name.replace("-", "_")] = scalars # type: ignore
process_grid_rows = _process_grid_rows_or_columns
process_grid_columns = _process_grid_rows_or_columns
def _process_integer(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], integer_help_text(name))
else:
token = tokens[0]
if token.name != "number" or not token.value.isdigit():
self.error(name, token, integer_help_text(name))
value = int(token.value)
if value == 0:
self.error(name, token, integer_help_text(name))
self.styles._rules[name.replace("-", "_")] = value # type: ignore
process_grid_gutter_horizontal = _process_integer
process_grid_gutter_vertical = _process_integer
process_column_span = _process_integer
process_row_span = _process_integer
process_grid_size_columns = _process_integer
process_grid_size_rows = _process_integer
process_line_pad = _process_integer
def process_grid_gutter(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) == 1:
token = tokens[0]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_gutter_horizontal"] = value
self.styles._rules["grid_gutter_vertical"] = value
elif len(tokens) == 2:
token = tokens[0]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_gutter_horizontal"] = value
token = tokens[1]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_gutter_vertical"] = value
else:
self.error(name, tokens[0], "expected two integers here")
def process_grid_size(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
if len(tokens) == 1:
token = tokens[0]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_size_columns"] = value
self.styles._rules["grid_size_rows"] = 0
elif len(tokens) == 2:
token = tokens[0]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_size_columns"] = value
token = tokens[1]
if token.name != "number":
self.error(name, token, integer_help_text(name))
value = max(0, int(token.value))
self.styles._rules["grid_size_rows"] = value
else:
self.error(name, tokens[0], "expected two integers here")
def process_overlay(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_OVERLAY)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_OVERLAY, context="css"),
)
else:
self.styles._rules[name] = value # type: ignore
def process_constrain(self, name: str, tokens: list[Token]) -> None:
if len(tokens) == 1:
try:
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_CONSTRAIN, context="css"),
)
else:
self.styles._rules["constrain_x"] = value # type: ignore
self.styles._rules["constrain_y"] = value # type: ignore
elif len(tokens) == 2:
constrain_x, constrain_y = self._process_enum_multiple(
name, tokens, VALID_CONSTRAIN, 2
)
self.styles._rules["constrain_x"] = constrain_x # type: ignore
self.styles._rules["constrain_y"] = constrain_y # type: ignore
else:
self.error(name, tokens[0], "one or two values expected here")
def process_constrain_x(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_CONSTRAIN, context="css"),
)
else:
self.styles._rules[name] = value # type: ignore
def process_constrain_y(self, name: str, tokens: list[Token]) -> None:
try:
value = self._process_enum(name, tokens, VALID_CONSTRAIN)
except StyleValueError:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_CONSTRAIN, context="css"),
)
else:
self.styles._rules[name] = value # type: ignore
def process_hatch(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
character: str | None = None
color = TRANSPARENT
opacity = 1.0
if len(tokens) == 1 and tokens[0].value == "none":
self.styles._rules[name] = "none"
return
if len(tokens) not in (2, 3):
self.error(name, tokens[0], "2 or 3 values expected here")
character_token, color_token, *opacity_tokens = tokens
if character_token.name == "token":
if character_token.value not in VALID_HATCH:
self.error(
name,
tokens[0],
string_enum_help_text(name, VALID_HATCH, context="css"),
)
character = HATCHES[character_token.value]
elif character_token.name == "string":
character = character_token.value[1:-1]
if len(character) != 1:
self.error(
name,
character_token,
f"Hatch type requires a string of length 1; got {character_token.value}",
)
if cell_len(character) != 1:
self.error(
name,
character_token,
f"Hatch type requires a string with a *cell length* of 1; got {character_token.value}",
)
if color_token.name in ("color", "token"):
try:
color = Color.parse(color_token.value)
except Exception as error:
self.error(
name,
color_token,
color_property_help_text(
name, context="css", error=error, value=color_token.value
),
)
else:
self.error(
name, color_token, f"Expected a color; found {color_token.value!r}"
)
if opacity_tokens:
opacity_token = opacity_tokens[0]
if opacity_token.name == "scalar":
opacity_scalar = opacity = Scalar.parse(opacity_token.value)
if opacity_scalar.unit != Unit.PERCENT:
self.error(
name,
opacity_token,
"hatch alpha must be given as a percentage.",
)
opacity = clamp(opacity_scalar.value / 100.0, 0, 1.0)
else:
self.error(
name,
opacity_token,
f"expected a percentage here; found {opacity_token.value!r}",
)
self.styles._rules[name] = (character or " ", color.multiply_alpha(opacity))
def process_expand(self, name: str, tokens: list[Token]):
if not tokens:
return
if len(tokens) != 1:
self.error(name, tokens[0], offset_single_axis_help_text(name))
else:
token = tokens[0]
if token.value not in VALID_EXPAND:
self.error(name, tokens[0], expand_help_text(name))
self.styles._rules["expand"] = token.value
def _get_suggested_property_name_for_rule(self, rule_name: str) -> str | None:
"""
Returns a valid CSS property "Python" name, or None if no close matches could be found.
Args:
rule_name: An invalid "Python-ised" CSS property (i.e. "offst_x" rather than "offst-x")
Returns:
The closest valid "Python-ised" CSS property.
Returns `None` if no close matches could be found.
Example: returns "background" for rule_name "bkgrund", "offset_x" for "ofset_x"
"""
processable_rules_name = [
attr[8:] for attr in dir(self) if attr.startswith("process_")
]
return get_suggestion(rule_name, processable_rules_name)