""" weasyprint.css -------------- This module takes care of steps 3 and 4 of “CSS 2.1 processing model”: Retrieve stylesheets associated with a document and annotate every element with a value for every CSS property. http://www.w3.org/TR/CSS21/intro.html#processing-model This module does this in more than two steps. The :func:`get_all_computed_styles` function does everything, but it is itsef based on other functions in this module. :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS. :license: BSD, see LICENSE for details. """ from collections import namedtuple from logging import DEBUG, WARNING import cssselect2 import tinycss2 from .. import CSS from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join from . import computed_values, media_queries from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES from .utils import remove_whitespace from .validation import preprocess_declarations from .validation.descriptors import preprocess_descriptors # Reject anything not in here: PSEUDO_ELEMENTS = (None, 'before', 'after', 'first-line', 'first-letter') PageType = namedtuple('PageType', ['side', 'blank', 'first', 'index', 'name']) class StyleFor: """Convenience function to get the computed styles for an element.""" def __init__(self, html, sheets, presentational_hints, target_collector): # keys: (element, pseudo_element_type) # element: an ElementTree Element or the '@page' string # pseudo_element_type: a string such as 'first' (for @page) or # 'after', or None for normal elements # values: dicts of # keys: property name as a string # values: (values, weight) # values: a PropertyValue-like object # weight: values with a greater weight take precedence, see # http://www.w3.org/TR/CSS21/cascade.html#cascading-order self._cascaded_styles = cascaded_styles = {} # keys: (element, pseudo_element_type), like cascaded_styles # values: style dict objects: # keys: property name as a string # values: a PropertyValue-like object self._computed_styles = computed_styles = {} PROGRESS_LOGGER.info('Step 3 - Applying CSS') for specificity, attributes in find_style_attributes( html.etree_element, presentational_hints, html.base_url): element, declarations, base_url = attributes for name, values, importance in preprocess_declarations( base_url, declarations): precedence = declaration_precedence('author', importance) weight = (precedence, specificity) add_declaration(cascaded_styles, name, values, weight, element) # First, add declarations and set computed styles for "real" elements # *in tree order*. Tree order is important so that parents have # computed styles before their children, for inheritance. # Iterate on all elements, even if there is no cascaded style for them. for element in html.wrapper_element.iter_subtree(): for sheet, origin, sheet_specificity in sheets: # Add declarations for matched elements for selector in sheet.matcher.match(element): specificity, order, pseudo_type, declarations = selector specificity = sheet_specificity or specificity for name, values, importance in declarations: precedence = declaration_precedence(origin, importance) weight = (precedence, specificity) add_declaration( cascaded_styles, name, values, weight, element.etree_element, pseudo_type) parent = element.parent.etree_element if element.parent else None self.set_computed_styles( element.etree_element, root=html.etree_element, parent=parent, base_url=html.base_url, target_collector=target_collector) page_names = {style['page'] for style in computed_styles.values()} for sheet, origin, sheet_specificity in sheets: # Add declarations for page elements for _rule, selector_list, declarations in sheet.page_rules: for selector in selector_list: specificity, pseudo_type, match = selector specificity = sheet_specificity or specificity for page_type in match(page_names): for name, values, importance in declarations: precedence = declaration_precedence( origin, importance) weight = (precedence, specificity) add_declaration( cascaded_styles, name, values, weight, page_type, pseudo_type) # Then computed styles for pseudo elements, in any order. # Pseudo-elements inherit from their associated element so they come # last. Do them in a second pass as there is no easy way to iterate # on the pseudo-elements for a given element with the current structure # of cascaded_styles. (Keys are (element, pseudo_type) tuples.) # Only iterate on pseudo-elements that have cascaded styles. (Others # might as well not exist.) for element, pseudo_type in cascaded_styles: if pseudo_type and not isinstance(element, PageType): self.set_computed_styles( element, pseudo_type=pseudo_type, # The pseudo-element inherits from the element. root=html.etree_element, parent=element, base_url=html.base_url, target_collector=target_collector) def __call__(self, element, pseudo_type=None): style = self._computed_styles.get((element, pseudo_type)) if style: if 'table' in style['display']: if (style['display'] in ('table', 'inline-table') and style['border_collapse'] == 'collapse'): # Padding do not apply for side in ['top', 'bottom', 'left', 'right']: style['padding_' + side] = computed_values.ZERO_PIXELS if (style['display'].startswith('table-') and style['display'] != 'table-caption'): # Margins do not apply for side in ['top', 'bottom', 'left', 'right']: style['margin_' + side] = computed_values.ZERO_PIXELS return style def set_computed_styles(self, element, parent, root=None, pseudo_type=None, base_url=None, target_collector=None): """Set the computed values of styles to ``element``. Take the properties left by ``apply_style_rule`` on an element or pseudo-element and assign computed values with respect to the cascade, declaration priority (ie. ``!important``) and selector specificity. """ cascaded_styles = self.get_cascaded_styles() computed_styles = self.get_computed_styles() if element == root and pseudo_type is None: assert parent is None parent_style = None root_style = { # When specified on the font-size property of the root element, # the rem units refer to the property’s initial value. 'font_size': INITIAL_VALUES['font_size'], } else: assert parent is not None parent_style = computed_styles[parent, None] root_style = computed_styles[root, None] cascaded = cascaded_styles.get((element, pseudo_type), {}) computed_styles[element, pseudo_type] = computed_from_cascaded( element, cascaded, parent_style, pseudo_type, root_style, base_url, target_collector) def get_cascaded_styles(self): return self._cascaded_styles def get_computed_styles(self): return self._computed_styles def get_child_text(element): """Return the text directly in the element, not descendants.""" content = [element.text] if element.text else [] for child in element: if child.tail: content.append(child.tail) return ''.join(content) def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url, font_config, page_rules): """Yield the stylesheets in ``element_tree``. The output order is the same as the source order. """ from ..html import element_has_link_type # Work around circular imports. for wrapper in wrapper_element.query_all('style', 'link'): element = wrapper.etree_element mime_type = element.get('type', 'text/css').split(';', 1)[0].strip() # Only keep 'type/subtype' from 'type/subtype ; param1; param2'. if mime_type != 'text/css': continue media_attr = element.get('media', '').strip() or 'all' media = [media_type.strip() for media_type in media_attr.split(',')] if not media_queries.evaluate_media_query(media, device_media_type): continue if element.tag == 'style': # Content is text that is directly in the