# coding: utf8

#  WeasyPrint converts web documents (HTML, CSS, ...) to PDF.
#  Copyright (C) 2011  Simon Sapin
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Affero General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Affero General Public License for more details.
#
#  You should have received a copy of the GNU Affero General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
Validator for all supported properties.

See http://www.w3.org/TR/CSS21/propidx.html for allowed values.

"""
# TODO: unit-test these validators


from __future__ import division, unicode_literals

import functools

from ..logger import LOGGER
from ..formatting_structure import counters
from .values import (get_keyword, get_single_keyword, as_css,
                     make_percentage_value)
from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA
from . import computed_values


# keyword -> (open, insert)
CONTENT_QUOTE_KEYWORDS = {
    'open-quote': (True, True),
    'close-quote': (False, True),
    'no-open-quote': (True, False),
    'no-close-quote': (False, False),
}

BACKGROUND_POSITION_PERCENTAGES = {
    'top': make_percentage_value(0),
    'left': make_percentage_value(0),
    'center': make_percentage_value(50),
    'bottom': make_percentage_value(100),
    'right': make_percentage_value(100),
}


# yes/no validators for non-shorthand properties
# Maps property names to functions taking a property name and a value list,
# returning a value or None for invalid.
# Also transform values: keyword and URLs are returned as strings.
# For properties that take a single value, that value is returned by itself
# instead of a list.
VALIDATORS = {}

EXPANDERS = {}

PREFIXED = set()
# The same replacement was done on property names:
PREFIX = '-weasy-'.replace('-', '_')


class InvalidValues(ValueError):
    """Invalid or unsupported values for a known CSS property."""


# Validators

def validator(property_name=None, prefixed=False):
    """Decorator adding a function to the ``VALIDATORS``.

    The name of the property covered by the decorated function is set to
    ``property_name`` if given, or is inferred from the function name
    (replacing underscores by hyphens).

    """
    def decorator(function):
        """Add ``function`` to the ``VALIDATORS``."""
        if property_name is None:
            name = function.__name__
        else:
            name = property_name.replace('-', '_')
        assert name in INITIAL_VALUES, name
        assert name not in VALIDATORS, name

        VALIDATORS[name] = function
        if prefixed:
            PREFIXED.add(name)
        return function
    return decorator


def single_keyword(function):
    """Decorator for validators that only accept a single keyword."""
    @functools.wraps(function)
    def keyword_validator(values):
        """Wrap a validator to call get_single_keyword on values."""
        keyword = get_single_keyword(values)
        if function(keyword):
            return keyword
    return keyword_validator


def single_value(function):
    """Decorator for validators that only accept a single value."""
    @functools.wraps(function)
    def single_value_validator(values):
        """Validate a property whose value is single."""
        if len(values) == 1:
            return function(values[0])
    single_value_validator.__func__ = function
    return single_value_validator


def is_dimension(value, negative=True):
    """Get if ``value`` is a dimension.

    The ``negative`` argument sets wether negative values are allowed.

    """
    type_ = value.type
    # Units may be ommited on zero lenghts.
    return (
        type_ == 'DIMENSION' and (negative or value.value >= 0) and (
            value.dimension in computed_values.LENGTHS_TO_PIXELS or
            value.dimension in ('em', 'ex'))
        ) or (type_ == 'NUMBER' and value.value == 0)


def is_dimension_or_percentage(value, negative=True):
    """Get if ``value`` is a dimension or a percentage.

    The ``negative`` argument sets wether negative values are allowed.

    """
    return is_dimension(value, negative) or (
        value.type == 'PERCENTAGE' and (negative or value.value >= 0))


def is_angle(value):
    """Return whether the argument is an angle value."""
    return value.type == 'DIMENSION' and \
        value.dimension in computed_values.ANGLE_TO_RADIANS


@validator()
@single_keyword
def background_attachment(keyword):
    """``background-attachment`` property validation."""
    return keyword in ('scroll', 'fixed')


@validator('background-color')
@validator('border-top-color')
@validator('border-right-color')
@validator('border-bottom-color')
@validator('border-left-color')
@validator('color')
@single_value
def color(value):
    """``*-color`` and ``color`` properties validation."""
    if value.type == 'COLOR_VALUE':
        return value
    if get_keyword(value) == 'currentColor':
        return 'inherit'


@validator('background-image')
@validator('list-style-image')
@single_value
def image(value):
    """``*-image`` properties validation."""
    if get_keyword(value) == 'none':
        return 'none'
    if value.type == 'URI':
        return value.absoluteUri


@validator('transform-origin', prefixed=True)  # Not in CR yet
@validator()
def background_position(values):
    """``background-position`` property validation.

    See http://www.w3.org/TR/CSS21/colors.html#propdef-background-position

    """
    if len(values) == 1:
        center = BACKGROUND_POSITION_PERCENTAGES['center']
        value = values[0]
        keyword = get_keyword(value)
        if keyword in BACKGROUND_POSITION_PERCENTAGES:
            return BACKGROUND_POSITION_PERCENTAGES[keyword], center
        elif is_dimension_or_percentage(value):
            return value, center

    elif len(values) == 2:
        value_1, value_2 = values
        keyword_1, keyword_2 = map(get_keyword, values)
        if is_dimension_or_percentage(value_1):
            if keyword_2 in ('top', 'center', 'bottom'):
                return value_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2]
            elif is_dimension_or_percentage(value_2):
                return value_1, value_2
        elif is_dimension_or_percentage(value_2):
            if keyword_1 in ('left', 'center', 'right'):
                return BACKGROUND_POSITION_PERCENTAGES[keyword_1], value_2
        elif (keyword_1 in ('left', 'center', 'right') and
              keyword_2 in ('top', 'center', 'bottom')):
            return (BACKGROUND_POSITION_PERCENTAGES[keyword_1],
                    BACKGROUND_POSITION_PERCENTAGES[keyword_2])
        elif (keyword_1 in ('top', 'center', 'bottom') and
              keyword_2 in ('left', 'center', 'right')):
            # Swap values. They need to be in (horizontal, vertical) order.
            return (BACKGROUND_POSITION_PERCENTAGES[keyword_2],
                    BACKGROUND_POSITION_PERCENTAGES[keyword_1])
    #else: invalid


@validator()
@single_keyword
def background_repeat(keyword):
    """``background-repeat`` property validation."""
    return keyword in ('repeat', 'repeat-x', 'repeat-y', 'no-repeat')


@validator()
def background_size(values):
    """Validation for ``background-size``."""
    if len(values) == 1:
        value = values[0]
        keyword = get_keyword(value)
        if keyword in ('contain', 'cover'):
            return keyword
        if keyword == 'auto':
            return ('auto', 'auto')
        if is_dimension_or_percentage(value, negative=False):
            return (value, 'auto')
    elif len(values) == 2:
        new_values = []
        for value in values:
            if get_keyword(value) == 'auto':
                new_values.append('auto')
            elif is_dimension_or_percentage(value, negative=False):
                new_values.append(value)
            else:
                return
        return tuple(values)


@validator('background_clip')
@validator('background_origin')
@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')


@validator()
def border_spacing(values):
    """Validator for the `border-spacing` property."""
    if all(is_dimension(value, negative=False) for value in values):
        if len(values) == 1:
            return (values[0], values[0])
        elif len(values) == 2:
            return tuple(values)


@validator('border-top-style')
@validator('border-right-style')
@validator('border-left-style')
@validator('border-bottom-style')
@single_keyword
def border_style(keyword):
    """``border-*-style`` properties validation."""
    return keyword in ('none', 'hidden', 'dotted', 'dashed', 'double',
                       'inset', 'outset', 'groove', 'ridge', 'solid')


@validator('border-top-width')
@validator('border-right-width')
@validator('border-left-width')
@validator('border-bottom-width')
@single_value
def border_width(value):
    """``border-*-width`` properties validation."""
    if is_dimension(value, negative=False):
        return value
    keyword = get_keyword(value)
    if keyword in ('thin', 'medium', 'thick'):
        return keyword


@validator()
@single_keyword
def box_sizing(keyword):
    """Validation for the ``box-sizing`` property from css3-ui"""
    return keyword in ('padding-box', 'border-box')


@validator()
@single_keyword
def caption_side(keyword):
    """``caption-side`` properties validation."""
    return keyword in ('top', 'bottom')


@validator()
@single_value
def clip(value):
    """Validation for the ``clip`` property."""
    function = parse_function(value)
    if function:
        name, args = function
        if name == 'rect' and len(args) == 4:
            values = []
            for arg in args:
                if get_keyword(arg) == 'auto':
                    values.append('auto')
                elif is_dimension(arg, negative=True):
                    values.append(arg)
                else:
                    raise InvalidValues
            return values
    if get_keyword(value) == 'auto':
        return []


@validator()
def content(values):
    """``content`` property validation."""
    keyword = get_single_keyword(values)
    if keyword in ('normal', 'none'):
        return keyword
    parsed_values = [validate_content_value(v) for v in values]
    if None not in parsed_values:
        return parsed_values


def validate_content_value(value):
    """Validation for a signle value for the ``content`` property.

    Return (type, content) or False for invalid values.

    """
    quote_type = CONTENT_QUOTE_KEYWORDS.get(get_keyword(value))
    if quote_type is not None:
        return ('QUOTE', quote_type)

    type_ = value.type
    if type_ == 'STRING':
        return ('STRING', value.value)
    if type_ == 'URI':
        return ('URI', value.absoluteUri)
    function = parse_function(value)
    if function:
        name, args = function
        prototype = (name, [a.type for a in args])
        args = [a.value for a in args]
        if prototype == ('attr', ['IDENT']):
            return (name, args[0])
        elif prototype in (('counter', ['IDENT']),
                           ('counters', ['IDENT', 'STRING'])):
            args.append('decimal')
            return (name, args)
        elif prototype in (('counter', ['IDENT', 'IDENT']),
                           ('counters', ['IDENT', 'STRING', 'IDENT'])):
            style = args[-1]
            if style in ('none', 'decimal') or style in counters.STYLES:
                return (name, args)


def parse_function(value):
    """Return ``(name, args)`` if the given value is a function
    with comma-separated arguments, or None.
    .
    """
    if value.type == 'FUNCTION':
        seq = [e.value for e in value.seq]
        # seq is expected to look like
        # ['name(', ARG_1, ',', ARG_2, ',', ..., ARG_N, ')']
        if (seq[0][-1] == '(' and seq[-1] == ')' and
                all(v == ',' for v in seq[2:-1:2])):
            name = seq[0][:-1]
            args = seq[1:-1:2]
            return name, args


@validator()
def counter_increment(values):
    """``counter-increment`` property validation."""
    return counter(values, default_integer=1)


@validator()
def counter_reset(values):
    """``counter-reset`` property validation."""
    return counter(values, default_integer=0)


def counter(values, default_integer):
    """``counter-increment`` and ``counter-reset`` properties validation."""
    if get_single_keyword(values) == 'none':
        return []
    values = iter(values)
    value = next(values, None)
    if value is None:
        return  # expected at least one value
    results = []
    while value is not None:
        counter_name = get_keyword(value)
        if counter_name is None:
            return  # expected a keyword here
        if counter_name in ('none', 'initial', 'inherit'):
            raise InvalidValues('Invalid counter name: '+ counter_name)
        value = next(values, None)
        if (value is not None and value.type == 'NUMBER' and
                isinstance(value.value, int)):
            # Found an integer. Use it and get the next value
            integer = value.value
            value = next(values, None)
        else:
            # Not an integer. Might be the next counter name.
            # Keep `value` for the next loop iteration.
            integer = default_integer
        results.append((counter_name, integer))
    return results


#@validator('top')
#@validator('right')
#@validator('left')
#@validator('bottom')
@validator('margin-top')
@validator('margin-right')
@validator('margin-bottom')
@validator('margin-left')
@single_value
def lenght_precentage_or_auto(value, negative=True):
    """``margin-*`` properties validation."""
    if is_dimension_or_percentage(value, negative):
        return value
    if get_keyword(value) == 'auto':
        return 'auto'


@validator('height')
@validator('width')
@single_value
def width_height(value):
    """Validation for the ``width`` and ``height`` properties."""
    return lenght_precentage_or_auto.__func__(value, negative=False)


@validator()
@single_keyword
def direction(keyword):
    """``direction`` property validation."""
    return keyword in ('ltr', 'rtl')


@validator()
@single_keyword
def display(keyword):
    """``display`` property validation."""
    if keyword in ('inline-block',):
        raise InvalidValues('value not supported yet')
    return keyword in (
        'inline', 'block', 'list-item', 'none',
        'table', 'inline-table', 'table-caption',
        'table-row-group', 'table-header-group', 'table-footer-group',
        'table-row', 'table-column-group', 'table-column', 'table-cell')


@validator()
def font_family(values):
    """``font-family`` property validation."""
    # TODO: we should split on commas only.
    # " If a sequence of identifiers is given as a font family name, the
    #   computed value is the name converted to a string by joining all the
    #   identifiers in the sequence by single spaces. "
    # http://www.w3.org/TR/CSS21/fonts.html#font-family-prop
    # eg. `font-family: Foo  bar, "baz
    if all(value.type in ('IDENT', 'STRING') for value in values):
        return [value.value for value in values]


@validator()
@single_value
def font_size(value):
    """``font-size`` property validation."""
    if is_dimension_or_percentage(value):
        return value
    font_size_keyword = get_keyword(value)
    if font_size_keyword in ('smaller', 'larger'):
        raise InvalidValues('value not supported yet')
    if (
        font_size_keyword in computed_values.FONT_SIZE_KEYWORDS #or
        #keyword in ('smaller', 'larger')
    ):
        return font_size_keyword


@validator()
@single_keyword
def font_style(keyword):
    """``font-style`` property validation."""
    return keyword in ('normal', 'italic', 'oblique')


@validator()
@single_keyword
def font_variant(keyword):
    """``font-variant`` property validation."""
    return keyword in ('normal', 'small-caps')


@validator()
@single_value
def font_weight(value):
    """``font-weight`` property validation."""
    keyword = get_keyword(value)
    if keyword in ('normal', 'bold', 'bolder', 'lighter'):
        return keyword
    if value.type == 'NUMBER':
        value = value.value
        if value in [100, 200, 300, 400, 500, 600, 700, 800, 900]:
            return value


@validator('letter_spacing')
@validator('word_spacing')
@single_value
def spacing(value):
    """Validation for ``letter-spacing`` and ``word-spacing``."""
    if get_keyword(value) == 'normal':
        return 'normal'
    if is_dimension(value):
        return value


@validator()
@single_value
def line_height(value):
    """``line-height`` property validation."""
    if get_keyword(value) == 'normal':
        return 'normal'
    if (value.type in ('NUMBER', 'DIMENSION', 'PERCENTAGE') and
            value.value >= 0):
        return value


@validator()
@single_keyword
def list_style_position(keyword):
    """``list-style-position`` property validation."""
    return keyword in ('inside', 'outside')


@validator()
@single_keyword
def list_style_type(keyword):
    """``list-style-type`` property validation."""
    return keyword in ('none', 'decimal') or keyword in counters.STYLES


@validator('padding-top')
@validator('padding-right')
@validator('padding-bottom')
@validator('padding-left')
@single_value
def length_or_precentage(value):
    """``padding-*`` properties validation."""
    if is_dimension_or_percentage(value, negative=False):
        return value


@validator()
@single_value
def opacity(value):
    """Validation for the ``opacity`` property."""
    if value.type == 'NUMBER':
        return min(1, max(0, value.value))


@validator()
@single_keyword
def overflow(keyword):
    """Validation for the ``overflow`` property."""
    return keyword in ('auto', 'visible', 'hidden', 'scroll')


@validator('page-break-before')
@validator('page-break-after')
@single_keyword
def page_break(keyword):
    """Validation for the ``page-break-before`` and ``page-break-after``
    properties.

    """
    if keyword == 'avoid':
        raise InvalidValues('value not supported yet')
    return keyword in ('auto', 'always', 'left', 'right')


# Not very useful, might as well ignore the property anyway.
# Keep it for completeness.
@validator()
@single_keyword
def page_break_inside(keyword):
    """Validation for the ``page-break-inside`` property."""
    if keyword == 'avoid':
        raise InvalidValues('value not supported yet')
    return keyword  ('auto',)


@validator()
@single_keyword
def position(keyword):
    """``position`` property validation."""
    if keyword in ('relative', 'absolute', 'fixed'):
        raise InvalidValues('value not supported yet')
    return keyword in ('static',)


@validator()
def quotes(values):
    """``quotes`` property validation."""
    if (values and len(values) % 2 == 0
            and all(v.type == 'STRING' for v in values)):
        strings = [v.value for v in values]
        # Separate open and close quotes.
        # eg.  ['«', '»', '“', '”']  -> (['«', '“'], ['»', '”'])
        return strings[::2], strings[1::2]


@validator()
@single_keyword
def text_align(keyword):
    """``text-align`` property validation."""
    return keyword in ('left', 'right', 'center', 'justify')


@validator()
def text_decoration(values):
    """``text-decoration`` property validation."""
    keywords = [get_keyword(v) for v in values]
    if keywords == ['none']:
        return 'none'
    if all(keyword in ('underline', 'overline', 'line-through', 'blink')
            for keyword in keywords):
        unique = frozenset(keywords)
        if len(unique) == len(keywords):
            # No duplicate
            return unique


@validator()
@single_value
def text_indent(value):
    """``text-indent`` property validation."""
    if is_dimension_or_percentage(value, negative=True):
        return value


@validator()
@single_keyword
def text_transform(keyword):
    """``text-align`` property validation."""
    return keyword in ('none', 'uppercase', 'lowercase', 'capitalize')


@validator()
@single_value
def vertical_align(value):
    """Validation for the ``vertical-align`` property"""
    if is_dimension_or_percentage(value, negative=True):
        return value
    keyword = get_keyword(value)
    if keyword in ('baseline', 'middle', 'sub', 'super',
                   'text-top', 'text-bottom', 'top', 'bottom'):
        return keyword


@validator()
@single_keyword
def visibility(keyword):
    """``white-space`` property validation."""
    return keyword in ('visible', 'hidden', 'collapse')


@validator()
@single_keyword
def white_space(keyword):
    """``white-space`` property validation."""
    return keyword in ('normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line')


@validator(prefixed=True)  # Taken from SVG
@single_keyword
def image_rendering(keyword):
    """Validation for ``image-rendering``."""
    return keyword in ('auto', 'optimizeSpeed', 'optimizeQuality')


@validator(prefixed=True)  # Not in CR yet
def size(values):
    """``size`` property validation.

    See http://www.w3.org/TR/css3-page/#page-size-prop

    """
    if is_dimension(values[0]):
        if len(values) == 1:
            return values * 2
        elif len(values) == 2 and is_dimension(values[1]):
            return values

    keywords = [get_keyword(v) for v in values]
    if len(keywords) == 1:
        keyword = keywords[0]
        if keyword in ('auto', 'portrait'):
            return INITIAL_VALUES['size']
        elif keyword == 'landscape':
            height, width = INITIAL_VALUES['size']
            return width, height
        elif keyword in computed_values.PAGE_SIZES:
            return computed_values.PAGE_SIZES[keyword]

    if len(keywords) == 2:
        if keywords[0] in ('portrait', 'landscape'):
            orientation, page_size = keywords
        elif keywords[1] in ('portrait', 'landscape'):
            page_size, orientation = keywords
        else:
            page_size = None
        if page_size in computed_values.PAGE_SIZES:
            width_height = computed_values.PAGE_SIZES[page_size]
            if orientation == 'portrait':
                return width_height
            else:
                height, width = width_height
                return width, height


@validator(prefixed=True)  # Not in CR yet
def transform(values):
    if get_single_keyword(values) == 'none':
        return 'none'
    else:
        return [transform_function(v) for v in values]


def transform_function(value):
    function = parse_function(value)
    if not function:
        raise InvalidValues
    name, args = function  # cssutils has already made name lower-case

    if len(args) == 1:
        if name in ('rotate', 'skewx', 'skewy') and is_angle(args[0]):
            return name, args[0]
        elif name in ('translatex', 'translate') and is_dimension_or_percentage(args[0]):
            return 'translate', (args[0], 0)
        elif name == 'translatey' and is_dimension_or_percentage(args[0]):
            return 'translate', (0, args[0])
        elif name == 'scalex' and args[0].type == 'NUMBER':
            return 'scale', (args[0].value, 1)
        elif name == 'scaley' and args[0].type == 'NUMBER':
            return 'scale', (1, args[0].value)
        elif name == 'scale' and args[0].type == 'NUMBER':
            return 'scale', (args[0].value,) * 2
    elif len(args) == 2:
        if name == 'scale' and all(a.type == 'NUMBER' for a in args):
            return name, [arg.value for arg in args]
        if name == 'translate' and all(map(is_dimension_or_percentage, args)):
            return name, args
    elif len(args) == 6 and name == 'matrix' and all(
            a.type == 'NUMBER' for a in args):
        return name, [arg.value for arg in args]
    raise InvalidValues


# Expanders

# Let's be consistent, always use ``name`` as an argument even
# when it is useless.
# pylint: disable=W0613

def expander(property_name):
    """Decorator adding a function to the ``EXPANDERS``."""
    property_name = property_name.replace('-', '_')
    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')
def expand_four_sides(name, values):
    """Expand properties setting a value for the four sides of a box."""
    # Make sure we have 4 values
    if len(values) == 1:
        values *= 4
    elif len(values) == 2:
        values *= 2  # (bottom, left) defaults to (top, right)
    elif len(values) == 3:
        values.append(values[1])  # left defaults to right
    elif len(values) != 4:
        raise InvalidValues(
            'Expected 1 to 4 value components got %i' % len(values))
    for suffix, value in zip(('_top', '_right', '_bottom', '_left'), values):
        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(new_name, [value], required=True)
        yield result


def generic_expander(*expanded_names):
    """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.

    """
    expanded_names = [name.replace('-', '_') for name in expanded_names]
    def generic_expander_decorator(wrapped):
        """Decorate the ``wrapped`` expander."""
        @functools.wraps(wrapped)
        def generic_expander_wrapper(name, values):
            """Wrap the expander."""
            keyword = get_single_keyword(values)
            if keyword in ('inherit', 'initial'):
                results = dict.fromkeys(expanded_names, keyword)
                skip_validation = True
            else:
                skip_validation = False
                results = {}
                for new_name, new_values in wrapped(name, values):
                    assert new_name in expanded_names, new_name
                    assert new_name not in results, new_name
                    results[new_name] = new_values

            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:
                    values = results[new_name]
                    if not skip_validation:
                        # validate_non_shorthand returns [(name, value)]
                        (actual_new_name, values), = validate_non_shorthand(
                            actual_new_name, values, required=True)
                else:
                    values = INITIAL_VALUES[actual_new_name]

                yield actual_new_name, values
        return generic_expander_wrapper
    return generic_expander_decorator


@expander('list-style')
@generic_expander('-type', '-position', '-image')
def expand_list_style(name, values):
    """Expand the ``list-style`` shorthand property.

    See http://www.w3.org/TR/CSS21/generate.html#propdef-list-style

    """
    type_specified = image_specified = False
    none_count = 0
    for value in values:
        if get_keyword(value) == 'none':
            # Can be either -style or -image, see at the end which is not
            # otherwise specified.
            none_count += 1
            none_value = value
            continue

        if list_style_type([value]) is not None:
            suffix = '_type'
            type_specified = True
        elif list_style_position([value]) is not None:
            suffix = '_position'
        elif image([value]) is not None:
            suffix = '_image'
            image_specified = True
        else:
            raise InvalidValues
        yield suffix, [value]

    if not type_specified and none_count:
        yield '_type', [none_value]
        none_count -= 1

    if not image_specified and none_count:
        yield '_image', [none_value]
        none_count -= 1

    if none_count:
        # Too many none values.
        raise InvalidValues


@expander('border')
def expand_border(name, values):
    """Expand the ``border`` shorthand property.

    See http://www.w3.org/TR/CSS21/box.html#propdef-border

    """
    for suffix in ('_top', '_right', '_bottom', '_left'):
        for new_prop in expand_border_side(name + suffix, values):
            yield new_prop


@expander('border-top')
@expander('border-right')
@expander('border-bottom')
@expander('border-left')
@generic_expander('-width', '-color', '-style')
def expand_border_side(name, values):
    """Expand the ``border-*`` shorthand properties.

    See http://www.w3.org/TR/CSS21/box.html#propdef-border-top

    """
    for value in values:
        if color([value]) is not None:
            suffix = '_color'
        elif border_width([value]) is not None:
            suffix = '_width'
        elif border_style([value]) is not None:
            suffix = '_style'
        else:
            raise InvalidValues
        yield suffix, [value]


def is_valid_background_positition(value):
    """Tell whether the value is valid for ``background-position``."""
    return (
        value.type in ('DIMENSION', 'PERCENTAGE') or
        (value.type == 'NUMBER' and value.value == 0) or
        get_keyword(value) in ('left', 'right', 'top', 'bottom', 'center'))


@expander('background')
@generic_expander('-color', '-image', '-repeat', '-attachment', '-position')
def expand_background(name, values):
    """Expand the ``background`` shorthand property.

    See http://www.w3.org/TR/CSS21/colors.html#propdef-background

    """
    # Make `values` a stack
    values = list(reversed(values))
    while values:
        value = values.pop()
        if color([value]) is not None:
            suffix = '_color'
        elif image([value]) is not None:
            suffix = '_image'
        elif background_repeat([value]) is not None:
            suffix = '_repeat'
        elif background_attachment([value]) is not None:
            suffix = '_attachment'
        elif background_position([value]):
            if values:
                next_value = values.pop()
                if background_position([value, next_value]):
                    # Two consecutive '-position' values, yield them together
                    yield '_position', [value, next_value]
                    continue
                else:
                    # The next value is not a '-position', put it back
                    # for the next loop iteration
                    values.append(next_value)
            # A single '-position' value
            suffix = '_position'
        else:
            raise InvalidValues
        yield suffix, [value]


@expander('font')
@generic_expander('-style', '-variant', '-weight', '-size',
                  'line-height', '-family')  # line-height is not a suffix
def expand_font(name, values):
    """Expand the ``font`` shorthand property.

    http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
    """
    expand_font_keyword = get_single_keyword(values)
    if expand_font_keyword in ('caption', 'icon', 'menu', 'message-box',
                               'small-caption', 'status-bar'):
        LOGGER.warn(
            'System fonts are not supported, `font: %s` ignored.',
            expand_font_keyword)
        return

    # Make `values` a stack
    values = list(reversed(values))
    # Values for font-style font-variant and font-weight can come in any
    # order and are all optional.
    while values:
        value = values.pop()
        if get_keyword(value) == 'normal':
            # Just ignore 'normal' keywords. Unspecified properties will get
            # their initial value, which is 'normal' for all three here.
            continue

        if font_style([value]) is not None:
            suffix = '_style'
        elif font_variant([value]) is not None:
            suffix = '_variant'
        elif font_weight([value]) is not None:
            suffix = '_weight'
        else:
            # We’re done with these three, continue with font-size
            break
        yield suffix, [value]

    # Then font-size is mandatory
    # Latest `value` from the loop.
    if font_size([value]) is None:
        raise InvalidValues
    yield '_size', [value]

    # Then line-height is optional, but font-family is not so the list
    # must not be empty yet
    if not values:
        raise InvalidValues

    value = values.pop()
    if line_height([value]) is not None:
        yield 'line_height', [value]
    else:
        # We pop()ed a font-family, add it back
        values.append(value)

    # Reverse the stack to get normal list
    values.reverse()
    if font_family(values) is None:
        raise InvalidValues
    yield '_family', values


def validate_non_shorthand(name, values, required=False):
    """Default validator for non-shorthand properties."""
    if not required and name not in INITIAL_VALUES:
        raise InvalidValues('unknown property')

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

    keyword = get_single_keyword(values)
    if keyword in ('initial', 'inherit'):
        value = keyword
    else:
        value = VALIDATORS[name](values)
        if value is None:
            raise InvalidValues
    return [(name, value)]


def validate_and_expand(name, values):
    """Expand and validate shorthand properties.

    The invalid or unsupported declarations are ignored and logged.

    Return a iterable of ``(name, values)`` tuples.

    """
    if name in PREFIXED and not name.startswith(PREFIX):
        level = 'warn'
        reason = ('the property is experimental, use ' +
                  (PREFIX + name).replace('_', '-'))
    elif name in NOT_PRINT_MEDIA:
        level = 'info'
        reason = 'the property does not apply for the print media'
    else:
        if name.startswith(PREFIX):
            unprefixed_name = name[len(PREFIX):]
            if unprefixed_name in PREFIXED:
                name = unprefixed_name
        expander_ = EXPANDERS.get(name, validate_non_shorthand)
        try:
            results = expander_(name, values)
            # Use list() to consume any generator now,
            # so that InvalidValues is caught.
            return list(results)
        except InvalidValues as exc:
            level = 'warn'
            if exc.args and exc.args[0]:
                reason = exc.args[0]
            else:
                reason = 'invalid value'
    getattr(LOGGER, level)('Ignored declaration: `%s: %s`, %s.',
        name.replace('_', '-'), as_css(values), reason)
    return []
