diff --git a/weasy/css/computed_values.py b/weasy/css/computed_values.py index 4b146e62..e6b2da41 100644 --- a/weasy/css/computed_values.py +++ b/weasy/css/computed_values.py @@ -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 ? +# How many CSS pixels is one ? # 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 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) diff --git a/weasy/css/validation.py b/weasy/css/validation.py index 886fbfd9..093b52a8 100644 --- a/weasy/css/validation.py +++ b/weasy/css/validation.py @@ -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) diff --git a/weasy/css/values.py b/weasy/css/values.py index ff943506..a2befefb 100644 --- a/weasy/css/values.py +++ b/weasy/css/values.py @@ -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)