def test_invalid_selectors(test): try: compile_selector_list(test['selector']) except SelectorError: pass else: # pragma: no cover raise AssertionError( f'Should be invalid: {test["selector"]!r} ({test["name"]})')
def test_invalid_selectors(test): if test.get('xfail'): pytest.xfail() try: compile_selector_list(test['selector']) except SelectorError: pass else: raise AssertionError('Should be invalid: %(selector)r %(name)s' % test)
def parse_stylesheets(tree, url, session=None): """Find and parse the stylesheets in ``tree``. Return two :class:`cssselect2.Matcher` objects, for normal and !important declarations. """ normal_matcher = cssselect2.Matcher() important_matcher = cssselect2.Matcher() for stylesheet in find_stylesheets(tree): for rule in find_stylesheets_rules(tree, stylesheet, url, session=session): normal_declarations, important_declarations = parse_declarations( rule.content) for selector in cssselect2.compile_selector_list(rule.prelude): if (selector.pseudo_element is None and not selector.never_matches): if normal_declarations: normal_matcher.add_selector(selector, normal_declarations) if important_declarations: important_matcher.add_selector(selector, important_declarations) return normal_matcher, important_matcher
def parse_stylesheets(tree, url): """Find stylesheets and return rule matchers in given tree.""" normal_matcher = cssselect2.Matcher() important_matcher = cssselect2.Matcher() # Find stylesheets # TODO: support contentStyleType on <svg> stylesheets = [] for element in tree.etree_element.iter(): # http://www.w3.org/TR/SVG/styling.html#StyleElement if (element.tag == '{http://www.w3.org/2000/svg}style' and element.get('type', 'text/css') == 'text/css' and element.text): # TODO: pass href for relative URLs # TODO: support media types # TODO: what if <style> has children elements? stylesheets.append(tinycss2.parse_stylesheet( element.text, skip_comments=True, skip_whitespace=True)) # Parse rules and fill matchers for stylesheet in stylesheets: for rule in find_stylesheets_rules(tree, stylesheet, url): normal_declarations, important_declarations = parse_declarations( rule.content) for selector in cssselect2.compile_selector_list(rule.prelude): if (selector.pseudo_element is None and not selector.never_matches): if normal_declarations: normal_matcher.add_selector( selector, normal_declarations) if important_declarations: important_matcher.add_selector( selector, important_declarations) return normal_matcher, important_matcher
def __init__(self, style_element): super().__init__() rules = tinycss2.parse_stylesheet(style_element.text if style_element is not None else '', skip_comments=True, skip_whitespace=True) for rule in rules: selectors = cssselect2.compile_selector_list(rule.prelude) declarations = [obj for obj in tinycss2.parse_declaration_list( rule.content, skip_whitespace=True) if obj.type == 'declaration'] for selector in selectors: self.add_selector(selector, declarations)
def __init__(self, stylesheet): '''Parse CSS and add rules to the matcher.''' super().__init__() rules = tinycss2.parse_stylesheet(stylesheet, skip_comments=True, skip_whitespace=True) for rule in rules: selectors = cssselect2.compile_selector_list(rule.prelude) declarations = [ obj for obj in tinycss2.parse_declaration_list( rule.content, skip_whitespace=True) if obj.type == 'declaration' ] for selector in selectors: self.add_selector(selector, declarations)
def __init__(self, style_content): super(CSSMatcher, self).__init__() self.rules = tinycss2.parse_stylesheet(style_content, skip_comments=True, skip_whitespace=True) for rule in self.rules: if not rule.prelude: continue selectors = cssselect2.compile_selector_list(rule.prelude) selector_string = tinycss2.serialize(rule.prelude) content_dict = dict( (attr.split(':')[0].strip(), attr.split(':')[1].strip()) for attr in tinycss2.serialize(rule.content).split(';') if ':' in attr) payload = (selector_string, content_dict) for selector in selectors: self.add_selector(selector, payload)
def select(rule: str, element: et.Element) -> 'PQuery': rules = tinycss2.parse_stylesheet(rule + ' {}', skip_whitespace=True) matcher = cssselect2.Matcher() for rule in rules: selectors = cssselect2.compile_selector_list(rule.prelude) for selector in selectors: matcher.add_selector(selector, None) wrapper = cssselect2.ElementWrapper.from_html_root(element) matching_elements: List[et.Element] = [] for element in wrapper.iter_subtree(): matches = matcher.match(element) if matches: matching_elements.append(element.etree_element) return PQuery(matching_elements)
def parse_stylesheets(tree, url): """Find and parse the stylesheets in ``tree``. Return two :class:`cssselect2.Matcher` objects, for normal and !important declarations. """ normal_matcher = cssselect2.Matcher() important_matcher = cssselect2.Matcher() for stylesheet in find_stylesheets(tree): for rule in find_stylesheets_rules(tree, stylesheet, url): normal_declarations, important_declarations = parse_declarations( rule.content) for selector in cssselect2.compile_selector_list(rule.prelude): if (selector.pseudo_element is None and not selector.never_matches): if normal_declarations: normal_matcher.add_selector( selector, normal_declarations) if important_declarations: important_matcher.add_selector( selector, important_declarations) return normal_matcher, important_matcher
def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, matcher, page_rules, fonts, font_config, ignore_imports=False): """Do the work that can be done early on stylesheet, before they are in a document. """ for rule in stylesheet_rules: if getattr(rule, 'content', None) is None and ( rule.type != 'at-rule' or rule.lower_at_keyword != 'import'): continue if rule.type == 'qualified-rule': declarations = list( preprocess_declarations( base_url, tinycss2.parse_declaration_list(rule.content))) if declarations: logger_level = WARNING 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: %s' % selector.pseudo_element) else: raise cssselect2.SelectorError( 'unknown pseudo-element: %s' % 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 elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import': if ignore_imports: LOGGER.warning( '@import rule "%s" not at the beginning of the ' 'the whole rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue tokens = remove_whitespace(rule.prelude) if tokens and tokens[0].type in ('url', 'string'): url = tokens[0].value else: continue media = parse_media_query(tokens[1:]) if media is None: LOGGER.warning( 'Invalid media type "%s" ' 'the whole @import rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue if not evaluate_media_query(media, device_media_type): continue url = url_join(base_url, url, allow_relative=False, context='@import at %s:%s', context_args=(rule.source_line, rule.source_column)) if url is not None: try: CSS(url=url, url_fetcher=url_fetcher, media_type=device_media_type, font_config=font_config, matcher=matcher, page_rules=page_rules) except URLFetchingError as exc: LOGGER.error('Failed to load stylesheet at %s : %s', url, exc) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'media': media = parse_media_query(rule.prelude) if media is None: LOGGER.warning( 'Invalid media type "%s" ' 'the whole @media rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue ignore_imports = True if not evaluate_media_query(media, device_media_type): continue content_rules = tinycss2.parse_rule_list(rule.content) preprocess_stylesheet(device_media_type, base_url, content_rules, url_fetcher, matcher, page_rules, fonts, font_config, ignore_imports=True) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page': 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 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)) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': ignore_imports = True content = tinycss2.parse_declaration_list(rule.content) rule_descriptors = dict(preprocess_descriptors(base_url, content)) for key in ('src', 'font_family'): if key not in rule_descriptors: LOGGER.warning( "Missing %s descriptor in '@font-face' rule at %s:%s", key.replace('_', '-'), rule.source_line, rule.source_column) break else: if font_config is not None: font_filename = font_config.add_font_face( rule_descriptors, url_fetcher) if font_filename: fonts.append(font_filename)
def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, matcher, page_rules, fonts, font_config, ignore_imports=False): """Do the work that can be done early on stylesheet, before they are in a document. """ for rule in stylesheet_rules: if getattr(rule, 'content', None) is None and ( rule.type != 'at-rule' or rule.lower_at_keyword != 'import'): continue if rule.type == 'qualified-rule': declarations = list(preprocess_declarations( base_url, tinycss2.parse_declaration_list(rule.content))) if declarations: logger_level = WARNING 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: %s' % selector.pseudo_element) else: raise cssselect2.SelectorError( 'unknown pseudo-element: %s' % 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 elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import': if ignore_imports: LOGGER.warning('@import rule "%s" not at the beginning of the ' 'the whole rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue tokens = remove_whitespace(rule.prelude) if tokens and tokens[0].type in ('url', 'string'): url = tokens[0].value else: continue media = media_queries.parse_media_query(tokens[1:]) if media is None: LOGGER.warning('Invalid media type "%s" ' 'the whole @import rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue if not media_queries.evaluate_media_query( media, device_media_type): continue url = url_join( base_url, url, allow_relative=False, context='@import at %s:%s', context_args=(rule.source_line, rule.source_column)) if url is not None: try: CSS( url=url, url_fetcher=url_fetcher, media_type=device_media_type, font_config=font_config, matcher=matcher, page_rules=page_rules) except URLFetchingError as exc: LOGGER.error( 'Failed to load stylesheet at %s : %s', url, exc) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'media': media = media_queries.parse_media_query(rule.prelude) if media is None: LOGGER.warning('Invalid media type "%s" ' 'the whole @media rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue ignore_imports = True if not media_queries.evaluate_media_query( media, device_media_type): continue content_rules = tinycss2.parse_rule_list(rule.content) preprocess_stylesheet( device_media_type, base_url, content_rules, url_fetcher, matcher, page_rules, fonts, font_config, ignore_imports=True) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page': 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 for page_type in data: specificity = page_type.pop('specificity') page_type = PageType(**page_type) content = tinycss2.parse_declaration_list(rule.content) declarations = list(preprocess_declarations(base_url, content)) if declarations: selector_list = [(specificity, None, page_type)] 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, page_type)] page_rules.append( (margin_rule, selector_list, declarations)) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': ignore_imports = True content = tinycss2.parse_declaration_list(rule.content) rule_descriptors = dict(preprocess_descriptors(base_url, content)) for key in ('src', 'font_family'): if key not in rule_descriptors: LOGGER.warning( "Missing %s descriptor in '@font-face' rule at %s:%s", key.replace('_', '-'), rule.source_line, rule.source_column) break else: if font_config is not None: font_filename = font_config.add_font_face( rule_descriptors, url_fetcher) if font_filename: fonts.append(font_filename)
def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, matcher, page_rules, fonts, font_config, counter_style, ignore_imports=False): """Do the work that can be done early on stylesheet, before they are in a document. """ for rule in stylesheet_rules: if getattr(rule, 'content', None) is None and ( rule.type != 'at-rule' or rule.lower_at_keyword != 'import'): continue if rule.type == 'qualified-rule': declarations = list( preprocess_declarations( base_url, tinycss2.parse_declaration_list(rule.content))) if declarations: logger_level = WARNING 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 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 elif rule.type == 'at-rule' and rule.lower_at_keyword == 'import': if ignore_imports: LOGGER.warning( '@import rule %r not at the beginning of the ' 'the whole rule was ignored at %d:%d.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue tokens = remove_whitespace(rule.prelude) url = None if tokens: if tokens[0].type == 'string': url = url_join(base_url, tokens[0].value, allow_relative=False, context='@import at %s:%s', context_args=(rule.source_line, rule.source_column)) else: url_tuple = get_url(tokens[0], base_url) if url_tuple and url_tuple[1][0] == 'external': url = url_tuple[1][1] if url is None: continue media = media_queries.parse_media_query(tokens[1:]) if media is None: LOGGER.warning( 'Invalid media type %r ' 'the whole @import rule was ignored at %d:%d.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue if not media_queries.evaluate_media_query(media, device_media_type): continue if url is not None: try: CSS(url=url, url_fetcher=url_fetcher, media_type=device_media_type, font_config=font_config, counter_style=counter_style, matcher=matcher, page_rules=page_rules) except URLFetchingError as exc: LOGGER.error('Failed to load stylesheet at %s : %s', url, exc) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'media': media = media_queries.parse_media_query(rule.prelude) if media is None: LOGGER.warning( 'Invalid media type %r ' 'the whole @media rule was ignored at %d:%d.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue ignore_imports = True if not media_queries.evaluate_media_query(media, device_media_type): continue content_rules = tinycss2.parse_rule_list(rule.content) preprocess_stylesheet(device_media_type, base_url, content_rules, url_fetcher, matcher, page_rules, fonts, font_config, counter_style, ignore_imports=True) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page': data = parse_page_selectors(rule) if data is None: LOGGER.warning( 'Unsupported @page selector %r, ' 'the whole @page rule was ignored at %d:%d.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue ignore_imports = True for page_type in data: specificity = page_type.pop('specificity') page_type = PageType(**page_type) content = tinycss2.parse_declaration_list(rule.content) declarations = list(preprocess_declarations(base_url, content)) if declarations: selector_list = [(specificity, None, page_type)] 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, f'@{margin_rule.lower_at_keyword}', page_type) ] page_rules.append( (margin_rule, selector_list, declarations)) elif rule.type == 'at-rule' and rule.lower_at_keyword == 'font-face': ignore_imports = True content = tinycss2.parse_declaration_list(rule.content) rule_descriptors = dict( preprocess_descriptors('font-face', base_url, content)) for key in ('src', 'font_family'): if key not in rule_descriptors: LOGGER.warning( "Missing %s descriptor in '@font-face' rule at %d:%d", key.replace('_', '-'), rule.source_line, rule.source_column) break else: if font_config is not None: font_filename = font_config.add_font_face( rule_descriptors, url_fetcher) if font_filename: fonts.append(font_filename) elif (rule.type == 'at-rule' and rule.lower_at_keyword == 'counter-style'): name = counters.parse_counter_style_name(rule.prelude, counter_style) if name is None: LOGGER.warning( 'Invalid counter style name %r, the whole ' '@counter-style rule was ignored at %d:%d.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue ignore_imports = True content = tinycss2.parse_declaration_list(rule.content) counter = { 'system': None, 'negative': None, 'prefix': None, 'suffix': None, 'range': None, 'pad': None, 'fallback': None, 'symbols': None, 'additive_symbols': None, } rule_descriptors = dict( preprocess_descriptors('counter-style', base_url, content)) for descriptor_name, descriptor_value in rule_descriptors.items(): counter[descriptor_name] = descriptor_value if counter['system'] is None: system = (None, 'symbolic', None) else: system = counter['system'] if system[0] is None: if system[1] in ('cyclic', 'fixed', 'symbolic'): if len(counter['symbols'] or []) < 1: LOGGER.warning( 'In counter style %r at %d:%d, ' 'counter style %r needs at least one symbol', name, rule.source_line, rule.source_column, system[1]) continue elif system[1] in ('alphabetic', 'numeric'): if len(counter['symbols'] or []) < 2: LOGGER.warning( 'In counter style %r at %d:%d, ' 'counter style %r needs at least two symbols', name, rule.source_line, rule.source_column, system[1]) continue elif system[1] == 'additive': if len(counter['additive_symbols'] or []) < 2: LOGGER.warning( 'In counter style %r at %d:%d, ' 'counter style "additive" ' 'needs at least two additive symbols', name, rule.source_line, rule.source_column) continue counter_style[name] = counter
import cssselect2 import tinycss2 # Parse CSS and add rules to the matcher matcher = cssselect2.Matcher() rules = tinycss2.parse_stylesheet(''' body { font-size: 2em } body p { background: red } p { color: blue } ''', skip_whitespace=True) for rule in rules: selectors = cssselect2.compile_selector_list(rule.prelude) selector_string = tinycss2.serialize(rule.prelude) content_string = tinycss2.serialize(rule.content) payload = (selector_string, content_string) for selector in selectors: matcher.add_selector(selector, payload) # Parse HTML and find CSS rules applying to each tag html_tree = ElementTree.fromstring(''' <html> <body> <p>Test</p> </body> </html>
def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, matcher, page_rules, fonts, font_config): """Do the work that can be done early on stylesheet, before they are in a document. """ for rule in stylesheet_rules: if rule.type == 'qualified-rule': declarations = list( preprocess_declarations( base_url, tinycss2.parse_declaration_list(rule.content))) if declarations: 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: raise cssselect2.SelectorError( 'Unknown pseudo-element: %s' % selector.pseudo_element) except cssselect2.SelectorError as exc: LOGGER.warning("Invalid or unsupported selector '%s', %s", tinycss2.serialize(rule.prelude), exc) continue elif rule.type == 'at-rule' and rule.at_keyword == 'import': tokens = remove_whitespace(rule.prelude) if tokens and tokens[0].type in ('url', 'string'): url = tokens[0].value else: continue media = parse_media_query(tokens[1:]) if media is None: LOGGER.warning( 'Invalid media type "%s" ' 'the whole @import rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) if not evaluate_media_query(media, device_media_type): continue url = url_join(base_url, url, allow_relative=False, context='@import at %s:%s', context_args=(rule.source_line, rule.source_column)) if url is not None: try: CSS(url=url, url_fetcher=url_fetcher, media_type=device_media_type, font_config=font_config, matcher=matcher, page_rules=page_rules) except URLFetchingError as exc: LOGGER.error('Failed to load stylesheet at %s : %s', url, exc) elif rule.type == 'at-rule' and rule.at_keyword == 'media': media = parse_media_query(rule.prelude) if media is None: LOGGER.warning( 'Invalid media type "%s" ' 'the whole @media rule was ignored at %s:%s.', tinycss2.serialize(rule.prelude), rule.source_line, rule.source_column) continue if not evaluate_media_query(media, device_media_type): continue content_rules = tinycss2.parse_rule_list(rule.content) preprocess_stylesheet(device_media_type, base_url, content_rules, url_fetcher, matcher, page_rules, fonts, font_config) elif rule.type == 'at-rule' and rule.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) continue 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)) for margin_rule in content: if margin_rule.type != 'at-rule': continue declarations = list( preprocess_declarations( base_url, tinycss2.parse_declaration_list(margin_rule.content))) if declarations: selector_list = [(specificity, '@' + margin_rule.at_keyword, match)] page_rules.append( (margin_rule, selector_list, declarations)) elif rule.type == 'at-rule' and rule.at_keyword == 'font-face': content = tinycss2.parse_declaration_list(rule.content) rule_descriptors = dict(preprocess_descriptors(base_url, content)) for key in ('src', 'font_family'): if key not in rule_descriptors: LOGGER.warning( "Missing %s descriptor in '@font-face' rule at %s:%s", key.replace('_', '-'), rule.source_line, rule.source_column) break else: if font_config is not None: font_filename = font_config.add_font_face( rule_descriptors, url_fetcher) if font_filename: fonts.append(font_filename)