Files
stiftung-management-system/app/.venv/Lib/site-packages/weasyprint/css/validation/expanders.py
2025-09-06 18:31:54 +02:00

1039 lines
36 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Validate properties expanders."""
import functools
from tinycss2.ast import DimensionToken, IdentToken, NumberToken
from tinycss2.color3 import parse_color
from ..properties import INITIAL_VALUES
from .descriptors import expand_font_variant
from ..utils import ( # isort:skip
InvalidValues, Pending, check_var_function, get_keyword, get_single_keyword,
split_on_comma)
from .properties import ( # isort:skip
background_attachment, background_image, background_position, background_repeat,
background_size, block_ellipsis, border_image_source, border_image_slice,
border_image_width, border_image_outset, border_image_repeat, border_style,
border_width, box, column_count, column_width, flex_basis, flex_direction,
flex_grow_shrink, flex_wrap, font_family, font_size, font_stretch,
font_style, font_weight, gap, grid_line, grid_template, line_height,
list_style_image, list_style_position, list_style_type, other_colors,
overflow_wrap, validate_non_shorthand)
EXPANDERS = {}
class PendingExpander(Pending):
"""Expander with validation done when defining calculated values."""
def __init__(self, tokens, validator):
super().__init__(tokens, validator.keywords['name'])
self.validator = validator
def validate(self, tokens, wanted_key):
for key, value in self.validator(tokens):
if key.startswith('-'):
key = f'{self.validator.keywords["name"]}{key}'
if key == wanted_key:
return value
raise KeyError
def _find_var(tokens, expander, expanded_names):
"""Return pending expanders when var is found in tokens."""
for token in tokens:
if check_var_function(token):
# Found CSS variable, keep pending-substitution values.
pending = PendingExpander(tokens, expander)
return {name: pending for name in expanded_names}
def expander(property_name):
"""Decorator adding a function to the ``EXPANDERS``."""
def expander_decorator(function):
"""Add ``function`` to the ``EXPANDERS``."""
assert property_name not in EXPANDERS, property_name
EXPANDERS[property_name] = function
return function
return expander_decorator
def generic_expander(*expanded_names, **kwargs):
"""Decorator helping expanders to handle ``inherit`` and ``initial``.
Wrap an expander so that it does not have to handle the 'inherit' and
'initial' cases, and can just yield name suffixes. Missing suffixes
get the initial value.
"""
wants_base_url = kwargs.pop('wants_base_url', False)
assert not kwargs
def generic_expander_decorator(wrapped):
"""Decorate the ``wrapped`` expander."""
@functools.wraps(wrapped)
def generic_expander_wrapper(tokens, name, base_url):
"""Wrap the expander."""
expander = functools.partial(
generic_expander_wrapper, name=name, base_url=base_url)
skip_validation = False
keyword = get_single_keyword(tokens)
if keyword in ('inherit', 'initial'):
results = {name: keyword for name in expanded_names}
skip_validation = True
else:
results = _find_var(tokens, expander, expanded_names)
if results:
skip_validation = True
if not skip_validation:
results = {}
if wants_base_url:
result = wrapped(tokens, name, base_url)
else:
result = wrapped(tokens, name)
for new_name, new_token in result:
assert new_name in expanded_names, new_name
if new_name in results:
raise InvalidValues(
f'got multiple {new_name.strip("-")} values '
f'in a {name} shorthand')
results[new_name] = new_token
for new_name in expanded_names:
if new_name.startswith('-'):
# new_name is a suffix
actual_new_name = f'{name}{new_name}'
else:
actual_new_name = new_name
if new_name in results:
value = results[new_name]
if not skip_validation:
# validate_non_shorthand returns ((name, value),)
(actual_new_name, value), = validate_non_shorthand(
value, actual_new_name, base_url, required=True)
else:
value = 'initial'
yield actual_new_name, value
return generic_expander_wrapper
return generic_expander_decorator
@expander('border-color')
@expander('border-style')
@expander('border-width')
@expander('margin')
@expander('padding')
@expander('bleed')
def expand_four_sides(tokens, name, base_url):
"""Expand properties setting a token for the four sides of a box."""
# Define expanded names.
expanded_names = []
for suffix in ('-top', '-right', '-bottom', '-left'):
if (i := name.rfind('-')) == -1:
expanded_names.append(f'{name}{suffix}')
else:
# eg. border-color becomes border-*-color, not border-color-*
expanded_names.append(f'{name[:i]}{suffix}{name[i:]}')
# Return pending expanders if var is found.
expander = functools.partial(
expand_four_sides, name=name, base_url=base_url)
if result := _find_var(tokens, expander, expanded_names):
yield from result.items()
return
# Make sure we have 4 tokens.
if len(tokens) == 1:
tokens *= 4
elif len(tokens) == 2:
tokens *= 2 # (bottom, left) defaults to (top, right)
elif len(tokens) == 3:
tokens += (tokens[1],) # left defaults to right
elif len(tokens) != 4:
raise InvalidValues(
f'Expected 1 to 4 token components got {len(tokens)}')
for expanded_name, token in zip(expanded_names, tokens):
# validate_non_shorthand returns ((name, value),), we want
# to yield (name, value).
result, = validate_non_shorthand(
[token], expanded_name, base_url, required=True)
yield result
@expander('border-radius')
@generic_expander(
'border-top-left-radius', 'border-top-right-radius',
'border-bottom-right-radius', 'border-bottom-left-radius',
wants_base_url=True)
def border_radius(tokens, name, base_url):
"""Validator for the ``border-radius`` property."""
current = horizontal = []
vertical = []
for token in tokens:
if token.type == 'literal' and token.value == '/':
if current is horizontal:
if token == tokens[-1]:
raise InvalidValues('Expected value after "/" separator')
else:
current = vertical
else:
raise InvalidValues('Expected only one "/" separator')
else:
current.append(token)
if not vertical:
vertical = horizontal[:]
for values in horizontal, vertical:
# Make sure we have 4 tokens
if len(values) == 1:
values *= 4
elif len(values) == 2:
values *= 2 # (br, bl) defaults to (tl, tr)
elif len(values) == 3:
values.append(values[1]) # bl defaults to tr
elif len(values) != 4:
raise InvalidValues(
f'Expected 1 to 4 token components got {len(values)}')
corners = ('top-left', 'top-right', 'bottom-right', 'bottom-left')
for corner, tokens in zip(corners, zip(horizontal, vertical)):
name = f'border-{corner}-radius'
validate_non_shorthand(tokens, name, base_url, required=True)
yield name, tokens
@expander('list-style')
@generic_expander('-type', '-position', '-image', wants_base_url=True)
def expand_list_style(tokens, name, base_url):
"""Expand the ``list-style`` shorthand property.
See https://www.w3.org/TR/CSS21/generate.html#propdef-list-style
"""
type_specified = image_specified = False
none_count = 0
for token in tokens:
if get_keyword(token) == 'none':
# Can be either -style or -image, see at the end which is not
# otherwise specified.
none_count += 1
none_token = token
continue
if list_style_image([token], base_url) is not None:
suffix = '-image'
image_specified = True
elif list_style_position([token]) is not None:
suffix = '-position'
elif list_style_type([token]) is not None:
suffix = '-type'
type_specified = True
else:
raise InvalidValues
yield suffix, [token]
if not type_specified and none_count:
yield '-type', [none_token]
none_count -= 1
if not image_specified and none_count:
yield '-image', [none_token]
none_count -= 1
if none_count:
# Too many none tokens.
raise InvalidValues
@expander('border')
def expand_border(tokens, name, base_url):
"""Expand the ``border`` shorthand property.
See https://www.w3.org/TR/CSS21/box.html#propdef-border
"""
for suffix in ('-top', '-right', '-bottom', '-left'):
for new_prop in expand_border_side(tokens, name + suffix, base_url):
yield new_prop
@expander('border-top')
@expander('border-right')
@expander('border-bottom')
@expander('border-left')
@expander('column-rule')
@expander('outline')
@generic_expander('-width', '-color', '-style')
def expand_border_side(tokens, name):
"""Expand the ``border-*`` shorthand properties.
See https://www.w3.org/TR/CSS21/box.html#propdef-border-top
"""
for token in tokens:
if parse_color(token) is not None:
suffix = '-color'
elif border_width([token]) is not None:
suffix = '-width'
elif border_style([token]) is not None:
suffix = '-style'
else:
raise InvalidValues
yield suffix, [token]
@expander('border-image')
@generic_expander('-outset', '-repeat', '-slice', '-source', '-width',
wants_base_url=True)
def expand_border_image(tokens, name, base_url):
"""Expand the ``border-image-*`` shorthand properties.
See https://drafts.csswg.org/css-backgrounds/#the-border-image
"""
tokens = list(tokens)
while tokens:
if border_image_source(tokens[:1], base_url):
yield '-source', [tokens.pop(0)]
elif border_image_repeat(tokens[:1]):
repeats = [tokens.pop(0)]
while tokens and border_image_repeat(tokens[:1]):
repeats.append(tokens.pop(0))
yield '-repeat', repeats
elif border_image_slice(tokens[:1]) or get_keyword(tokens[0]) == 'fill':
slices = [tokens.pop(0)]
while tokens and border_image_slice(slices + tokens[:1]):
slices.append(tokens.pop(0))
yield '-slice', slices
if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':
# slices / *
tokens.pop(0)
else:
# slices other
continue
if not tokens:
# slices /
raise InvalidValues
if border_image_width(tokens[:1]):
widths = [tokens.pop(0)]
while tokens and border_image_width(widths + tokens[:1]):
widths.append(tokens.pop(0))
yield '-width', widths
if tokens and tokens[0].type == 'literal' and tokens[0].value == '/':
# slices / widths / slash *
tokens.pop(0)
else:
# slices / widths other
continue
elif tokens and tokens[0].type == 'literal' and tokens[0].value == '/':
# slices / / *
tokens.pop(0)
else:
# slices / other
raise InvalidValues
if not tokens:
# slices / * /
raise InvalidValues
if border_image_outset(tokens[:1]):
outsets = [tokens.pop(0)]
while tokens and border_image_outset(outsets + tokens[:1]):
outsets.append(tokens.pop(0))
yield '-outset', outsets
else:
# slash / * / other
raise InvalidValues
else:
raise InvalidValues
@expander('background')
def expand_background(tokens, name, base_url):
"""Expand the ``background`` shorthand property.
See https://drafts.csswg.org/css-backgrounds-3/#the-background
"""
expanded_names = (
'background-color', 'background-image', 'background-repeat',
'background-attachment', 'background-position', 'background-size',
'background-clip', 'background-origin')
keyword = get_single_keyword(tokens)
if keyword in ('initial', 'inherit'):
for name in expanded_names:
yield name, keyword
return
expander = functools.partial(
expand_background, name=name, base_url=base_url)
if result := _find_var(tokens, expander, expanded_names):
yield from result.items()
return
def parse_layer(tokens, final_layer=False):
results = {}
def add(name, value):
if value is None:
return False
name = f'background-{name}'
if name in results:
raise InvalidValues
results[name] = value
return True
# Make `tokens` a stack
tokens = tokens[::-1]
while tokens:
if add('repeat',
background_repeat.single_value(tokens[-2:][::-1])):
del tokens[-2:]
continue
token = tokens[-1:]
if final_layer and add('color', other_colors(token)):
tokens.pop()
continue
if add('image', background_image.single_value(token, base_url)):
tokens.pop()
continue
if add('repeat', background_repeat.single_value(token)):
tokens.pop()
continue
if add('attachment', background_attachment.single_value(token)):
tokens.pop()
continue
for n in (4, 3, 2, 1)[-len(tokens):]:
n_tokens = tokens[-n:][::-1]
position = background_position.single_value(n_tokens)
if position is not None:
assert add('position', position)
del tokens[-n:]
if (tokens and tokens[-1].type == 'literal' and
tokens[-1].value == '/'):
for n in (3, 2)[-len(tokens):]:
# n includes the '/' delimiter.
n_tokens = tokens[-n:-1][::-1]
size = background_size.single_value(n_tokens)
if size is not None:
assert add('size', size)
del tokens[-n:]
break
if position is not None:
continue
if add('origin', box.single_value(token)):
tokens.pop()
next_token = tokens[-1:]
if add('clip', box.single_value(next_token)):
tokens.pop()
else:
# The same keyword sets both
add('clip', box.single_value(token))
continue
raise InvalidValues
color = results.pop(
'background-color', INITIAL_VALUES['background_color'])
for name in expanded_names:
if name not in results and name != 'background-color':
results[name] = INITIAL_VALUES[name.replace('-', '_')][0]
return color, results
layers = reversed(split_on_comma(tokens))
color, last_layer = parse_layer(next(layers), final_layer=True)
results = {key: [value] for key, value in last_layer.items()}
for tokens in layers:
_, layer = parse_layer(tokens)
for name, value in layer.items():
results[name].append(value)
for name, values in results.items():
yield name, values[::-1] # "Un-reverse"
yield 'background-color', color
@expander('text-decoration')
@generic_expander('-line', '-color', '-style')
def expand_text_decoration(tokens, name):
"""Expand the ``text-decoration`` shorthand property."""
text_decoration_line = []
text_decoration_color = []
text_decoration_style = []
none_in_line = False
for token in tokens:
keyword = get_keyword(token)
if keyword in (
'none', 'underline', 'overline', 'line-through', 'blink'):
text_decoration_line.append(token)
if none_in_line:
raise InvalidValues
elif keyword == 'none':
none_in_line = True
elif keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
if text_decoration_style:
raise InvalidValues
else:
text_decoration_style.append(token)
else:
color = parse_color(token)
if color is None:
raise InvalidValues
elif text_decoration_color:
raise InvalidValues
else:
text_decoration_color.append(token)
if text_decoration_line:
yield '-line', text_decoration_line
if text_decoration_color:
yield '-color', text_decoration_color
if text_decoration_style:
yield '-style', text_decoration_style
def expand_page_break_before_after(tokens, name):
"""Expand legacy ``page-break-before`` and ``page-break-after`` properties.
See https://www.w3.org/TR/css-break-3/#page-break-properties
"""
keyword = get_single_keyword(tokens)
new_name = name.split('-', 1)[1]
if keyword in ('auto', 'left', 'right', 'avoid'):
yield new_name, tokens
elif keyword == 'always':
token = IdentToken(
tokens[0].source_line, tokens[0].source_column, 'page')
yield new_name, [token]
else:
raise InvalidValues
@expander('page-break-after')
@generic_expander('break-after')
def expand_page_break_after(tokens, name):
"""Expand legacy ``page-break-after`` property.
See https://www.w3.org/TR/css-break-3/#page-break-properties
"""
return expand_page_break_before_after(tokens, name)
@expander('page-break-before')
@generic_expander('break-before')
def expand_page_break_before(tokens, name):
"""Expand legacy ``page-break-before`` property.
See https://www.w3.org/TR/css-break-3/#page-break-properties
"""
return expand_page_break_before_after(tokens, name)
@expander('page-break-inside')
@generic_expander('break-inside')
def expand_page_break_inside(tokens, name):
"""Expand the legacy ``page-break-inside`` property.
See https://www.w3.org/TR/css-break-3/#page-break-properties
"""
keyword = get_single_keyword(tokens)
if keyword in ('auto', 'avoid'):
yield 'break-inside', tokens
else:
raise InvalidValues
@expander('columns')
@generic_expander('column-width', 'column-count')
def expand_columns(tokens, name):
"""Expand the ``columns`` shorthand property."""
name = None
if len(tokens) == 2 and get_keyword(tokens[0]) == 'auto':
tokens = tokens[::-1]
for token in tokens:
if column_width([token]) is not None and name != 'column-width':
name = 'column-width'
elif column_count([token]) is not None:
name = 'column-count'
else:
raise InvalidValues
yield name, [token]
if len(tokens) == 1:
name = 'column-width' if name == 'column-count' else 'column-count'
token = IdentToken(
tokens[0].source_line, tokens[0].source_column, 'auto')
yield name, [token]
@expander('font-variant')
@generic_expander('-alternates', '-caps', '-east-asian', '-ligatures',
'-numeric', '-position')
def font_variant(tokens, name):
"""Expand the ``font-variant`` shorthand property.
https://www.w3.org/TR/css-fonts-3/#font-variant-prop
"""
return expand_font_variant(tokens)
@expander('font')
@generic_expander('-style', '-variant-caps', '-weight', '-stretch', '-size',
'line-height', '-family') # line-height is not a suffix
def expand_font(tokens, name):
"""Expand the ``font`` shorthand property.
https://www.w3.org/TR/css-fonts-3/#font-prop
"""
expand_font_keyword = get_single_keyword(tokens)
if expand_font_keyword in ('caption', 'icon', 'menu', 'message-box',
'small-caption', 'status-bar'):
raise InvalidValues('System fonts are not supported')
# Make `tokens` a stack
tokens = list(reversed(tokens))
# Values for font-style, font-variant-caps, font-weight and font-stretch
# can come in any order and are all optional.
for _ in range(4):
token = tokens.pop()
if get_keyword(token) == 'normal':
# Just ignore 'normal' keywords. Unspecified properties will get
# their initial token, which is 'normal' for all four here.
continue
if font_style([token]) is not None:
suffix = '-style'
elif get_keyword(token) in ('normal', 'small-caps'):
suffix = '-variant-caps'
elif font_weight([token]) is not None:
suffix = '-weight'
elif font_stretch([token]) is not None:
suffix = '-stretch'
else:
# Were done with these four, continue with font-size
break
yield suffix, [token]
if not tokens:
raise InvalidValues
else:
if not tokens:
raise InvalidValues
token = tokens.pop()
# Then font-size is mandatory
# Latest `token` from the loop.
if font_size([token]) is None:
raise InvalidValues
yield '-size', [token]
# Then line-height is optional, but font-family is not so the list
# must not be empty yet
if not tokens:
raise InvalidValues
token = tokens.pop()
if token.type == 'literal' and token.value == '/':
token = tokens.pop()
if line_height([token]) is None:
raise InvalidValues
yield 'line-height', [token]
else:
# We pop()ed a font-family, add it back
tokens.append(token)
# Reverse the stack to get normal list
tokens.reverse()
if font_family(tokens) is None:
raise InvalidValues
yield '-family', tokens
@expander('word-wrap')
@generic_expander('overflow-wrap')
def expand_word_wrap(tokens, name):
"""Expand the ``word-wrap`` legacy property.
See https://www.w3.org/TR/css-text-3/#overflow-wrap
"""
keyword = overflow_wrap(tokens)
if keyword is None:
raise InvalidValues
yield 'overflow-wrap', tokens
@expander('flex')
@generic_expander('-grow', '-shrink', '-basis')
def expand_flex(tokens, name):
"""Expand the ``flex`` property."""
keyword = get_single_keyword(tokens)
if keyword == 'none':
line, column = tokens[0].source_line, tokens[0].source_column
zero_token = NumberToken(line, column, 0, 0, '0')
auto_token = IdentToken(line, column, 'auto')
yield '-grow', [zero_token]
yield '-shrink', [zero_token]
yield '-basis', [auto_token]
else:
grow, shrink, basis = 1, 1, None
grow_found, shrink_found, basis_found = False, False, False
for token in tokens:
# "A unitless zero that is not already preceded by two flex factors
# must be interpreted as a flex factor."
forced_flex_factor = (
token.type == 'number' and token.int_value == 0 and
not all((grow_found, shrink_found)))
if not basis_found and not forced_flex_factor:
new_basis = flex_basis([token])
if new_basis is not None:
basis = token
basis_found = True
continue
if not grow_found:
new_grow = flex_grow_shrink([token])
if new_grow is None:
raise InvalidValues
else:
grow = new_grow
grow_found = True
continue
elif not shrink_found:
new_shrink = flex_grow_shrink([token])
if new_shrink is None:
raise InvalidValues
else:
shrink = new_shrink
shrink_found = True
continue
else:
raise InvalidValues
line, column = tokens[0].source_line, tokens[0].source_column
int_grow = int(grow) if float(grow).is_integer() else None
int_shrink = int(shrink) if float(shrink).is_integer() else None
grow_token = NumberToken(line, column, grow, int_grow, str(grow))
shrink_token = NumberToken(
line, column, shrink, int_shrink, str(shrink))
if not basis_found:
basis = DimensionToken(line, column, 0, 0, '0', 'px')
yield '-grow', [grow_token]
yield '-shrink', [shrink_token]
yield '-basis', [basis]
@expander('flex-flow')
@generic_expander('flex-direction', 'flex-wrap')
def expand_flex_flow(tokens, name):
"""Expand the ``flex-flow`` property."""
if len(tokens) == 2:
for sorted_tokens in tokens, tokens[::-1]:
direction = flex_direction([sorted_tokens[0]])
wrap = flex_wrap([sorted_tokens[1]])
if direction and wrap:
yield 'flex-direction', [sorted_tokens[0]]
yield 'flex-wrap', [sorted_tokens[1]]
break
else:
raise InvalidValues
elif len(tokens) == 1:
direction = flex_direction([tokens[0]])
if direction:
yield 'flex-direction', [tokens[0]]
else:
wrap = flex_wrap([tokens[0]])
if wrap:
yield 'flex-wrap', [tokens[0]]
else:
raise InvalidValues
else:
raise InvalidValues
def _expand_grid_template(tokens, name):
line, column = tokens[0].source_line, tokens[0].source_column
none = IdentToken(line, column, 'none')
if len(tokens) == 1 and get_keyword(tokens[0]) == 'none':
yield '-columns', [none]
yield '-rows', [none]
yield '-areas', [none]
return
slash_separated = [[]]
for token in tokens:
if token.type == 'literal' and token.value == '/':
slash_separated.append([])
else:
slash_separated[-1].append(token)
if len(slash_separated) == 2:
rows = grid_template(slash_separated[0])
columns = grid_template(slash_separated[1])
if columns:
if rows:
yield '-columns', slash_separated[1]
yield '-rows', slash_separated[0]
yield '-areas', [none]
return
columns = slash_separated[1]
else:
raise InvalidValues
elif len(slash_separated) == 1:
columns = [none]
else:
raise InvalidValues
# TODO: Handle last syntax.
raise InvalidValues
@expander('grid-template')
@generic_expander('-columns', '-rows', '-areas')
def expand_grid_template(tokens, name):
"""Expand the ``grid-template`` property."""
yield from _expand_grid_template(tokens, name)
@expander('grid')
@generic_expander('-template-columns', '-template-rows', '-template-areas',
'-auto-columns', '-auto-rows', '-auto-flow')
def expand_grid(tokens, name):
"""Expand the ``grid`` property."""
line, column = tokens[0].source_line, tokens[0].source_column
auto = IdentToken(line, column, 'auto')
none = IdentToken(line, column, 'none')
row = IdentToken(line, column, 'row')
column = IdentToken(line, column, 'column')
try:
template = tuple(_expand_grid_template(tokens, 'grid-template'))
except InvalidValues:
pass
else:
for key, value in template:
yield f'-template-{key.split("-")[-1]}', value
yield '-auto-columns', [auto]
yield '-auto-rows', [auto]
yield '-auto-flow', [row]
return
split_tokens = [[]]
for token in tokens:
if token.type == 'literal' and token.value == '/':
split_tokens.append([])
continue
split_tokens[-1].append(token)
if len(split_tokens) != 2:
raise InvalidValues
auto_track = None
dense = None
templates = {'row': [], 'column': []}
iterable = zip(split_tokens, templates.items())
for tokens, (track, track_templates) in iterable:
auto_flow_token = False
for token in tokens:
if get_keyword(token) == 'dense':
if dense or (auto_track and auto_track != track):
raise InvalidValues
dense = token
auto_track = track
elif get_keyword(token) == 'auto-flow':
if auto_flow_token or (auto_track and auto_track != track):
raise InvalidValues
auto_flow_token = True
auto_track = track
elif token == tokens[-1]:
track_templates.append(token)
else:
raise InvalidValues
if not auto_track:
raise InvalidValues
non_auto_track = 'row' if auto_track == 'column' else 'column'
auto_track_token = column if auto_track == 'column' else row
yield '-auto-flow', (
(auto_track_token, dense) if dense else (auto_track_token,))
yield f'-auto-{auto_track}s', tuple(templates[auto_track])
yield f'-auto-{non_auto_track}s', [auto]
yield f'-template-{auto_track}s', [none]
yield f'-template-{non_auto_track}s', tuple(templates[non_auto_track])
yield '-template-areas', [none]
def _expand_grid_column_row_area(tokens, max_number):
grid_lines = [[]]
for token in tokens:
if token.type == 'literal' and token.value == '/':
grid_lines.append([])
continue
grid_lines[-1].append(token)
if not 1 <= len(grid_lines) <= max_number:
raise InvalidValues
validations = []
for tokens in grid_lines:
if not (validation := grid_line(tokens)):
raise InvalidValues
validations.append(validation)
yield tuple(tokens)
auto = IdentToken(token.source_line, token.source_column, 'auto')
if (lines := len(grid_lines)) <= 1:
custom_ident = set(validations[0][:2]) == {None}
value = tuple(grid_lines[0]) if custom_ident else (auto,)
grid_lines.append(tokens)
validations.append(validations[0])
yield value
if lines <= 2 < max_number:
custom_ident = set(validations[0][:2]) == {None}
yield tuple(grid_lines[0]) if custom_ident else (auto,)
if lines <= 3 < max_number:
custom_ident = set(validations[1][:2]) == {None}
yield tuple(grid_lines[1]) if custom_ident else (auto,)
@expander('grid-column')
@expander('grid-row')
@generic_expander('-start', '-end')
def expand_grid_column_row(tokens, name):
"""Expand the ``grid-[column|row]`` properties."""
tokens_list = _expand_grid_column_row_area(tokens, 2)
for tokens, side in zip(tokens_list, ('start', 'end')):
yield f'-{side}', tokens
@expander('grid-area')
@generic_expander('grid-row-start', 'grid-row-end',
'grid-column-start', 'grid-column-end')
def expand_grid_area(tokens, name):
"""Expand the ``grid-area`` property."""
tokens_list = _expand_grid_column_row_area(tokens, 4)
sides = ('row-start', 'column-start', 'row-end', 'column-end')
for tokens, side in zip(tokens_list, sides):
yield f'grid-{side}', tokens
@expander('grid-gap')
@expander('gap')
@generic_expander('column-gap', 'row-gap')
def expand_gap(tokens, name):
"""Expand the ``gap`` property."""
if len(tokens) == 1:
if gap(tokens) is None:
raise InvalidValues
yield 'row-gap', tokens
yield 'column-gap', tokens
elif len(tokens) == 2:
column_gap, row_gap = gap(tokens[0:1]), gap(tokens[1:2])
if None in (column_gap, row_gap):
raise InvalidValues
yield 'row-gap', tokens[0:1]
yield 'column-gap', tokens[1:2]
else:
raise InvalidValues
@expander('grid-column-gap')
@generic_expander('column-gap')
def expand_legacy_column_gap(tokens, name):
"""Expand legacy ``grid-column-gap`` property."""
keyword = gap(tokens)
if keyword is None:
raise InvalidValues
yield 'column-gap', tokens
@expander('grid-row-gap')
@generic_expander('row-gap')
def expand_legacy_row_gap(tokens, name):
"""Expand legacy ``grid-row-gap`` property."""
keyword = gap(tokens)
if keyword is None:
raise InvalidValues
yield 'row-gap', tokens
@expander('place-content')
@generic_expander('align-content', 'justify-content')
def expand_place_content(tokens, name):
"""Expand the ``place-content`` property."""
# TODO
raise InvalidValues
@expander('place-items')
@generic_expander('align-items', 'justify-items')
def expand_place_items(tokens, name):
"""Expand the ``place-items`` property."""
# TODO
raise InvalidValues
@expander('place-self')
@generic_expander('align-self', 'justify-self')
def expand_place_self(tokens, name):
"""Expand the ``place-self`` property."""
# TODO
raise InvalidValues
@expander('line-clamp')
@generic_expander('max-lines', 'continue', 'block-ellipsis')
def expand_line_clamp(tokens, name):
"""Expand the ``line-clamp`` property."""
if len(tokens) == 1:
keyword = get_single_keyword(tokens)
if keyword == 'none':
line, column = tokens[0].source_line, tokens[0].source_column
none_token = IdentToken(line, column, 'none')
auto_token = IdentToken(line, column, 'auto')
yield 'max-lines', [none_token]
yield 'continue', [auto_token]
yield 'block-ellipsis', [none_token]
elif tokens[0].type == 'number' and tokens[0].int_value is not None:
line, column = tokens[0].source_line, tokens[0].source_column
auto_token = IdentToken(line, column, 'auto')
discard_token = IdentToken(line, column, 'discard')
yield 'max-lines', [tokens[0]]
yield 'continue', [discard_token]
yield 'block-ellipsis', [auto_token]
else:
raise InvalidValues
elif len(tokens) == 2:
if tokens[0].type == 'number':
max_lines = tokens[0].int_value
ellipsis = block_ellipsis([tokens[1]])
if max_lines and ellipsis is not None:
line, column = tokens[0].source_line, tokens[0].source_column
discard_token = IdentToken(line, column, 'discard')
yield 'max-lines', [tokens[0]]
yield 'continue', [discard_token]
yield 'block-ellipsis', [tokens[1]]
else:
raise InvalidValues
else:
raise InvalidValues
else:
raise InvalidValues
@expander('text-align')
@generic_expander('-all', '-last')
def expand_text_align(tokens, name):
"""Expand the ``text-align`` property."""
if len(tokens) == 1:
keyword = get_single_keyword(tokens)
if keyword is None:
raise InvalidValues
if keyword == 'justify-all':
line, column = tokens[0].source_line, tokens[0].source_column
align_all = IdentToken(line, column, 'justify')
else:
align_all = tokens[0]
yield '-all', [align_all]
if keyword == 'justify':
line, column = tokens[0].source_line, tokens[0].source_column
align_last = IdentToken(line, column, 'start')
else:
align_last = align_all
yield '-last', [align_last]
else:
raise InvalidValues