diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 2a83d0eaf..94b852af5 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -23,11 +23,14 @@ from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join from . import counters, media_queries -from .computed_values import COMPUTER_FUNCTIONS, ZERO_PIXELS, compute_var +from .computed_values import ( + COMPUTER_FUNCTIONS, ZERO_PIXELS, compute_var, resolve_var) from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES -from .utils import get_url, remove_whitespace +from .utils import check_var_function, get_url, remove_whitespace from .validation import preprocess_declarations from .validation.descriptors import preprocess_descriptors +from .validation.expanders import PendingExpander +from .validation.properties import validate_non_shorthand # Reject anything not in here: PSEUDO_ELEMENTS = ( @@ -728,6 +731,21 @@ def __missing__(self, key): # Value already computed and saved: return. return self[key] + if isinstance(value, PendingExpander): + solved_tokens = [] + for token in value.tokens: + variable = check_var_function(token) + if variable: + variable_name, default = variable[1] + tokens = resolve_var( + self, variable_name, default, parent_style) + solved_tokens.extend(tokens) + else: + solved_tokens.append(token) + original_key = key.replace('_', '-') + solved = value.solve(solved_tokens, original_key) + value = validate_non_shorthand(None, original_key, solved)[0][1] + if not computed and key in COMPUTER_FUNCTIONS: # Value not computed yet: compute. value = COMPUTER_FUNCTIONS[key](self, key, value) diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index ad5044c80..c699cd533 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -150,7 +150,7 @@ def _computing_order(): COMPUTER_FUNCTIONS = {} -def _resolve_var(computed, variable_name, default, parent_style): +def resolve_var(computed, variable_name, default, parent_style): known_variable_names = [variable_name] computed_value = computed[variable_name] @@ -246,7 +246,7 @@ def compute_var(name, computed_style, parent_style): validator = PROPERTIES[validation_name] for i, variable in variables.items(): variable_name, default = variable - value = _resolve_var( + value = resolve_var( computed_style, variable_name, default, parent_style) if value is not None: diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 75a3e9b01..cd860a96e 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -111,11 +111,11 @@ def validation_error(level, reason): validation_error('debug', 'prefixed selectors are ignored') continue - expander_ = EXPANDERS.get(name, validate_non_shorthand) + expander = EXPANDERS.get(name, validate_non_shorthand) tokens = remove_whitespace(declaration.value) try: # Use list() to consume generators now and catch any error. - result = list(expander_(base_url, name, tokens)) + result = list(expander(base_url, name, tokens)) except InvalidValues as exc: validation_error( 'warning', diff --git a/weasyprint/css/validation/expanders.py b/weasyprint/css/validation/expanders.py index 310d30eaa..4a14d955a 100644 --- a/weasyprint/css/validation/expanders.py +++ b/weasyprint/css/validation/expanders.py @@ -7,7 +7,8 @@ from ..properties import INITIAL_VALUES from ..utils import ( - InvalidValues, get_keyword, get_single_keyword, split_on_comma) + InvalidValues, check_var_function, get_keyword, get_single_keyword, + split_on_comma) from .descriptors import expand_font_variant from .properties import ( background_attachment, background_image, background_position, @@ -21,6 +22,20 @@ EXPANDERS = {} +class PendingExpander: + def __init__(self, tokens, expander): + self.tokens = tokens + self.expander = expander + + def solve(self, tokens, wanted_key): + for key, value in self.expander(tokens=tokens): + if key.startswith('-'): + key = f'{self.expander.keywords["name"]}{key}' + if key == wanted_key: + return value + raise KeyError + + def expander(property_name): """Decorator adding a function to the ``EXPANDERS``.""" def expander_decorator(function): @@ -31,39 +46,6 @@ def expander_decorator(function): return expander_decorator -@expander('border-color') -@expander('border-style') -@expander('border-width') -@expander('margin') -@expander('padding') -@expander('bleed') -def expand_four_sides(base_url, name, tokens): - """Expand properties setting a token for the four sides of a box.""" - # Make sure we have 4 tokens - if len(tokens) == 1: - tokens *= 4 - elif len(tokens) == 2: - tokens *= 2 # (bottom, left) defaults to (top, right) - elif len(tokens) == 3: - tokens += (tokens[1],) # left defaults to right - elif len(tokens) != 4: - raise InvalidValues( - f'Expected 1 to 4 token components got {len(tokens)}') - for suffix, token in zip(('-top', '-right', '-bottom', '-left'), tokens): - i = name.rfind('-') - if i == -1: - new_name = name + suffix - else: - # eg. border-color becomes border-*-color, not border-color-* - new_name = name[:i] + suffix + name[i:] - - # validate_non_shorthand returns ((name, value),), we want - # to yield (name, value) - result, = validate_non_shorthand( - base_url, new_name, [token], required=True) - yield result - - def generic_expander(*expanded_names, **kwargs): """Decorator helping expanders to handle ``inherit`` and ``initial``. @@ -80,17 +62,27 @@ def generic_expander_decorator(wrapped): @functools.wraps(wrapped) def generic_expander_wrapper(base_url, name, tokens): """Wrap the expander.""" + expander = functools.partial(wrapped, name=name) + if wants_base_url: + expander = functools.partial(expander, base_url=base_url) + + skip_validation = False keyword = get_single_keyword(tokens) if keyword in ('inherit', 'initial'): results = dict.fromkeys(expanded_names, keyword) skip_validation = True else: - skip_validation = False + for token in tokens: + if check_var_function(token): + # Found CSS variable, keep pending-substitution values. + pending = PendingExpander(tokens, expander) + results = dict.fromkeys(expanded_names, pending) + skip_validation = True + break + + if not skip_validation: results = {} - if wants_base_url: - result = wrapped(name, tokens, base_url) - else: - result = wrapped(name, tokens) + result = expander(tokens=tokens) for new_name, new_token in result: assert new_name in expanded_names, new_name if new_name in results: @@ -120,6 +112,39 @@ def generic_expander_wrapper(base_url, name, tokens): return generic_expander_decorator +@expander('border-color') +@expander('border-style') +@expander('border-width') +@expander('margin') +@expander('padding') +@expander('bleed') +def expand_four_sides(base_url, name, tokens): + """Expand properties setting a token for the four sides of a box.""" + # Make sure we have 4 tokens + if len(tokens) == 1: + tokens *= 4 + elif len(tokens) == 2: + tokens *= 2 # (bottom, left) defaults to (top, right) + elif len(tokens) == 3: + tokens += (tokens[1],) # left defaults to right + elif len(tokens) != 4: + raise InvalidValues( + f'Expected 1 to 4 token components got {len(tokens)}') + for suffix, token in zip(('-top', '-right', '-bottom', '-left'), tokens): + i = name.rfind('-') + if i == -1: + new_name = name + suffix + else: + # eg. border-color becomes border-*-color, not border-color-* + new_name = name[:i] + suffix + name[i:] + + # validate_non_shorthand returns ((name, value),), we want + # to yield (name, value) + result, = validate_non_shorthand( + base_url, new_name, [token], required=True) + yield result + + @expander('border-radius') @generic_expander( 'border-top-left-radius', 'border-top-right-radius', diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index eb40f9aed..6cdfcb003 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -76,7 +76,7 @@ def decorator(function): def validate_non_shorthand(base_url, name, tokens, required=False): - """Default validator for non-shorthand properties.""" + """Validator for non-shorthand properties.""" if name.startswith('--'): # TODO: validate content return ((name, tokens),)