diff --git a/docs/install.rst b/docs/install.rst index 342ec22c9..aa1da27e8 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -10,7 +10,7 @@ WeasyPrint |version| depends on: * CFFI_ ≥ 0.6 * html5lib_ ≥ 0.999999999 * cairocffi_ ≥ 0.9.0 -* tinycss2_ ≥ 0.5 +* tinycss2_ ≥ 1.0.0 * cssselect2_ ≥ 0.1 * CairoSVG_ ≥ 1.0.20 * Pyphen_ ≥ 0.8 diff --git a/setup.cfg b/setup.cfg index 6448c1cbe..94461793d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = cffi>=0.6 html5lib>=0.999999999 cairocffi>=0.9.0 - tinycss2>=0.5 + tinycss2>=1.0.0 cssselect2>=0.1 CairoSVG>=1.0.20 Pyphen>=0.8 diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index e2df8750b..34ff7cb34 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -489,8 +489,9 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, # Fast path for anonymous boxes: # no cascaded style, only implicitly initial or inherited values. computed = dict(INITIAL_VALUES) - for name in INHERITED: - computed[name] = parent_style[name] + for name in parent_style: + if name in INHERITED or name.startswith('__'): + computed[name] = parent_style[name] # page is not inherited but taken from the ancestor if 'auto' computed['page'] = parent_style['page'] # border-*-style is none, so border-width computes to zero. @@ -504,6 +505,15 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, # Handle inheritance and initial values specified = {} computed = {} + + if parent_style: + for name in parent_style: + if name.startswith('__'): + specified[name] = parent_style[name] + for name in cascaded: + if name.startswith('__'): + specified[name] = cascaded[name][0] + for name, initial in INITIAL_VALUES.items(): if name in cascaded: value, _precedence = cascaded[name] diff --git a/weasyprint/css/computed_values.py b/weasyprint/css/computed_values.py index 6c5476e9b..bdce0b1fa 100644 --- a/weasyprint/css/computed_values.py +++ b/weasyprint/css/computed_values.py @@ -17,9 +17,10 @@ from .. import text from ..logger import LOGGER from ..urls import get_link_attribute -from .properties import INITIAL_VALUES, Dimension +from .properties import INHERITED, INITIAL_VALUES, Dimension from .utils import ( - ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, safe_urljoin) + ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, check_var_function, + safe_urljoin) ZERO_PIXELS = Dimension(0, 'px') @@ -139,6 +140,33 @@ def _computing_order(): COMPUTER_FUNCTIONS = {} +def _resolve_var(computed, variable_name, default): + known_variable_names = [variable_name] + + computed_value = computed.get(variable_name) + if computed_value and len(computed_value) == 1: + value = computed_value[0] + if value.type == 'ident' and value.value == 'initial': + return default + + computed_value = computed.get(variable_name, default) + while (computed_value and + isinstance(computed_value, tuple) + and len(computed_value) == 1): + var_function = check_var_function(computed_value[0]) + if var_function: + new_variable_name, new_default = var_function[1] + if new_variable_name in known_variable_names: + computed_value = default + break + known_variable_names.append(new_variable_name) + computed_value = computed.get(new_variable_name, new_default) + default = new_default + else: + break + return computed_value + + def register_computer(name): """Decorator registering a property ``name`` for a function.""" name = name.replace('-', '_') @@ -167,6 +195,7 @@ def compute(element, pseudo_type, specified, computed, parent_style, :param target_collector: A target collector used to get computed targets. """ + from .validation.properties import PROPERTIES def computer(): """Dummy object that holds attributes.""" @@ -187,6 +216,10 @@ def computer(): getter = COMPUTER_FUNCTIONS.get + for name in specified: + if name.startswith('__'): + computed[name] = specified[name] + for name in COMPUTING_ORDER: if name in computed: # Already computed @@ -194,6 +227,33 @@ def computer(): value = specified[name] function = getter(name) + + if value and isinstance(value, tuple) and value[0] == 'var()': + variable_name, default = value[1] + computed_value = _resolve_var(computed, variable_name, default) + if computed_value is None: + new_value = None + else: + new_value = PROPERTIES[name.replace('_', '-')](computed_value) + + # See https://drafts.csswg.org/css-variables/#invalid-variables + if new_value is None: + try: + computed_value = ''.join( + token.serialize() for token in computed_value) + except BaseException: + pass + LOGGER.warning( + 'Unsupported computed value `%s` set in variable `%s` ' + 'for property `%s`.', computed_value, + variable_name.replace('_', '-'), name.replace('_', '-')) + if name in INHERITED and parent_style: + value = parent_style[name] + else: + value = INITIAL_VALUES[name] + else: + value = new_value + if function is not None: value = function(computer, name, value) # else: same as specified diff --git a/weasyprint/css/utils.py b/weasyprint/css/utils.py index 3de4c9017..e1ca951b0 100644 --- a/weasyprint/css/utils.py +++ b/weasyprint/css/utils.py @@ -377,7 +377,7 @@ def parse_function(function_token): space-separated arguments. Return ``None`` otherwise. """ - if not function_token.type == 'function': + if not getattr(function_token, 'type', None) == 'function': return content = list(remove_whitespace(function_token.arguments)) @@ -397,6 +397,8 @@ def parse_function(function_token): if argument_function is None: return arguments.append(token) + if last_is_comma: + return return function_token.lower_name, arguments @@ -499,6 +501,21 @@ def check_string_function(token): return ('string()', (custom_ident, ident)) +def check_var_function(token): + function = parse_function(token) + if function is None: + return + name, args = function + if name == 'var' and args: + ident = args.pop(0) + if ident.type != 'ident' or not ident.value.startswith('--'): + return + + # TODO: we should check authorized tokens + # https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value + return ('var()', (ident.value.replace('-', '_'), args or None)) + + def get_string(token): """Parse a token.""" if token.type == 'string': diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 450a02eff..e61b40a2d 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -78,7 +78,9 @@ def preprocess_declarations(base_url, declarations): if declaration.type != 'declaration': continue - name = declaration.lower_name + name = declaration.name + if not name.startswith('--'): + name = declaration.lower_name def validation_error(level, reason): getattr(LOGGER, level)( @@ -114,7 +116,7 @@ def validation_error(level, reason): unprefixed_name) continue - if name.startswith('-'): + if name.startswith('-') and not name.startswith('--'): validation_error('debug', 'prefixed selectors are ignored') continue diff --git a/weasyprint/css/validation/properties.py b/weasyprint/css/validation/properties.py index db6105985..436bfa176 100644 --- a/weasyprint/css/validation/properties.py +++ b/weasyprint/css/validation/properties.py @@ -16,10 +16,11 @@ from .. import computed_values from ..properties import KNOWN_PROPERTIES, Dimension from ..utils import ( - InvalidValues, comma_separated_list, get_angle, get_content_list, - get_content_list_token, get_custom_ident, get_image, get_keyword, - get_length, get_resolution, get_single_keyword, get_url, parse_2d_position, - parse_background_position, parse_function, single_keyword, single_token) + InvalidValues, check_var_function, comma_separated_list, get_angle, + get_content_list, get_content_list_token, get_custom_ident, get_image, + get_keyword, get_length, get_resolution, get_single_keyword, get_url, + parse_2d_position, parse_background_position, parse_function, + single_keyword, single_token) PREFIX = '-weasy-' PROPRIETARY = set() @@ -80,6 +81,10 @@ def decorator(function): def validate_non_shorthand(base_url, name, tokens, required=False): """Default validator for non-shorthand properties.""" + if name.startswith('--'): + # TODO: validate content + return ((name, tokens),) + if not required and name not in KNOWN_PROPERTIES: hyphens_name = name.replace('_', '-') if hyphens_name in KNOWN_PROPERTIES: @@ -90,6 +95,11 @@ def validate_non_shorthand(base_url, name, tokens, required=False): if not required and name not in PROPERTIES: raise InvalidValues('property not supported yet') + for token in tokens: + var_function = check_var_function(token) + if var_function: + return ((name, var_function),) + keyword = get_single_keyword(tokens) if keyword in ('initial', 'inherit'): value = keyword diff --git a/weasyprint/layout/inlines.py b/weasyprint/layout/inlines.py index c54ac6bc3..d2324c7b0 100644 --- a/weasyprint/layout/inlines.py +++ b/weasyprint/layout/inlines.py @@ -1230,14 +1230,13 @@ def add_word_spacing(context, box, justification_spacing, x_advance): box.position_x += x_advance nb_spaces = count_spaces(box) if nb_spaces > 0: - layout, _, resume_at, width, _, _ = split_first_line( + layout, _, resume_at, _, _, _ = split_first_line( box.text, box.style, context, float('inf'), box.justification_spacing) assert resume_at is None - # XXX new_box.width - box.width is always 0??? - # x_advance += new_box.width - box.width - x_advance += justification_spacing * nb_spaces - box.width = width + extra_space = justification_spacing * nb_spaces + x_advance += extra_space + box.width += extra_space box.pango_layout = layout elif isinstance(box, (boxes.LineBox, boxes.InlineBox)): box.position_x += x_advance diff --git a/weasyprint/tests/test_variables.py b/weasyprint/tests/test_variables.py new file mode 100644 index 000000000..60b89bc4d --- /dev/null +++ b/weasyprint/tests/test_variables.py @@ -0,0 +1,97 @@ +""" + weasyprint.tests.test_variables + ------------------------------- + + Test CSS custom proproperties, also known as CSS variables. + + :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS. + :license: BSD, see LICENSE for details. + +""" + +from .test_boxes import render_pages as parse + + +def test_variable_simple(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10 + + +def test_variable_inherit(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10 + + +def test_variable_inherit_override(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10 + + +def test_variable_case_sensitive(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10 + + +def test_variable_chain(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10 + + +def test_variable_initial(): + page, = parse(''' + +

+ ''') + html, = page.children + body, = html.children + paragraph, = body.children + assert paragraph.width == 10