""" 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. """ from collections import namedtuple from logging import DEBUG, WARNING import cssselect2 import tinycss2 import tinycss2.nth from .. import CSS from ..logger import LOGGER, PROGRESS_LOGGER from ..urls import URLFetchingError, get_url_attribute, url_join from . import computed_values, counters, media_queries from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES from .utils import get_url, remove_whitespace from .validation import preprocess_declarations from .validation.descriptors import preprocess_descriptors # Reject anything not in here: PSEUDO_ELEMENTS = ( None, 'before', 'after', 'marker', 'first-line', 'first-letter', 'footnote-call', 'footnote-marker') 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 = {} self._sheets = sheets 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 style = cascaded_styles.setdefault((element, None), {}) for name, values, importance in preprocess_declarations( base_url, declarations): precedence = declaration_precedence('author', importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight # 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 style = cascaded_styles.setdefault( (element.etree_element, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence(origin, importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight 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) # 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) # Clear the cascaded styles, we don't need them anymore. Keep the # dictionary, it is used later for page margins. self._cascaded_styles.clear() def __call__(self, element, pseudo_type=None): style = self._computed_styles.get((element, pseudo_type)) if style: if ('table' in style['display'] and style['border_collapse'] == 'collapse'): # Padding do not apply for side in ['top', 'bottom', 'left', 'right']: style[f'padding_{side}'] = computed_values.ZERO_PIXELS if (len(style['display']) == 1 and style['display'][0].startswith('table-') and style['display'][0] != 'table-caption'): # Margins do not apply for side in ['top', 'bottom', 'left', 'right']: style[f'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) # The style of marker is deleted when display is different from # list-item. if pseudo_type is None: for pseudo in (None, 'before', 'after'): pseudo_style = cascaded_styles.get((element, pseudo), {}) if 'display' in pseudo_style: if 'list-item' in pseudo_style['display'][0]: break else: if (element, 'marker') in cascaded_styles: del cascaded_styles[element, 'marker'] def add_page_declarations(self, page_type): for sheet, origin, sheet_specificity in self._sheets: for _rule, selector_list, declarations in sheet.page_rules: for selector in selector_list: specificity, pseudo_type, selector_page_type = selector if self._page_type_match(selector_page_type, page_type): specificity = sheet_specificity or specificity style = self._cascaded_styles.setdefault( (page_type, pseudo_type), {}) for name, values, importance in declarations: precedence = declaration_precedence( origin, importance) weight = (precedence, specificity) old_weight = style.get(name, (None, None))[1] if old_weight is None or old_weight <= weight: style[name] = values, weight def get_cascaded_styles(self): return self._cascaded_styles def get_computed_styles(self): return self._computed_styles @staticmethod def _page_type_match(selector_page_type, page_type): if selector_page_type.side not in (None, page_type.side): return False if selector_page_type.blank not in (None, page_type.blank): return False if selector_page_type.first not in (None, page_type.first): return False if selector_page_type.name not in (None, page_type.name): return False if selector_page_type.index is not None: a, b, group = selector_page_type.index # TODO: handle group if a: if (page_type.index + 1 - b) % a: return False else: if page_type.index + 1 != b: return False return True 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, counter_style, 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 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