From 1cb5e508deb537dbceb4055e3b1baf4db60d6fef Mon Sep 17 00:00:00 2001 From: Lucie Anglade Date: Fri, 5 Jan 2024 19:43:48 +0100 Subject: [PATCH 01/11] Handle nested qualified rules --- weasyprint/css/__init__.py | 42 ++++++++++++++++----------- weasyprint/css/validation/__init__.py | 17 +++++++++-- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index eae07bc71..7d489b3dc 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -13,6 +13,7 @@ """ from collections import namedtuple +from itertools import groupby from logging import DEBUG, WARNING import cssselect2 @@ -919,25 +920,32 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, continue if rule.type == 'qualified-rule': - declarations = list(preprocess_declarations( - base_url, tinycss2.parse_declaration_list(rule.content))) - if declarations: + selectors_declarations = list( + preprocess_declarations( + base_url, tinycss2.parse_declaration_list(rule.content), + rule.prelude)) + + if selectors_declarations: logger_level = WARNING + selectors_declarations = groupby( + selectors_declarations, key=lambda x: x[0]) try: - selectors = cssselect2.compile_selector_list(rule.prelude) - for selector in selectors: - matcher.add_selector(selector, declarations) - if selector.pseudo_element not in PSEUDO_ELEMENTS: - if selector.pseudo_element.startswith('-'): - logger_level = DEBUG - raise cssselect2.SelectorError( - 'ignored prefixed pseudo-element: ' - f'{selector.pseudo_element}') - else: - raise cssselect2.SelectorError( - 'unknown pseudo-element: ' - f'{selector.pseudo_element}') - ignore_imports = True + for selectors, declarations in selectors_declarations: + declarations = [ + declaration[1] for declaration in declarations] + for selector in selectors: + matcher.add_selector(selector, declarations) + if selector.pseudo_element not in PSEUDO_ELEMENTS: + if selector.pseudo_element.startswith('-'): + logger_level = DEBUG + raise cssselect2.SelectorError( + 'ignored prefixed pseudo-element: ' + f'{selector.pseudo_element}') + else: + raise cssselect2.SelectorError( + 'unknown pseudo-element: ' + f'{selector.pseudo_element}') + ignore_imports = True except cssselect2.SelectorError as exc: LOGGER.log( logger_level, diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 1e24b1e7f..b2b9e76a8 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -1,7 +1,8 @@ """Validate properties, expanders and descriptors.""" -from tinycss2 import serialize +from cssselect2 import compile_selector_list +from tinycss2 import parse_declaration_list, serialize from ... import LOGGER from ..utils import InvalidValues, remove_whitespace @@ -106,7 +107,7 @@ } -def preprocess_declarations(base_url, declarations): +def preprocess_declarations(base_url, declarations, prelude=None): """Expand shorthand properties, filter unsupported properties and values. Log a warning for every ignored declaration. @@ -121,6 +122,11 @@ def preprocess_declarations(base_url, declarations): declaration.message, declaration.source_line, declaration.source_column) + if declaration.type == 'qualified-rule': + yield from preprocess_declarations( + base_url, parse_declaration_list(declaration.content), + declaration.prelude) + if declaration.type != 'declaration': continue @@ -183,4 +189,9 @@ def validation_error(level, reason): important = declaration.important for long_name, value in result: - yield long_name.replace('-', '_'), value, important + if prelude is not None: + selectors = compile_selector_list(prelude) + declaration = (long_name.replace('-', '_'), value, important) + yield selectors, declaration + else: + yield long_name.replace('-', '_'), value, important From 7b02f6719956dbd1a8ae07176aac1f75730969cf Mon Sep 17 00:00:00 2001 From: Lucie Anglade Date: Fri, 19 Jan 2024 16:46:50 +0100 Subject: [PATCH 02/11] Handle & selector --- weasyprint/css/validation/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index b2b9e76a8..e8edb767e 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -3,6 +3,7 @@ from cssselect2 import compile_selector_list from tinycss2 import parse_declaration_list, serialize +from tinycss2.ast import FunctionBlock, LiteralToken from ... import LOGGER from ..utils import InvalidValues, remove_whitespace @@ -123,9 +124,18 @@ def preprocess_declarations(base_url, declarations, prelude=None): declaration.source_line, declaration.source_column) if declaration.type == 'qualified-rule': + declaration_prelude = declaration.prelude + if LiteralToken(1, 1, '&') in declaration.prelude: + is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) + declaration_prelude = [] + for token in declaration.prelude: + if token == LiteralToken(1, 1, '&'): + declaration_prelude.extend(is_token) + else: + declaration_prelude.append(token) yield from preprocess_declarations( base_url, parse_declaration_list(declaration.content), - declaration.prelude) + declaration_prelude) if declaration.type != 'declaration': continue From 3c537a32ccd2d87cd570de12fa9ae1bd4baaa871 Mon Sep 17 00:00:00 2001 From: Lucie Anglade Date: Fri, 19 Jan 2024 17:28:32 +0100 Subject: [PATCH 03/11] Improve error handling for selectors --- weasyprint/css/__init__.py | 36 ++++++++++++++------------- weasyprint/css/validation/__init__.py | 7 ++++-- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 7d489b3dc..e31a68aea 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -920,40 +920,42 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, continue if rule.type == 'qualified-rule': - selectors_declarations = list( - preprocess_declarations( - base_url, tinycss2.parse_declaration_list(rule.content), - rule.prelude)) - - if selectors_declarations: + try: logger_level = WARNING - selectors_declarations = groupby( - selectors_declarations, key=lambda x: x[0]) - try: + selectors_declarations = list( + preprocess_declarations( + base_url, tinycss2.parse_declaration_list(rule.content), + rule.prelude)) + + if selectors_declarations: + selectors_declarations = groupby( + selectors_declarations, key=lambda x: x[0]) for selectors, declarations in selectors_declarations: declarations = [ declaration[1] for declaration in declarations] for selector in selectors: matcher.add_selector(selector, declarations) if selector.pseudo_element not in PSEUDO_ELEMENTS: + prelude_string = tinycss2.serialize(prelude) if selector.pseudo_element.startswith('-'): logger_level = DEBUG raise cssselect2.SelectorError( + f"'{prelude_string}', " 'ignored prefixed pseudo-element: ' f'{selector.pseudo_element}') else: raise cssselect2.SelectorError( + f"'{prelude_string}', " 'unknown pseudo-element: ' f'{selector.pseudo_element}') ignore_imports = True - except cssselect2.SelectorError as exc: - LOGGER.log( - logger_level, - "Invalid or unsupported selector '%s', %s", - tinycss2.serialize(rule.prelude), exc) - continue - else: - ignore_imports = True + else: + ignore_imports = True + except cssselect2.SelectorError as exc: + LOGGER.log( + logger_level, + "Invalid or unsupported selector, %s", exc) + continue elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import': if ignore_imports: diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index e8edb767e..5113588e1 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -1,7 +1,7 @@ """Validate properties, expanders and descriptors.""" -from cssselect2 import compile_selector_list +from cssselect2 import SelectorError, compile_selector_list from tinycss2 import parse_declaration_list, serialize from tinycss2.ast import FunctionBlock, LiteralToken @@ -200,7 +200,10 @@ def validation_error(level, reason): important = declaration.important for long_name, value in result: if prelude is not None: - selectors = compile_selector_list(prelude) + try: + selectors = compile_selector_list(prelude) + except SelectorError as exc: + raise SelectorError(f"'{serialize(prelude)}'") declaration = (long_name.replace('-', '_'), value, important) yield selectors, declaration else: From 9026b06917b31a4bb3285647a99f4157bbf1f841 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 17 Mar 2024 17:31:32 +0100 Subject: [PATCH 04/11] Use parse_blocks_contents() --- tests/css/test_descriptors.py | 12 ++++++------ tests/css/test_expanders.py | 2 +- tests/css/test_validation.py | 2 +- weasyprint/css/__init__.py | 13 +++++++------ weasyprint/css/validation/__init__.py | 16 ++++++++++------ weasyprint/svg/css.py | 2 +- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/tests/css/test_descriptors.py b/tests/css/test_descriptors.py index 0f7a4c386..0ee38b84c 100644 --- a/tests/css/test_descriptors.py +++ b/tests/css/test_descriptors.py @@ -19,7 +19,7 @@ def test_font_face_1(): assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ( 'src', (('external', 'https://example.com/fonts/Gentium.woff'),)) @@ -40,7 +40,7 @@ def test_font_face_2(): font_family, src, font_style, font_weight, font_stretch = list( preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ( 'src', (('external', 'https://weasyprint.org/foo/Fonty-Smiley.woff'),)) @@ -60,7 +60,7 @@ def test_font_face_3(): assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', None),)) @@ -77,7 +77,7 @@ def test_font_face_4(): assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'),)) @@ -96,7 +96,7 @@ def test_font_face_5(): with capture_logs() as logs: font_family, src = list(preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'),)) assert len(logs) == 1 @@ -119,7 +119,7 @@ def test_font_face_bad_1(): font_family, src, font_stretch = list( preprocess_descriptors( 'font-face', 'https://weasyprint.org/foo/', - tinycss2.parse_declaration_list(at_rule.content))) + tinycss2.parse_blocks_contents(at_rule.content))) assert font_family == ('font_family', 'Bad Font') assert src == ( 'src', (('external', 'https://weasyprint.org/foo/BadFont.woff'),)) diff --git a/tests/css/test_expanders.py b/tests/css/test_expanders.py index d780fc9ac..f7c2c2e05 100644 --- a/tests/css/test_expanders.py +++ b/tests/css/test_expanders.py @@ -12,7 +12,7 @@ def expand_to_dict(css, expected_error=None): """Helper to test shorthand properties expander functions.""" - declarations = tinycss2.parse_declaration_list(css) + declarations = tinycss2.parse_blocks_contents(css) with capture_logs() as logs: base_url = 'https://weasyprint.org/foo/' diff --git a/tests/css/test_validation.py b/tests/css/test_validation.py index 643166307..f3308a99d 100644 --- a/tests/css/test_validation.py +++ b/tests/css/test_validation.py @@ -12,7 +12,7 @@ def get_value(css, expected_error=None): - declarations = tinycss2.parse_declaration_list(css) + declarations = tinycss2.parse_blocks_contents(css) with capture_logs() as logs: base_url = 'https://weasyprint.org/foo/' diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index e31a68aea..9cbaa823c 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -298,7 +298,7 @@ def find_style_attributes(tree, presentational_hints=False, base_url=None): """ def check_style_attribute(element, style_attribute): - declarations = tinycss2.parse_declaration_list(style_attribute) + declarations = tinycss2.parse_blocks_contents(style_attribute) return element, declarations, base_url for element in tree.iter(): @@ -924,7 +924,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, logger_level = WARNING selectors_declarations = list( preprocess_declarations( - base_url, tinycss2.parse_declaration_list(rule.content), + base_url, tinycss2.parse_blocks_contents(rule.content), rule.prelude)) if selectors_declarations: @@ -933,6 +933,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, for selectors, declarations in selectors_declarations: declarations = [ declaration[1] for declaration in declarations] + print(selectors, declarations) for selector in selectors: matcher.add_selector(selector, declarations) if selector.pseudo_element not in PSEUDO_ELEMENTS: @@ -1036,7 +1037,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, for page_type in data: specificity = page_type.pop('specificity') page_type = PageType(**page_type) - content = tinycss2.parse_declaration_list(rule.content) + content = tinycss2.parse_blocks_contents(rule.content) declarations = list(preprocess_declarations(base_url, content)) if declarations: @@ -1049,7 +1050,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, continue declarations = list(preprocess_declarations( base_url, - tinycss2.parse_declaration_list(margin_rule.content))) + tinycss2.parse_blocks_contents(margin_rule.content))) if declarations: selector_list = [( specificity, f'@{margin_rule.lower_at_keyword}', @@ -1059,7 +1060,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': ignore_imports = True - content = tinycss2.parse_declaration_list(rule.content) + content = tinycss2.parse_blocks_contents(rule.content) rule_descriptors = dict( preprocess_descriptors('font-face', base_url, content)) for key in ('src', 'font_family'): @@ -1086,7 +1087,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, continue ignore_imports = True - content = tinycss2.parse_declaration_list(rule.content) + content = tinycss2.parse_blocks_contents(rule.content) counter = { 'system': None, 'negative': None, diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 5113588e1..abe5b34e0 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -2,7 +2,7 @@ from cssselect2 import SelectorError, compile_selector_list -from tinycss2 import parse_declaration_list, serialize +from tinycss2 import parse_blocks_contents, serialize from tinycss2.ast import FunctionBlock, LiteralToken from ... import LOGGER @@ -116,6 +116,12 @@ def preprocess_declarations(base_url, declarations, prelude=None): Return a iterable of ``(name, value, important)`` tuples. """ + if prelude is not None: + try: + selectors = compile_selector_list(prelude) + except SelectorError as exc: + raise SelectorError(f"'{serialize(prelude)}'") + for declaration in declarations: if declaration.type == 'error': LOGGER.warning( @@ -124,6 +130,8 @@ def preprocess_declarations(base_url, declarations, prelude=None): declaration.source_line, declaration.source_column) if declaration.type == 'qualified-rule': + if prelude is None: + continue declaration_prelude = declaration.prelude if LiteralToken(1, 1, '&') in declaration.prelude: is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) @@ -134,7 +142,7 @@ def preprocess_declarations(base_url, declarations, prelude=None): else: declaration_prelude.append(token) yield from preprocess_declarations( - base_url, parse_declaration_list(declaration.content), + base_url, parse_blocks_contents(declaration.content), declaration_prelude) if declaration.type != 'declaration': @@ -200,10 +208,6 @@ def validation_error(level, reason): important = declaration.important for long_name, value in result: if prelude is not None: - try: - selectors = compile_selector_list(prelude) - except SelectorError as exc: - raise SelectorError(f"'{serialize(prelude)}'") declaration = (long_name.replace('-', '_'), value, important) yield selectors, declaration else: diff --git a/weasyprint/svg/css.py b/weasyprint/svg/css.py index 6a4c18009..39d61ec73 100644 --- a/weasyprint/svg/css.py +++ b/weasyprint/svg/css.py @@ -35,7 +35,7 @@ def parse_declarations(input): """Parse declarations in a given rule content.""" normal_declarations = [] important_declarations = [] - for declaration in tinycss2.parse_declaration_list(input): + for declaration in tinycss2.parse_blocks_contents(input): # TODO: warn on error # if declaration.type == 'error': if (declaration.type == 'declaration' and From 9df0a5bf637b3035f97b35f5fae6ee2a2f9db899 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 20 Jan 2024 10:50:33 +0100 Subject: [PATCH 05/11] Fix and clean stylesheet preprocessing --- weasyprint/css/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 9cbaa823c..eac13793c 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -933,20 +933,19 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, for selectors, declarations in selectors_declarations: declarations = [ declaration[1] for declaration in declarations] - print(selectors, declarations) for selector in selectors: matcher.add_selector(selector, declarations) if selector.pseudo_element not in PSEUDO_ELEMENTS: - prelude_string = tinycss2.serialize(prelude) + prelude = tinycss2.serialize(rule.prelude) if selector.pseudo_element.startswith('-'): logger_level = DEBUG raise cssselect2.SelectorError( - f"'{prelude_string}', " + f"'{prelude}', " 'ignored prefixed pseudo-element: ' f'{selector.pseudo_element}') else: raise cssselect2.SelectorError( - f"'{prelude_string}', " + f"'{prelude}', " 'unknown pseudo-element: ' f'{selector.pseudo_element}') ignore_imports = True From c7b5ff34df50d8022291cfeb005630984c626814 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 12 Feb 2024 17:46:23 +0100 Subject: [PATCH 06/11] Handle nested selectors with no & --- weasyprint/css/validation/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index abe5b34e0..e7595d9c6 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -3,7 +3,7 @@ from cssselect2 import SelectorError, compile_selector_list from tinycss2 import parse_blocks_contents, serialize -from tinycss2.ast import FunctionBlock, LiteralToken +from tinycss2.ast import FunctionBlock, LiteralToken, WhitespaceToken from ... import LOGGER from ..utils import InvalidValues, remove_whitespace @@ -141,6 +141,10 @@ def preprocess_declarations(base_url, declarations, prelude=None): declaration_prelude.extend(is_token) else: declaration_prelude.append(token) + else: + is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) + declaration_prelude = [ + *is_token, WhitespaceToken(1, 1, ' '), *declaration.prelude] yield from preprocess_declarations( base_url, parse_blocks_contents(declaration.content), declaration_prelude) From 31f4cefa59479aba161e021894425ff595e51b96 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Mon, 12 Feb 2024 18:34:22 +0100 Subject: [PATCH 07/11] Handle & selector in non-nested rules --- weasyprint/css/validation/__init__.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index e7595d9c6..4025ca5e9 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -3,7 +3,8 @@ from cssselect2 import SelectorError, compile_selector_list from tinycss2 import parse_blocks_contents, serialize -from tinycss2.ast import FunctionBlock, LiteralToken, WhitespaceToken +from tinycss2.ast import ( + FunctionBlock, IdentToken, LiteralToken, WhitespaceToken) from ... import LOGGER from ..utils import InvalidValues, remove_whitespace @@ -106,6 +107,8 @@ 'scrollbar-gutter', 'scrollbar-width', } +NESTING_SELECTOR = LiteralToken(1, 1, '&') +ROOT_TOKEN = LiteralToken(1, 1, ':'), IdentToken(1, 1, 'root') def preprocess_declarations(base_url, declarations, prelude=None): @@ -118,10 +121,21 @@ def preprocess_declarations(base_url, declarations, prelude=None): """ if prelude is not None: try: + if NESTING_SELECTOR in prelude: + # Handle & selector in non-nested rule. MDN explains that & is + # then equivalent to :scope, and :scope is equivalent to :root + # as we don’t support :scope yet. + original_prelude, prelude = prelude, [] + for token in original_prelude: + if token == NESTING_SELECTOR: + prelude.extend(ROOT_TOKEN) + else: + prelude.append(token) selectors = compile_selector_list(prelude) - except SelectorError as exc: + except SelectorError: raise SelectorError(f"'{serialize(prelude)}'") + is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) for declaration in declarations: if declaration.type == 'error': LOGGER.warning( @@ -133,11 +147,10 @@ def preprocess_declarations(base_url, declarations, prelude=None): if prelude is None: continue declaration_prelude = declaration.prelude - if LiteralToken(1, 1, '&') in declaration.prelude: - is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) + if NESTING_SELECTOR in declaration.prelude: declaration_prelude = [] for token in declaration.prelude: - if token == LiteralToken(1, 1, '&'): + if token == NESTING_SELECTOR: declaration_prelude.extend(is_token) else: declaration_prelude.append(token) From ede51e2edd61f85636bbf94be6070e8a784931ae Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Tue, 13 Feb 2024 21:58:21 +0100 Subject: [PATCH 08/11] Add some comments --- weasyprint/css/validation/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 4025ca5e9..3787df5be 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -119,6 +119,7 @@ def preprocess_declarations(base_url, declarations, prelude=None): Return a iterable of ``(name, value, important)`` tuples. """ + # Compile list of selectors. if prelude is not None: try: if NESTING_SELECTOR in prelude: @@ -135,6 +136,7 @@ def preprocess_declarations(base_url, declarations, prelude=None): except SelectorError: raise SelectorError(f"'{serialize(prelude)}'") + # Yield declarations. is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) for declaration in declarations: if declaration.type == 'error': @@ -144,10 +146,12 @@ def preprocess_declarations(base_url, declarations, prelude=None): declaration.source_line, declaration.source_column) if declaration.type == 'qualified-rule': + # Nested rule. if prelude is None: continue declaration_prelude = declaration.prelude if NESTING_SELECTOR in declaration.prelude: + # Replace & selector by parent. declaration_prelude = [] for token in declaration.prelude: if token == NESTING_SELECTOR: @@ -155,6 +159,7 @@ def preprocess_declarations(base_url, declarations, prelude=None): else: declaration_prelude.append(token) else: + # No & selector, prepend parent. is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) declaration_prelude = [ *is_token, WhitespaceToken(1, 1, ' '), *declaration.prelude] From c6cd4b7b06e307169ac467caedd68d138aa3b24d Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sun, 17 Mar 2024 17:36:11 +0100 Subject: [PATCH 09/11] Test CSS nesting --- tests/css/test_nesting.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/css/test_nesting.py diff --git a/tests/css/test_nesting.py b/tests/css/test_nesting.py new file mode 100644 index 000000000..f3b4630cd --- /dev/null +++ b/tests/css/test_nesting.py @@ -0,0 +1,27 @@ +"""Test CSS nesting.""" + +import pytest + +from ..testing_utils import assert_no_logs, render_pages + + +@assert_no_logs +@pytest.mark.parametrize('style', ( + 'div { p { width: 10px } }', + 'p { div & { width: 10px } }', + 'p { width: 20px; div & { width: 10px } }', + 'p { div & { width: 10px } width: 20px }', + 'div { & { & { p { & { width: 10px } } } } }', + '@media print { div { p { width: 10px } } }', +)) +def test_nesting_block(style): + page, = render_pages(''' + +

+ ''' % style) + html, = page.children + body, = html.children + div, p = body.children + div_p, = div.children + assert div_p.width == 10 + assert p.width != 10 From 8784ab8a3e44406db31ac4b06528f7bf6ce99643 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Sat, 17 Feb 2024 12:48:58 +0100 Subject: [PATCH 10/11] Update required version of tinycss2 --- docs/first_steps.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/first_steps.rst b/docs/first_steps.rst index b5833aa9d..ac89e1e13 100644 --- a/docs/first_steps.rst +++ b/docs/first_steps.rst @@ -14,7 +14,7 @@ WeasyPrint |version| depends on: * pydyf_ ≥ 0.8.0 * CFFI_ ≥ 0.6 * html5lib_ ≥ 1.1 -* tinycss2_ ≥ 1.0.0 +* tinycss2_ ≥ 1.3.0 * cssselect2_ ≥ 0.1 * Pyphen_ ≥ 0.9.1 * Pillow_ ≥ 9.1.0 diff --git a/pyproject.toml b/pyproject.toml index d3e1eb2e1..d793c988c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ 'pydyf >=0.8.0', 'cffi >=0.6', 'html5lib >=1.1', - 'tinycss2 >=1.0.0', + 'tinycss2 >=1.3.0', 'cssselect2 >=0.1', 'Pyphen >=0.9.1', 'Pillow >=9.1.0', From 982f74c07dcafc521deb5c4df9a04d986245ac71 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Wed, 24 Apr 2024 10:30:26 +0200 Subject: [PATCH 11/11] Split long lines --- weasyprint/css/validation/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/weasyprint/css/validation/__init__.py b/weasyprint/css/validation/__init__.py index 3787df5be..9b084fbbc 100644 --- a/weasyprint/css/validation/__init__.py +++ b/weasyprint/css/validation/__init__.py @@ -160,9 +160,12 @@ def preprocess_declarations(base_url, declarations, prelude=None): declaration_prelude.append(token) else: # No & selector, prepend parent. - is_token = LiteralToken(1, 1, ':'), FunctionBlock(1, 1, 'is', prelude) + is_token = ( + LiteralToken(1, 1, ':'), + FunctionBlock(1, 1, 'is', prelude)) declaration_prelude = [ - *is_token, WhitespaceToken(1, 1, ' '), *declaration.prelude] + *is_token, WhitespaceToken(1, 1, ' '), + *declaration.prelude] yield from preprocess_declarations( base_url, parse_blocks_contents(declaration.content), declaration_prelude)