From 04ca0bc6db3d3dbfc1e78959987bf86614f004f0 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 10 Dec 2023 14:27:20 +0100 Subject: [PATCH] Allow variables in properties with multiple values Related to #1219. --- tests/test_variables.py | 50 ++++++++++++++++++- weasyprint/css/computed_values.py | 66 ++++++++++++------------- weasyprint/css/validation/properties.py | 61 ++++++++++++++--------- 3 files changed, 117 insertions(+), 60 deletions(-) diff --git a/tests/test_variables.py b/tests/test_variables.py index 622d9646f..dddea37e8 100644 --- a/tests/test_variables.py +++ b/tests/test_variables.py @@ -315,7 +315,7 @@ def test_variable_fallback(prop): @assert_no_logs -def test_variable_list(): +def test_variable_list_content(): # Regression test for https://github.com/Kozea/WeasyPrint/issues/1287 page, = render_pages(''' +
+ ''' % (var, display)) + html, = page.children + body, = html.children + section, = body.children + child, = section.children + assert type(child).__name__ == 'LineBox' + + +@assert_no_logs +@pytest.mark.parametrize('var, font', ( + ('weasyprint', 'var(--var)'), + ('"weasyprint"', 'var(--var)'), + ('weasyprint', 'var(--var), monospace'), + ('weasyprint, monospace', 'var(--var)'), + ('monospace', 'weasyprint, var(--var)'), +)) +def test_variable_list_font(var, font): + page, = render_pages(''' + +
aa
+ ''' % (var, font)) + html, = page.children + body, = html.children + div, = body.children + line, = div.children + text, = line.children + assert text.width == 4 diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 62efb0c80..b37d06fe1 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -16,6 +16,7 @@ from .utils import ( ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function, safe_urljoin) +from .validation.expanders import PendingExpander from .validation.properties import MULTIVAL_PROPERTIES, PROPERTIES ZERO_PIXELS = Dimension(0, 'px') @@ -216,9 +217,12 @@ def compute_var(name, computed_style, parent_style): validation_name = name.replace('_', '-') multiple_values = validation_name in MULTIVAL_PROPERTIES + if isinstance(original_values, PendingExpander): + return original_values, False + if multiple_values: # Property with multiple values. - values = original_values + values = list(original_values) else: # Property with single value, put in a list. values = [original_values] @@ -231,54 +235,46 @@ def compute_var(name, computed_style, parent_style): # No variable, return early. return original_values, False - if not multiple_values: - # Don’t modify original list of values. - values = values.copy() - if name in INHERITED and parent_style: - inherited = True computed = True + default_values = parent_style[name] else: - inherited = False computed = name not in INITIAL_NOT_COMPUTED + default_values = INITIAL_VALUES[name] # Replace variables by real values. validator = PROPERTIES[validation_name] for i, variable in variables.items(): variable_name, default = variable - resolved_value = value = resolve_var( + value = resolve_var( computed_style, variable_name, default, parent_style) - - if value is not None: - # Validate value. - if validator.wants_base_url: - value = validator(value, computed_style.base_url) - else: - value = validator(value) - if value is None: - # Invalid variable value, see - # https://www.w3.org/TR/css-variables-1/#invalid-variables. - with suppress(BaseException): - value = ''.join(token.serialize() for token in value) LOGGER.warning( - 'Unsupported computed value "%s" set in variable %r ' - 'for property %r.', resolved_value, + 'Unknown variable %r set for property %r.', variable_name.replace('_', '-'), validation_name) - values[i] = (parent_style if inherited else INITIAL_VALUES)[name] - elif multiple_values: - # Replace original variable by possibly multiple validated values. - values[i:i+1] = value - computed = False - else: - # Save variable by single validated value. - values[i] = value - computed = False - - if not multiple_values: - # Property with single value, unpack list. - values, = values + return default_values, computed + values[i:i+1] = value + # Validate value. + original_values = values + if validator.wants_base_url: + values = validator(values, computed_style.base_url) + else: + values = validator(values) + + if values is None: + # Invalid variable value, see + # https://www.w3.org/TR/css-variables-1/#invalid-variables. + with suppress(BaseException): + original_values = ''.join( + token.serialize() for token in original_values) + LOGGER.warning( + 'Unsupported computed value "%s" set in variable %r ' + 'for property %r.', original_values, + variable_name.replace('_', '-'), validation_name) + return default_values, computed + + computed = False return values, computed diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index 134814fc8..c8f971ff7 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -49,6 +49,8 @@ def property(property_name=None, proprietary=False, unstable=False, the Candidate Recommandation stage. They can be used both vendor-prefixed or unprefixed. See https://www.w3.org/TR/CSS/#unstable-syntax + :param multiple_values: + Mark property as returning multiple values. :param wants_base_url: The function takes the stylesheet’s base URL as an additional parameter. @@ -91,7 +93,18 @@ def validate_non_shorthand(tokens, name, base_url=None, required=False): if not required and name not in PROPERTIES: raise InvalidValues('property not supported yet') - if name not in MULTIVAL_PROPERTIES: + if name in MULTIVAL_PROPERTIES: + var_found = False + new_tokens = [] + for token in tokens: + if var_function := check_var_function(token): + new_tokens.append(var_function) + var_found = True + else: + new_tokens.append(token) + if var_found: + return ((name, tuple(new_tokens)),) + else: for token in tokens: var_function = check_var_function(token) if var_function: @@ -111,7 +124,7 @@ def validate_non_shorthand(tokens, name, base_url=None, required=False): return ((name, value),) -@property() +@property(multiple_values=True) @comma_separated_list @single_keyword def background_attachment(keyword): @@ -164,7 +177,7 @@ def color(token): return result -@property('background-image', wants_base_url=True) +@property('background-image', multiple_values=True, wants_base_url=True) @comma_separated_list @single_token def background_image(token, base_url): @@ -186,7 +199,7 @@ def list_style_image(token, base_url): return 'url', parsed_url[1][1] -@property() +@property(multiple_values=True) def transform_origin(tokens): """``transform-origin`` property validation.""" if len(tokens) == 3: @@ -195,21 +208,21 @@ def transform_origin(tokens): return parse_2d_position(tokens) -@property() +@property(multiple_values=True) @comma_separated_list def background_position(tokens): """``background-position`` property validation.""" return parse_position(tokens) -@property() +@property(multiple_values=True) @comma_separated_list def object_position(tokens): """``object-position`` property validation.""" return parse_position(tokens) -@property() +@property(multiple_values=True) @comma_separated_list def background_repeat(tokens): """``background-repeat`` property validation.""" @@ -226,7 +239,7 @@ def background_repeat(tokens): return keywords -@property() +@property(multiple_values=True) @comma_separated_list def background_size(tokens): """Validation for ``background-size``.""" @@ -252,8 +265,8 @@ def background_size(tokens): return tuple(values) -@property('background-clip') -@property('background-origin') +@property('background-clip', multiple_values=True) +@property('background-origin', multiple_values=True) @comma_separated_list @single_keyword def box(keyword): @@ -262,7 +275,7 @@ def box(keyword): return keyword in ('border-box', 'padding-box', 'content-box') -@property() +@property(multiple_values=True) def border_spacing(tokens): """Validator for the `border-spacing` property.""" lengths = [get_length(token, negative=False) for token in tokens] @@ -273,10 +286,10 @@ def border_spacing(tokens): return tuple(lengths) -@property('border-top-right-radius') -@property('border-bottom-right-radius') -@property('border-bottom-left-radius') -@property('border-top-left-radius') +@property('border-top-right-radius', multiple_values=True) +@property('border-bottom-right-radius', multiple_values=True) +@property('border-bottom-left-radius', multiple_values=True) +@property('border-top-left-radius', multiple_values=True) def border_corner_radius(tokens): """Validator for the `border-*-radius` properties.""" lengths = [ @@ -382,7 +395,7 @@ def bleed(token): return get_length(token) -@property(unstable=True) +@property(unstable=True, multiple_values=True) def marks(tokens): """``marks`` property validation.""" if len(tokens) == 2: @@ -616,7 +629,7 @@ def direction(keyword): return keyword in ('ltr', 'rtl') -@property() +@property(multiple_values=True) def display(tokens): """``display`` property validation.""" for token in tokens: @@ -669,7 +682,7 @@ def float_(keyword): # XXX do not hide the "float" builtin return keyword in ('left', 'right', 'footnote', 'none') -@property() +@property(multiple_values=True) @comma_separated_list def font_family(tokens): """``font-family`` property validation.""" @@ -771,7 +784,7 @@ def font_variant_numeric(tokens): return tuple(values) -@property() +@property(multiple_values=True) def font_feature_settings(tokens): """``font-feature-settings`` property validation.""" if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal': @@ -841,7 +854,7 @@ def font_variant_east_asian(tokens): return tuple(values) -@property() +@property(multiple_values=True) def font_variation_settings(tokens): """``font-variation-settings`` property validation.""" if len(tokens) == 1 and get_keyword(tokens[0]) == 'normal': @@ -1088,7 +1101,7 @@ def position(token): return keyword -@property() +@property(multiple_values=True) def quotes(tokens): """``quotes`` property validation.""" if (tokens and len(tokens) % 2 == 0 and @@ -1417,7 +1430,7 @@ def hyphenate_limit_zone(token): return get_length(token, negative=False, percentage=True) -@property(unstable=True) +@property(unstable=True, multiple_values=True) def hyphenate_limit_chars(tokens): """Validation for ``hyphenate-limit-chars``.""" if len(tokens) == 1: @@ -1473,7 +1486,7 @@ def lang(token): return ('string', token.value) -@property(unstable=True, wants_base_url=True) +@property(unstable=True, multiple_values=True, wants_base_url=True) def bookmark_label(tokens, base_url): """Validation for ``bookmark-label``.""" parsed_tokens = tuple( @@ -1515,7 +1528,7 @@ def footnote_policy(keyword): return keyword in ('auto', 'line', 'block') -@property(unstable=True, wants_base_url=True) +@property(unstable=True, multiple_values=True, wants_base_url=True) @comma_separated_list def string_set(tokens, base_url): """Validation for ``string-set``."""