def test_font_face_bad_1(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: "Bad Font";' ' src: url(BadFont.woff);' ' font-stretch: expanded;' ' font-style: wrong;' ' font-weight: bolder;' ' font-stretch: wrong;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' with capture_logs() as logs: font_family, src, font_stretch = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Bad Font') assert src == ( 'src', (('external', 'http://weasyprint.org/foo/BadFont.woff'),)) assert font_stretch == ('font_stretch', 'expanded') assert logs == [ 'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.', 'WARNING: Ignored `font-weight: bolder` at 1:111, invalid value.', 'WARNING: Ignored `font-stretch: wrong` at 1:133, invalid value.']
def get_declarations(rule): """Get the declarations in ``rule``.""" if rule.type == 'qualified-rule': for declaration in tinycss2.parse_declaration_list( rule.content, skip_comments=True, skip_whitespace=True): value = ''.join(part.serialize() for part in declaration.value) # TODO: filter out invalid values yield declaration.lower_name, value, declaration.important
def parse_properties(tokens): asts = tinycss2.parse_declaration_list(tokens, skip_comments=True, skip_whitespace=True) d = [] for prop in asts: value = "".join([t.serialize() for t in prop.value]).strip() # We don't bother simplifying repeated properties in the same block, # since we have to implement a similar but more complete pass later # anyway. d.append(Property(prop.lower_name, value, prop.important)) return d
def test_font_face_3(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: local();' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', None),))
def test_font_face_1(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: url(http://example.com/fonts/Gentium.woff);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list(preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ( 'src', (('external', 'http://example.com/fonts/Gentium.woff'),))
def parse_declarations(input): normal_declarations = [] important_declarations = [] for declaration in tinycss2.parse_declaration_list(input): # TODO: warn on error # if declaration.type == 'error': if (declaration.type == 'declaration' and not declaration.name.startswith('-')): # Serializing perfectly good tokens just to re-parse them later :( value = tinycss2.serialize(declaration.value).strip() declarations = ( important_declarations if declaration.important else normal_declarations) declarations.append((declaration.lower_name, value)) return normal_declarations, important_declarations
def expand_to_dict(css, expected_error=None): """Helper to test shorthand properties expander functions.""" declarations = tinycss2.parse_declaration_list(css) with capture_logs() as logs: base_url = 'http://weasyprint.org/foo/' declarations = list(preprocess_declarations(base_url, declarations)) if expected_error: assert len(logs) == 1 assert expected_error in logs[0] else: assert not logs return dict( (name, value) for name, value, _priority in declarations if value != 'initial')
def check_css(css): """Check that a parsed stylsheet looks like resources/utf8-test.css""" # Using 'encoding' adds a CSSCharsetRule rule = css.rules[-1][0] assert tinycss2.serialize(rule.prelude) == 'h1::before ' content, background = tinycss2.parse_declaration_list( rule.content, skip_whitespace=True) assert content.name == 'content' string, = remove_whitespace(content.value) assert string.value == 'I løvë Unicode' assert background.name == 'background-image' url_value, = remove_whitespace(background.value) assert url_value.type == 'url' url = urljoin(css.base_url, url_value.value) assert url.startswith('file:') assert url.endswith('weasyprint/tests/resources/pattern.png')
def remove_declaration(self, selector, decl_name): rs, idx = self.get_ruleset(selector) if not rs: return declarations = tinycss2.parse_declaration_list(rs.content, True, True) for declaration in declarations: if decl_name == declaration.name: new_content = self._remove_declaration_from_content( declaration, rs.content) if not new_content: self.stylesheet.remove(rs) break self.stylesheet[idx].content = new_content break
def test_font_face_2(): stylesheet = tinycss2.parse_stylesheet('@font-face {' ' font-family: "Fonty Smiley";' ' src: url(Fonty-Smiley.woff);' ' font-style: italic;' ' font-weight: 200;' ' font-stretch: condensed;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src, font_style, font_weight, font_stretch = list( preprocess_descriptors( 'font-face', 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ('src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'), )) assert font_style == ('font_style', 'italic') assert font_weight == ('font_weight', 200) assert font_stretch == ('font_stretch', 'condensed')
def test_font_face_2(): stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: "Fonty Smiley";' ' src: url(Fonty-Smiley.woff);' ' font-style: italic;' ' font-weight: 200;' ' font-stretch: condensed;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src, font_style, font_weight, font_stretch = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ( 'src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'),)) assert font_style == ('font_style', 'italic') assert font_weight == ('font_weight', 200) assert font_stretch == ('font_stretch', 'condensed')
def parse_rule(rule): name, state = None, 'normal' try: declaration = tinycss2.parse_one_declaration(rule.prelude, skip_comments=True) for token in declaration.value: if token.type == 'ident': name, state = declaration.name, token.value except (ValueError, TypeError, AttributeError): for token in rule.prelude: if token.type == 'ident': name = token.value if name is not None: for token in tinycss2.parse_declaration_list(rule.content, skip_comments=True, skip_whitespace=True): if token.type == 'declaration': for t in token.value: yield name, state, token.name, t
def parse(stylesheet): """Parse a stylesheet using tinycss2 and return a StyleSheet instance. :param stylesheet: A string of an existing stylesheet. """ parsed_stylesheet = tinycss2.parse_stylesheet(stylesheet, skip_comments=True, skip_whitespace=True) css = qstylizer.style.StyleSheet() for node in parsed_stylesheet: if node.type == "error": raise ValueError("Cannot parse Stylesheet: " + node.message) selector = tinycss2.serialize(node.prelude).strip() declaration_list = tinycss2.parse_declaration_list( node.content, skip_comments=True, skip_whitespace=True) for declaration in declaration_list: if declaration.type == "declaration": prop = declaration.name.strip() css[selector][prop] = tinycss2.serialize( declaration.value).strip() return css
def _format_css_declarations(content: list, indent_level: int) -> str: """ Helper function for CSS formatting that formats a list of CSS properties, like `margin: 1em;`. INPUTS content: A list of component values generated by the tinycss2 library OUTPUTS A string of formatted CSS """ output = "" for token in tinycss2.parse_declaration_list(content): if token.type == "error": raise se.InvalidCssException(token.message) if token.type == "declaration": output += ("\t" * indent_level) + token.lower_name + ": " output += _format_css_component_list(token.value) if token.important: output += " !important" output += ";\n" if token.type == "comment": output = output.rstrip() if output == "": output += ("\t" * indent_level ) + "/* " + token.value.strip() + " */\n" else: output += " /* " + token.value.strip() + " */\n" return output.rstrip()
def check_style_attribute(element, style_attribute): declarations = tinycss2.parse_declaration_list(style_attribute) return element, declarations, base_url
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 parse_rules(css: str): """ Apply a CSS stylesheet to an XHTML tree. The application is naive and should not be expected to be browser-grade. CSS declarationerties on specific elements can be returned using EasyXmlElement.get_css_declarationerty() For example, for node in dom.xpath("//em")" print(node.get_css_declarationerty("font-style")) """ rules = [] # Parse the stylesheet to break it into rules and their associated declarationerties for token in tinycss2.parse_stylesheet(css, skip_comments=True): if token.type == "error": raise se.InvalidCssException(token.message) # A CSS rule if token.type == "qualified-rule": selectors = tinycss2.serialize(token.prelude).strip() # First, get a list of declarations within the { } block. # Parse each declaration and add it to the rule declarations = [] for item in tinycss2.parse_declaration_list(token.content): if item.type == "error": raise se.InvalidCssException( "Couldn’t parse CSS. Exception: {token.message}") if item.type == "declaration": declaration = CssDeclaration(item.lower_name, item.value, item.important) declarations += declaration.expand() # We can have multiple selectors in a rule separated by `,` for selector in selectors.split(","): # Skip selectors containing pseudo elements if "::" in selector: continue selector = selector.strip() rule = CssRule(selector) # Calculate the specificity of the selector # See https://www.w3.org/TR/CSS2/cascade.html#specificity # a = 0 always (no style attributes apply here) # First remove strings, because they can contain `:` selector = regex.sub(r"\"[^\"]+?\"", "", selector) # b = number of ID attributes specificity_b = len(regex.findall(r"#", selector)) # c = number of other attributes or pseudo classes specificity_c = len(regex.findall(r"[\.\[\:]", selector)) # d = number of element names and pseudo elements (which will be 0 for us) specificity_d = len( regex.findall(r"(?:^[a-z]|\s[a-z])", selector)) rule.specificity = (specificity_b, specificity_c, specificity_d) rule.specificity_number = specificity_b * 100 + specificity_c * 10 + specificity_d # Done with specificity, assign the declarations and save the rule rule.declarations = declarations rules.append(rule) return rules
def validate_qualified_rule(self, rule): prelude_errors = self.validate_component_values(rule.prelude) declarations = tinycss2.parse_declaration_list(rule.content) declaration_errors = self.validate_declaration_list(declarations) return itertools.chain(prelude_errors, declaration_errors)
def fixCss(self, soup): ''' So, because the color scheme of our interface can vary from the original, we need to fix any cases of white text. However, I want to preserve *most* of the color information. Therefore, we look at all the inline CSS, and just patch where needed. ''' # Match the CSS ASCII color classes hexr = re.compile('((?:[a-fA-F0-9]{6})|(?:[a-fA-F0-9]{3}))') def clamp_hash_token(intok, high): old = hexr.findall(intok.value) for match in old: color = webcolors.hex_to_rgb("#"+match) mean = sum(color)/len(color) if high: if mean > 150: color = tuple((max(255-cval, 0) for cval in color)) new = webcolors.rgb_to_hex(color) intok.value = intok.value.replace(match, new) else: if mean < 100: color = tuple((min(cval, 100) for cval in color)) new = webcolors.rgb_to_hex(color).replace("#", "") intok.value = intok.value.replace(match, new) return intok def clamp_css_color(toks, high=True): toks = [tok for tok in toks if tok.type != 'whitespace'] for tok in toks: if tok.type == 'hash': clamp_hash_token(tok, high) if tok.type == 'string': tok.value = "" return toks hascss = soup.find_all(True, attrs={"style" : True}) initial_keys = [ 'font', 'font-family' ] empty_keys = [ 'width', 'height', 'display', 'max-width', 'max-height', 'background-image', ] foreground_color_keys = [ 'color', ] background_color_keys = [ 'background', 'background-color', ] for item in hascss: if item['style']: try: parsed_style = tinycss2.parse_declaration_list(item['style']) for style_chunk in parsed_style: if style_chunk.type == 'declaration': if any([dec_str == style_chunk.name for dec_str in initial_keys]): style_chunk.value = [tinycss2.ast.IdentToken(1, 1, "Sans-Serif")] if any([dec_str == style_chunk.name for dec_str in empty_keys]): style_chunk.value = [] if any([dec_str == style_chunk.name for dec_str in foreground_color_keys]): style_chunk.value = clamp_css_color(style_chunk.value) if any([dec_str == style_chunk.name for dec_str in background_color_keys]): style_chunk.value = clamp_css_color(style_chunk.value, high=False) # Force overflow to be visible if style_chunk.name == "overflow": style_chunk.value = [tinycss2.ast.IdentToken(1, 1, "visible")] parsed_style = [chunk for chunk in parsed_style if chunk.value] item['style'] = tinycss2.serialize(parsed_style) except AttributeError: # If the parser encountered an error, it'll produce 'ParseError' tokens without # the 'value' attribute. This produces attribute errors. # If the style is f****d, just clobber it. item['style'] = "" return soup
def fixCss(self, soup): ''' So, because the color scheme of our interface can vary from the original, we need to fix any cases of white text. However, I want to preserve *most* of the color information. Therefore, we look at all the inline CSS, and just patch where needed. ''' # Match the CSS ASCII color classes hexr = re.compile('((?:[a-fA-F0-9]{6})|(?:[a-fA-F0-9]{3}))') def clamp_hash_token(intok, high): old = hexr.findall(intok.value) for match in old: color = webcolors.hex_to_rgb("#" + match) mean = sum(color) / len(color) if high: if mean > 150: color = tuple((max(255 - cval, 0) for cval in color)) new = webcolors.rgb_to_hex(color) intok.value = intok.value.replace(match, new) else: if mean < 100: color = tuple((min(cval, 100) for cval in color)) new = webcolors.rgb_to_hex(color).replace("#", "") intok.value = intok.value.replace(match, new) return intok def clamp_css_color(toks, high=True): toks = [tok for tok in toks if tok.type != 'whitespace'] for tok in toks: if tok.type == 'hash': clamp_hash_token(tok, high) if tok.type == 'string': tok.value = "" return toks hascss = soup.find_all(True, attrs={"style": True}) initial_keys = ['font', 'font-family'] empty_keys = [ 'font-size', 'width', 'height', 'display', 'max-width', 'max-height', 'background-image', 'margin-bottom', 'line-height', 'vertical-align', 'white-space', 'font-size', 'box-sizing', 'cursor', 'display', 'height', 'left', 'margin-bottom', 'margin-right', 'margin', 'object-fit', 'overflow', 'position', 'right', 'text-align', 'top', 'visibility', 'width', 'z-index', ] foreground_color_keys = [ 'color', ] background_color_keys = [ 'background', 'background-color', ] for item in hascss: if 'tony-yon-ka.blogspot.com' in self.pageUrl: item['style'] = "" elif item['style']: try: parsed_style = tinycss2.parse_declaration_list( item['style']) for style_chunk in parsed_style: if style_chunk.type == 'declaration': if any([ dec_str == style_chunk.name for dec_str in initial_keys ]): style_chunk.value = [ tinycss2.ast.IdentToken( 1, 1, "Sans-Serif") ] if any([ dec_str == style_chunk.name for dec_str in empty_keys ]): style_chunk.value = [] if any([ dec_str == style_chunk.name for dec_str in foreground_color_keys ]): style_chunk.value = clamp_css_color( style_chunk.value) if any([ dec_str == style_chunk.name for dec_str in background_color_keys ]): style_chunk.value = clamp_css_color( style_chunk.value, high=False) # Force overflow to be visible if style_chunk.name == "overflow": style_chunk.value = [ tinycss2.ast.IdentToken(1, 1, "visible") ] parsed_style = [ chunk for chunk in parsed_style if chunk.value ] item['style'] = tinycss2.serialize(parsed_style) except AttributeError: # If the parser encountered an error, it'll produce 'ParseError' tokens without # the 'value' attribute. This produces attribute errors. # If the style is f****d, just clobber it. item['style'] = "" return soup
def test_declaration_list(input): return parse_declaration_list(input, **SKIP)
def test_bad_font_face(): """Test bad ``font-face`` rules.""" stylesheet = tinycss2.parse_stylesheet('@font-face {' ' font-family: "Bad Font";' ' src: url(BadFont.woff);' ' font-stretch: expanded;' ' font-style: wrong;' ' font-weight: bolder;' ' font-stretch: wrong;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' with capture_logs() as logs: font_family, src, font_stretch = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Bad Font') assert src == ('src', (('external', 'http://weasyprint.org/foo/BadFont.woff'), )) assert font_stretch == ('font_stretch', 'expanded') assert logs == [ 'WARNING: Ignored `font-style: wrong` at 1:91, invalid value.', 'WARNING: Ignored `font-weight: bolder` at 1:111, invalid value.', 'WARNING: Ignored `font-stretch: wrong` at 1:133, invalid value.' ] stylesheet = tinycss2.parse_stylesheet('@font-face{}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ "WARNING: Missing src descriptor in '@font-face' rule at 1:1" ] stylesheet = tinycss2.parse_stylesheet('@font-face{src: url(test.woff)}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1" ] stylesheet = tinycss2.parse_stylesheet('@font-face{font-family: test}') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ "WARNING: Missing src descriptor in '@font-face' rule at 1:1" ] stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: test; src: wrong }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ 'WARNING: Ignored `src: wrong ` at 1:33, invalid value.', "WARNING: Missing src descriptor in '@font-face' rule at 1:1" ] stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: good, bad; src: url(test.woff) }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', "WARNING: Missing font-family descriptor in '@font-face' rule at 1:1" ] stylesheet = tinycss2.parse_stylesheet( '@font-face { font-family: good, bad; src: really bad }') with capture_logs() as logs: descriptors = [] preprocess_stylesheet('print', 'http://wp.org/foo/', stylesheet, None, None, None, descriptors, None) assert not descriptors assert logs == [ 'WARNING: Ignored `font-family: good, bad` at 1:14, invalid value.', 'WARNING: Ignored `src: really bad ` at 1:38, invalid value.', "WARNING: Missing src descriptor in '@font-face' rule at 1:1" ]
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
def test_font_face(): """Test the ``font-face`` rule.""" stylesheet = tinycss2.parse_stylesheet( '@font-face {' ' font-family: Gentium Hard;' ' src: url(http://example.com/fonts/Gentium.woff);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('external', 'http://example.com/fonts/Gentium.woff'), )) stylesheet = tinycss2.parse_stylesheet('@font-face {' ' font-family: "Fonty Smiley";' ' src: url(Fonty-Smiley.woff);' ' font-style: italic;' ' font-weight: 200;' ' font-stretch: condensed;' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src, font_style, font_weight, font_stretch = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Fonty Smiley') assert src == ('src', (('external', 'http://weasyprint.org/foo/Fonty-Smiley.woff'), )) assert font_style == ('font_style', 'italic') assert font_weight == ('font_weight', 200) assert font_stretch == ('font_stretch', 'condensed') stylesheet = tinycss2.parse_stylesheet('@font-face {' ' font-family: Gentium Hard;' ' src: local();' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', None), )) # See bug #487 stylesheet = tinycss2.parse_stylesheet('@font-face {' ' font-family: Gentium Hard;' ' src: local(Gentium Hard);' '}') at_rule, = stylesheet assert at_rule.at_keyword == 'font-face' font_family, src = list( preprocess_descriptors( 'http://weasyprint.org/foo/', tinycss2.parse_declaration_list(at_rule.content))) assert font_family == ('font_family', 'Gentium Hard') assert src == ('src', (('local', 'Gentium Hard'), ))
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)
def get_node_style(node): styles = tinycss2.parse_declaration_list(node.get("style", "")) return dict((d.lower_name, "".join([v.serialize() for v in d.value]).strip()) for d in styles if d.type == "declaration")
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 test_serialize_declarations(): source = 'color: #123; /**/ @top-left {} width:7px !important;' rules = parse_declaration_list(source) assert serialize(rules) == source
def __build_data_structures(self, multiprop): """Call after stylesheet has been parsed :param multiprop: As in __init__ :returns: (props, rules) props is dict from prop_name (string) -> (!important (bool), specificity (a, b, c)) -> dict from (selector (parsed_tree), value (normalised string with !important)) -> pair of: line_no (int, not precise line_no but keeps order, and is last occurrence of sel, v in file) boolean -- True if rule appears only once in file rules is set of pairs (S, P) where S is set of cssselect Selector and props is a list of pairs (p, v) of strings property and value """ def get_decl_value(decl): """Returns decl.value normalised and with !important appended if needed""" val = tinycss2.serialize(decl.value) normed = self.__normalise_css_value(val) important = "!important" if decl.important else "" return normed + important props = defaultdict(lambda: defaultdict(dict)) rules = list() line_no = 1 for rule in islice(self.stylesheet, self.first_rule_idx, self.last_rule_idx): sels = set() declarations = None if rule.type != "qualified-rule": rule_str = tinycss2.serialize([rule]) self.ignored_rules.append(rule_str) print "WARNING: merely copying rule ", rule_str continue parsed_rule_decls = tinycss2.parse_declaration_list( rule.content, skip_whitespace=True, skip_comments=True) rule_declarations = [] for i, decl in enumerate(parsed_rule_decls): if decl.type == "error": print "WARNING: parser error in", i + 1, "th declaration of" print tinycss2.serialize(rule.content) print decl.message print "Ignoring declaration" continue rule_declarations.append(decl) if not multiprop: declarations = rule_declarations else: declarations = [] # keep !important and not important separate, to be on the safe # side combined = defaultdict(list) for decl in rule_declarations: val = self.__normalise_css_value( tinycss2.serialize(decl.value)) combined[(decl.lower_name, decl.important)].append(val) def build_val(vals): return multiprop_separator.join(vals) for ((name, priority), vals) in combined.iteritems(): decl = FakeDeclaration(name, priority, build_val(vals)) declarations.append(decl) decls = [(decl.lower_name, get_decl_value(decl)) for decl in rule_declarations] # reset line number to beginning of rule after each selector line_no_start = line_no for sel in self.__parse_selector(rule.prelude): sels.add(sel) line_no = line_no_start for decl in declarations: v = get_decl_value(decl) specificity = (decl.important, sel.parsed_tree.specificity()) tup = (sel, v) m = props[decl.lower_name][specificity] m[tup] = (line_no, not tup in m) line_no += 1 rules.append(CSSRule(sels, decls)) return (props, rules)
def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher, rules, fonts, font_config): """Do the work that can be done early on stylesheet, before they are in a document. """ selector_to_xpath = cssselect.HTMLTranslator().selector_to_xpath for rule in stylesheet_rules: if rule.type == 'qualified-rule': declarations = list( preprocess_declarations( base_url, tinycss2.parse_declaration_list(rule.content))) if declarations: selector_string = tinycss2.serialize(rule.prelude) try: selector_list = [] for selector in cssselect.parse(selector_string): xpath = selector_to_xpath(selector) try: lxml_xpath = lxml.etree.XPath(xpath) except ValueError as exc: # TODO: Some characters are not supported by lxml's # XPath implementation (including control # characters), but these characters are valid in # the CSS2.1 specification. raise cssselect.SelectorError(str(exc)) selector_list.append( Selector((0, ) + selector.specificity(), selector.pseudo_element, lxml_xpath)) for selector in selector_list: if selector.pseudo_element not in PSEUDO_ELEMENTS: raise cssselect.ExpressionError( 'Unknown pseudo-element: %s' % selector.pseudo_element) except cssselect.SelectorError as exc: LOGGER.warning("Invalid or unsupported selector '%s', %s", selector_string, exc) continue rules.append((rule, selector_list, declarations)) 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: stylesheet = CSS(url=url, url_fetcher=url_fetcher, media_type=device_media_type, font_config=font_config) except URLFetchingError as exc: LOGGER.warning('Failed to load stylesheet at %s : %s', url, exc) else: for result in stylesheet.rules: rules.append(result) 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, rules, fonts, font_config) elif rule.type == 'at-rule' and rule.at_keyword == 'page': tokens = remove_whitespace(rule.prelude) # TODO: support named pages (see CSS3 Paged Media) if not tokens: pseudo_class = None specificity = (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 specificity = { 'first': (1, 0), 'blank': (1, 0), 'left': (0, 1), 'right': (0, 1), }.get(pseudo_class) if not specificity: 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 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 content = tinycss2.parse_declaration_list(rule.content) declarations = list(preprocess_declarations(base_url, content)) # Use a double lambda to have a closure that holds page_types match = (lambda page_types: lambda _document: page_types)( PAGE_PSEUDOCLASS_TARGETS[pseudo_class]) if declarations: selector_list = [Selector(specificity, None, match)] 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 = [ Selector(specificity, '@' + margin_rule.at_keyword, match) ] 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)
def parse_style(self, node=None): if node is None: node = self.node declarations = tinycss2.parse_declaration_list(node.get("style", "")) style = {declaration.lower_name: declaration.value[0].serialize() for declaration in declarations} return style