Skip to content

Commit

Permalink
Fix many corner cases with CSS variables
Browse files Browse the repository at this point in the history
Fix #1287, fix #2010.
  • Loading branch information
liZe committed Nov 25, 2023
1 parent a09ded7 commit 6eb5f96
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 67 deletions.
25 changes: 25 additions & 0 deletions tests/draw/test_current_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,28 @@ def test_current_color_svg_2(assert_pixels):
width="2" height="2">
<rect width="2" height="2" fill="currentColor"></rect>
</svg>''')


@assert_no_logs
def test_current_color_variable(assert_pixels):
# Regression test for https://github.com/Kozea/WeasyPrint/issues/2010
assert_pixels('GG\nGG', '''
<style>
@font-face { src: url(weasyprint.otf); font-family: weasyprint }
@page { size: 2px }
html { color: lime; font-family: weasyprint; --var: currentColor }
div { color: var(--var); font-size: 2px; line-height: 1 }
</style>
<div>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', '''
<style>
@page { size: 2px }
html { color: lime; --var: currentColor }
div { color: var(--var); width: 0; height: 0; border: 1px solid }
</style>
<div>''')
19 changes: 19 additions & 0 deletions tests/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,22 @@ def test_variable_fallback(prop):
</style>
<div></div>
''' % prop)


@assert_no_logs
def test_variable_list():
# Regression test for https://github.com/Kozea/WeasyPrint/issues/1287
page, = parse('''
<style>
:root { --var: "Page " counter(page) "/" counter(pages) }
div::before { content: var(--var) }
</style>
<div></div>
''')
html, = page.children
body, = html.children
div, = body.children
line, = div.children
before, = line.children
text, = before.children
assert text.text == 'Page 1/1'
73 changes: 34 additions & 39 deletions weasyprint/css/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
88 changes: 60 additions & 28 deletions weasyprint/css/computed_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 6eb5f96

Please sign in to comment.