diff --git a/weasyprint/css/__init__.py b/weasyprint/css/__init__.py index 193ae1267..61e1e76f7 100644 --- a/weasyprint/css/__init__.py +++ b/weasyprint/css/__init__.py @@ -586,6 +586,82 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None, base_url)) +def parse_page_selectors(rule): + """Parse a page selector rule. + + Return a list of page data if the rule is correctly parsed. Page data are a + dict containing: + + - 'side' ('left', 'right' or None), + - 'blank' (True or False), + - 'first' (True or False), + - 'name' (page name string or None), and + - 'spacificity' (list of numbers). + + Return ``None` if something went wrong while parsing the rule. + + """ + # See https://drafts.csswg.org/css-page-3/#syntax-page-selector + + tokens = list(remove_whitespace(rule.prelude)) + page_data = [] + + # TODO: Specificity is probably wrong, should clean and test that. + if not tokens: + page_data.append({ + 'side': None, 'blank': False, 'first': False, 'name': None, + 'specificity': [0, 0, 0]}) + return page_data + + while tokens: + types = { + 'side': None, 'blank': False, 'first': False, 'name': None, + 'specificity': [0, 0, 0]} + + if tokens[0].type == 'ident': + token = tokens.pop(0) + types['name'] = token.value + types['specificity'][0] = 1 + + if len(tokens) == 1: + return None + elif not tokens: + page_data.append(types) + return page_data + + while tokens: + literal = tokens.pop(0) + if literal.type != 'literal': + return None + + if literal.value == ':': + if not tokens or tokens[0].type != 'ident': + return None + ident = tokens.pop(0) + pseudo_class = ident.lower_value + if pseudo_class in ('left', 'right'): + if types['side']: + return None + types['side'] = pseudo_class + types['specificity'][2] += 1 + elif pseudo_class in ('blank', 'first'): + if types[pseudo_class]: + return None + types[pseudo_class] = True + types['specificity'][1] += 1 + else: + return None + elif literal.value == ',': + if tokens and any(types['specificity']): + break + else: + return None + + page_data.append(types) + + return page_data + + def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, matcher, page_rules, fonts, font_config, ignore_imports=False): @@ -671,64 +747,44 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, matcher, page_rules, fonts, font_config, ignore_imports=True) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page': - tokens = remove_whitespace(rule.prelude) - types = { - 'side': None, 'blank': False, 'first': False, 'name': None} - # TODO: Specificity is probably wrong, should clean and test that. - if not tokens: - specificity = (0, 0, 0) - elif (len(tokens) == 2 and - tokens[0].type == 'literal' and - tokens[0].value == ':' and - tokens[1].type == 'ident'): - pseudo_class = tokens[1].lower_value - if pseudo_class in ('left', 'right'): - types['side'] = pseudo_class - specificity = (0, 0, 1) - elif pseudo_class in ('blank', 'first'): - types[pseudo_class] = True - specificity = (0, 1, 0) - else: - LOGGER.warning('Unknown @page pseudo-class "%s", ' - 'the whole @page rule was ignored ' - 'at %s:%s.', - pseudo_class, - rule.source_line, rule.source_column) - continue - elif len(tokens) == 1 and tokens[0].type == 'ident': - types['name'] = tokens[0].value - specificity = (1, 0, 0) - else: - LOGGER.warning('Unsupported @page selector "%s", ' - 'the whole @page rule was ignored at %s:%s.', - tinycss2.serialize(rule.prelude), - rule.source_line, rule.source_column) + data = parse_page_selectors(rule) + + if data is None: + LOGGER.warning( + 'Unsupported @page selector "%s", ' + 'the whole @page rule was ignored at %s:%s.', + tinycss2.serialize(rule.prelude), + rule.source_line, rule.source_column) continue - ignore_imports = True - page_type = PageType(**types) - # Use a double lambda to have a closure that holds page_types - match = (lambda page_type: lambda page_names: list( - matching_page_types(page_type, names=page_names)))(page_type) - content = tinycss2.parse_declaration_list(rule.content) - declarations = list(preprocess_declarations(base_url, content)) - if declarations: - selector_list = [(specificity, None, match)] - page_rules.append((rule, selector_list, declarations)) + ignore_imports = True + for page_type in data: + specificity = page_type.pop('specificity') + page_type = PageType(**page_type) + # Use a double lambda to have a closure that holds page_types + match = (lambda page_type: lambda page_names: list( + matching_page_types(page_type, names=page_names)))( + page_type) + content = tinycss2.parse_declaration_list(rule.content) + declarations = list(preprocess_declarations(base_url, content)) - for margin_rule in content: - if margin_rule.type != 'at-rule' or ( - margin_rule.content is None): - continue - declarations = list(preprocess_declarations( - base_url, - tinycss2.parse_declaration_list(margin_rule.content))) if declarations: - selector_list = [( - specificity, '@' + margin_rule.lower_at_keyword, - match)] - page_rules.append( - (margin_rule, selector_list, declarations)) + selector_list = [(specificity, None, match)] + page_rules.append((rule, selector_list, declarations)) + + for margin_rule in content: + if margin_rule.type != 'at-rule' or ( + margin_rule.content is None): + continue + declarations = list(preprocess_declarations( + base_url, + tinycss2.parse_declaration_list(margin_rule.content))) + if declarations: + selector_list = [( + specificity, '@' + margin_rule.lower_at_keyword, + match)] + page_rules.append( + (margin_rule, selector_list, declarations)) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': ignore_imports = True diff --git a/weasyprint/tests/test_css.py b/weasyprint/tests/test_css.py index bf8bef852..2fd53336c 100644 --- a/weasyprint/tests/test_css.py +++ b/weasyprint/tests/test_css.py @@ -12,10 +12,11 @@ from __future__ import division, unicode_literals +import tinycss2 from pytest import raises from .. import CSS, css, default_url_fetcher -from ..css import PageType, get_all_computed_styles +from ..css import PageType, get_all_computed_styles, parse_page_selectors from ..css.computed_values import strut_layout from ..layout.pages import set_page_type_computed_styles from ..urls import open_data_url, path2url @@ -288,6 +289,71 @@ def test_page(): assert style.font_size == 10 +@assert_no_logs +def test_page_selectors(): + """Test the ``@page`` selectors parsing.""" + at_rule, = tinycss2.parse_stylesheet('@page {}') + assert parse_page_selectors(at_rule) == [ + {'side': None, 'blank': False, 'first': False, 'name': None, + 'specificity': [0, 0, 0]}] + + at_rule, = tinycss2.parse_stylesheet('@page :left {}') + assert parse_page_selectors(at_rule) == [ + {'side': 'left', 'blank': False, 'first': False, 'name': None, + 'specificity': [0, 0, 1]}] + + at_rule, = tinycss2.parse_stylesheet('@page:first:left {}') + assert parse_page_selectors(at_rule) == [ + {'side': 'left', 'blank': False, 'first': True, 'name': None, + 'specificity': [0, 1, 1]}] + + at_rule, = tinycss2.parse_stylesheet('@page pagename {}') + assert parse_page_selectors(at_rule) == [ + {'side': None, 'blank': False, 'first': False, 'name': 'pagename', + 'specificity': [1, 0, 0]}] + + at_rule, = tinycss2.parse_stylesheet('@page pagename:first:right:blank {}') + assert parse_page_selectors(at_rule) == [ + {'side': 'right', 'blank': True, 'first': True, 'name': 'pagename', + 'specificity': [1, 2, 1]}] + + at_rule, = tinycss2.parse_stylesheet('@page pagename, :first {}') + assert parse_page_selectors(at_rule) == [ + {'side': None, 'blank': False, 'first': False, 'name': 'pagename', + 'specificity': [1, 0, 0]}, + {'side': None, 'blank': False, 'first': True, 'name': None, + 'specificity': [0, 1, 0]}] + + at_rule, = tinycss2.parse_stylesheet('@page page page {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :left page {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :left, {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page , {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :left, test, {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :wrong {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :left:wrong {}') + assert parse_page_selectors(at_rule) is None + + # TODO: The rules following this line should probably be correct and + # ignored, but they are currently rejected. + at_rule, = tinycss2.parse_stylesheet('@page :first:first {}') + assert parse_page_selectors(at_rule) is None + + at_rule, = tinycss2.parse_stylesheet('@page :left:right {}') + assert parse_page_selectors(at_rule) is None + + @assert_no_logs def test_warnings(): """Check that appropriate warnings are logged."""