From 6eb5f96328624adf5e9c3adaa5989f887127c292 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 25 Nov 2023 17:24:15 +0100 Subject: [PATCH] Fix many corner cases with CSS variables Fix #1287, fix #2010. --- tests/draw/test_current_color.py | 25 +++++++++ tests/test_variables.py | 19 +++++++ weasyprint/css/__init__.py | 73 ++++++++++++------------- weasyprint/css/computed_values.py | 88 +++++++++++++++++++++---------- 4 files changed, 138 insertions(+), 67 deletions(-) diff --git a/tests/draw/test_current_color.py b/tests/draw/test_current_color.py index 3a279cbfa..b0de5fab0 100644 --- a/tests/draw/test_current_color.py +++ b/tests/draw/test_current_color.py @@ -80,3 +80,28 @@ def test_current_color_svg_2(assert_pixels): width="2" height="2"> ''') + + +@assert_no_logs +def test_current_color_variable(assert_pixels): + # Regression test for https://github.com/Kozea/WeasyPrint/issues/2010 + assert_pixels('GG\nGG', ''' + +
aa''') + + +@assert_no_logs +def test_current_color_variable_border(assert_pixels): + # Regression test for https://github.com/Kozea/WeasyPrint/issues/2010 + assert_pixels('GG\nGG', ''' + +
''') diff --git a/tests/test_variables.py b/tests/test_variables.py index af9dfa77a..3db12ca55 100644 --- a/tests/test_variables.py +++ b/tests/test_variables.py @@ -149,3 +149,22 @@ def test_variable_fallback(prop):
''' % prop) + + +@assert_no_logs +def test_variable_list(): + # Regression test for https://github.com/Kozea/WeasyPrint/issues/1287 + page, = parse(''' + +
+ ''') + html, = page.children + body, = html.children + div, = body.children + line, = div.children + before, = line.children + text, = before.children + assert text.text == 'Page 1/1' diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 788a7c9ef..4c708991f 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -22,7 +22,8 @@ from .. import CSS from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join -from . import computed_values, counters, media_queries +from . import counters, media_queries +from .computed_values import compute_variables, COMPUTER_FUNCTIONS, ZERO_PIXELS from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES from .utils import get_url, remove_whitespace from .validation import preprocess_declarations @@ -124,13 +125,13 @@ def __call__(self, element, pseudo_type=None): style['border_collapse'] == 'collapse'): # Padding do not apply for side in ('top', 'bottom', 'left', 'right'): - style[f'padding_{side}'] = computed_values.ZERO_PIXELS + style[f'padding_{side}'] = ZERO_PIXELS if (len(style['display']) == 1 and style['display'][0].startswith('table-') and style['display'][0] != 'table-caption'): # Margins do not apply for side in ('top', 'bottom', 'left', 'right'): - style[f'margin_{side}'] = computed_values.ZERO_PIXELS + style[f'margin_{side}'] = ZERO_PIXELS return style @@ -667,75 +668,69 @@ def copy(self): def __missing__(self, key): if key == 'float': - # Set specified value for position, needed for computed value + # Set specified value for position, needed for computed value. self['position'] elif key == 'display': - # Set specified value for float, needed for computed value + # Set specified value for float, needed for computed value. self['float'] + parent_style = self.parent_style + if key in self.cascaded: - value = keyword = self.cascaded[key][0] + # Property defined in cascaded properties. + keyword, computed = compute_variables(key, self, parent_style) + value = keyword else: + # Property not defined in cascaded properties, define as inherited + # or initial value. + computed = False if key in INHERITED or key[:2] == '__': keyword = 'inherit' else: keyword = 'initial' - if keyword == 'inherit' and self.parent_style is None: + if keyword == 'inherit' and parent_style is None: # On the root element, 'inherit' from initial values keyword = 'initial' if keyword == 'initial': value = None if key[:2] == '__' else INITIAL_VALUES[key] if key not in INITIAL_NOT_COMPUTED: - # The value is the same as when computed + # The value is the same as when computed. self[key] = value elif keyword == 'inherit': # Values in parent_style are already computed. - self[key] = value = self.parent_style[key] + self[key] = value = parent_style[key] - if key[:16] == 'text_decoration_' and self.parent_style: + if key[:16] == 'text_decoration_' and parent_style is not None: + # Text decorations are not inherited but propagated. See + # https://www.w3.org/TR/css-text-decor-3/#line-decoration. value = text_decoration( - key, value, self.parent_style[key], key in self.cascaded) + key, value, parent_style[key], key in self.cascaded) if key in self: del self[key] elif key == 'page' and value == 'auto': - # The page property does not inherit. However, if the page - # value on an element is auto, then its used value is the value - # specified on its nearest ancestor with a non-auto value. When - # specified on the root element, the used value for auto is the - # empty string. - value = ( - '' if self.parent_style is None else self.parent_style['page']) + # The page property does not inherit. However, if the page value on + # an element is auto, then its used value is the value specified on + # its nearest ancestor with a non-auto value. When specified on the + # root element, the used value for auto is the empty string. See + # https://www.w3.org/TR/css-page-3/#using-named-pages. + value = '' if parent_style is None else parent_style['page'] if key in self: del self[key] elif key in ('position', 'float', 'display'): + # Save specified values to define computed values for these + # specific properties. See + # https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo. self.specified[key] = value if key in self: + # Value already computed and saved: return. return self[key] - function = computed_values.COMPUTER_FUNCTIONS.get(key) - already_computed_value = False - - if value: - converted_to_list = False - - if not isinstance(value, list): - converted_to_list = True - value = [value] - - for i, v in enumerate(value): - value[i], already_computed_value = ( - computed_values.compute_variable( - v, key, self, self.base_url, self.parent_style)) - - if converted_to_list: - value, = value - - if function is not None and not already_computed_value: - value = function(self, key, value) - # else: same as specified + if not computed and key in COMPUTER_FUNCTIONS: + # Value not computed yet: compute. + value = COMPUTER_FUNCTIONS[key](self, key, value) self[key] = value return value diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 707ee5331..fade56e0b 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -211,42 +211,74 @@ def decorator(function): return decorator -def compute_variable(value, name, computed, base_url, parent_style): - already_computed_value = False +def compute_variables(name, computed_style, parent_style): + original_values = computed_style.cascaded[name][0] - if value and isinstance(value, tuple) and value[0] == 'var()': - variable_name, default = value[1] - computed_value = _resolve_var( - computed, variable_name, default, parent_style) - if computed_value is None: - new_value = None - else: - prop = PROPERTIES[name.replace('_', '-')] - if prop.wants_base_url: - new_value = prop(computed_value, base_url) + if isinstance(original_values, list): + # Property with multiple values. + transformed_to_list = False + values = original_values + else: + # Property with single value, put in a list. + transformed_to_list = True + values = [original_values] + + # Find variables. + variables = { + i: value[1] for i, value in enumerate(values) + if value and isinstance(value, tuple) and value[0] == 'var()'} + if not variables: + # No variable, return early. + return original_values, False + + if not transformed_to_list: + # Don’t modify original list of values. + values = values.copy() + + if name in INHERITED and parent_style: + inherited = True + computed = True + else: + inherited = False + computed = name not in INITIAL_NOT_COMPUTED + + # Replace variables by real values. + for i, variable in variables.items(): + variable_name, default = variable + value = _resolve_var( + computed_style, variable_name, default, parent_style) + + if value is not None: + # Validate value. + validator = PROPERTIES[name.replace('_', '-')] + if validator.wants_base_url: + value = validator(value, computed_style.base_url) else: - new_value = prop(computed_value) + value = validator(value) - # See https://drafts.csswg.org/css-variables/#invalid-variables - if new_value is None: + if value is None: + # Invalid variable value, see + # https://www.w3.org/TR/css-variables-1/#invalid-variables. with suppress(BaseException): - computed_value = ''.join( - token.serialize() for token in computed_value) + value = ''.join(token.serialize() for token in value) LOGGER.warning( 'Unsupported computed value "%s" set in variable %r ' - 'for property %r.', computed_value, + 'for property %r.', value, variable_name.replace('_', '-'), name.replace('_', '-')) - if name in INHERITED and parent_style: - already_computed_value = True - value = parent_style[name] - else: - already_computed_value = name not in INITIAL_NOT_COMPUTED - value = INITIAL_VALUES[name] - elif isinstance(new_value, list): - value, = new_value + values[i] = (parent_style if inherited else INITIAL_VALUES)[name] + elif not transformed_to_list: + # Validator returns multiple values. Replace original variable by + # possibly multiple computed values. + values[i:i+1] = value else: - value = new_value - return value, already_computed_value + # Save variable by single computed value. + values[i] = value + + if transformed_to_list: + # Property with single value, unpack list. + values, = values + + return values, computed @register_computer('background-image')