INITIAL_VALUES = {
    # CSS 2.1: http://www.w3.org/TR/CSS21/propidx.html
    'bottom': 'auto',
    'caption_side': 'top',
    'clear': 'none',
    'clip': (),  # computed value for 'auto'
    'color': parse_color('black'),  # chosen by the user agent
    'direction': 'ltr',
    'display': ('inline', 'flow'),
    'empty_cells': 'show',
    'float': 'none',
    'height': 'auto',
    'left': 'auto',
    'line_height': 'normal',
    'margin_top': Dimension(0, 'px'),
    'margin_right': Dimension(0, 'px'),
    'margin_bottom': Dimension(0, 'px'),
    'margin_left': Dimension(0, 'px'),
    'max_height': Dimension(inf, 'px'),  # parsed value for 'none'
    'max_width': Dimension(inf, 'px'),
    'padding_top': Dimension(0, 'px'),
    'padding_right': Dimension(0, 'px'),
    'padding_bottom': Dimension(0, 'px'),
    'padding_left': Dimension(0, 'px'),
    'position': 'static',
    'right': 'auto',
    'table_layout': 'auto',
    'top': 'auto',
    'unicode_bidi': 'normal',
    'vertical_align': 'baseline',
    'visibility': 'visible',
    'width': 'auto',
    'z_index': 'auto',

    # Backgrounds and Borders 3 (CR): https://www.w3.org/TR/css3-background/
    'background_attachment': ('scroll',),
    'background_clip': ('border-box',),
    'background_color': parse_color('transparent'),
    'background_image': (('none', None),),
    'background_origin': ('padding-box',),
    'background_position': (('left', Dimension(0, '%'),
                             'top', Dimension(0, '%')),),
    'background_repeat': (('repeat', 'repeat'),),
    'background_size': (('auto', 'auto'),),
    'border_bottom_color': 'currentColor',
    'border_bottom_left_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_bottom_right_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_bottom_style': 'none',
    'border_bottom_width': 3,
    'border_collapse': 'separate',
    'border_left_color': 'currentColor',
    'border_left_style': 'none',
    'border_left_width': 3,
    'border_right_color': 'currentColor',
    'border_right_style': 'none',
    'border_right_width': 3,
    'border_spacing': (0, 0),
    'border_top_color': 'currentColor',
    'border_top_left_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_top_right_radius': (Dimension(0, 'px'), Dimension(0, 'px')),
    'border_top_style': 'none',
    'border_top_width': 3,  # computed value for 'medium'

    # Color 3 (REC): https://www.w3.org/TR/css3-color/
    'opacity': 1,

    # Multi-column Layout (WD): https://www.w3.org/TR/css-multicol-1/
    'column_width': 'auto',
    'column_count': 'auto',
    'column_gap': Dimension(1, 'em'),
    'column_rule_color': 'currentColor',
    'column_rule_style': 'none',
    'column_rule_width': 'medium',
    'column_fill': 'balance',
    'column_span': 'none',

    # Fonts 3 (REC): https://www.w3.org/TR/css-fonts-3/
    'font_family': ('serif',),  # depends on user agent
    'font_feature_settings': 'normal',
    'font_kerning': 'auto',
    'font_language_override': 'normal',
    'font_size': 16,  # actually medium, but we define medium from this
    'font_stretch': 'normal',
    'font_style': 'normal',
    'font_variant': 'normal',
    'font_variant_alternates': 'normal',
    'font_variant_caps': 'normal',
    'font_variant_east_asian': 'normal',
    'font_variant_ligatures': 'normal',
    'font_variant_numeric': 'normal',
    'font_variant_position': 'normal',
    'font_weight': 400,

    # Fragmentation 3/4 (CR/WD): https://www.w3.org/TR/css-break-4/
    'box_decoration_break': 'slice',
    'break_after': 'auto',
    'break_before': 'auto',
    'break_inside': 'auto',
    'margin_break': 'auto',
    'orphans': 2,
    'widows': 2,

    # Generated Content 3 (WD): https://www.w3.org/TR/css-content-3/
    'bookmark_label': (('content', 'text'),),
    'bookmark_level': 'none',
    'bookmark_state': 'open',
    'content': 'normal',
    'footnote_display': 'block',
    'footnote_policy': 'auto',
    'quotes': list('“”‘’'),  # chosen by the user agent
    'string_set': 'none',

    # Images 3/4 (CR/WD): https://www.w3.org/TR/css4-images/
    'image_resolution': 1,  # dppx
    'image_rendering': 'auto',
    # https://drafts.csswg.org/css-images-3/
    'object_fit': 'fill',
    'object_position': (('left', Dimension(50, '%'),
                         'top', Dimension(50, '%')),),

    # Paged Media 3 (WD): https://www.w3.org/TR/css-page-3/
    'size': None,  # set to A4 in computed_values
    'page': 'auto',
    'bleed_left': 'auto',
    'bleed_right': 'auto',
    'bleed_top': 'auto',
    'bleed_bottom': 'auto',
    'marks': (),  # computed value for 'none'

    # Text 3/4 (WD/WD): https://www.w3.org/TR/css-text-4/
    'hyphenate_character': '‐',  # computed value chosen by the user agent
    'hyphenate_limit_chars': (5, 2, 2),
    'hyphenate_limit_zone': Dimension(0, 'px'),
    'hyphens': 'manual',
    'letter_spacing': 'normal',
    'tab_size': 8,
    'text_align_all': 'start',
    'text_align_last': 'auto',
    'text_indent': Dimension(0, 'px'),
    'text_transform': 'none',
    'white_space': 'normal',
    'word_break': 'normal',
    'word_spacing': 0,  # computed value for 'normal'

    # Transforms 1 (CR): https://www.w3.org/TR/css-transforms-1/
    'transform_origin': (Dimension(50, '%'), Dimension(50, '%')),
    'transform': (),  # computed value for 'none'

    # User Interface 3 (REC): https://www.w3.org/TR/css-ui-3/
    'box_sizing': 'content-box',
    'outline_color': 'currentColor',  # invert is not supported
    'outline_style': 'none',
    'outline_width': 3,  # computed value for 'medium'
    'overflow_wrap': 'normal',

    # Flexible Box Layout Module 1 (CR): https://www.w3.org/TR/css-flexbox-1/
    'align_content': 'stretch',
    'align_items': 'stretch',
    'align_self': 'auto',
    'flex_basis': 'auto',
    'flex_direction': 'row',
    'flex_grow': 0,
    'flex_shrink': 1,
    'flex_wrap': 'nowrap',
    'justify_content': 'flex-start',
    'min_height': 'auto',
    'min_width': 'auto',
    'order': 0,

    # Text Decoration Module 3 (CR): https://www.w3.org/TR/css-text-decor-3/
    'text_decoration_line': 'none',
    'text_decoration_color': 'currentColor',
    'text_decoration_style': 'solid',

    # Overflow Module 3 (WD): https://www.w3.org/TR/css-overflow-3/
    'block_ellipsis': 'none',
    'continue': 'auto',
    'max_lines': 'none',
    'overflow': 'visible',
    'text_overflow': 'clip',

    # Lists Module 3 (WD): https://drafts.csswg.org/css-lists-3/
    # Means 'none', but allow `display: list-item` to increment the
    # list-item counter. If we ever have a way for authors to query
    # computed values (JavaScript?), this value should serialize to 'none'.
    'counter_increment': 'auto',
    'counter_reset': (),  # parsed value for 'none'
    'counter_set': (),  # parsed value for 'none'
    'list_style_image': ('none', None),
    'list_style_position': 'outside',
    'list_style_type': 'disc',

    # Proprietary
    'anchor': None,  # computed value of 'none'
    'link': None,  # computed value of 'none'
    'lang': None,  # computed value of 'none'
} class InvalidValues(ValueError):
    """Invalid or unsupported values for a known CSS property.""" def get_keyword(token):
    """If ``token`` is a keyword, return its lowercase name.

    Otherwise return ``None``.

    """
    if token.type == 'ident':
        return token.lower_value def get_single_keyword(tokens):
    """If ``values`` is a 1-element list of keywords, return its name.

    Otherwise return ``None``.

    """
    if len(tokens) == 1:
        token = tokens[0]
        if token.type == 'ident':
            return token.lower_value def split_on_comma(tokens):
    """Split a list of tokens on commas, ie ``LiteralToken(',')``.

    Only "top-level" comma tokens are splitting points, not commas inside a
    function or blocks.

    """
    parts = []
    this_part = []
    for token in tokens:
        if token.type == 'literal' and token.value == ',':
            parts.append(this_part)
            this_part = []
        else:
            this_part.append(token)
    parts.append(this_part)
    return tuple(parts) def expand_font_variant(tokens):
    keyword = get_single_keyword(tokens)
    if keyword in ('normal', 'none'):
        for suffix in (
                '-alternates', '-caps', '-east-asian', '-numeric',
                '-position'):
            yield suffix, [NormalFakeToken]
        token = NormalFakeToken if keyword == 'normal' else NoneFakeToken
        yield '-ligatures', [token]
    else:
        features = {
            'alternates': [],
            'caps': [],
            'east-asian': [],
            'ligatures': [],
            'numeric': [],
            'position': []}
        for token in tokens:
            keyword = get_keyword(token)
            if keyword == 'normal':
                # We don't allow 'normal', only the specific values
                raise InvalidValues
            for feature in features:
                function_name = f'font_variant_{feature.replace("-", "_")}'
                if getattr(properties, function_name)([token]):
                    features[feature].append(token)
                    break
            else:
                raise InvalidValues
        for feature, tokens in features.items():
            if tokens:
                yield (f'-{feature}', tokens) @property()
@comma_separated_list
@single_keyword
def background_attachment(keyword):
    """``background-attachment`` property validation."""
    return keyword in ('scroll', 'fixed', 'local') @property('background-image', wants_base_url=True)
@comma_separated_list
@single_token
def background_image(token, base_url):
    if token.type != 'function':
        if get_keyword(token) == 'none':
            return 'none', None
    return get_image(token, base_url) @property()
@comma_separated_list
def background_position(tokens):
    """``background-position`` property validation."""
    return parse_position(tokens) @property()
@comma_separated_list
def background_repeat(tokens):
    """``background-repeat`` property validation."""
    keywords = tuple(map(get_keyword, tokens))
    if keywords == ('repeat-x',):
        return ('repeat', 'no-repeat')
    if keywords == ('repeat-y',):
        return ('no-repeat', 'repeat')
    if keywords in (('no-repeat',), ('repeat',), ('space',), ('round',)):
        return keywords * 2
    if len(keywords) == 2 and all(
            k in ('no-repeat', 'repeat', 'space', 'round')
            for k in keywords):
        return keywords @property()
@comma_separated_list
def background_size(tokens):
    """Validation for ``background-size``."""
    if len(tokens) == 1:
        token = tokens[0]
        keyword = get_keyword(token)
        if keyword in ('contain', 'cover'):
            return keyword
        if keyword == 'auto':
            return ('auto', 'auto')
        length = get_length(token, negative=False, percentage=True)
        if length:
            return (length, 'auto')
    elif len(tokens) == 2:
        values = []
        for token in tokens:
            length = get_length(token, negative=False, percentage=True)
            if length:
                values.append(length)
            elif get_keyword(token) == 'auto':
                values.append('auto')
        if len(values) == 2:
            return tuple(values) @property()
@single_token
def block_ellipsis(token):
    """``box-ellipsis`` property validation."""
    if token.type == 'string':
        return ('string', token.value)
    else:
        keyword = get_keyword(token)
        if keyword in ('none', 'auto'):
            return keyword @property('border-top-style')
@property('border-right-style')
@property('border-left-style')
@property('border-bottom-style')
@property('column-rule-style', unstable=True)
@single_keyword
def border_style(keyword):
    """``border-*-style`` properties validation."""
    return keyword in ('none', 'hidden', 'dotted', 'dashed', 'double',
                       'inset', 'outset', 'groove', 'ridge', 'solid') @property('border-top-width')
@property('border-right-width')
@property('border-left-width')
@property('border-bottom-width')
@property('column-rule-width', unstable=True)
@property('outline-width')
@single_token
def border_width(token):
    """Border, column rule and outline widths properties validation."""
    length = get_length(token, negative=False)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword in ('thin', 'medium', 'thick'):
        return keyword @property('background-clip')
@property('background-origin')
@comma_separated_list
@single_keyword
def box(keyword):
    """Validation for the ``<box>`` type used in ``background-clip``
    and ``background-origin``."""
    return keyword in ('border-box', 'padding-box', 'content-box') @property(unstable=True)
@single_token
def column_count(token):
    """Validation for the ``column-count`` property."""
    if token.type == 'number' and token.int_value is not None:
        value = token.int_value
        if value >= 1:
            return value
    if get_keyword(token) == 'auto':
        return 'auto' @property(unstable=True)
@single_token
def column_width(token):
    """``column-width`` property validation."""
    length = get_length(token, negative=False)
    if length:
        return length
    keyword = get_keyword(token)
    if keyword == 'auto':
        return keyword @property()
@single_token
def flex_basis(token):
    """``flex-basis`` property validation."""
    basis = width_height([token])
    if basis is not None:
        return basis
    if get_keyword(token) == 'content':
        return 'content' @property()
@single_keyword
def flex_direction(keyword):
    """``flex-direction`` property validation."""
    return keyword in ('row', 'row-reverse', 'column', 'column-reverse') @property('flex-grow')
@property('flex-shrink')
@single_token
def flex_grow_shrink(token):
    if token.type == 'number':
        return token.value @property()
@single_keyword
def flex_wrap(keyword):
    """``flex-wrap`` property validation."""
    return keyword in ('nowrap', 'wrap', 'wrap-reverse') @property()
@comma_separated_list
def font_family(tokens):
    """``font-family`` property validation."""
    if len(tokens) == 1 and tokens[0].type == 'string':
        return tokens[0].value
    elif tokens and all(token.type == 'ident' for token in tokens):
        return ' '.join(token.value for token in tokens) @property()
@single_token
def font_size(token):
    """``font-size`` property validation."""
    length = get_length(token, negative=False, percentage=True)
    if length:
        return length
    font_size_keyword = get_keyword(token)
    if font_size_keyword in ('smaller', 'larger'):
        return font_size_keyword
    if font_size_keyword in computed_values.FONT_SIZE_KEYWORDS:
        return font_size_keyword @property()
@single_keyword
def font_stretch(keyword):
    """Validation for the ``font-stretch`` property."""
    return keyword in (
        'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed',
        'normal',
        'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded') @property()
@single_keyword
def font_style(keyword):
    """``font-style`` property validation."""
    return keyword in ('normal', 'italic', 'oblique') @property()
@single_token
def font_weight(token):
    """``font-weight`` property validation."""
    keyword = get_keyword(token)
    if keyword in ('normal', 'bold', 'bolder', 'lighter'):
        return keyword
    if token.type == 'number' and token.int_value is not None:
        if token.int_value in (100, 200, 300, 400, 500, 600, 700, 800, 900):
            return token.int_value @property()
@single_token
def line_height(token):
    """``line-height`` property validation."""
    if get_keyword(token) == 'normal':
        return 'normal'
    if token.type == 'number' and token.value >= 0:
        return Dimension(token.value, None)
    if token.type == 'percentage' and token.value >= 0:
        return Dimension(token.value, '%')
    elif token.type == 'dimension' and token.value >= 0:
        return get_length(token) @property('list-style-image', wants_base_url=True)
@single_token
def list_style_image(token, base_url):
    """``list-style-image`` property validation."""
    if get_keyword(token) == 'none':
        return 'none', None
    parsed_url = get_url(token, base_url)
    if parsed_url:
        if parsed_url[0] == 'url' and parsed_url[1][0] == 'external':
            return 'url', parsed_url[1][1] @property()
@single_keyword
def list_style_position(keyword):
    """``list-style-position`` property validation."""
    return keyword in ('inside', 'outside') @property()
@single_token
def list_style_type(token):
    """``list-style-type`` property validation."""
    if token.type == 'ident':
        return token.value
    elif token.type == 'string':
        return ('string', token.value)
    elif token.type == 'function' and token.name == 'symbols':
        allowed_types = (
            'cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')
        function_arguments = remove_whitespace(token.arguments)
        if len(function_arguments) >= 1:
            arguments = []
            if function_arguments[0].type == 'ident':
                if function_arguments[0].value in allowed_types:
                    index = 1
                    arguments.append(function_arguments[0].value)
                else:
                    return
            else:
                arguments.append('symbolic')
                index = 0
            if len(function_arguments) < index + 1:
                return
            for i in range(index, len(function_arguments)):
                if function_arguments[i].type != 'string':
                    return
                arguments.append(function_arguments[i].value)
            if arguments[0] in ('alphabetic', 'numeric'):
                if len(arguments) < 3:
                    return
            return ('symbols()', tuple(arguments)) @property('background-color')
@property('border-top-color')
@property('border-right-color')
@property('border-bottom-color')
@property('border-left-color')
@property('column-rule-color', unstable=True)
@property('text-decoration-color')
@single_token
def other_colors(token):
    return parse_color(token) @property()
@single_keyword
def overflow_wrap(keyword):
    """``overflow-wrap`` property validation."""
    return keyword in ('anywhere', 'normal', 'break-word') def validate_non_shorthand(base_url, name, tokens, required=False):
    """Default validator for non-shorthand properties."""
    if name.startswith('--'):
        # TODO: validate content
        return ((name, tokens),)

    if not required and name not in KNOWN_PROPERTIES:
        hyphens_name = name.replace('_', '-')
        if hyphens_name in KNOWN_PROPERTIES:
            raise InvalidValues(f'you probably mean "{hyphens_name}"')
        else:
            raise InvalidValues('unknown property')

    if not required and name not in PROPERTIES:
        raise InvalidValues('property not supported yet')

    if name not in MULTIVAL_PROPERTIES:
        for token in tokens:
            var_function = check_var_function(token)
            if var_function:
                return ((name, var_function),)

    keyword = get_single_keyword(tokens)
    if keyword in ('initial', 'inherit'):
        value = keyword
    else:
        function = PROPERTIES[name]
        if function.wants_base_url:
            value = function(tokens, base_url)
        else:
            value = function(tokens)
        if value is None:
            raise InvalidValues
    return ((name, value),)
import functools
from tinycss2.ast import DimensionToken, IdentToken, NumberToken
from tinycss2.color3 import parse_color
from ..properties import INITIAL_VALUES
from ..utils import (
    InvalidValues, get_keyword, get_single_keyword, split_on_comma)
from .descriptors import expand_font_variant
from .properties import (
    background_attachment, background_image, background_position,
    background_repeat, background_size, block_ellipsis, 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, line_height, list_style_image,
    list_style_position, list_style_type, other_colors, overflow_wrap,
    validate_non_shorthand)
"""Validate properties expanders."""




EXPANDERS = {}


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


@expander('border-color')
@expander('border-style')
@expander('border-width')
@expander('margin')
@expander('padding')
@expander('bleed')
def expand_four_sides(base_url, name, tokens):
    """Expand properties setting a token for the four sides of a box."""
    # 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 suffix, token in zip(('-top', '-right', '-bottom', '-left'), tokens):
        i = name.rfind('-')
        if i == -1:
            new_name = name + suffix
        else:
            # eg. border-color becomes border-*-color, not border-color-*
            new_name = name[:i] + suffix + name[i:]

        # validate_non_shorthand returns ((name, value),), we want
        # to yield (name, value)
        result, = validate_non_shorthand(
            base_url, new_name, [token], required=True)
        yield result


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(base_url, name, tokens):
            """Wrap the expander."""
            keyword = get_single_keyword(tokens)
            if keyword in ('inherit', 'initial'):
                results = dict.fromkeys(expanded_names, keyword)
                skip_validation = True
            else:
                skip_validation = False
                results = {}
                if wants_base_url:
                    result = wrapped(name, tokens, base_url)
                else:
                    result = wrapped(name, tokens)
                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 = 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(