1
1
mirror of https://github.com/Kozea/WeasyPrint.git synced 2024-10-05 00:21:15 +03:00
WeasyPrint/weasyprint/css/validation.py
2012-03-21 11:36:24 +01:00

1137 lines
35 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.

# 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('orphans')
@validator('widows')
@single_value
def orphans_widows(value):
"""Validation for the ``orphans`` or ``widows`` properties."""
if value.type == 'NUMBER': # INTEGER
value = value.value
if int(value) == value and value >= 1:
return 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."""
return keyword in ('auto', 'avoid')
@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:
# Were 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 []