mirror of
https://github.com/Kozea/WeasyPrint.git
synced 2024-10-05 00:21:15 +03:00
Clean weasy/css/*
This commit is contained in:
parent
39e1987408
commit
496f0e2ead
@ -18,8 +18,8 @@
|
||||
|
||||
"""
|
||||
Normalize values as much as possible without rendering the document.
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
import functools
|
||||
@ -28,23 +28,22 @@ import cssutils.helper
|
||||
from cssutils.css import PropertyValue, Value
|
||||
|
||||
from .properties import INITIAL_VALUES
|
||||
from .values import (get_single_keyword, get_keyword, get_pixel_value,
|
||||
get_single_pixel_value,
|
||||
make_pixel_value, make_number, make_keyword)
|
||||
from .values import (
|
||||
get_single_keyword, get_keyword, get_pixel_value, get_single_pixel_value,
|
||||
make_pixel_value, make_number, make_keyword)
|
||||
|
||||
|
||||
# How many CSS pixels is one <unit> ?
|
||||
# How many CSS pixels is one <unit>?
|
||||
# http://www.w3.org/TR/CSS21/syndata.html#length-units
|
||||
LENGTHS_TO_PIXELS = {
|
||||
'px': 1,
|
||||
'pt': 1. / 0.75,
|
||||
'pc': 16., # LENGTHS_TO_PIXELS['pt'] * 12
|
||||
'in': 96., # LENGTHS_TO_PIXELS['pt'] * 72
|
||||
'cm': 96. / 2.54, # LENGTHS_TO_PIXELS['in'] / 2.54
|
||||
'mm': 96. / 25.4, # LENGTHS_TO_PIXELS['in'] / 25.4
|
||||
'pc': 16., # LENGTHS_TO_PIXELS['pt'] * 12
|
||||
'in': 96., # LENGTHS_TO_PIXELS['pt'] * 72
|
||||
'cm': 96. / 2.54, # LENGTHS_TO_PIXELS['in'] / 2.54
|
||||
'mm': 96. / 25.4, # LENGTHS_TO_PIXELS['in'] / 25.4
|
||||
}
|
||||
|
||||
|
||||
# Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for
|
||||
# medium, and scaling factors given in CSS3 for others:
|
||||
# http://www.w3.org/TR/css3-fonts/#font-size-prop
|
||||
@ -63,7 +62,6 @@ FONT_SIZE_KEYWORDS = collections.OrderedDict(
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
# These are unspecified, other than 'thin' <='medium' <= 'thick'.
|
||||
# Values are in pixels.
|
||||
BORDER_WIDTH_KEYWORDS = {
|
||||
@ -72,7 +70,6 @@ BORDER_WIDTH_KEYWORDS = {
|
||||
'thick': make_pixel_value(5),
|
||||
}
|
||||
|
||||
|
||||
# http://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight
|
||||
FONT_WEIGHT_RELATIVE = dict(
|
||||
bolder={
|
||||
@ -99,7 +96,6 @@ FONT_WEIGHT_RELATIVE = dict(
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# http://www.w3.org/TR/css3-page/#size
|
||||
# name=(width in pixels, height in pixels)
|
||||
PAGE_SIZES = dict(
|
||||
@ -139,9 +135,10 @@ PAGE_SIZES = dict(
|
||||
|
||||
|
||||
class StyleDict(dict):
|
||||
"""
|
||||
Allow attribute access to values, eg. style.font_size instead of
|
||||
style['font-size'].
|
||||
"""Allow attribute access to values.
|
||||
|
||||
Allow eg. ``style.font_size`` instead of ``style['font-size']``.
|
||||
|
||||
"""
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
@ -153,29 +150,31 @@ class StyleDict(dict):
|
||||
self[key.replace('_', '-')] = value
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
Same as dict.copy, but return an object of the same class.
|
||||
(dict.copy() always return a dict.)
|
||||
"""Copy the ``StyleDict``.
|
||||
|
||||
Same as ``dict.copy``, but return an object of the same class
|
||||
(``dict.copy()`` always returns a ``dict``).
|
||||
|
||||
"""
|
||||
return self.__class__(self)
|
||||
|
||||
|
||||
class Computer(object):
|
||||
"""
|
||||
Things that compute are computers, right?
|
||||
"""Things that compute are computers, right?
|
||||
|
||||
:param element: The HTML element these style apply to
|
||||
:param pseudo_type: The type of pseudo-element, eg 'before', None
|
||||
:param specified: a StyleDict of specified values. Should contain values
|
||||
for all properties.
|
||||
:param computed: a StyleDict of already known computed values. Only
|
||||
contains some properties (or none).
|
||||
:param parent_values: a StyleDict of computed values of the parent element
|
||||
(should contain values for all properties),
|
||||
or None if `element` is the root element.
|
||||
:param specified: a :class:`StyleDict` of specified values. Should contain
|
||||
values for all properties.
|
||||
:param computed: a :class:`StyleDict` of already known computed values.
|
||||
Only contains some properties (or none).
|
||||
:param parent_values: a :class:`StyleDict` of computed values of the parent
|
||||
element (should contain values for all properties),
|
||||
or ``None`` if ``element`` is the root element.
|
||||
|
||||
Once instanciated, this object will have completed the `computed` dict
|
||||
Once instanciated, this object will have completed the ``computed`` dict
|
||||
so that is has values for all properties.
|
||||
|
||||
"""
|
||||
def __init__(self, element, pseudo_type, specified, computed,
|
||||
parent_style):
|
||||
@ -190,9 +189,11 @@ class Computer(object):
|
||||
self.get_computed(name)
|
||||
|
||||
def get_computed(self, name):
|
||||
"""
|
||||
Call a "computer" function as needed, populate the `computed` dict
|
||||
and return the computed value for the `name` property.
|
||||
"""Return the computed value for the ``name`` property.
|
||||
|
||||
Call a "computer" function as needed and populate the `computed` dict
|
||||
before return the value.
|
||||
|
||||
"""
|
||||
if name in self.computed:
|
||||
# Already computed
|
||||
@ -212,15 +213,19 @@ class Computer(object):
|
||||
|
||||
@classmethod
|
||||
def register(cls, name):
|
||||
"""Decorator registering a property ``name`` for a function."""
|
||||
def decorator(function):
|
||||
"""Register the property ``name`` for ``function``."""
|
||||
cls.COMPUTER_FUNCTIONS[name] = function
|
||||
return function
|
||||
return decorator
|
||||
|
||||
|
||||
def single_value(function):
|
||||
"""Decorator validating and computing the single-value properties."""
|
||||
@functools.wraps(function)
|
||||
def wrapper(computer, name, values):
|
||||
"""Compute a single-value property."""
|
||||
assert len(values) == 1
|
||||
new_value = function(computer, name, values[0])
|
||||
assert new_value is not None
|
||||
@ -228,12 +233,16 @@ def single_value(function):
|
||||
return wrapper
|
||||
|
||||
|
||||
# Let's be coherent, always use ``name`` as an argument even when it is useless
|
||||
# pylint: disable=W0613
|
||||
|
||||
@Computer.register('background-color')
|
||||
@Computer.register('border-top-color')
|
||||
@Computer.register('border-right-color')
|
||||
@Computer.register('border-bottom-color')
|
||||
@Computer.register('border-left-color')
|
||||
def other_color(computer, name, values):
|
||||
"""Compute the ``*-color`` properties."""
|
||||
if get_single_keyword(values) == 'currentColor':
|
||||
return computer.get_computed('color')
|
||||
else:
|
||||
@ -243,6 +252,7 @@ def other_color(computer, name, values):
|
||||
|
||||
@Computer.register('color')
|
||||
def color(computer, name, values):
|
||||
"""Compute the ``color`` property."""
|
||||
if get_single_keyword(values) == 'currentColor':
|
||||
if computer.parent_style is None:
|
||||
return INITIAL_VALUES['color']
|
||||
@ -271,13 +281,12 @@ def color(computer, name, values):
|
||||
@Computer.register('padding-bottom')
|
||||
@Computer.register('padding-left')
|
||||
def lengths(computer, name, values):
|
||||
"""Compute the properties with a list of lengths."""
|
||||
return [compute_length(computer, value) for value in values]
|
||||
|
||||
|
||||
def compute_length(computer, value):
|
||||
"""
|
||||
Return the computed value for one non-font-size dimension value.
|
||||
"""
|
||||
"""Compute a length ``value``."""
|
||||
if value.type != 'DIMENSION' or value.dimension == 'px':
|
||||
# No conversion needed.
|
||||
return value
|
||||
@ -300,6 +309,7 @@ def compute_length(computer, value):
|
||||
@Computer.register('border-bottom-width')
|
||||
@single_value
|
||||
def border_width(computer, name, value):
|
||||
"""Compute the ``border-*-width`` properties."""
|
||||
style = computer.get_computed(name.replace('width', 'style'))
|
||||
if get_single_keyword(style) in ('none', 'hidden'):
|
||||
return make_number(0)
|
||||
@ -311,32 +321,15 @@ def border_width(computer, name, value):
|
||||
return compute_length(computer, value)
|
||||
|
||||
|
||||
def compute_content_value(computer, value):
|
||||
if value.type == 'FUNCTION':
|
||||
# value.seq is *NOT* part of the public API
|
||||
# TODO: patch cssutils to provide a public API for arguments
|
||||
# in CSSFunction objects
|
||||
assert value.value.startswith('attr(')
|
||||
args = [v.value for v in value.seq if isinstance(v.value, Value)]
|
||||
assert len(args) == 1
|
||||
attr_name = args[0].value
|
||||
content = computer.element.get(attr_name, '')
|
||||
# TODO: find a way to build a string Value without serializing
|
||||
# and re-parsing.
|
||||
value = PropertyValue(cssutils.helper.string(content))[0]
|
||||
assert value.type == 'STRING'
|
||||
assert value.value == content
|
||||
return value
|
||||
|
||||
|
||||
@Computer.register('content')
|
||||
def content(computer, name, values):
|
||||
"""Compute the ``content`` property."""
|
||||
if computer.pseudo_type in ('before', 'after'):
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword == 'normal':
|
||||
return [make_keyword('none')]
|
||||
else:
|
||||
return [compute_content_value(computer, v) for v in values]
|
||||
return [compute_content_value(computer, value) for value in values]
|
||||
else:
|
||||
# CSS 2.1 says it computes to 'normal' for elements, but does not say
|
||||
# anything for pseudo-elements other than :before and :after
|
||||
@ -345,33 +338,56 @@ def content(computer, name, values):
|
||||
return [make_keyword('normal')]
|
||||
|
||||
|
||||
def compute_content_value(computer, value):
|
||||
"""Compute a content ``value``."""
|
||||
if value.type == 'FUNCTION':
|
||||
# value.seq is *NOT* part of the public API
|
||||
# TODO: patch cssutils to provide a public API for arguments
|
||||
# in CSSFunction objects
|
||||
assert value.value.startswith('attr(')
|
||||
args = [v.value for v in value.seq if isinstance(v.value, Value)]
|
||||
assert len(args) == 1
|
||||
attr_name = args[0].value
|
||||
content_value = computer.element.get(attr_name, '')
|
||||
# TODO: find a way to build a string Value without serializing
|
||||
# and re-parsing.
|
||||
value = PropertyValue(cssutils.helper.string(content_value))[0]
|
||||
assert value.type == 'STRING'
|
||||
assert value.value == content
|
||||
return value
|
||||
|
||||
|
||||
@Computer.register('display')
|
||||
def display(computer, name, values):
|
||||
"""
|
||||
http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
"""Compute the ``display`` property.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
|
||||
"""
|
||||
float_ = get_single_keyword(computer.specified['float'])
|
||||
position = get_single_keyword(computer.specified['position'])
|
||||
if position in ('absolute', 'fixed') or float_ != 'none' or \
|
||||
computer.parent_style is None:
|
||||
display = get_single_keyword(computer.specified['display'])
|
||||
if display == 'inline-table':
|
||||
display_value = get_single_keyword(computer.specified['display'])
|
||||
if display_value == 'inline-table':
|
||||
return [make_keyword('table')]
|
||||
elif display in ('inline', 'table-row-group', 'table-column',
|
||||
'table-column-group', 'table-header-group',
|
||||
'table-footer-group', 'table-row', 'table-cell',
|
||||
'table-caption', 'inline-block'):
|
||||
elif display_value in ('inline', 'table-row-group', 'table-column',
|
||||
'table-column-group', 'table-header-group',
|
||||
'table-footer-group', 'table-row', 'table-cell',
|
||||
'table-caption', 'inline-block'):
|
||||
return [make_keyword('block')]
|
||||
return values
|
||||
|
||||
|
||||
@Computer.register('float')
|
||||
def float_(computer, name, values):
|
||||
def compute_float(computer, name, values):
|
||||
"""Compute the ``float`` property.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
|
||||
"""
|
||||
http://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo
|
||||
"""
|
||||
if get_single_keyword(computer.specified['position']) in (
|
||||
'absolute', 'fixed'):
|
||||
position_value = get_single_keyword(computer.specified['position'])
|
||||
if position_value in ('absolute', 'fixed'):
|
||||
return [make_keyword('none')]
|
||||
else:
|
||||
return values
|
||||
@ -380,6 +396,7 @@ def float_(computer, name, values):
|
||||
@Computer.register('font-size')
|
||||
@single_value
|
||||
def font_size(computer, name, value):
|
||||
"""Compute the ``font-size`` property."""
|
||||
keyword = get_keyword(value)
|
||||
if keyword in FONT_SIZE_KEYWORDS:
|
||||
return FONT_SIZE_KEYWORDS[keyword]
|
||||
@ -414,6 +431,7 @@ def font_size(computer, name, value):
|
||||
@Computer.register('font-weight')
|
||||
@single_value
|
||||
def font_weight(computer, name, value):
|
||||
"""Compute the ``font-weight`` property."""
|
||||
keyword = get_keyword(value)
|
||||
if keyword == 'normal':
|
||||
return make_number(400)
|
||||
@ -439,6 +457,7 @@ def font_weight(computer, name, value):
|
||||
@Computer.register('line-height')
|
||||
@single_value
|
||||
def line_height(computer, name, value):
|
||||
"""Compute the ``line-height`` property."""
|
||||
if get_keyword(value) == 'normal':
|
||||
# a "reasonable" value
|
||||
# http://www.w3.org/TR/CSS21/visudet.html#line-height
|
||||
@ -450,15 +469,18 @@ def line_height(computer, name, value):
|
||||
factor = value.value / 100.
|
||||
elif value.type == 'DIMENSION':
|
||||
return compute_length(computer, value)
|
||||
font_size = get_single_pixel_value(computer.get_computed('font-size'))
|
||||
font_size_value = get_single_pixel_value(
|
||||
computer.get_computed('font-size'))
|
||||
# Raise if `factor` is not defined. It should be, because of validation.
|
||||
return make_pixel_value(factor * font_size)
|
||||
return make_pixel_value(factor * font_size_value)
|
||||
|
||||
|
||||
@Computer.register('size')
|
||||
def size(computer, name, values):
|
||||
"""
|
||||
"""Compute the ``size`` property.
|
||||
|
||||
See CSS3 Paged Media.
|
||||
|
||||
"""
|
||||
if computer.element != '@page':
|
||||
return [None]
|
||||
@ -467,7 +489,7 @@ def size(computer, name, values):
|
||||
|
||||
keywords = map(get_keyword, values)
|
||||
if keywords == ['auto']:
|
||||
keywords = ['A4'] # Chosen by the UA. (That’s me!)
|
||||
keywords = ['A4'] # Chosen by the UA. (That’s me!)
|
||||
|
||||
if values[0].type == 'DIMENSION':
|
||||
assert values[0].dimension == 'px'
|
||||
@ -480,17 +502,17 @@ def size(computer, name, values):
|
||||
return values * 2 # list product, same as [values[0], values[0]]
|
||||
else:
|
||||
orientation = None
|
||||
size = None
|
||||
size_value = None
|
||||
for keyword in keywords:
|
||||
if keyword in ('portrait', 'landscape'):
|
||||
orientation = keyword
|
||||
elif keyword in PAGE_SIZES:
|
||||
size = keyword
|
||||
size_value = keyword
|
||||
else:
|
||||
raise ValueError("Illegal value for 'size': %r", keyword)
|
||||
if size is None:
|
||||
size = 'A4'
|
||||
width, height = PAGE_SIZES[size]
|
||||
if size_value is None:
|
||||
size_value = 'A4'
|
||||
width, height = PAGE_SIZES[size_value]
|
||||
if (orientation == 'portrait' and width > height) or \
|
||||
(orientation == 'landscape' and height > width):
|
||||
width, height = height, width
|
||||
@ -500,6 +522,7 @@ def size(computer, name, values):
|
||||
@Computer.register('text-align')
|
||||
@single_value
|
||||
def text_align(computer, name, value):
|
||||
"""Compute the ``text-align`` property."""
|
||||
if get_keyword(value) == 'start':
|
||||
if get_single_keyword(computer.get_computed('direction')) == 'rtl':
|
||||
return make_keyword('right')
|
||||
@ -512,6 +535,7 @@ def text_align(computer, name, value):
|
||||
@Computer.register('word-spacing')
|
||||
@single_value
|
||||
def word_spacing(computer, name, value):
|
||||
"""Compute the ``word-spacing`` property."""
|
||||
if get_keyword(value) == 'normal':
|
||||
return make_number(0)
|
||||
|
||||
|
@ -35,31 +35,30 @@ from . import computed_values
|
||||
|
||||
LOGGER = logging.getLogger('WEASYPRINT')
|
||||
|
||||
|
||||
# yes/no validators for non-shorthand properties
|
||||
# Maps property names to functions taking a property name and a value list,
|
||||
# returning True for valid, False for invalid.
|
||||
VALIDATORS = {}
|
||||
|
||||
|
||||
EXPANDERS = {}
|
||||
|
||||
|
||||
class InvalidValues(ValueError):
|
||||
"""
|
||||
Exception for invalid or unsupported values for a known CSS property.
|
||||
"""
|
||||
"""Invalid or unsupported values for a known CSS property."""
|
||||
|
||||
class NotSupportedYet(InvalidValues):
|
||||
def __init__(self):
|
||||
super(NotSupportedYet, self).__init__('property not supported yet')
|
||||
|
||||
# Validators
|
||||
|
||||
def validator(property_name=None):
|
||||
"""
|
||||
Decorator to add a function to the VALIDATORS dict.
|
||||
"""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__.replace('_', '-')
|
||||
else:
|
||||
@ -73,55 +72,56 @@ def validator(property_name=None):
|
||||
|
||||
|
||||
def keyword(function):
|
||||
"""Decorator validating properties whose value is a defined key word."""
|
||||
valid_keywords = function()
|
||||
@functools.wraps(function)
|
||||
def validator(values):
|
||||
def keyword_validator(values):
|
||||
"""Validate a property whose value is a defined key word."""
|
||||
return get_single_keyword(values) in valid_keywords
|
||||
return validator
|
||||
return keyword_validator
|
||||
|
||||
|
||||
def single_value(function):
|
||||
"""Decorator validating properties whose value is single."""
|
||||
@functools.wraps(function)
|
||||
def validator(values):
|
||||
def single_value_validator(values):
|
||||
"""Validate a property whose value is single."""
|
||||
if len(values) != 1:
|
||||
return False
|
||||
else:
|
||||
return function(values[0])
|
||||
return validator
|
||||
return single_value_validator
|
||||
|
||||
|
||||
def is_dimension(value, negative=True):
|
||||
"""
|
||||
`negative` means that negative values are allowed.
|
||||
"""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
|
||||
(
|
||||
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
|
||||
)
|
||||
value.dimension in ('em', 'ex'))
|
||||
) or (type_ == 'NUMBER' and value.value == 0)
|
||||
|
||||
|
||||
def is_dimension_or_percentage(value, negative=True):
|
||||
"""
|
||||
`negative` means that negative values are allowed.
|
||||
"""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)
|
||||
)
|
||||
value.type == 'PERCENTAGE' and (negative or value.value >= 0))
|
||||
|
||||
|
||||
@validator()
|
||||
@keyword
|
||||
def background_attachment():
|
||||
"""``background-attachment`` property validation."""
|
||||
return 'scroll', 'fixed'
|
||||
|
||||
|
||||
@ -133,6 +133,7 @@ def background_attachment():
|
||||
@validator('color')
|
||||
@single_value
|
||||
def color(value):
|
||||
"""``*-color`` and ``color`` properties validation."""
|
||||
return value.type == 'COLOR_VALUE' or get_keyword(value) == 'currentColor'
|
||||
|
||||
|
||||
@ -140,26 +141,28 @@ def color(value):
|
||||
@validator('list-style-image')
|
||||
@single_value
|
||||
def image(value):
|
||||
"""``*-image`` properties validation."""
|
||||
return get_keyword(value) == 'none' or value.type == 'URI'
|
||||
|
||||
|
||||
@validator()
|
||||
def background_position(values):
|
||||
"""://www.w3.org/TR/CSS21/colors.html#propdef-background-position
|
||||
"""``background-position`` property validation.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/colors.html#propdef-background-position
|
||||
|
||||
"""
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
return (
|
||||
is_dimension_or_percentage(value) or
|
||||
get_keyword(value) in ('left', 'right', 'top', 'bottom', 'center')
|
||||
)
|
||||
get_keyword(value) in ('left', 'right', 'top', 'bottom', 'center'))
|
||||
if len(values) == 2:
|
||||
value_1, value_2 = values
|
||||
if is_dimension_or_percentage(value_1):
|
||||
return (
|
||||
is_dimension_or_percentage(value_2) or
|
||||
get_keyword(value_2) in ('top', 'center', 'bottom')
|
||||
)
|
||||
get_keyword(value_2) in ('top', 'center', 'bottom'))
|
||||
elif is_dimension_or_percentage(value_2):
|
||||
return get_keyword(value_1) in ('left', 'center', 'right')
|
||||
else:
|
||||
@ -169,8 +172,7 @@ def background_position(values):
|
||||
keyword_2 in ('top', 'center', 'bottom')
|
||||
) or (
|
||||
keyword_1 in ('top', 'center', 'bottom') and
|
||||
keyword_2 in ('left', 'center', 'right')
|
||||
)
|
||||
keyword_2 in ('left', 'center', 'right'))
|
||||
else:
|
||||
return False
|
||||
|
||||
@ -178,6 +180,7 @@ def background_position(values):
|
||||
@validator()
|
||||
@keyword
|
||||
def background_repeat():
|
||||
"""``background-repeat`` property validation."""
|
||||
return 'repeat', 'repeat-x', 'repeat-y', 'no-repeat'
|
||||
|
||||
|
||||
@ -187,6 +190,7 @@ def background_repeat():
|
||||
@validator('border-bottom-style')
|
||||
@keyword
|
||||
def border_style():
|
||||
"""``border-*-style`` properties validation."""
|
||||
return ('none', 'hidden', 'dotted', 'dashed', 'solid',
|
||||
'double', 'groove', 'ridge', 'inset', 'outset')
|
||||
|
||||
@ -197,16 +201,11 @@ def border_style():
|
||||
@validator('border-bottom-width')
|
||||
@single_value
|
||||
def border_width(value):
|
||||
"""``border-*-width`` properties validation."""
|
||||
return is_dimension(value, negative=False) or get_keyword(value) in (
|
||||
'thin', 'medium', 'thick')
|
||||
|
||||
|
||||
@validator()
|
||||
def content(values):
|
||||
# TODO: implement validation for 'content'
|
||||
return True
|
||||
|
||||
|
||||
#@validator('top')
|
||||
#@validator('right')
|
||||
#@validator('left')
|
||||
@ -217,56 +216,59 @@ def content(values):
|
||||
@validator('margin-left')
|
||||
@single_value
|
||||
def lenght_precentage_or_auto(value):
|
||||
"""``margin-*`` properties validation."""
|
||||
return (
|
||||
is_dimension_or_percentage(value) or
|
||||
get_keyword(value) == 'auto'
|
||||
)
|
||||
get_keyword(value) == 'auto')
|
||||
|
||||
|
||||
@validator('height')
|
||||
@validator('width')
|
||||
@single_value
|
||||
def positive_lenght_precentage_or_auto(value):
|
||||
"""``width`` and ``height`` properties validation."""
|
||||
return (
|
||||
is_dimension_or_percentage(value, negative=False) or
|
||||
get_keyword(value) == 'auto'
|
||||
)
|
||||
get_keyword(value) == 'auto')
|
||||
|
||||
|
||||
@validator()
|
||||
@keyword
|
||||
def direction():
|
||||
"""``direction`` property validation."""
|
||||
return 'ltr', 'rtl'
|
||||
|
||||
|
||||
@validator()
|
||||
def display(values):
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword in (
|
||||
'inline-block', 'table', 'inline-table',
|
||||
'table-row-group', 'table-header-group', 'table-footer-group',
|
||||
'table-row', 'table-column-group', 'table-column', 'table-cell',
|
||||
'table-caption'
|
||||
):
|
||||
"""``display`` property validation."""
|
||||
display_keyword = get_single_keyword(values)
|
||||
if display_keyword in (
|
||||
'inline-block', 'table', 'inline-table',
|
||||
'table-row-group', 'table-header-group', 'table-footer-group',
|
||||
'table-row', 'table-column-group', 'table-column', 'table-cell',
|
||||
'table-caption'):
|
||||
raise InvalidValues('value not supported yet')
|
||||
return keyword in ('inline', 'block', 'list-item', 'none')
|
||||
return display_keyword in ('inline', 'block', 'list-item', 'none')
|
||||
|
||||
|
||||
@validator()
|
||||
def font_family(values):
|
||||
"""``font-family`` property validation."""
|
||||
return all(value.type in ('IDENT', 'STRING') for value in values)
|
||||
|
||||
|
||||
@validator()
|
||||
@single_value
|
||||
def font_size(value):
|
||||
"""``font-size`` property validation."""
|
||||
if is_dimension_or_percentage(value):
|
||||
return True
|
||||
keyword = get_keyword(value)
|
||||
if keyword in ('smaller', 'larger'):
|
||||
font_size_keyword = get_keyword(value)
|
||||
if font_size_keyword in ('smaller', 'larger'):
|
||||
raise InvalidValues('value not supported yet')
|
||||
return (
|
||||
keyword in computed_values.FONT_SIZE_KEYWORDS #or
|
||||
font_size_keyword in computed_values.FONT_SIZE_KEYWORDS #or
|
||||
#keyword in ('smaller', 'larger')
|
||||
)
|
||||
|
||||
@ -274,56 +276,60 @@ def font_size(value):
|
||||
@validator()
|
||||
@keyword
|
||||
def font_style():
|
||||
"""``font-style`` property validation."""
|
||||
return 'normal', 'italic', 'oblique'
|
||||
|
||||
|
||||
@validator()
|
||||
@keyword
|
||||
def font_variant():
|
||||
"""``font-variant`` property validation."""
|
||||
return 'normal', 'small-caps'
|
||||
|
||||
|
||||
@validator()
|
||||
@single_value
|
||||
def font_weight(value):
|
||||
"""``font-weight`` property validation."""
|
||||
return (
|
||||
get_keyword(value) in ('normal', 'bold', 'bolder', 'lighter') or (
|
||||
value.type == 'NUMBER' and
|
||||
value.value in (100, 200, 300, 400, 500, 600, 700, 800, 900)
|
||||
)
|
||||
)
|
||||
value.value in (100, 200, 300, 400, 500, 600, 700, 800, 900)))
|
||||
|
||||
|
||||
@validator('letter-spacing')
|
||||
@validator()
|
||||
@single_value
|
||||
def letter_spacing(value):
|
||||
"""``letter-spacing`` property validation."""
|
||||
return get_keyword(value) == 'normal' or is_dimension(value)
|
||||
|
||||
|
||||
@validator()
|
||||
@single_value
|
||||
def line_height(value):
|
||||
"""``line-height`` property validation."""
|
||||
return get_keyword(value) == 'normal' or (
|
||||
value.type in ('NUMBER', 'DIMENSION', 'PERCENTAGE') and
|
||||
value.value >= 0
|
||||
)
|
||||
value.value >= 0)
|
||||
|
||||
|
||||
@validator()
|
||||
@keyword
|
||||
def list_style_position():
|
||||
"""``list-style-position`` property validation."""
|
||||
return 'inside', 'outside'
|
||||
|
||||
|
||||
@validator()
|
||||
def list_style_type(values):
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword in ('decimal', 'decimal-leading-zero',
|
||||
"""``list-style-type`` property validation."""
|
||||
font_size_keyword = get_single_keyword(values)
|
||||
if font_size_keyword in ('decimal', 'decimal-leading-zero',
|
||||
'lower-roman', 'upper-roman', 'lower-greek', 'lower-latin',
|
||||
'upper-latin', 'armenian', 'georgian', 'lower-alpha',
|
||||
'upper-alpha'):
|
||||
raise InvalidValues('value not supported yet')
|
||||
return keyword in ('disc', 'circle', 'square', 'none')
|
||||
return font_size_keyword in ('disc', 'circle', 'square', 'none')
|
||||
|
||||
|
||||
@validator('padding-top')
|
||||
@ -332,47 +338,52 @@ def list_style_type(values):
|
||||
@validator('padding-left')
|
||||
@single_value
|
||||
def length_or_precentage(value):
|
||||
"""``padding-*`` properties validation."""
|
||||
return is_dimension_or_percentage(value, negative=False)
|
||||
|
||||
|
||||
@validator()
|
||||
def position(values):
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword in ('relative', 'absolute', 'fixed'):
|
||||
"""``position`` property validation."""
|
||||
position_keyword = get_single_keyword(values)
|
||||
if position_keyword in ('relative', 'absolute', 'fixed'):
|
||||
raise InvalidValues('value not supported yet')
|
||||
return keyword in ('static',)
|
||||
return position_keyword in ('static',)
|
||||
|
||||
|
||||
@validator()
|
||||
def text_align(values):
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword in ('right', 'center', 'justify'):
|
||||
"""``text-align`` property validation."""
|
||||
text_align_keyword = get_single_keyword(values)
|
||||
if text_align_keyword in ('right', 'center', 'justify'):
|
||||
raise InvalidValues('value not supported yet')
|
||||
return keyword in ('left',)
|
||||
return text_align_keyword in ('left',)
|
||||
|
||||
|
||||
@validator()
|
||||
def text_decoration(values):
|
||||
"""``text-decoration`` property validation."""
|
||||
return (
|
||||
get_single_keyword(values) == 'none' or
|
||||
all(
|
||||
get_keyword(value) in ('underline', 'overline', 'line-through',
|
||||
'blink')
|
||||
for value in values
|
||||
)
|
||||
)
|
||||
get_keyword(value) in (
|
||||
'underline', 'overline', 'line-through', 'blink')
|
||||
for value in values))
|
||||
|
||||
|
||||
@validator()
|
||||
@keyword
|
||||
def white_space():
|
||||
"""``white-space`` property validation."""
|
||||
return 'normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line'
|
||||
|
||||
|
||||
@validator()
|
||||
def size(values):
|
||||
"""
|
||||
http://www.w3.org/TR/css3-page/#page-size-prop
|
||||
"""``size`` property validation.
|
||||
|
||||
See http://www.w3.org/TR/css3-page/#page-size-prop
|
||||
|
||||
"""
|
||||
return (
|
||||
len(values) == 1 and (
|
||||
@ -393,15 +404,19 @@ def size(values):
|
||||
)
|
||||
|
||||
|
||||
# Expanders
|
||||
|
||||
# Let's be coherent, always use ``name`` as an argument even when it is useless
|
||||
# pylint: disable=W0613
|
||||
|
||||
def expander(property_name):
|
||||
"""
|
||||
Decorator to add a function to the EXPANDERS dict.
|
||||
"""
|
||||
def decorator(function):
|
||||
"""Decorator adding a function to the ``EXPANDERS``."""
|
||||
def expander_decorator(function):
|
||||
"""Add ``function`` to the ``EXPANDERS``."""
|
||||
assert property_name not in EXPANDERS, property_name
|
||||
EXPANDERS[property_name] = function
|
||||
return function
|
||||
return decorator
|
||||
return expander_decorator
|
||||
|
||||
|
||||
@expander('border-color')
|
||||
@ -410,19 +425,17 @@ def expander(property_name):
|
||||
@expander('margin')
|
||||
@expander('padding')
|
||||
def expand_four_sides(name, values):
|
||||
"""
|
||||
Expand properties that set a value for each of the four sides of a box.
|
||||
"""
|
||||
"""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)
|
||||
values *= 2 # (bottom, left) defaults to (top, right)
|
||||
elif len(values) == 3:
|
||||
values.append(values[1]) # left defaults to right
|
||||
values.append(values[1]) # left defaults to right
|
||||
elif len(values) != 4:
|
||||
raise InvalidValues('Expected 1 to 4 value components got %i'
|
||||
% len(values))
|
||||
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:
|
||||
@ -437,14 +450,18 @@ def expand_four_sides(name, values):
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
def decorator(wrapped):
|
||||
def generic_expander_decorator(wrapped):
|
||||
"""Decorate the ``wrapped`` expander."""
|
||||
@functools.wraps(wrapped)
|
||||
def wrapper(name, values):
|
||||
def generic_expander_wrapper(name, values):
|
||||
"""Wrap the expander."""
|
||||
if get_single_keyword(values) in ('inherit', 'initial'):
|
||||
results = dict.fromkeys(expanded_names, values)
|
||||
else:
|
||||
@ -467,17 +484,17 @@ def generic_expander(*expanded_names):
|
||||
values = INITIAL_VALUES[actual_new_name]
|
||||
validate_non_shorthand(actual_new_name, values, required=True)
|
||||
yield actual_new_name, values
|
||||
return wrapper
|
||||
return decorator
|
||||
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.
|
||||
"""Expand the ``list-style`` shorthand property.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/generate.html#propdef-list-style
|
||||
|
||||
http://www.w3.org/TR/CSS21/generate.html#propdef-list-style
|
||||
"""
|
||||
type_specified = image_specified = False
|
||||
none_count = 0
|
||||
@ -516,10 +533,10 @@ def expand_list_style(name, values):
|
||||
|
||||
@expander('border')
|
||||
def expand_border(name, values):
|
||||
"""
|
||||
Expand the 'border' shorthand.
|
||||
"""Expand the ``border`` shorthand property.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/box.html#propdef-border
|
||||
|
||||
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):
|
||||
@ -532,10 +549,10 @@ def expand_border(name, values):
|
||||
@expander('border-left')
|
||||
@generic_expander('-width', '-color', '-style')
|
||||
def expand_border_side(name, values):
|
||||
"""
|
||||
Expand 'border-top' and such.
|
||||
"""Expand the ``border-*`` shorthand properties.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/box.html#propdef-border-top
|
||||
|
||||
http://www.w3.org/TR/CSS21/box.html#propdef-border-top
|
||||
"""
|
||||
for value in values:
|
||||
if color([value]):
|
||||
@ -550,23 +567,20 @@ def expand_border_side(name, values):
|
||||
|
||||
|
||||
def is_valid_background_positition(value):
|
||||
"""
|
||||
Tell whether the value a valid background-position.
|
||||
"""
|
||||
"""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')
|
||||
)
|
||||
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.
|
||||
"""Expand the ``background`` shorthand property.
|
||||
|
||||
See http://www.w3.org/TR/CSS21/colors.html#propdef-background
|
||||
|
||||
http://www.w3.org/TR/CSS21/colors.html#propdef-background
|
||||
"""
|
||||
# Make `values` a stack
|
||||
values = list(reversed(values))
|
||||
@ -602,16 +616,16 @@ def expand_background(name, values):
|
||||
@generic_expander('-style', '-variant', '-weight', '-size',
|
||||
'line-height', '-family') # line-height is not a suffix
|
||||
def expand_font(name, values):
|
||||
"""
|
||||
Expand the 'font' shorthand.
|
||||
"""Expand the ``font`` shorthand property.
|
||||
|
||||
http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
|
||||
"""
|
||||
keyword = get_single_keyword(values)
|
||||
if keyword in ('caption', 'icon', 'menu', 'message-box',
|
||||
'small-caption', 'status-bar'):
|
||||
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.', keyword)
|
||||
'System fonts are not supported, `font: %s` ignored.',
|
||||
expand_font_keyword)
|
||||
return
|
||||
|
||||
# Make `values` a stack
|
||||
@ -658,27 +672,27 @@ def expand_font(name, 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')
|
||||
|
||||
if (
|
||||
get_single_keyword(values) in ('initial', 'inherit') or
|
||||
VALIDATORS[name](values)
|
||||
):
|
||||
if (get_single_keyword(values) in ('initial', 'inherit') or
|
||||
VALIDATORS[name](values)):
|
||||
return [(name, values)]
|
||||
else:
|
||||
raise InvalidValues
|
||||
|
||||
|
||||
def validate_and_expand(name, values):
|
||||
"""
|
||||
Ignore and log invalid or unsupported declarations, and expand shorthand
|
||||
properties.
|
||||
"""Expand and validate shorthand properties.
|
||||
|
||||
The invalid or unsupported declarations are ignored and logged.
|
||||
|
||||
Return a iterable of ``(name, values)`` tuples.
|
||||
|
||||
Return a iterable of (name, values) tuples.
|
||||
"""
|
||||
# Defaults
|
||||
level = 'warn'
|
||||
@ -688,9 +702,9 @@ def validate_and_expand(name, values):
|
||||
level = 'info'
|
||||
reason = 'the property does not apply for the print media'
|
||||
else:
|
||||
expander_ = EXPANDERS.get(name, validate_non_shorthand)
|
||||
try:
|
||||
expander = EXPANDERS.get(name, validate_non_shorthand)
|
||||
results = expander(name, values)
|
||||
results = expander_(name, values)
|
||||
# Use list() to consume any generator now,
|
||||
# so that InvalidValues is caught.
|
||||
return list(results)
|
||||
|
@ -18,46 +18,46 @@
|
||||
|
||||
"""
|
||||
Utility functions and methods used by various modules in the css package.
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
from cssutils.css import Value, DimensionValue
|
||||
|
||||
|
||||
def get_keyword(value):
|
||||
"""
|
||||
If the given Value object is a keyword (identifier in cssutils), return its
|
||||
name. Otherwise return None.
|
||||
"""If ``value`` is a keyword, return its name.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if value.type == 'IDENT':
|
||||
return value.value
|
||||
|
||||
|
||||
def get_single_keyword(values):
|
||||
"""If ``values`` is a 1-element list of keywords, return its name.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
If the given list of Value object is a single keyword (identifier in
|
||||
cssutils), return its name. Otherwise return None.
|
||||
"""
|
||||
# Unsafe, fast way:
|
||||
# Fast but unsafe, as it depends on private attributes
|
||||
if len(values) == 1:
|
||||
value = values[0]
|
||||
if value._type == 'IDENT':
|
||||
return value._value
|
||||
# if len(values) == 1:
|
||||
# return get_keyword(values[0])
|
||||
|
||||
|
||||
def get_pixel_value(value):
|
||||
"""
|
||||
Return the numeric value of a pixel length or None.
|
||||
"""If ``value`` is a pixel length, return its value.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
value_type = value.type
|
||||
value_value = value.value
|
||||
if (
|
||||
(value_type == 'DIMENSION' and value.dimension == 'px') or
|
||||
# Units may be ommited on 0
|
||||
(value_type == 'NUMBER' and value_value == 0)
|
||||
):
|
||||
if ((value_type == 'DIMENSION' and value.dimension == 'px') or
|
||||
# Units may be ommited on 0
|
||||
(value_type == 'NUMBER' and value_value == 0)):
|
||||
# cssutils promises that `DimensionValue.value` is an int or float
|
||||
assert isinstance(value_value, (int, float))
|
||||
return value_value
|
||||
@ -67,16 +67,20 @@ def get_pixel_value(value):
|
||||
|
||||
|
||||
def get_single_pixel_value(values):
|
||||
"""
|
||||
Return the numeric value of a single pixel length or None.
|
||||
"""If ``values`` is a 1-element list of pixel lengths, return its value.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if len(values) == 1:
|
||||
return get_pixel_value(values[0])
|
||||
|
||||
|
||||
def get_percentage_value(value):
|
||||
"""
|
||||
Return the numeric value of a percentage or None.
|
||||
"""If ``value`` is a percentage, return its value.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if value.type == 'PERCENTAGE':
|
||||
# cssutils promises that `DimensionValue.value` is an int or float
|
||||
@ -86,17 +90,22 @@ def get_percentage_value(value):
|
||||
# Not a percentage
|
||||
return None
|
||||
|
||||
|
||||
def get_single_percentage_value(values):
|
||||
"""
|
||||
Return the numeric value of a single percentage or None.
|
||||
"""If ``values`` is a 1-element list of percentages, return its value.
|
||||
|
||||
Otherwise return ``None``.
|
||||
|
||||
"""
|
||||
if len(values) == 1:
|
||||
return get_percentage_value(values[0])
|
||||
|
||||
|
||||
def make_pixel_value(pixels):
|
||||
"""
|
||||
Make a pixel DimensionValue. Reverse of get_single_pixel_value.
|
||||
"""Make a pixel :class:`DimensionValue`.
|
||||
|
||||
Reverse of :func:`get_single_pixel_value`.
|
||||
|
||||
"""
|
||||
value = DimensionValue()
|
||||
value._value = pixels
|
||||
@ -106,9 +115,7 @@ def make_pixel_value(pixels):
|
||||
|
||||
|
||||
def make_number(number):
|
||||
"""
|
||||
Make a number DimensionValue.
|
||||
"""
|
||||
"""Make a number :class:`DimensionValue`."""
|
||||
value = DimensionValue()
|
||||
value._value = number
|
||||
value._type = 'NUMBER'
|
||||
@ -116,8 +123,10 @@ def make_number(number):
|
||||
|
||||
|
||||
def make_keyword(keyword):
|
||||
"""
|
||||
Make a keyword Value. Reverse of get_keyword.
|
||||
"""Make a keyword :class:`Value`.
|
||||
|
||||
Reverse of :func:`get_keyword`.
|
||||
|
||||
"""
|
||||
value = Value()
|
||||
value._value = keyword
|
||||
@ -126,7 +135,5 @@ def make_keyword(keyword):
|
||||
|
||||
|
||||
def as_css(values):
|
||||
"""
|
||||
Retur a string reperesentation for a value list.
|
||||
"""
|
||||
"""Return the string reperesentation of the ``values`` list."""
|
||||
return ' '.join(value.cssText for value in values)
|
||||
|
Loading…
Reference in New Issue
Block a user