"""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: # We’re 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