From 88c027117c27ae2dcff1a39dedfe01037756a28e Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Tue, 3 Apr 2012 14:59:06 +0200 Subject: [PATCH] Do not keep tokens for percentages after validation. Make (value, unit) namedtuples instead. Pixel values that could be percentages are not plain numbers anymore. --- weasyprint/css/computed_values.py | 165 +++++++++-------- weasyprint/css/properties.py | 53 +++--- weasyprint/css/validation.py | 223 +++++++++++++---------- weasyprint/css/values.py | 61 ------- weasyprint/draw.py | 22 +-- weasyprint/formatting_structure/boxes.py | 5 +- weasyprint/formatting_structure/build.py | 3 +- weasyprint/layout/blocks.py | 4 +- weasyprint/layout/percentages.py | 15 +- weasyprint/tests/test_boxes.py | 22 +-- weasyprint/tests/test_css.py | 68 +++---- weasyprint/tests/test_css_properties.py | 74 ++++---- weasyprint/tests/test_layout.py | 1 - 13 files changed, 350 insertions(+), 366 deletions(-) delete mode 100644 weasyprint/css/values.py diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index d7b176fb..1a2a681f 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -15,7 +15,10 @@ from __future__ import division, unicode_literals import math -from .properties import INITIAL_VALUES +from .properties import INITIAL_VALUES, Dimension + + +ZERO_PIXELS = Dimension(0, 'px') # How many CSS pixels is one ? @@ -95,42 +98,45 @@ FONT_WEIGHT_RELATIVE = dict( # name=(width in pixels, height in pixels) PAGE_SIZES = dict( A5=( - 148 * LENGTHS_TO_PIXELS['mm'], - 210 * LENGTHS_TO_PIXELS['mm'], + Dimension(148, 'mm'), + Dimension(210, 'mm'), ), A4=( - 210 * LENGTHS_TO_PIXELS['mm'], - 297 * LENGTHS_TO_PIXELS['mm'], + Dimension(210, 'mm'), + Dimension(297, 'mm'), ), A3=( - 297 * LENGTHS_TO_PIXELS['mm'], - 420 * LENGTHS_TO_PIXELS['mm'], + Dimension(297, 'mm'), + Dimension(420, 'mm'), ), B5=( - 176 * LENGTHS_TO_PIXELS['mm'], - 250 * LENGTHS_TO_PIXELS['mm'], + Dimension(176, 'mm'), + Dimension(250, 'mm'), ), B4=( - 250 * LENGTHS_TO_PIXELS['mm'], - 353 * LENGTHS_TO_PIXELS['mm'], + Dimension(250, 'mm'), + Dimension(353, 'mm'), ), letter=( - 8.5 * LENGTHS_TO_PIXELS['in'], - 11 * LENGTHS_TO_PIXELS['in'], + Dimension(8.5, 'in'), + Dimension(11, 'in'), ), legal=( - 8.5 * LENGTHS_TO_PIXELS['in'], - 14 * LENGTHS_TO_PIXELS['in'], + Dimension(8.5, 'in'), + Dimension(14, 'in'), ), ledger=( - 11 * LENGTHS_TO_PIXELS['in'], - 17 * LENGTHS_TO_PIXELS['in'], + Dimension(11, 'in'), + Dimension(17, 'in'), ), ) +# In "portrait" orientation. for w, h in PAGE_SIZES.values(): - assert w < h + assert w.value < h.value -INITIAL_VALUES['size'] = PAGE_SIZES['A4'] +INITIAL_PAGE_SIZE = PAGE_SIZES['A4'] +INITIAL_VALUES['size'] = tuple( + d.value * LENGTHS_TO_PIXELS[d.unit] for d in INITIAL_PAGE_SIZE) def _computing_order(): @@ -199,7 +205,8 @@ def compute(element, pseudo_type, specified, computed, parent_style): return computed -# Let's be coherent, always use ``name`` as an argument even when it is useless +# Let's be consistent, always use ``name`` as an argument even when +# it is useless. # pylint: disable=W0613 @register_computer('background-color') @@ -217,13 +224,19 @@ def other_color(computer, name, value): @register_computer('background-position') +@register_computer('transform-origin') +def length_or_percentage_tuple(computer, name, values): + """Compute the lists of lengths that can be percentages.""" + return tuple(length(computer, name, value) for value in values) + + @register_computer('border-spacing') @register_computer('size') @register_computer('clip') -@register_computer('transform-origin') -def length_list(computer, name, values): +def length_tuple(computer, name, values): """Compute the properties with a list of lengths.""" - return [length(computer, name, value) for value in values] + return tuple(length(computer, name, value, pixels_only=True) + for value in values) @register_computer('top') @@ -236,35 +249,42 @@ def length_list(computer, name, values): @register_computer('margin-left') @register_computer('height') @register_computer('width') -@register_computer('letter-spacing') @register_computer('padding-top') @register_computer('padding-right') @register_computer('padding-bottom') @register_computer('padding-left') @register_computer('text-indent') -def length(computer, name, value, font_size=None): +def length(computer, name, value, font_size=None, pixels_only=False): """Compute a length ``value``.""" - if (getattr(value, 'type', 'other') in ('NUMBER', 'INTEGER') - and value.value == 0): - return 0 + if value.value == 0: + return 0 if pixels_only else ZERO_PIXELS - if getattr(value, 'type', 'other') != 'DIMENSION': - # No conversion needed. - return value - - if value.unit in LENGTHS_TO_PIXELS: + unit = value.unit + if unit == 'px': + return value.value if pixels_only else value + elif unit in LENGTHS_TO_PIXELS: # Convert absolute lengths to pixels - factor = LENGTHS_TO_PIXELS[value.unit] - elif value.unit in ('em', 'ex'): + factor = LENGTHS_TO_PIXELS[unit] + elif unit in ('em', 'ex'): if font_size is None: factor = computer.computed.font_size else: factor = font_size + if unit == 'ex': + # TODO: find a better way to measure ex, see + # http://www.w3.org/TR/CSS21/syndata.html#length-units + factor *= 0.5 + else: + # A percentage or 'auto': no conversion needed. + return value - if value.unit == 'ex': - factor *= 0.5 + result = value.value * factor + return result if pixels_only else Dimension(result, 'px') - return value.value * factor + +@register_computer('letter-spacing') +def pixel_length(computer, name, value): + return length(computer, name, value, pixels_only=True) @register_computer('background-size') @@ -273,7 +293,7 @@ def background_size(computer, name, value): if value in ('contain', 'cover'): return value else: - return length_list(computer, name, value) + return length_or_percentage_tuple(computer, name, value) @register_computer('border-top-width') @@ -289,7 +309,7 @@ def border_width(computer, name, value): if value in BORDER_WIDTH_KEYWORDS: return BORDER_WIDTH_KEYWORDS[value] - return length(computer, name, value) + return length(computer, name, value, pixels_only=True) @register_computer('content') @@ -342,27 +362,14 @@ def font_size(computer, name, value): """Compute the ``font-size`` property.""" if value in FONT_SIZE_KEYWORDS: return FONT_SIZE_KEYWORDS[value] + # TODO: support 'larger' and 'smaller' parent_font_size = computer.parent_style['font_size'] - - if value.type == 'DIMENSION': - if value.unit == 'px': - factor = 1 - elif value.unit == 'em': - factor = parent_font_size - elif value.unit == 'ex': - # TODO: find a better way to measure ex, see - # http://www.w3.org/TR/CSS21/syndata.html#length-units - factor = parent_font_size * 0.5 - elif value.unit in LENGTHS_TO_PIXELS: - factor = LENGTHS_TO_PIXELS[value.unit] - elif value.type == 'PERCENTAGE': - factor = parent_font_size / 100. - elif value.type in ('NUMBER', 'INTEGER') and value.value == 0: - return 0 - - # Raise if `factor` is not defined. It should be, because of validation. - return value.value * factor + if value.unit == '%': + return value.value * parent_font_size / 100. + else: + return length(computer, name, value, pixels_only=True, + font_size=parent_font_size) @register_computer('font-weight') @@ -384,38 +391,52 @@ def font_weight(computer, name, value): @register_computer('line-height') def line_height(computer, name, value): """Compute the ``line-height`` property.""" - # No .type attribute: already computed - if value == 'normal' or not hasattr(value, 'type'): + if value == 'normal': return value - elif value.type in ('NUMBER', 'INTEGER'): + elif not value.unit: return ('NUMBER', value.value) - elif value.type == 'PERCENTAGE': + elif value.unit == '%': factor = value.value / 100. font_size_value = computer.computed.font_size pixels = factor * font_size_value else: - assert value.type == 'DIMENSION' - pixels = length(computer, name, value) + pixels = length(computer, name, value, pixels_only=True) return ('PIXELS', pixels) +@register_computer('transform') +def transform(computer, name, value): + """Compute the ``transform`` property.""" + result = [] + for function, args in value: + if function in ('rotate', 'skewx', 'skewy'): + args = args.value * ANGLE_TO_RADIANS[args.unit] + elif function == 'translate': + args = length_or_percentage_tuple(computer, name, args) + result.append((function, args)) + return result + + @register_computer('vertical-align') def vertical_align(computer, name, value): """Compute the ``vertical-align`` property.""" # Use +/- half an em for super and sub, same as Pango. # (See the SUPERSUB_RISE constant in pango-markup.c) - if value == 'super': + if value in ('baseline', 'middle', 'text-top', 'text-bottom', + 'top', 'bottom'): + return value + elif value == 'super': return computer.computed.font_size * 0.5 elif value == 'sub': return computer.computed.font_size * -0.5 - elif getattr(value, 'type', 'other') == 'PERCENTAGE': + elif value.unit == '%': height = used_line_height({ 'line_height': computer.computed.line_height, 'font_size': computer.computed.font_size }) return height * value.value / 100. else: - return length(computer, name, value) + return length(computer, name, value, pixels_only=True) @register_computer('word-spacing') @@ -424,7 +445,7 @@ def word_spacing(computer, name, value): if value == 'normal': return 0 else: - return length(computer, name, value) + return length(computer, name, value, pixels_only=True) def used_line_height(style): @@ -440,9 +461,3 @@ def used_line_height(style): return value * style['font_size'] else: return value - - -def angle_to_radian(value): - """Take a DIMENSION token for an angle and return the value in radians. - """ - return value.value * ANGLE_TO_RADIANS[value.unit] diff --git a/weasyprint/css/properties.py b/weasyprint/css/properties.py index 1af0a73c..ce6739c8 100644 --- a/weasyprint/css/properties.py +++ b/weasyprint/css/properties.py @@ -11,21 +11,30 @@ """ from __future__ import division, unicode_literals +import collections from tinycss.color3 import COLOR_KEYWORDS -from .values import make_percentage_value + +Dimension = collections.namedtuple('Dimension', ['value', 'unit']) + + +class _Auto(str): + unit = None + value = 'auto' +AUTO = _Auto('auto') # for values that can be 'auto' as well as a dimension + # See http://www.w3.org/TR/CSS21/propidx.html INITIAL_VALUES = { 'background_attachment': 'scroll', 'background_color': COLOR_KEYWORDS['transparent'], 'background_image': 'none', - 'background_position': (make_percentage_value(0),) * 2, + 'background_position': (Dimension(0, '%'), Dimension(0, '%')), 'background_repeat': 'repeat', 'background_clip': 'border-box', # CSS3 'background_origin': 'padding-box', # CSS3 - 'background_size': ('auto', 'auto'), # CSS3 + 'background_size': (AUTO, AUTO), # CSS3 'border_collapse': 'separate', # http://www.w3.org/TR/css3-color/#currentcolor 'border_top_color': 'currentColor', @@ -41,7 +50,7 @@ INITIAL_VALUES = { 'border_right_width': 3, 'border_bottom_width': 3, 'border_left_width': 3, - 'bottom': 'auto', + 'bottom': AUTO, 'caption_side': 'top', 'clear': 'none', 'clip': (), # empty collection, computed value for 'auto' @@ -62,46 +71,46 @@ INITIAL_VALUES = { 'font_style': 'normal', 'font_variant': 'normal', 'font_weight': 400, - 'height': 'auto', - 'left': 'auto', + 'height': AUTO, + 'left': AUTO, 'letter_spacing': 'normal', 'line_height': 'normal', 'list_style_image': 'none', 'list_style_position': 'outside', 'list_style_type': 'disc', - 'margin_top': 0, - 'margin_right': 0, - 'margin_bottom': 0, - 'margin_left': 0, + 'margin_top': Dimension(0, 'px'), + 'margin_right': Dimension(0, 'px'), + 'margin_bottom': Dimension(0, 'px'), + 'margin_left': Dimension(0, 'px'), 'max_height': 'none', 'max_width': 'none', - 'min_height': 0, - 'min_width': 0, + 'min_height': Dimension(0, 'px'), + 'min_width': Dimension(0, 'px'), 'orphans': 2, 'overflow': 'visible', - 'padding_top': 0, - 'padding_right': 0, - 'padding_bottom': 0, - 'padding_left': 0, + 'padding_top': Dimension(0, 'px'), + 'padding_right': Dimension(0, 'px'), + 'padding_bottom': Dimension(0, 'px'), + 'padding_left': Dimension(0, 'px'), 'page_break_after': 'auto', 'page_break_before': 'auto', 'page_break_inside': 'auto', 'quotes': list('“”‘’'), # depends on user agent 'position': 'static', - 'right': 'auto', + 'right': AUTO, 'table_layout': 'auto', 'text_align': '-weasy-start', # Taken from CSS3 Text. # The only other supported value form CSS3 is -weasy-end. 'text_decoration': 'none', - 'text_indent': 0, + 'text_indent': Dimension(0, 'px'), 'text_transform': 'none', - 'top': 'auto', + 'top': AUTO, 'unicode_bidi': 'normal', 'vertical_align': 'baseline', 'visibility': 'visible', 'white_space': 'normal', 'widows': 2, - 'width': 'auto', + 'width': AUTO, 'word_spacing': 0, # computed value for 'normal' 'z_index': 'auto', @@ -115,8 +124,8 @@ INITIAL_VALUES = { 'opacity': 1, # CSS3 2D Transforms: http://www.w3.org/TR/css3-2d-transforms - 'transform_origin': (make_percentage_value(50),) * 2, - 'transform': (), # computed value for 'none' + 'transform_origin': (Dimension(50, '%'), Dimension(50, '%')), + 'transform': (), # empty sequence: computed value for 'none' # Taken from SVG: # http://www.w3.org/TR/SVG/painting.html#ImageRenderingProperty diff --git a/weasyprint/css/validation.py b/weasyprint/css/validation.py index 8173da20..fc9a78b6 100644 --- a/weasyprint/css/validation.py +++ b/weasyprint/css/validation.py @@ -21,13 +21,16 @@ from tinycss.parsing import split_on_comma from ..logger import LOGGER from ..formatting_structure import counters from ..compat import urljoin -from .values import get_keyword, get_single_keyword, make_percentage_value -from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA +from .properties import INITIAL_VALUES, NOT_PRINT_MEDIA, AUTO, Dimension from . import computed_values - # TODO: unit-test these validators + +# get the sets of keys +LENGTH_UNITS = set(computed_values.LENGTHS_TO_PIXELS) | set(['ex', 'em']) +ANGLE_UNITS = set(computed_values.ANGLE_TO_RADIANS) + # keyword -> (open, insert) CONTENT_QUOTE_KEYWORDS = { 'open-quote': (True, True), @@ -37,11 +40,11 @@ CONTENT_QUOTE_KEYWORDS = { } BACKGROUND_POSITION_PERCENTAGES = { - 'top': make_percentage_value(0), - 'left': make_percentage_value(0), - 'center': make_percentage_value(50), - 'bottom': make_percentage_value(100), - 'right': make_percentage_value(100), + 'top': Dimension(0, '%'), + 'left': Dimension(0, '%'), + 'center': Dimension(50, '%'), + 'bottom': Dimension(100, '%'), + 'right': Dimension(100, '%'), } @@ -91,6 +94,28 @@ def validator(property_name=None, prefixed=False, wants_base_url=False): return decorator +def get_keyword(token): + """If ``value`` is a keyword, return its name. + + Otherwise return ``None``. + + """ + if token.type == 'IDENT': + return token.value.lower() + + +def get_single_keyword(tokens): + """If ``values`` is a 1-element list of keywords, return its name. + + Otherwise return ``None``. + + """ + if len(tokens) == 1: + token = tokens[0] + if token.type == 'IDENT': + return token.value.lower() + + def single_keyword(function): """Decorator for validators that only accept a single keyword.""" @functools.wraps(function) @@ -113,35 +138,17 @@ def single_token(function): return single_token_validator -def is_dimension(token, negative=True): - """Get if ``token`` is a dimension. - - The ``negative`` argument sets wether negative tokens are allowed. - - """ - type_ = token.type - # Units may be ommited on zero lenghts. - return ( - type_ == 'DIMENSION' and (negative or token.value >= 0) and ( - token.unit in computed_values.LENGTHS_TO_PIXELS or - token.unit in ('em', 'ex')) - ) or (type_ in ('NUMBER', 'INTEGER') and token.value == 0) +def get_length(token, negative=True, percentage=False): + if (token.unit in LENGTH_UNITS or (percentage and token.unit == '%') + or (token.value == 0 and token.type in ('INTEGER', 'NUMBER')) + ) and (negative or token.value >= 0): + return Dimension(token.value, token.unit) -def is_dimension_or_percentage(token, negative=True): - """Get if ``token`` is a dimension or a percentage. - - The ``negative`` argument sets wether negative tokens are allowed. - - """ - return is_dimension(token, negative) or ( - token.type == 'PERCENTAGE' and (negative or token.value >= 0)) - - -def is_angle(token): +def get_angle(token): """Return whether the argument is an angle token.""" - return token.type == 'DIMENSION' and \ - token.unit in computed_values.ANGLE_TO_RADIANS + if token.unit in ANGLE_UNITS: + return Dimension(token.value, token.unit) @validator() @@ -197,20 +204,26 @@ def background_position(tokens): keyword = get_keyword(token) if keyword in BACKGROUND_POSITION_PERCENTAGES: return BACKGROUND_POSITION_PERCENTAGES[keyword], center - elif is_dimension_or_percentage(token): - return token, center + else: + length = get_length(token, percentage=True) + if length: + return length, center elif len(tokens) == 2: token_1, token_2 = tokens keyword_1, keyword_2 = map(get_keyword, tokens) - if is_dimension_or_percentage(token_1): + length_1 = get_length(token_1, percentage=True) + if length_1: if keyword_2 in ('top', 'center', 'bottom'): - return token_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2] - elif is_dimension_or_percentage(token_2): - return token_1, token_2 - elif is_dimension_or_percentage(token_2): + return length_1, BACKGROUND_POSITION_PERCENTAGES[keyword_2] + length_2 = get_length(token_2, percentage=True) + if length_2: + return length_1, length_2 + raise InvalidValues + length_2 = get_length(token_2, percentage=True) + if length_2: if keyword_1 in ('left', 'center', 'right'): - return BACKGROUND_POSITION_PERCENTAGES[keyword_1], token_2 + return BACKGROUND_POSITION_PERCENTAGES[keyword_1], length_2 elif (keyword_1 in ('left', 'center', 'right') and keyword_2 in ('top', 'center', 'bottom')): return (BACKGROUND_POSITION_PERCENTAGES[keyword_1], @@ -239,19 +252,22 @@ def background_size(tokens): if keyword in ('contain', 'cover'): return keyword if keyword == 'auto': - return ('auto', 'auto') - if is_dimension_or_percentage(token, negative=False): - return (token, 'auto') + return (AUTO, AUTO) + length = get_length(token, negative=False) + if length: + return (length, AUTO) elif len(tokens) == 2: - new_tokens = [] + values = [] for token in tokens: if get_keyword(token) == 'auto': - new_tokens.append('auto') - elif is_dimension_or_percentage(token, negative=False): - new_tokens.append(token) + new_tokens.append(AUTO) else: - return - return tuple(tokens) + length = get_length(token, negative=False) + if length: + new_tokens.append(token) + break + else: + return tuple(values) @validator('background_clip') @@ -266,11 +282,12 @@ def box(keyword): @validator() def border_spacing(tokens): """Validator for the `border-spacing` property.""" - if all(is_dimension(token, negative=False) for token in tokens): - if len(tokens) == 1: - return (tokens[0], tokens[0]) - elif len(tokens) == 2: - return tuple(tokens) + lengths = [get_length(token, negative=False) for token in tokens] + if all(lengths): + if len(lengths) == 1: + return (lengths[0], lengths[0]) + elif len(lengths) == 2: + return tuple(lengths) @validator('border-top-style') @@ -291,8 +308,9 @@ def border_style(keyword): @single_token def border_width(token): """``border-*-width`` properties validation.""" - if is_dimension(token, negative=False): - return token + length = get_length(token, negative=False) + if length: + return length keyword = get_keyword(token) if keyword in ('thin', 'medium', 'thick'): return keyword @@ -323,11 +341,13 @@ def clip(token): tokens = [] for arg in args: if get_keyword(arg) == 'auto': - tokens.append('auto') - elif is_dimension(arg, negative=True): - tokens.append(arg) + tokens.append(AUTO) else: - raise InvalidValues + length = get_length(arg) + if length: + tokens.append(length) + else: + raise InvalidValues return tokens if get_keyword(token) == 'auto': return [] @@ -443,10 +463,11 @@ def counter(tokens, default_integer): @single_token def lenght_precentage_or_auto(token, negative=True): """``margin-*`` properties validation.""" - if is_dimension_or_percentage(token, negative): - return token + length = get_length(token, negative, percentage=True) + if length: + return length if get_keyword(token) == 'auto': - return 'auto' + return AUTO @validator('height') @@ -497,8 +518,9 @@ def font_family(tokens): @single_token def font_size(token): """``font-size`` property validation.""" - if is_dimension_or_percentage(token): - return token + length = get_length(token, percentage=True) + if length: + return length font_size_keyword = get_keyword(token) if font_size_keyword in ('smaller', 'larger'): raise InvalidValues('value not supported yet') @@ -542,8 +564,9 @@ def spacing(token): """Validation for ``letter-spacing`` and ``word-spacing``.""" if get_keyword(token) == 'normal': return 'normal' - if is_dimension(token): - return token + length = get_length(token) + if length: + return length @validator() @@ -554,7 +577,7 @@ def line_height(token): return 'normal' if (token.type in ('NUMBER', 'INTEGER', 'DIMENSION', 'PERCENTAGE') and token.value >= 0): - return token + return Dimension(token.value, token.unit) @validator() @@ -578,8 +601,9 @@ def list_style_type(keyword): @single_token def length_or_precentage(token): """``padding-*`` properties validation.""" - if is_dimension_or_percentage(token, negative=False): - return token + length = get_length(token, negative=False) + if length: + return length @validator() @@ -675,8 +699,9 @@ def text_decoration(tokens): @single_token def text_indent(token): """``text-indent`` property validation.""" - if is_dimension_or_percentage(token, negative=True): - return token + length = get_length(token, percentage=True) + if length: + return length @validator() @@ -690,8 +715,9 @@ def text_transform(keyword): @single_token def vertical_align(token): """Validation for the ``vertical-align`` property""" - if is_dimension_or_percentage(token, negative=True): - return token + length = get_length(token, percentage=True) + if length: + return length keyword = get_keyword(token) if keyword in ('baseline', 'middle', 'sub', 'super', 'text-top', 'text-bottom', 'top', 'bottom'): @@ -716,7 +742,7 @@ def white_space(keyword): @single_keyword def image_rendering(keyword): """Validation for ``image-rendering``.""" - return keyword in ('auto', 'optimizeSpeed', 'optimizeQuality') + return keyword in ('auto', 'optimizespeed', 'optimizequality') @validator(prefixed=True) # Not in CR yet @@ -726,22 +752,22 @@ def size(tokens): See http://www.w3.org/TR/css3-page/#page-size-prop """ - if is_dimension(tokens[0]): - if len(tokens) == 1: - return tokens * 2 - elif len(tokens) == 2 and is_dimension(tokens[1]): - return tokens + lengths = [get_length(token, negative=False) for token in tokens] + if all(lengths): + if len(lengths) == 1: + return (lengths[0], lengths[0]) + elif len(lengths) == 2: + return tuple(lengths) keywords = [get_keyword(v) for v in tokens] if len(keywords) == 1: keyword = keywords[0] - if keyword in ('auto', 'portrait'): - return INITIAL_VALUES['size'] - elif keyword == 'landscape': - height, width = INITIAL_VALUES['size'] - return width, height - elif keyword in computed_values.PAGE_SIZES: + if keyword in computed_values.PAGE_SIZES: return computed_values.PAGE_SIZES[keyword] + elif keyword in ('auto', 'portrait'): + return computed_values.INITIAL_PAGE_SIZE + elif keyword == 'landscape': + return computed_values.INITIAL_PAGE_SIZE[::-1] if len(keywords) == 2: if keywords[0] in ('portrait', 'landscape'): @@ -762,7 +788,7 @@ def size(tokens): @validator(prefixed=True) # Not in CR yet def transform(tokens): if get_single_keyword(tokens) == 'none': - return 'none' + return [] else: return [transform_function(v) for v in tokens] @@ -774,12 +800,14 @@ def transform_function(token): name, args = function if len(args) == 1: - if name in ('rotate', 'skewx', 'skewy') and is_angle(args[0]): - return name, args[0] - elif name in ('translatex', 'translate') and is_dimension_or_percentage(args[0]): - return 'translate', (args[0], 0) - elif name == 'translatey' and is_dimension_or_percentage(args[0]): - return 'translate', (0, args[0]) + angle = get_angle(args[0]) + length = get_length(args[0], percentage=True) + if name in ('rotate', 'skewx', 'skewy') and angle: + return name, angle + elif name in ('translatex', 'translate') and length: + return 'translate', (length, computed_values.ZERO_PIXELS) + elif name == 'translatey' and length: + return 'translate', (computed_values.ZERO_PIXELS, length) elif name == 'scalex' and args[0].type in ('NUMBER', 'INTEGER'): return 'scale', (args[0].value, 1) elif name == 'scaley' and args[0].type in ('NUMBER', 'INTEGER'): @@ -790,8 +818,9 @@ def transform_function(token): if name == 'scale' and all(a.type in ('NUMBER', 'INTEGER') for a in args): return name, [arg.value for arg in args] - if name == 'translate' and all(map(is_dimension_or_percentage, args)): - return name, args + lengths = [get_length(token, percentage=True) for token in args] + if name == 'translate' and all(lengths): + return name, lengths elif len(args) == 6 and name == 'matrix' and all( a.type in ('NUMBER', 'INTEGER') for a in args): return name, [arg.value for arg in args] diff --git a/weasyprint/css/values.py b/weasyprint/css/values.py deleted file mode 100644 index 87906189..00000000 --- a/weasyprint/css/values.py +++ /dev/null @@ -1,61 +0,0 @@ -# coding: utf8 -""" - weasyprint.css.values - --------------------- - - Utility function to work with tinycss tokens. - - :copyright: Copyright 2011-2012 Simon Sapin and contributors, see AUTHORS. - :license: BSD, see LICENSE for details. - -""" - -from __future__ import division, unicode_literals - -import collections - - -def get_keyword(value): - """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``. - - """ - # Fast but unsafe, as it depends on private attributes - if len(values) == 1: - value = values[0] - if value.type == 'IDENT': - return value.value - - -def get_percentage_value(value): - """If ``value`` is a percentage, return its value. - - Otherwise return ``None``. - - """ - if getattr(value, 'type', 'other') == 'PERCENTAGE': - return value.value - else: - # Not a percentage - return None - - -class FakePercentage(object): - type = 'PERCENTAGE' - - def __init__(self, value): - self.value = value - self.as_css = '{}%'.format(value) - -make_percentage_value = FakePercentage diff --git a/weasyprint/draw.py b/weasyprint/draw.py index e35fbfa6..8daa7c5c 100644 --- a/weasyprint/draw.py +++ b/weasyprint/draw.py @@ -18,15 +18,15 @@ import math import cairo from .formatting_structure import boxes -from .css.values import get_percentage_value from .css import computed_values # Map values of the image-rendering property to cairo FILTER values: +# Values are normalized to lower case. IMAGE_RENDERING_TO_FILTER = dict( - optimizeSpeed=cairo.FILTER_FAST, + optimizespeed=cairo.FILTER_FAST, auto=cairo.FILTER_GOOD, - optimizeQuality=cairo.FILTER_BEST, + optimizequality=cairo.FILTER_BEST, ) @@ -188,11 +188,11 @@ def draw_box_background(document, context, page, box): def percentage(value, refer_to): """Return the evaluated percentage value, or the value unchanged.""" - percentage_value = get_percentage_value(value) - if percentage_value is None: - return value + if value.unit == 'px': + return value.value else: - return refer_to * percentage_value / 100 + assert value.unit == '%' + return refer_to * value.value / 100 def draw_background(document, context, style, painting_area, positioning_area): @@ -566,18 +566,14 @@ def apply_2d_transforms(context, box): origin_x += box.border_box_x() origin_y += box.border_box_y() - def length(value, font_size=box.style.font_size): - return computed_values.length(None, None, value, font_size) - angle = computed_values.angle_to_radian - context.translate(origin_x, origin_y) for name, args in box.style.transform: if name == 'scale': context.scale(*args) elif name == 'rotate': - context.rotate(angle(args)) + context.rotate(args) elif name == 'translate': - translate_x, translate_y = map(length, args) + translate_x, translate_y = args context.translate( percentage(translate_x, border_width), percentage(translate_y, border_height), diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py index ec9dd663..134a9253 100644 --- a/weasyprint/formatting_structure/boxes.py +++ b/weasyprint/formatting_structure/boxes.py @@ -60,6 +60,7 @@ from __future__ import division, unicode_literals from ..compat import xrange +from ..css.computed_values import ZERO_PIXELS # The *Box classes have many attributes and methods, but that's the way it is @@ -193,8 +194,8 @@ class Box(object): setattr(self, 'padding_%s' % side, 0) setattr(self, 'border_%s_width' % side, 0) - self.style['margin_%s' % side] = 0 - self.style['padding_%s' % side] = 0 + self.style['margin_%s' % side] = ZERO_PIXELS + self.style['padding_%s' % side] = ZERO_PIXELS self.style['border_%s_width' % side] = 0 # Positioning schemes diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py index ac29c0af..ac313f89 100644 --- a/weasyprint/formatting_structure/build.py +++ b/weasyprint/formatting_structure/build.py @@ -277,7 +277,8 @@ def add_box_marker(document, counter_values, box): position = style.list_style_position if position == 'inside': side = 'right' if style.direction == 'ltr' else 'left' - marker_box.style['margin_' + side] = style.font_size * 0.5 + margin = style.font_size * 0.5 + marker_box.style['margin_' + side] = properties.Dimension(margin, 'px') yield marker_box elif position == 'outside': box.outside_list_marker = marker_box diff --git a/weasyprint/layout/blocks.py b/weasyprint/layout/blocks.py index ccb597b0..37963d66 100644 --- a/weasyprint/layout/blocks.py +++ b/weasyprint/layout/blocks.py @@ -17,6 +17,7 @@ from .markers import list_marker_layout from .tables import table_layout, fixed_table_layout from .percentages import resolve_percentages from ..formatting_structure import boxes +from ..css.properties import Dimension def block_level_layout(document, box, max_position_y, skip_stack, @@ -386,7 +387,8 @@ def block_table_wrapper(document, wrapper, max_position_y, skip_stack, table.margin_right = 0 fixed_table_layout(table) - wrapper.width = wrapper.style.width = table.border_width() + wrapper.width = table.border_width() + wrapper.style.width = Dimension(wrapper.width, 'px') return block_box_layout(document, wrapper, max_position_y, skip_stack, containing_block, device_size, page_is_empty, adjoining_margins) diff --git a/weasyprint/layout/percentages.py b/weasyprint/layout/percentages.py index 70d4a80d..ccaa68ae 100644 --- a/weasyprint/layout/percentages.py +++ b/weasyprint/layout/percentages.py @@ -13,7 +13,6 @@ from __future__ import division, unicode_literals from ..formatting_structure import boxes -from ..css.values import get_percentage_value def resolve_one_percentage(box, property_name, refer_to, @@ -25,23 +24,21 @@ def resolve_one_percentage(box, property_name, refer_to, """ # box.style has computed values - values = box.style[property_name] - if isinstance(values, (int, float)): - # Absolute length (was converted to pixels in "computed values") - result = values + value = box.style[property_name] + if value.unit == 'px': + result = value.value else: - percentage = get_percentage_value(values) - if percentage is not None: + if value.unit == '%': if isinstance(refer_to, (int, float)): # A percentage - result = percentage * refer_to / 100. + result = value.value * refer_to / 100. else: # Replace percentages when we have no refer_to that # makes sense. result = refer_to else: # Some other values such as 'auto' may be allowed - result = values + result = value assert allowed_keywords and result in allowed_keywords # box attributes are used values setattr(box, property_name, result) diff --git a/weasyprint/tests/test_boxes.py b/weasyprint/tests/test_boxes.py index fcd77a2a..46dd4989 100644 --- a/weasyprint/tests/test_boxes.py +++ b/weasyprint/tests/test_boxes.py @@ -336,9 +336,9 @@ def test_styles(): assert child.style.color == (0, 0, 1, 1) # blue # Only non-anonymous boxes have margins if child.style.anonymous: - assert child.style.margin_top == 0 + assert child.style.margin_top == (0, 'px') else: - assert child.style.margin_top == 42 + assert child.style.margin_top == (42, 'px') @assert_no_logs @@ -390,10 +390,10 @@ def test_page_style(): def assert_page_margins(page_type, top, right, bottom, left): """Check the page margin values.""" style = document.style_for(page_type) - assert style.margin_top == top - assert style.margin_right == right - assert style.margin_bottom == bottom - assert style.margin_left == left + assert style.margin_top == (top, 'px') + assert style.margin_right == (right, 'px') + assert style.margin_bottom == (bottom, 'px') + assert style.margin_left == (left, 'px') assert_page_margins('first_left_page', top=20, right=3, bottom=3, left=10) assert_page_margins('first_right_page', top=20, right=10, bottom=3, left=3) @@ -591,10 +591,10 @@ def test_table_style(): table, = wrapper.children assert isinstance(wrapper, boxes.BlockBox) assert isinstance(table, boxes.TableBox) - assert wrapper.style.margin_top == 1 - assert wrapper.style.padding_top == 0 - assert table.style.margin_top == 0 - assert table.style.padding_top == 2 + assert wrapper.style.margin_top == (1, 'px') + assert wrapper.style.padding_top == (0, 'px') + assert table.style.margin_top == (0, 'px') + assert table.style.padding_top == (2, 'px') @assert_no_logs @@ -610,7 +610,7 @@ def test_column_style(): table, = wrapper.children colgroup, = table.column_groups widths = [col.style.width for col in colgroup.children] - assert widths == [10, 10, 10, 'auto', 'auto'] + assert widths == [(10, 'px'), (10, 'px'), (10, 'px'), 'auto', 'auto'] assert [col.grid_x for col in colgroup.children] == [0, 1, 2, 3, 4] # copies, not the same box object assert colgroup.children[0] is not colgroup.children[1] diff --git a/weasyprint/tests/test_css.py b/weasyprint/tests/test_css.py index 3447557c..abed8af8 100644 --- a/weasyprint/tests/test_css.py +++ b/weasyprint/tests/test_css.py @@ -118,16 +118,16 @@ def test_expand_shorthands(): assert 'margin-top' not in style style = dict( - (name, value.as_css) + (name, value) for _rule, _selectors, declarations in sheet.rules for name, value, _priority in declarations) assert 'margin' not in style - assert style['margin_top'] == '2em' - assert style['margin_right'] == '0' - assert style['margin_bottom'] == '2em', \ + assert style['margin_top'] == (2, 'em') + assert style['margin_right'] == (0, None) + assert style['margin_bottom'] == (2, 'em'), \ '3em was before the shorthand, should be masked' - assert style['margin_left'] == '4em', \ + assert style['margin_left'] == (4, 'em'), \ '4em was after the shorthand, should not be masked' @@ -163,17 +163,17 @@ def test_annotate_document(): assert h1.font_size == 32 # 4ex # 32px = 1em * font-size = x-large = 3/2 * initial 16px = 24px - assert p.margin_top == 24 - assert p.margin_right == 0 - assert p.margin_bottom == 24 - assert p.margin_left == 0 + assert p.margin_top == (24, 'px') + assert p.margin_right == (0, 'px') + assert p.margin_bottom == (24, 'px') + assert p.margin_left == (0, 'px') assert p.background_color == (0, 0, 1, 1) # blue # 80px = 2em * 5ex = 10 * half of initial 16px - assert ul.margin_top == 80 - assert ul.margin_right == 80 - assert ul.margin_bottom == 80 - assert ul.margin_left == 80 + assert ul.margin_top == (80, 'px') + assert ul.margin_right == (80, 'px') + assert ul.margin_bottom == (80, 'px') + assert ul.margin_left == (80, 'px') assert ul.font_weight == 700 # thick = 5px, 0.25 inches = 96*.25 = 24px @@ -184,17 +184,17 @@ def test_annotate_document(): assert li_0.font_weight == 900 assert li_0.font_size == 8 # 6pt - assert li_0.margin_top == 16 # 2em - assert li_0.margin_right == 0 - assert li_0.margin_bottom == 16 - assert li_0.margin_left == 32 # 4em + assert li_0.margin_top == (16, 'px') # 2em + assert li_0.margin_right == (0, 'px') + assert li_0.margin_bottom == (16, 'px') + assert li_0.margin_left == (32, 'px') # 4em assert a.text_decoration == frozenset(['underline']) assert a.font_size == 24 # 300% of 8px - assert a.padding_top == 1 - assert a.padding_right == 2 - assert a.padding_bottom == 3 - assert a.padding_left == 4 + assert a.padding_top == (1, 'px') + assert a.padding_right == (2, 'px') + assert a.padding_bottom == (3, 'px') + assert a.padding_left == (4, 'px') assert a.color == (1, 0, 0, 1) @@ -247,27 +247,27 @@ def test_page(): ]) style = document.style_for('first_left_page') - assert style.margin_top == 5 - assert style.margin_left == 10 - assert style.margin_bottom == 10 + assert style.margin_top == (5, 'px') + assert style.margin_left == (10, 'px') + assert style.margin_bottom == (10, 'px') assert style.color == (1, 0, 0, 1) # red, inherited from html style = document.style_for('first_right_page') - assert style.margin_top == 5 - assert style.margin_left == 10 - assert style.margin_bottom == 16 + assert style.margin_top == (5, 'px') + assert style.margin_left == (10, 'px') + assert style.margin_bottom == (16, 'px') assert style.color == (0, 0, 1, 1) # blue style = document.style_for('left_page') - assert style.margin_top == 10 - assert style.margin_left == 10 - assert style.margin_bottom == 10 + assert style.margin_top == (10, 'px') + assert style.margin_left == (10, 'px') + assert style.margin_bottom == (10, 'px') assert style.color == (1, 0, 0, 1) # red, inherited from html style = document.style_for('right_page') - assert style.margin_top == 10 - assert style.margin_left == 10 - assert style.margin_bottom == 16 + assert style.margin_top == (10, 'px') + assert style.margin_left == (10, 'px') + assert style.margin_bottom == (16, 'px') assert style.color == (0, 0, 1, 1) # blue style = document.style_for('first_left_page', '@top-left') @@ -275,7 +275,7 @@ def test_page(): style = document.style_for('first_right_page', '@top-left') assert style.font_size == 20 # inherited from @page - assert style.width == 200 + assert style.width == (200, 'px') style = document.style_for('first_right_page', '@top-right') assert style.font_size == 10 diff --git a/weasyprint/tests/test_css_properties.py b/weasyprint/tests/test_css_properties.py index a3aa95bb..887f19ee 100644 --- a/weasyprint/tests/test_css_properties.py +++ b/weasyprint/tests/test_css_properties.py @@ -28,11 +28,7 @@ def expand_to_dict(short_name, short_values): assert not errors assert len(declarations) == 1 tokens = remove_whitespace(declarations[0].value) - expanded = validation.EXPANDERS[short_name]('', short_name, tokens) - return dict((name, tuple(v.as_css for v in value) - if name == 'background_position' - else getattr(value, 'as_css', value)) - for name, value in expanded) + return dict(validation.EXPANDERS[short_name]('', short_name, tokens)) @assert_no_logs @@ -45,28 +41,28 @@ def test_expand_four_sides(): 'margin_left': 'inherit', } assert expand_to_dict('margin', '1em') == { - 'margin_top': '1em', - 'margin_right': '1em', - 'margin_bottom': '1em', - 'margin_left': '1em', + 'margin_top': (1, 'em'), + 'margin_right': (1, 'em'), + 'margin_bottom': (1, 'em'), + 'margin_left': (1, 'em'), } assert expand_to_dict('padding', '1em 0') == { - 'padding_top': '1em', - 'padding_right': '0', - 'padding_bottom': '1em', - 'padding_left': '0', + 'padding_top': (1, 'em'), + 'padding_right': (0, None), + 'padding_bottom': (1, 'em'), + 'padding_left': (0, None), } assert expand_to_dict('padding', '1em 0 2em') == { - 'padding_top': '1em', - 'padding_right': '0', - 'padding_bottom': '2em', - 'padding_left': '0', + 'padding_top': (1, 'em'), + 'padding_right': (0, None), + 'padding_bottom': (2, 'em'), + 'padding_left': (0, None), } assert expand_to_dict('padding', '1em 0 2em 5px') == { - 'padding_top': '1em', - 'padding_right': '0', - 'padding_bottom': '2em', - 'padding_left': '5px', + 'padding_top': (1, 'em'), + 'padding_right': (0, None), + 'padding_bottom': (2, 'em'), + 'padding_left': (5, 'px'), } with raises(ValueError): expand_to_dict('padding', '1 2 3 4 5') @@ -76,39 +72,39 @@ def test_expand_four_sides(): def test_expand_borders(): """Test the ``border`` property.""" assert expand_to_dict('border_top', '3px dotted red') == { - 'border_top_width': '3px', + 'border_top_width': (3, 'px'), 'border_top_style': 'dotted', 'border_top_color': (1, 0, 0, 1), # red } assert expand_to_dict('border_top', '3px dotted') == { - 'border_top_width': '3px', + 'border_top_width': (3, 'px'), 'border_top_style': 'dotted', 'border_top_color': 'currentColor', } assert expand_to_dict('border_top', '3px red') == { - 'border_top_width': '3px', + 'border_top_width': (3, 'px'), 'border_top_style': 'none', 'border_top_color': (1, 0, 0, 1), # red } assert expand_to_dict('border_top', 'solid') == { - 'border_top_width': 3, + 'border_top_width': 3, # initial value 'border_top_style': 'solid', 'border_top_color': 'currentColor', } assert expand_to_dict('border', '6px dashed lime') == { - 'border_top_width': '6px', + 'border_top_width': (6, 'px'), 'border_top_style': 'dashed', 'border_top_color': (0, 1, 0, 1), # lime - 'border_left_width': '6px', + 'border_left_width': (6, 'px'), 'border_left_style': 'dashed', 'border_left_color': (0, 1, 0, 1), # lime - 'border_bottom_width': '6px', + 'border_bottom_width': (6, 'px'), 'border_bottom_style': 'dashed', 'border_bottom_color': (0, 1, 0, 1), # lime - 'border_right_width': '6px', + 'border_right_width': (6, 'px'), 'border_right_style': 'dashed', 'border_right_color': (0, 1, 0, 1), # lime } @@ -162,7 +158,7 @@ def test_expand_background(): image='none', repeat='repeat', attachment='scroll', - position=('0%', '0%'), + position=((0, '%'), (0, '%')), ) assert_background( @@ -171,7 +167,7 @@ def test_expand_background(): image='foo.png', ## repeat='repeat', attachment='scroll', - position=('0%', '0%'), + position=((0, '%'), (0, '%')), ) assert_background( 'no-repeat', @@ -179,7 +175,7 @@ def test_expand_background(): image='none', repeat='no-repeat', ## attachment='scroll', - position=('0%', '0%'), + position=((0, '%'), (0, '%')), ) assert_background( 'fixed', @@ -187,7 +183,7 @@ def test_expand_background(): image='none', repeat='repeat', attachment='fixed', ## - position=('0%', '0%'), + position=((0, '%'), (0, '%')), ) assert_background( 'top right', @@ -196,7 +192,7 @@ def test_expand_background(): repeat='repeat', attachment='scroll', # Order swapped to be in (horizontal, vertical) order. - position=('100%', '0%'), ## + position=((100, '%'), (0, '%')), ## ) assert_background( 'url(bar) #f00 repeat-y center left fixed', @@ -205,7 +201,7 @@ def test_expand_background(): repeat='repeat-y', ## attachment='fixed', ## # Order swapped to be in (horizontal, vertical) order. - position=('0%', '50%'), ## + position=((0, '%'), (50, '%')), ## ) assert_background( '#00f 10% 200px', @@ -213,7 +209,7 @@ def test_expand_background(): image='none', repeat='repeat', attachment='scroll', - position=('10%', '200px'), ## + position=((10, '%'), (200, 'px')), ## ) assert_background( 'right 78px fixed', @@ -221,7 +217,7 @@ def test_expand_background(): image='none', repeat='repeat', attachment='fixed', ## - position=('100%', '78px'), ## + position=((100, '%'), (78, 'px')), ## ) @@ -232,7 +228,7 @@ def test_font(): 'font_style': 'normal', 'font_variant': 'normal', 'font_weight': 400, - 'font_size': '12px', ## + 'font_size': (12, 'px'), ## 'line_height': 'normal', 'font_family': ['My Fancy Font', 'serif'], ## } @@ -241,7 +237,7 @@ def test_font(): 'font_variant': 'normal', 'font_weight': 400, 'font_size': 'small', ## - 'line_height': '1.2', ## + 'line_height': (1.2, None), ## 'font_family': ['Some Font', 'serif'], ## } assert expand_to_dict('font', 'small-caps italic 700 large serif') == { diff --git a/weasyprint/tests/test_layout.py b/weasyprint/tests/test_layout.py index 9e75b2bd..f5f41c5d 100644 --- a/weasyprint/tests/test_layout.py +++ b/weasyprint/tests/test_layout.py @@ -691,7 +691,6 @@ def test_table_page_breaks(): for i, page in enumerate(pages): html, = page.children body, = html.children - print(body.children) if i == 0: body_children = body.children[1:] # skip h1 else: