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

1156 lines
36 KiB
Python
Raw Normal View History

# coding: utf8
"""
weasyprint.css.validation
-------------------------
Expand shorthands and validate property values.
See http://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.
:copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from __future__ import division, unicode_literals
import functools
from tinycss.color3 import parse_color
from tinycss.parsing import split_on_comma
from ..logger import LOGGER
from ..formatting_structure import counters
from ..compat import urljoin
2012-04-02 16:45:44 +04:00
from .values import get_keyword, get_single_keyword, make_percentage_value
from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA
from . import computed_values
# TODO: unit-test these validators
2011-12-02 21:02:58 +04:00
# 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):
2011-08-23 19:35:10 +04:00
"""Invalid or unsupported values for a known CSS property."""
2011-08-23 19:35:10 +04:00
# Validators
def validator(property_name=None, prefixed=False, wants_base_url=False):
2011-08-23 19:35:10 +04:00
"""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):
2011-08-23 19:35:10 +04:00
"""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
function.wants_base_url = wants_base_url
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)
2011-08-23 19:35:10 +04:00
def keyword_validator(values):
"""Wrap a validator to call get_single_keyword on values."""
keyword = get_single_keyword(values)
if function(keyword):
return keyword
2011-08-23 19:35:10 +04:00
return keyword_validator
def single_value(function):
"""Decorator for validators that only accept a single value."""
@functools.wraps(function)
def single_value_validator(values, *args):
2011-08-23 19:35:10 +04:00
"""Validate a property whose value is single."""
if len(values) == 1:
return function(values[0], *args)
single_value_validator.__func__ = function
2011-08-23 19:35:10 +04:00
return single_value_validator
def is_dimension(value, negative=True):
2011-08-23 19:35:10 +04:00
"""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 (
2011-08-23 19:35:10 +04:00
type_ == 'DIMENSION' and (negative or value.value >= 0) and (
value.unit in computed_values.LENGTHS_TO_PIXELS or
value.unit in ('em', 'ex'))
) or (type_ in ('NUMBER', 'INTEGER') and value.value == 0)
def is_dimension_or_percentage(value, negative=True):
2011-08-23 19:35:10 +04:00
"""Get if ``value`` is a dimension or a percentage.
The ``negative`` argument sets wether negative values are allowed.
"""
return is_dimension(value, negative) or (
2011-08-23 19:35:10 +04:00
value.type == 'PERCENTAGE' and (negative or value.value >= 0))
2012-02-08 18:44:03 +04:00
def is_angle(value):
"""Return whether the argument is an angle value."""
return value.type == 'DIMENSION' and \
value.unit in computed_values.ANGLE_TO_RADIANS
2012-02-08 18:44:03 +04:00
@validator()
@single_keyword
def background_attachment(keyword):
2011-08-23 19:35:10 +04:00
"""``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')
2012-04-02 16:45:44 +04:00
@single_value
def other_colors(value):
return parse_color(value)
@validator('color')
@single_value
def color(value):
2011-08-23 19:35:10 +04:00
"""``*-color`` and ``color`` properties validation."""
value = parse_color(value)
if value == 'currentColor':
return 'inherit'
else:
return value
@validator('background-image', wants_base_url=True)
@validator('list-style-image', wants_base_url=True)
@single_value
def image(value, base_url):
2011-08-23 19:35:10 +04:00
"""``*-image`` properties validation."""
if get_keyword(value) == 'none':
return 'none'
if value.type == 'URI':
return urljoin(base_url, value.value)
2012-02-08 18:44:03 +04:00
@validator('transform-origin', prefixed=True) # Not in CR yet
@validator()
def background_position(values):
2011-08-23 19:35:10 +04:00
"""``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):
2011-08-23 19:35:10 +04:00
"""``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."""
2011-12-16 16:44:25 +04:00
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):
2011-08-23 19:35:10 +04:00
"""``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):
2011-08-23 19:35:10 +04:00
"""``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):
2011-12-26 15:47:26 +04:00
"""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')
2012-02-07 21:06:59 +04:00
@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(wants_base_url=True)
def content(values, base_url):
"""``content`` property validation."""
keyword = get_single_keyword(values)
if keyword in ('normal', 'none'):
return keyword
parsed_values = [validate_content_value(base_url, v) for v in values]
2011-12-08 14:32:35 +04:00
if None not in parsed_values:
return parsed_values
def validate_content_value(base_url, value):
"""Validation for a signle value for the ``content`` property.
Return (type, content) or False for invalid values.
"""
2011-12-02 21:02:58 +04:00
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', urljoin(base_url, value.value))
2012-02-07 21:06:59 +04:00
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':
content = [token for token in value.content if token.type != 'S']
if len(content) % 2:
for token in content[1::2]:
if token.type != 'DELIM' or token.value != ',':
break
else:
return value.function_name.lower(), content[::2]
@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 == 'INTEGER':
# 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):
2011-08-23 19:35:10 +04:00
"""``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
2011-12-26 15:47:26 +04:00
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):
2011-08-23 19:35:10 +04:00
"""``direction`` property validation."""
return keyword in ('ltr', 'rtl')
@validator()
@single_keyword
def display(keyword):
2011-08-23 19:35:10 +04:00
"""``display`` property validation."""
if keyword in ('inline-block',):
raise InvalidValues('value not supported yet')
return keyword in (
'inline', 'block', 'list-item', 'none',
2011-11-15 21:19:36 +04:00
'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):
2011-08-23 19:35:10 +04:00
"""``font-family`` property validation."""
parts = split_on_comma(values)
families = []
for part in parts:
if len(part) == 1 and part[0].type == 'STRING':
families.append(part[0].value)
elif part and all(token.type == 'IDENT' for token in part):
families.append(' '.join(token.value for token in part))
else:
break
else:
return families
@validator()
@single_value
def font_size(value):
2011-08-23 19:35:10 +04:00
"""``font-size`` property validation."""
if is_dimension_or_percentage(value):
return value
2011-08-23 19:35:10 +04:00
font_size_keyword = get_keyword(value)
if font_size_keyword in ('smaller', 'larger'):
raise InvalidValues('value not supported yet')
if (
2011-08-23 19:35:10 +04:00
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):
2011-08-23 19:35:10 +04:00
"""``font-style`` property validation."""
return keyword in ('normal', 'italic', 'oblique')
@validator()
@single_keyword
def font_variant(keyword):
2011-08-23 19:35:10 +04:00
"""``font-variant`` property validation."""
return keyword in ('normal', 'small-caps')
@validator()
@single_value
def font_weight(value):
2011-08-23 19:35:10 +04:00
"""``font-weight`` property validation."""
keyword = get_keyword(value)
if keyword in ('normal', 'bold', 'bolder', 'lighter'):
return keyword
if value.type == 'INTEGER':
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):
2011-08-23 19:35:10 +04:00
"""``line-height`` property validation."""
if get_keyword(value) == 'normal':
return 'normal'
if (value.type in ('NUMBER', 'INTEGER', 'DIMENSION', 'PERCENTAGE') and
value.value >= 0):
return value
@validator()
@single_keyword
def list_style_position(keyword):
2011-08-23 19:35:10 +04:00
"""``list-style-position`` property validation."""
return keyword in ('inside', 'outside')
@validator()
@single_keyword
def list_style_type(keyword):
2011-08-23 19:35:10 +04:00
"""``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):
2011-08-23 19:35:10 +04:00
"""``padding-*`` properties validation."""
if is_dimension_or_percentage(value, negative=False):
return value
2012-02-07 21:26:23 +04:00
@validator()
@single_value
def opacity(value):
"""Validation for the ``opacity`` property."""
if value.type in ('NUMBER', 'INTEGER'):
2012-02-07 21:26:23 +04:00
return min(1, max(0, value.value))
2012-03-14 22:33:24 +04:00
@validator('orphans')
@validator('widows')
@single_value
def orphans_widows(value):
"""Validation for the ``orphans`` or ``widows`` properties."""
if value.type == 'INTEGER':
2012-03-14 22:33:24 +04:00
value = value.value
if int(value) == value and value >= 1:
return value
2012-02-07 19:59:22 +04:00
@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."""
2012-03-16 19:45:31 +04:00
return keyword in ('auto', 'avoid')
@validator()
@single_keyword
def position(keyword):
2011-08-23 19:35:10 +04:00
"""``position`` property validation."""
if keyword in ('relative', 'absolute', 'fixed'):
raise InvalidValues('value not supported yet')
return keyword in ('static',)
2011-12-02 21:02:58 +04:00
@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):
2011-08-23 19:35:10 +04:00
"""``text-align`` property validation."""
return keyword in ('left', 'right', 'center', 'justify')
@validator()
def text_decoration(values):
2011-08-23 19:35:10 +04:00
"""``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):
2011-12-26 15:47:26 +04:00
"""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
2011-10-21 13:15:06 +04:00
@validator()
@single_keyword
def visibility(keyword):
"""``white-space`` property validation."""
return keyword in ('visible', 'hidden', 'collapse')
@validator()
@single_keyword
def white_space(keyword):
2011-08-23 19:35:10 +04:00
"""``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):
2011-08-23 19:35:10 +04:00
"""``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'):
2011-12-26 15:47:26 +04:00
orientation, page_size = keywords
elif keywords[1] in ('portrait', 'landscape'):
2011-12-26 15:47:26 +04:00
page_size, orientation = keywords
else:
2011-12-26 15:47:26 +04:00
page_size = None
if page_size in computed_values.PAGE_SIZES:
width_height = computed_values.PAGE_SIZES[page_size]
if orientation == 'portrait':
2011-12-26 15:47:26 +04:00
return width_height
else:
2011-12-26 15:47:26 +04:00
height, width = width_height
return width, height
2012-02-08 18:44:03 +04:00
@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]
2012-02-08 18:44:03 +04:00
def transform_function(value):
function = parse_function(value)
if not function:
raise InvalidValues
name, args = function
2012-02-08 18:44:03 +04:00
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]):
2012-02-08 18:44:03 +04:00
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 in ('NUMBER', 'INTEGER'):
2012-02-08 18:44:03 +04:00
return 'scale', (args[0].value, 1)
elif name == 'scaley' and args[0].type in ('NUMBER', 'INTEGER'):
2012-02-08 18:44:03 +04:00
return 'scale', (1, args[0].value)
elif name == 'scale' and args[0].type in ('NUMBER', 'INTEGER'):
return 'scale', (args[0].value,) * 2
2012-02-08 18:44:03 +04:00
elif len(args) == 2:
if name == 'scale' and all(a.type in ('NUMBER', 'INTEGER')
for a in args):
2012-02-08 18:44:03 +04:00
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 in ('NUMBER', 'INTEGER') for a in args):
2012-02-08 18:44:03 +04:00
return name, [arg.value for arg in args]
raise InvalidValues
2011-08-23 19:35:10 +04:00
# Expanders
# Let's be consistent, always use ``name`` as an argument even
# when it is useless.
2011-08-23 19:35:10 +04:00
# pylint: disable=W0613
def expander(property_name):
2011-08-23 19:35:10 +04:00
"""Decorator adding a function to the ``EXPANDERS``."""
property_name = property_name.replace('-', '_')
2011-08-23 19:35:10 +04:00
def expander_decorator(function):
"""Add ``function`` to the ``EXPANDERS``."""
assert property_name not in EXPANDERS, property_name
EXPANDERS[property_name] = function
return function
2011-08-23 19:35:10 +04:00
return expander_decorator
@expander('border-color')
@expander('border-style')
@expander('border-width')
@expander('margin')
@expander('padding')
def expand_four_sides(base_url, name, values):
2011-08-23 19:35:10 +04:00
"""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:
2011-08-23 19:35:10 +04:00
values *= 2 # (bottom, left) defaults to (top, right)
elif len(values) == 3:
2011-08-23 19:35:10 +04:00
values.append(values[1]) # left defaults to right
elif len(values) != 4:
2011-08-23 19:35:10 +04:00
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(
base_url, new_name, [value], required=True)
yield result
def generic_expander(*expanded_names, **kwargs):
2011-08-23 19:35:10 +04:00
"""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.
2011-08-23 19:35:10 +04:00
"""
wants_base_url = kwargs.pop('wants_base_url', False)
assert not kwargs
expanded_names = [name.replace('-', '_') for name in expanded_names]
2011-08-23 19:35:10 +04:00
def generic_expander_decorator(wrapped):
"""Decorate the ``wrapped`` expander."""
@functools.wraps(wrapped)
def generic_expander_wrapper(base_url, name, values):
2011-08-23 19:35:10 +04:00
"""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 = {}
if wants_base_url:
result = wrapped(name, values, base_url)
else:
result = wrapped(name, values)
for new_name, new_values in result:
assert new_name in expanded_names, new_name
if new_name in results:
raise InvalidValues(
'got multiple %s values in a %s shorthand'
% (new_name.strip('_'), 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(
base_url, actual_new_name, values, required=True)
else:
values = INITIAL_VALUES[actual_new_name]
yield actual_new_name, values
2011-08-23 19:35:10 +04:00
return generic_expander_wrapper
return generic_expander_decorator
@expander('list-style')
@generic_expander('-type', '-position', '-image', wants_base_url=True)
def expand_list_style(name, values, base_url):
2011-08-23 19:35:10 +04:00
"""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], base_url) 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(base_url, name, values):
2011-08-23 19:35:10 +04:00
"""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(base_url, 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):
2011-08-23 19:35:10 +04:00
"""Expand the ``border-*`` shorthand properties.
See http://www.w3.org/TR/CSS21/box.html#propdef-border-top
"""
for value in values:
2012-04-02 16:45:44 +04:00
if parse_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):
2011-08-23 19:35:10 +04:00
"""Tell whether the value is valid for ``background-position``."""
return (
value.type in ('DIMENSION', 'PERCENTAGE') or
(value.type in ('NUMBER', 'INTEGER') and value.value == 0) or
2011-08-23 19:35:10 +04:00
get_keyword(value) in ('left', 'right', 'top', 'bottom', 'center'))
@expander('background')
@generic_expander('-color', '-image', '-repeat', '-attachment', '-position',
wants_base_url=True)
def expand_background(name, values, base_url):
2011-08-23 19:35:10 +04:00
"""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()
2012-04-02 16:45:44 +04:00
if parse_color(value) is not None:
suffix = '_color'
elif image([value], base_url) 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):
2011-08-23 19:35:10 +04:00
"""Expand the ``font`` shorthand property.
http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
"""
2011-08-23 19:35:10 +04:00
expand_font_keyword = get_single_keyword(values)
if expand_font_keyword in ('caption', 'icon', 'menu', 'message-box',
'small-caption', 'status-bar'):
LOGGER.warn(
2011-08-23 19:35:10 +04:00
'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
2011-10-11 14:09:37 +04:00
if not values:
raise InvalidValues
value = values.pop()
if value.type == 'DELIM' and value.value == '/':
value = values.pop()
if line_height([value]) is None:
raise InvalidValues
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(base_url, name, values, required=False):
2011-08-23 19:35:10 +04:00
"""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:
function = VALIDATORS[name]
if function.wants_base_url:
value = function(values, base_url)
else:
value = function(values)
if value is None:
raise InvalidValues
return [(name, value)]
def validate_and_expand(base_url, name, values):
2011-08-23 19:35:10 +04:00
"""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
2011-08-23 19:35:10 +04:00
expander_ = EXPANDERS.get(name, validate_non_shorthand)
try:
tokens = [token for token in values if token.type != 'S']
results = expander_(base_url, name, tokens)
# 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'
2011-11-25 20:22:59 +04:00
getattr(LOGGER, level)('Ignored declaration: `%s: %s`, %s.',
2012-03-31 20:22:55 +04:00
name.replace('_', '-'), ''.join(v.as_css for v in values), reason)
return []