1
1
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:
Guillaume Ayoub 2011-08-23 17:35:10 +02:00
parent 39e1987408
commit 496f0e2ead
3 changed files with 284 additions and 239 deletions

View File

@ -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. (Thats me!)
keywords = ['A4'] # Chosen by the UA. (Thats 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)

View File

@ -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)

View File

@ -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)