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.']
Beispiel #2
0
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
Beispiel #3
0
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'),))
Beispiel #6
0
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 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')
Beispiel #10
0
    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
Beispiel #11
0
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')
Beispiel #13
0
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
Beispiel #14
0
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
Beispiel #15
0
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()
Beispiel #16
0
 def check_style_attribute(element, style_attribute):
     declarations = tinycss2.parse_declaration_list(style_attribute)
     return element, declarations, base_url
Beispiel #17
0
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)
Beispiel #18
0
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
Beispiel #19
0
 def check_style_attribute(element, style_attribute):
     declarations = tinycss2.parse_declaration_list(style_attribute)
     return element, declarations, base_url
Beispiel #20
0
 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
Beispiel #22
0
    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
Beispiel #23
0
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"
    ]
Beispiel #25
0
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'), ))
Beispiel #27
0
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)
Beispiel #28
0
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")
Beispiel #29
0
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)
Beispiel #30
0
def test_serialize_declarations():
    source = 'color: #123; /**/ @top-left {} width:7px !important;'
    rules = parse_declaration_list(source)
    assert serialize(rules) == source
Beispiel #31
0
 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)
Beispiel #32
0
    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)
Beispiel #34
0
 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