text: """
      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')
  
  
  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']:
                  if (style['display'] in ('table', 'inline-table') 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 (style['display'].startswith('table-') and
                          style['display'] != '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 pseudo_style['display'][0] == 'list-item':
                          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  # 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 <style> element, not its
              # descendants
              content = get_child_text(element)
              # ElementTree should give us either unicode or ASCII-only
              # bytestrings, so we don't need `encoding` here.
              css = CSS(
                  string=content, base_url=base_url,
                  url_fetcher=url_fetcher, media_type=device_media_type,
                  font_config=font_config, counter_style=counter_style,
                  page_rules=page_rules)
              yield css
          elif element.tag == 'link' and element.get('href'):
              if not element_has_link_type(element, 'stylesheet') or \
                      element_has_link_type(element, 'alternate'):
                  continue
              href = get_url_attribute(element, 'href', base_url)
              if href is not None:
                  try:
                      yield CSS(
                          url=href, url_fetcher=url_fetcher,
                          _check_mime_type=True, media_type=device_media_type,
                          font_config=font_config, counter_style=counter_style,
                          page_rules=page_rules)
                  except URLFetchingError as exc:
                      LOGGER.error(
                          'Failed to load stylesheet at %s: %s', href, exc)
  
  
  def find_style_attributes(tree, presentational_hints=False, base_url=None):
      """Yield ``specificity, (element, declaration, base_url)`` rules.
  
      Rules from "style" attribute are returned with specificity
      ``(1, 0, 0)``.
  
      If ``presentational_hints`` is ``True``, rules from presentational hints
      are returned with specificity ``(0, 0, 0)``.
  
      """
      def check_style_attribute(element, style_attribute):
          declarations = tinycss2.parse_declaration_list(style_attribute)
          return element, declarations, base_url
  
      for element in tree.iter():
          specificity = (1, 0, 0)
          style_attribute = element.get('style')
          if style_attribute:
              yield specificity, check_style_attribute(element, style_attribute)
          if not presentational_hints:
              continue
          specificity = (0, 0, 0)
          if element.tag == 'body':
              # TODO: we should check the container frame element
              for part, position in (
                      ('height', 'top'), ('height', 'bottom'),
                      ('width', 'left'), ('width', 'right')):
                  style_attribute = None
                  for prop in (f'margin{part}', f'{position}margin'):
                      if element.get(prop):
                          style_attribute = (
                              f'margin-{position}:{element.get(prop)}px')
                          break
                  if style_attribute:
                      yield specificity, check_style_attribute(
                          element, style_attribute)
              if element.get('background'):
                  style_attribute = (
                      f'background-image:url({element.get("background")})')
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('bgcolor'):
                  style_attribute = f'background-color:{element.get("bgcolor")}'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('text'):
                  style_attribute = f'color:{element.get("text")}'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              # TODO: we should support link, vlink, alink
          elif element.tag == 'center':
              yield specificity, check_style_attribute(
                  element, 'text-align:center')
          elif element.tag == 'div':
              align = element.get('align', '').lower()
              if align == 'middle':
                  yield specificity, check_style_attribute(
                      element, 'text-align:center')
              elif align in ('center', 'left', 'right', 'justify'):
                  yield specificity, check_style_attribute(
                      element, f'text-align:{align}')
          elif element.tag == 'font':
              if element.get('color'):
                  yield specificity, check_style_attribute(
                      element, f'color:{element.get("color")}')
              if element.get('face'):
                  yield specificity, check_style_attribute(
                      element, f'font-family:{element.get("face")}')
              if element.get('size'):
                  size = element.get('size').strip()
                  relative_plus = size.startswith('+')
                  relative_minus = size.startswith('-')
                  if relative_plus or relative_minus:
                      size = size[1:].strip()
                  try:
                      size = int(size)
                  except ValueError:
                      LOGGER.warning('Invalid value for size: %s', size)
                  else:
                      font_sizes = {
                          1: 'x-small',
                          2: 'small',
                          3: 'medium',
                          4: 'large',
                          5: 'x-large',
                          6: 'xx-large',
                          7: '48px',  # 1.5 * xx-large
                      }
                      if relative_plus:
                          size += 3
                      elif relative_minus:
                          size -= 3
                      size = max(1, min(7, size))
                      yield specificity, check_style_attribute(
                          element, f'font-size:{font_sizes[size]}')
          elif element.tag == 'table':
              if element.get('cellspacing'):
                  yield specificity, check_style_attribute(
                      element, f'border-spacing:{element.get("cellspacing")}px')
              if element.get('cellpadding'):
                  cellpadding = element.get('cellpadding')
                  if cellpadding.isdigit():
                      cellpadding += 'px'
                  # TODO: don't match subtables cells
                  for subelement in element.iter():
                      if subelement.tag in ('td', 'th'):
                          yield specificity, check_style_attribute(
                              subelement,
                              f'padding-left:{cellpadding};'
                              f'padding-right:{cellpadding};'
                              f'padding-top:{cellpadding};'
                              f'padding-bottom:{cellpadding};')
              if element.get('hspace'):
                  hspace = element.get('hspace')
                  if hspace.isdigit():
                      hspace += 'px'
                  yield specificity, check_style_attribute(
                      element, f'margin-left:{hspace};margin-right:{hspace}')
              if element.get('vspace'):
                  vspace = element.get('vspace')
                  if vspace.isdigit():
                      vspace += 'px'
                  yield specificity, check_style_attribute(
                      element, f'margin-top:{vspace};margin-bottom:{vspace}')
              if element.get('width'):
                  style_attribute = f'width:{element.get("width")}'
                  if element.get('width').isdigit():
                      style_attribute += 'px'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('height'):
                  style_attribute = f'height:{element.get("height")}'
                  if element.get('height').isdigit():
                      style_attribute += 'px'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('background'):
                  style_attribute = (
                      f'background-image:url({element.get("background")})')
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('bgcolor'):
                  style_attribute = f'background-color:{element.get("bgcolor")}'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('bordercolor'):
                  style_attribute = f'border-color:{element.get("bordercolor")}'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('border'):
                  style_attribute = f'border-width:{element.get("border")}px'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
          elif element.tag in ('tr', 'td', 'th', 'thead', 'tbody', 'tfoot'):
              align = element.get('align', '').lower()
              # TODO: we should align descendants too
              if align == 'middle':
                  yield specificity, check_style_attribute(
                      element, 'text-align:center')
              elif align in ('center', 'left', 'right', 'justify'):
                  yield specificity, check_style_attribute(
                      element, f'text-align:{align}')
              if element.get('background'):
                  style_attribute = (
                      f'background-image:url({element.get("background")})')
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('bgcolor'):
                  style_attribute = f'background-color:{element.get("bgcolor")}'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.tag in ('tr', 'td', 'th'):
                  if element.get('height'):
                      style_attribute = f'height:{element.get("height")}'
                      if element.get('height').isdigit():
                          style_attribute += 'px'
                      yield specificity, check_style_attribute(
                          element, style_attribute)
                  if element.tag in ('td', 'th'):
                      if element.get('width'):
                          style_attribute = f'width:{element.get("width")}'
                          if element.get('width').isdigit():
                              style_attribute += 'px'
                          yield specificity, check_style_attribute(
                              element, style_attribute)
          elif element.tag == 'caption':
              align = element.get('align', '').lower()
              # TODO: we should align descendants too
              if align == 'middle':
                  yield specificity, check_style_attribute(
                      element, 'text-align:center')
              elif align in ('center', 'left', 'right', 'justify'):
                  yield specificity, check_style_attribute(
                      element, f'text-align:{align}')
          elif element.tag == 'col':
              if element.get('width'):
                  style_attribute = f'width:{element.get("width")}'
                  if element.get('width').isdigit():
                      style_attribute += 'px'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
          elif element.tag == 'hr':
              size = 0
              if element.get('size'):
                  try:
                      size = int(element.get('size'))
                  except ValueError:
                      LOGGER.warning('Invalid value for size: %s', size)
              if (element.get('color'), element.get('noshade')) != (None, None):
                  if size >= 1:
                      yield specificity, check_style_attribute(
                          element, f'border-width:{size / 2}px')
              elif size == 1:
                  yield specificity, check_style_attribute(
                      element, 'border-bottom-width:0')
              elif size > 1:
                  yield specificity, check_style_attribute(
                      element, f'height:{size - 2}px')
              if element.get('width'):
                  style_attribute = f'width:{element.get("width")}'
                  if element.get('width').isdigit():
                      style_attribute += 'px'
                  yield specificity, check_style_attribute(
                      element, style_attribute)
              if element.get('color'):
                  yield specificity, check_style_attribute(
                      element, f'color:{element.get("color")}')
          elif element.tag in (
                  'iframe', 'applet', 'embed', 'img', 'input', 'object'):
              if (element.tag != 'input' or
                      element.get('type', '').lower() == 'image'):
                  align = element.get('align', '').lower()
                  if align in ('middle', 'center'):
                      # TODO: middle and center values are wrong
                      yield specificity, check_style_attribute(
                          element, 'vertical-align:middle')
                  if element.get('hspace'):
                      hspace = element.get('hspace')
                      if hspace.isdigit():
                          hspace += 'px'
                      yield specificity, check_style_attribute(
                          element, f'margin-left:{hspace};margin-right:{hspace}')
                  if element.get('vspace'):
                      vspace = element.get('vspace')
                      if vspace.isdigit():
                          vspace += 'px'
                      yield specificity, check_style_attribute(
                          element, f'margin-top:{vspace};margin-bottom:{vspace}')
                  # TODO: img seems to be excluded for width and height, but a
                  # lot of W3C tests rely on this attribute being applied to img
                  if element.get('width'):
                      style_attribute = f'width:{element.get("width")}'
                      if element.get('width').isdigit():
                          style_attribute += 'px'
                      yield specificity, check_style_attribute(
                          element, style_attribute)
                  if element.get('height'):
                      style_attribute = f'height:{element.get("height")}'
                      if element.get('height').isdigit():
                          style_attribute += 'px'
                      yield specificity, check_style_attribute(
                          element, style_attribute)
                  if element.tag in ('img', 'object', 'input'):
                      if element.get('border'):
                          yield specificity, check_style_attribute(
                              element,
                              f'border-width:{element.get("border")}px;'
                              f'border-style:solid')
          elif element.tag == 'ol':
              # From https://www.w3.org/TR/css-lists-3/#ua-stylesheet
              if element.get('start'):
                  yield specificity, check_style_attribute(
                      element,
                      f'counter-reset:list-item {element.get("start")};'
                      'counter-increment:list-item -1')
          elif element.tag == 'li':
              # From https://www.w3.org/TR/css-lists-3/#ua-stylesheet
              if element.get('value'):
                  yield specificity, check_style_attribute(
                      element,
                      f'counter-reset:list-item {element.get("value")};'
                      'counter-increment:none')
  
  
  def declaration_precedence(origin, importance):
      """Return the precedence for a declaration.
  
      Precedence values have no meaning unless compared to each other.
  
      Acceptable values for ``origin`` are the strings ``'author'``, ``'user'``
      and ``'user agent'``.
  
      """
      # See http://www.w3.org/TR/CSS21/cascade.html#cascading-order
      if origin == 'user agent':
          return 1
      elif origin == 'user' and not importance:
          return 2
      elif origin == 'author' and not importance:
          return 3
      elif origin == 'author':  # and importance
          return 4
      else:
          assert origin == 'user'  # and importance
          return 5
  
  
  class Style(dict):
      @property
      def variables(self):
          return {
              key: value for (key, value) in self.items()
              if key.startswith('__')}
  
  
  class AnonymousStyle(Style):
      """Computed style used for anonymous boxes."""
      def __init__(self, parent_style):
          # border-*-style is none, so border-width computes to zero.
          # Other than that, properties that would need computing are
          # border-*-color, but they do not apply.
          self.update({
              'border_top_width': 0,
              'border_bottom_width': 0,
              'border_left_width': 0,
              'border_right_width': 0,
              'outline_width': 0,
          })
          self.parent_style = parent_style
          self.specified = self
          if parent_style:
              self.update(parent_style.variables)
  
      def copy(self):
          copy = AnonymousStyle(self.parent_style)
          copy.update(self)
          return copy
  
      def __missing__(self, key):
          if key in INHERITED or key.startswith('__'):
              self[key] = self.parent_style[key]
          elif key == 'page':
              # page is not inherited but taken from the ancestor if 'auto'
              self[key] = self.parent_style[key]
          else:
              self[key] = INITIAL_VALUES[key]
          return self[key]
  
  
  class ComputedStyle(Style):
      """Computed style used for non-anonymous boxes."""
      def __init__(self, parent_style, cascaded, element, pseudo_type,
                   root_style, base_url):
          self.specified = {}
          self.parent_style = parent_style
          self.cascaded = cascaded
          self.is_root_element = parent_style is None
          self.element = element
          self.pseudo_type = pseudo_type
          self.root_style = root_style
          self.base_url = base_url
          if parent_style:
              self.update(parent_style.variables)
          self.update({
              key: value[0] for (key, value) in cascaded.items()
              if key.startswith('__')})
  
      def copy(self):
          copy = ComputedStyle(
              self.parent_style, self.cascaded, self.element, self.pseudo_type,
              self.root_style, self.base_url)
          copy.update(self)
          copy.specified = self.specified.copy()
          return copy
  
      def __missing__(self, key):
          if key == 'float':
              # Set specified value for position, needed for computed value
              self['position']
          elif key == 'display':
              # Set specified value for float, needed for computed value
              self['float']
  
          if key in self.cascaded:
              value = keyword = self.cascaded[key][0]
          else:
              keyword = 'inherit' if key in INHERITED else 'initial'
  
          if keyword == 'inherit' and self.parent_style is None:
              # On the root element, 'inherit' from initial values
              keyword = 'initial'
  
          if keyword == 'initial':
              value = INITIAL_VALUES[key]
              if key not in INITIAL_NOT_COMPUTED:
                  # The value is the same as when computed
                  self[key] = value
          elif keyword == 'inherit':
              # Values in parent_style are already computed.
              self[key] = value = self.parent_style[key]
  
          if key == 'page' and value == 'auto':
              # The page property does not inherit. However, if the page
              # value on an element is auto, then its used value is the value
              # specified on its nearest ancestor with a non-auto value. When
              # specified on the root element, the used value for auto is the
              # empty string.
              self['page'] = value = (
                  '' if self.parent_style is None else self.parent_style['page'])
  
          if key in ('position', 'float', 'display'):
              self.specified[key] = value
  
          if key in self:
              return self[key]
  
          getter = computed_values.COMPUTER_FUNCTIONS.get
          function = getter(key)
          already_computed_value = False
  
          if value:
              converted_to_list = False
  
              if not isinstance(value, list):
                  converted_to_list = True
                  value = [value]
  
              for i, v in enumerate(value):
                  value[i], already_computed_value = (
                      computed_values.compute_variable(
                          v, key, self, self.base_url, self.parent_style))
  
              if converted_to_list:
                  value, = value
  
          if function is not None and not already_computed_value:
              value = function(self, key, value)
          # else: same as specified
  
          self[key] = value
          return value
  
  
  def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
                             root_style=None, base_url=None,
                             target_collector=None):
      """Get a dict of computed style mixed from parent and cascaded styles."""
      if not cascaded and parent_style is not None:
          return AnonymousStyle(parent_style)
  
      style = ComputedStyle(
          parent_style, cascaded, element, pseudo_type, root_style, base_url)
      if target_collector:
          target_collector.collect_anchor(style['anchor'])
      return style
  
  
  def parse_page_selectors(rule):
      """Parse a page selector rule.
  
      Return a list of page data if the rule is correctly parsed. Page data are a
      dict containing:
  
      - 'side' ('left', 'right' or None),
      - 'blank' (True or None),
      - 'first' (True or None),
      - 'index' (page number or None),
      - 'name' (page name string or None), and
      - 'specificity' (list of numbers).
  
      Return ``None` if something went wrong while parsing the rule.
  
      """
      # See https://drafts.csswg.org/css-page-3/#syntax-page-selector
  
      tokens = list(remove_whitespace(rule.prelude))
      page_data = []
  
      # TODO: Specificity is probably wrong, should clean and test that.
      if not tokens:
          page_data.append({
              'side': None, 'blank': None, 'first': None, 'index': None,
              'name': None, 'specificity': [0, 0, 0]})
          return page_data
  
      while tokens:
          types = {
              'side': None, 'blank': None, 'first': None, 'index': None,
              'name': None, 'specificity': [0, 0, 0]}
  
          if tokens[0].type == 'ident':
              token = tokens.pop(0)
              types['name'] = token.value
              types['specificity'][0] = 1
  
          if len(tokens) == 1:
              return None
          elif not tokens:
              page_data.append(types)
              return page_data
  
          while tokens:
              literal = tokens.pop(0)
              if literal.type != 'literal':
                  return None
  
              if literal.value == ':':
                  if not tokens:
                      return None
  
                  if tokens[0].type == 'ident':
                      ident = tokens.pop(0)
                      pseudo_class = ident.lower_value
  
                      if pseudo_class in ('left', 'right'):
                          if types['side'] and types['side'] != pseudo_class:
                              return None
                          types['side'] = pseudo_class
                          types['specificity'][2] += 1
                          continue
  
                      elif pseudo_class in ('blank', 'first'):
                          types[pseudo_class] = True
                          types['specificity'][1] += 1
                          continue
  
                  elif tokens[0].type == 'function':
                      function = tokens.pop(0)
                      if function.name != 'nth':
                          return None
                      for i, argument in enumerate(function.arguments):
                          if argument.type == 'ident' and argument.value == 'of':
                              nth = function.arguments[:i - 1]
                              group = function.arguments[i:]
                              break
                      else:
                          nth = function.arguments
                          group = None
  
                      nth_values = tinycss2.nth.parse_nth(nth)
                      if nth_values is None:
                          return None
  
                      if group is not None:
                          group = [
                              token for token in group
                              if token.type not in (
                                  'comment', 'whitespacespace')]
                          if len(group) != 1:
                              return None
                          group, = group
                          if group.type != 'ident':
                              return None
                          group = group.value
  
                          # TODO: handle page groups
                          return None
  
                      types['index'] = (*nth_values, group)
                      # TODO: specificity is not specified yet
                      # https://github.com/w3c/csswg-drafts/issues/3524
                      types['specificity'][1] += 1
                      continue
  
                  return None
              elif literal.value == ',':
                  if tokens and any(types['specificity']):
                      break
                  else:
                      return None
  
          page_data.append(types)
  
      return page_data
  
  
  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 get_all_computed_styles(html, user_stylesheets=None,
                              presentational_hints=False, font_config=None,
                              counter_style=None, page_rules=None,
                              target_collector=None):
      """Compute all the computed styles of all elements in ``html`` document.
  
      Do everything from finding author stylesheets to parsing and applying them.
  
      Return a ``style_for`` function that takes an element and an optional
      pseudo-element type, and return a style dict object.
  
      """
      # List stylesheets. Order here is not important ('origin' is).
      sheets = []
      if counter_style is None:
          counter_style = counters.CounterStyle()
      for style in html._ua_counter_style():
          for key, value in style.items():
              counter_style[key] = value
      for sheet in (html._ua_stylesheets() or []):
          sheets.append((sheet, 'user agent', None))
      if presentational_hints:
          for sheet in (html._ph_stylesheets() or []):
              sheets.append((sheet, 'author', (0, 0, 0)))
      for sheet in find_stylesheets(
              html.wrapper_element, html.media_type, html.url_fetcher,
              html.base_url, font_config, counter_style, page_rules):
          sheets.append((sheet, 'author', None))
      for sheet in (user_stylesheets or []):
          sheets.append((sheet, 'user', None))
  
      return StyleFor(html, sheets, presentational_hints, target_collector)
  