Skip to content

Commit

Permalink
Use "Pending*" objects for all properties containing var() functions
Browse files Browse the repository at this point in the history
  • Loading branch information
liZe committed Dec 29, 2023
1 parent 4e235fe commit 1d7cf97
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 188 deletions.
88 changes: 35 additions & 53 deletions weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +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, resolve_var)
from .computed_values import COMPUTER_FUNCTIONS, ZERO_PIXELS, resolve_var
from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES
from .utils import (
InvalidValues, 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 PendingProperty

# Reject anything not in here:
PSEUDO_ELEMENTS = (
Expand Down Expand Up @@ -167,18 +167,6 @@ def set_computed_styles(self, element, parent, root=None, pseudo_type=None,
element, cascaded, parent_style, pseudo_type, root_style, base_url,
target_collector)

# The style of marker is deleted when display is different from
# list-item.
if pseudo_type is None:
for pseudo in (None, 'before', 'after'):
pseudo_style = cascaded_styles.get((element, pseudo), {})
if 'display' in pseudo_style:
if 'list-item' in pseudo_style['display'][0]:
break
else:
if (element, 'marker') in cascaded_styles:
del cascaded_styles[element, 'marker']

def add_page_declarations(self, page_type):
for sheet, origin, sheet_specificity in self._sheets:
for _rule, selector_list, declarations in sheet.page_rules:
Expand Down Expand Up @@ -681,27 +669,49 @@ def __missing__(self, key):

if key in self.cascaded:
# Property defined in cascaded properties.
keyword, computed = compute_var(key, self, parent_style)
value = keyword
value = self.cascaded[key][0]
else:
# Property not defined in cascaded properties, define as inherited
# or initial value.
computed = False
if key in INHERITED or key[:2] == '__':
keyword = 'inherit'
value = 'inherit'
else:
keyword = 'initial'
value = 'initial'

if keyword == 'inherit' and parent_style is None:
if value == 'inherit' and parent_style is None:
# On the root element, 'inherit' from initial values
keyword = 'initial'
value = 'initial'

if keyword == 'initial':
value = None if key[:2] == '__' else INITIAL_VALUES[key]
if isinstance(value, (PendingProperty, PendingExpander)):
# Property with pending values, validate them.
solved_tokens = []
for token in value.tokens:
if variable := check_var_function(token):
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('_', '-')
try:
value = value.solve(solved_tokens, original_key)
except InvalidValues:
if key in INHERITED:
# Values in parent_style are already computed.
self[key] = value = parent_style[key]
else:
value = INITIAL_VALUES[key]
if key not in INITIAL_NOT_COMPUTED:
# The value is the same as when computed.
self[key] = value

if value == 'initial':
value = [] if key[:2] == '__' else INITIAL_VALUES[key]
if key not in INITIAL_NOT_COMPUTED:
# The value is the same as when computed.
self[key] = value
elif keyword == 'inherit':
elif value == 'inherit':
# Values in parent_style are already computed.
self[key] = value = parent_style[key]

Expand Down Expand Up @@ -731,35 +741,7 @@ def __missing__(self, key):
# Value already computed and saved: return.
return self[key]

if isinstance(value, PendingExpander):
# Expander with pending values, validate them.
computed = False
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('_', '-')
try:
value = value.solve(solved_tokens, original_key)
except InvalidValues:
if key in INHERITED:
# Values in parent_style are already computed.
self[key] = value = parent_style[key]
return value
else:
value = INITIAL_VALUES[key]
if key not in INITIAL_NOT_COMPUTED:
# The value is the same as when computed.
self[key] = value
return value

if not computed and key in COMPUTER_FUNCTIONS:
if key in COMPUTER_FUNCTIONS:
# Value not computed yet: compute.
value = COMPUTER_FUNCTIONS[key](self, key, value)

Expand Down
72 changes: 1 addition & 71 deletions weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Convert specified property values into computed values."""

from collections import OrderedDict
from contextlib import suppress
from math import pi
from urllib.parse import unquote

Expand All @@ -11,13 +10,10 @@
from ..text.ffi import ffi, pango, units_to_double
from ..text.line_break import Layout, first_line_metrics
from ..urls import get_link_attribute
from .properties import (
INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, Dimension)
from .properties import INITIAL_VALUES, Dimension
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')

Expand Down Expand Up @@ -212,72 +208,6 @@ def decorator(function):
return decorator


def compute_var(name, computed_style, parent_style):
original_values = computed_style.cascaded[name][0]
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 = list(original_values)
else:
# Property with single value, put in a list.
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 name in INHERITED and parent_style:
computed = True
default_values = parent_style[name]
else:
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
value = resolve_var(
computed_style, variable_name, default, parent_style)
if value is None:
LOGGER.warning(
'Unknown variable %r set for property %r.',
variable_name.replace('_', '-'), validation_name)
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


def compute_attr(style, values):
# TODO: use real token parsing instead of casting with Python types
func_name, value = values
Expand Down
2 changes: 1 addition & 1 deletion weasyprint/css/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def check_var_function(token):

# TODO: we should check authorized tokens
# https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
return ('var()', (ident.value.replace('-', '_'), args or None))
return ('var()', (ident.value.replace('-', '_'), args))


def get_string(token):
Expand Down
4 changes: 2 additions & 2 deletions weasyprint/css/validation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,15 @@ def validation_error(level, reason):
validation_error('debug', 'prefixed selectors are ignored')
continue

expander = EXPANDERS.get(name, validate_non_shorthand)
validator = EXPANDERS.get(name, validate_non_shorthand)
tokens = remove_whitespace(declaration.value)
try:
# Having no tokens is allowed by grammar but refused by all
# properties and expanders.
if not tokens:
raise InvalidValues('no value')
# Use list() to consume generators now and catch any error.
result = list(expander(tokens, name, base_url))
result = list(validator(tokens, name, base_url))
except InvalidValues as exc:
validation_error(
'warning',
Expand Down
19 changes: 8 additions & 11 deletions weasyprint/css/validation/expanders.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,27 @@
class PendingExpander:
"""Expander with validation done when defining calculated values."""
# See https://drafts.csswg.org/css-variables-2/#variables-in-shorthands.
def __init__(self, tokens, expander):
def __init__(self, tokens, validator):
self.tokens = tokens
self.expander = expander
self.validator = validator
self._reported_error = False

def solve(self, tokens, wanted_key):
"""Get validated value for wanted key."""
try:
for key, value in self.expander(tokens):
for key, value in self.validator(tokens):
if key.startswith('-'):
key = f'{self.expander.keywords["name"]}{key}'
key = f'{self.validator.keywords["name"]}{key}'
if key == wanted_key:
return value
except InvalidValues as exc:
if self._reported_error:
raise exc
source_line = tokens[0].source_line
source_column = tokens[0].source_column
prop = self.expander.keywords["name"]
prop = self.validator.keywords['name']
source_line = self.tokens[0].source_line
source_column = self.tokens[0].source_column
value = ' '.join(token.serialize() for token in tokens)
if exc.args and exc.args[0]:
message = exc.args[0]
else:
message = 'invalid value'
message = (exc.args and exc.args[0]) or 'invalid value'
LOGGER.warning(
'Ignored `%s: %s` at %d:%d, %s.',
prop, value, source_line, source_column, message)
Expand Down
Loading

0 comments on commit 1d7cf97

Please sign in to comment.