Handle @page rule grammar correctly

Fix #562.
This commit is contained in:
Guillaume Ayoub 2018-01-28 22:17:26 +01:00
parent 1636a37d99
commit dffab39c37
2 changed files with 177 additions and 55 deletions

View File

@ -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))
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))
if 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))
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

View File

@ -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."""