text: """
      weasyprint.css.expanders
      ------------------------
  
      Validate properties expanders.
  
      :copyright: Copyright 2011-2019 Simon Sapin and contributors, see AUTHORS.
      :license: BSD, see LICENSE for details.
  
  """
  
  import functools
  
  from tinycss2.color3 import parse_color
  
  from ..properties import INITIAL_VALUES, Dimension
  from ..utils import (
      InvalidValues, get_keyword, get_single_keyword, split_on_comma)
  from .descriptors import expand_font_variant
  from .properties import (
      background_attachment, background_image, background_position,
      background_repeat, background_size, border_style, border_width, box,
      column_count, column_width, flex_basis, flex_direction, flex_grow_shrink,
      flex_wrap, font_family, font_size, font_stretch, font_style, font_weight,
      line_height, list_style_image, list_style_position, list_style_type,
      other_colors, overflow_wrap, validate_non_shorthand)
  
  EXPANDERS = {}
  
  
  def expander(property_name):
      """Decorator adding a function to the ``EXPANDERS``."""
      def expander_decorator(function):
          """Add ``function`` to the ``EXPANDERS``."""
          assert property_name not in EXPANDERS, property_name
          EXPANDERS[property_name] = function
          return function
      return expander_decorator
  
  
  @expander('border-color')
  @expander('border-style')
  @expander('border-width')
  @expander('margin')
  @expander('padding')
  @expander('bleed')
  def expand_four_sides(base_url, name, tokens):
      """Expand properties setting a token for the four sides of a box."""
      # Make sure we have 4 tokens
      if len(tokens) == 1:
          tokens *= 4
      elif len(tokens) == 2:
          tokens *= 2  # (bottom, left) defaults to (top, right)
      elif len(tokens) == 3:
          tokens += (tokens[1],)  # left defaults to right
      elif len(tokens) != 4:
          raise InvalidValues(
              'Expected 1 to 4 token components got %i' % len(tokens))
      for suffix, token in zip(('-top', '-right', '-bottom', '-left'), tokens):
          i = name.rfind('-')
          if i == -1:
              new_name = name + suffix
          else:
              # eg. border-color becomes border-*-color, not border-color-*
              new_name = name[:i] + suffix + name[i:]
  
          # validate_non_shorthand returns ((name, value),), we want
          # to yield (name, value)
          result, = validate_non_shorthand(
              base_url, new_name, [token], required=True)
          yield result
  
  
  @expander('border-radius')
  def border_radius(base_url, name, tokens):
      """Validator for the `border-radius` property."""
      current = horizontal = []
      vertical = []
      for token in tokens:
          if token.type == 'literal' and token.value == '/':
              if current is horizontal:
                  if token == tokens[-1]:
                      raise InvalidValues('Expected value after "/" separator')
                  else:
                      current = vertical
              else:
                  raise InvalidValues('Expected only one "/" separator')
          else:
              current.append(token)
  
      if not vertical:
          vertical = horizontal[:]
  
      for values in horizontal, vertical:
          # Make sure we have 4 tokens
          if len(values) == 1:
              values *= 4
          elif len(values) == 2:
              values *= 2  # (br, bl) defaults to (tl, tr)
          elif len(values) == 3:
              values.append(values[1])  # bl defaults to tr
          elif len(values) != 4:
              raise InvalidValues(
                  'Expected 1 to 4 token components got %i' % len(values))
      corners = ('top-left', 'top-right', 'bottom-right', 'bottom-left')
      for corner, tokens in zip(corners, zip(horizontal, vertical)):
          new_name = 'border-%s-radius' % corner
          # validate_non_shorthand returns [(name, value)], we want
          # to yield (name, value)
          result, = validate_non_shorthand(
              base_url, new_name, tokens, required=True)
          yield result
  
  
  def generic_expander(*expanded_names, **kwargs):
      """Decorator helping expanders to handle ``inherit`` and ``initial``.
  
      Wrap an expander so that it does not have to handle the 'inherit' and
      'initial' cases, and can just yield name suffixes. Missing suffixes
      get the initial value.
  
      """
      wants_base_url = kwargs.pop('wants_base_url', False)
      assert not kwargs
  
      def generic_expander_decorator(wrapped):
          """Decorate the ``wrapped`` expander."""
          @functools.wraps(wrapped)
          def generic_expander_wrapper(base_url, name, tokens):
              """Wrap the expander."""
              keyword = get_single_keyword(tokens)
              if keyword in ('inherit', 'initial'):
                  results = dict.fromkeys(expanded_names, keyword)
                  skip_validation = True
              else:
                  skip_validation = False
                  results = {}
                  if wants_base_url:
                      result = wrapped(name, tokens, base_url)
                  else:
                      result = wrapped(name, tokens)
                  for new_name, new_token in result:
                      assert new_name in expanded_names, new_name
                      if new_name in results:
                          raise InvalidValues(
                              'got multiple %s values in a %s shorthand'
                              % (new_name.strip('-'), name))
                      results[new_name] = new_token
  
              for new_name in expanded_names:
                  if new_name.startswith('-'):
                      # new_name is a suffix
                      actual_new_name = name + new_name
                  else:
                      actual_new_name = new_name
  
                  if new_name in results:
                      value = results[new_name]
                      if not skip_validation:
                          # validate_non_shorthand returns ((name, value),)
                          (actual_new_name, value), = validate_non_shorthand(
                              base_url, actual_new_name, value, required=True)
                  else:
                      value = 'initial'
  
                  yield actual_new_name, value
          return generic_expander_wrapper
      return generic_expander_decorator
  
  
  @expander('list-style')
  @generic_expander('-type', '-position', '-image', wants_base_url=True)
  def expand_list_style(name, tokens, base_url):
      """Expand the ``list-style`` shorthand property.
  
      See http://www.w3.org/TR/CSS21/generate.html#propdef-list-style
  
      """
      type_specified = image_specified = False
      none_count = 0
      for token in tokens:
          if get_keyword(token) == 'none':
              # Can be either -style or -image, see at the end which is not
              # otherwise specified.
              none_count += 1
              none_token = token
              continue
  
          if list_style_image([token], base_url) is not None:
              suffix = '-image'
              image_specified = True
          elif list_style_position([token]) is not None:
              suffix = '-position'
          elif list_style_type([token]) is not None:
              suffix = '-type'
              type_specified = True
          else:
              raise InvalidValues
          yield suffix, [token]
  
      if not type_specified and none_count:
          yield '-type', [none_token]
          none_count -= 1
  
      if not image_specified and none_count:
          yield '-image', [none_token]
          none_count -= 1
  
      if none_count:
          # Too many none tokens.
          raise InvalidValues
  
  
  @expander('border')
  def expand_border(base_url, name, tokens):
      """Expand the ``border`` shorthand property.
  
      See http://www.w3.org/TR/CSS21/box.html#propdef-border
  
      """
      for suffix in ('-top', '-right', '-bottom', '-left'):
          for new_prop in expand_border_side(base_url, name + suffix, tokens):
              yield new_prop
  
  
  @expander('border-top')
  @expander('border-right')
  @expander('border-bottom')
  @expander('border-left')
  @expander('column-rule')
  @expander('outline')
  @generic_expander('-width', '-color', '-style')
  def expand_border_side(name, tokens):
      """Expand the ``border-*`` shorthand properties.
  
      See http://www.w3.org/TR/CSS21/box.html#propdef-border-top
  
      """
      for token in tokens:
          if parse_color(token) is not None:
              suffix = '-color'
          elif border_width([token]) is not None:
              suffix = '-width'
          elif border_style([token]) is not None:
              suffix = '-style'
          else:
              raise InvalidValues
          yield suffix, [token]
  
  
  @expander('background')
  def expand_background(base_url, name, tokens):
      """Expand the ``background`` shorthand property.
  
      See http://dev.w3.org/csswg/css3-background/#the-background
  
      """
      properties = [
          'background_color', 'background_image', 'background_repeat',
          'background_attachment', 'background_position', 'background_size',
          'background_clip', 'background_origin']
      keyword = get_single_keyword(tokens)
      if keyword in ('initial', 'inherit'):
          for name in properties:
              yield name, keyword
          return
  
      def parse_layer(tokens, final_layer=False):
          results = {}
  
          def add(name, value):
              if value is None:
                  return False
              name = 'background_' + name
              if name in results:
                  raise InvalidValues
              results[name] = value
              return True
  
          # Make `tokens` a stack
          tokens = tokens[::-1]
          while tokens:
              if add('repeat',
                     background_repeat.single_value(tokens[-2:][::-1])):
                  del tokens[-2:]
                  continue
              token = tokens[-1:]
              if final_layer and add('color', other_colors(token)):
                  tokens.pop()
                  continue
              if add('image', background_image.single_value(token, base_url)):
                  tokens.pop()
                  continue
              if add('repeat', background_repeat.single_value(token)):
                  tokens.pop()
                  continue
              if add('attachment', background_attachment.single_value(token)):
                  tokens.pop()
                  continue
              for n in (4, 3, 2, 1)[-len(tokens):]:
                  n_tokens = tokens[-n:][::-1]
                  position = background_position.single_value(n_tokens)
                  if position is not None:
                      assert add('position', position)
                      del tokens[-n:]
                      if (tokens and tokens[-1].type == 'literal' and
                              tokens[-1].value == '/'):
                          for n in (3, 2)[-len(tokens):]:
                              # n includes the '/' delimiter.
                              n_tokens = tokens[-n:-1][::-1]
                              size = background_size.single_value(n_tokens)
                              if size is not None:
                                  assert add('size', size)
                                  del tokens[-n:]
                      break
              if position is not None:
                  continue
              if add('origin', box.single_value(token)):
                  tokens.pop()
                  next_token = tokens[-1:]
                  if add('clip', box.single_value(next_token)):
                      tokens.pop()
                  else:
                      # The same keyword sets both:
                      assert add('clip', box.single_value(token))
                  continue
              raise InvalidValues
  
          color = results.pop(
              'background_color', INITIAL_VALUES['background_color'])
          for name in properties:
              if name not in results:
                  results[name] = INITIAL_VALUES[name][0]
          return color, results
  
      layers = reversed(split_on_comma(tokens))
      color, last_layer = parse_layer(next(layers), final_layer=True)
      results = dict((k, [v]) for k, v in last_layer.items())
      for tokens in layers:
          _, layer = parse_layer(tokens)
          for name, value in layer.items():
              results[name].append(value)
      for name, values in results.items():
          yield name, values[::-1]  # "Un-reverse"
      yield 'background-color', color
  
  
  @expander('text-decoration')
  def expand_text_decoration(base_url, name, tokens):
  
      text_decoration_line = set()
      text_decoration_color = None
      text_decoration_style = None
  
      for token in tokens:
          keyword = get_keyword(token)
          if keyword in (
                  'none', 'underline', 'overline', 'line-through', 'blink'):
              text_decoration_line.add(keyword)
          elif keyword in ('solid', 'double', 'dotted', 'dashed', 'wavy'):
              if text_decoration_style is not None:
                  raise InvalidValues
              else:
                  text_decoration_style = keyword
          else:
              color = parse_color(token)
              if color is None:
                  raise InvalidValues
              elif text_decoration_color is not None:
                  raise InvalidValues
              else:
                  text_decoration_color = color
  
      if 'none' in text_decoration_line:
          if len(text_decoration_line) != 1:
              raise InvalidValues
          text_decoration_line = 'none'
      elif not text_decoration_line:
          text_decoration_line = 'none'
  
      yield 'text_decoration_line', text_decoration_line
      yield 'text_decoration_color', text_decoration_color or 'currentColor'
      yield 'text_decoration_style', text_decoration_style or 'solid'
  
  
  @expander('page-break-after')
  @expander('page-break-before')
  def expand_page_break_before_after(base_url, name, tokens):
      """Expand legacy ``page-break-before`` and ``page-break-after`` properties.
  
      See https://www.w3.org/TR/css-break-3/#page-break-properties
  
      """
      keyword = get_single_keyword(tokens)
      new_name = name.split('-', 1)[1]
      if keyword in ('auto', 'left', 'right', 'avoid'):
          yield new_name, keyword
      elif keyword == 'always':
          yield new_name, 'page'
  
  
  @expander('page-break-inside')
  def expand_page_break_inside(base_url, name, tokens):
      """Expand the legacy ``page-break-inside`` property.
  
      See https://www.w3.org/TR/css-break-3/#page-break-properties
  
      """
      keyword = get_single_keyword(tokens)
      if keyword in ('auto', 'avoid'):
          yield 'break-inside', keyword
  
  
  @expander('columns')
  @generic_expander('column-width', 'column-count')
  def expand_columns(name, tokens):
      """Expand the ``columns`` shorthand property."""
      name = None
      if len(tokens) == 2 and get_keyword(tokens[0]) == 'auto':
          tokens = tokens[::-1]
      for token in tokens:
          if column_width([token]) is not None and name != 'column-width':
              name = 'column-width'
          elif column_count([token]) is not None:
              name = 'column-count'
          else:
              raise InvalidValues
          yield name, [token]
  
  
  @expander('font-variant')
  @generic_expander('-alternates', '-caps', '-east-asian', '-ligatures',
                    '-numeric', '-position')
  def font_variant(name, tokens):
      """Expand the ``font-variant`` shorthand property.
  
      https://www.w3.org/TR/css-fonts-3/#font-variant-prop
  
      """
      return expand_font_variant(tokens)
  
  
  @expander('font')
  @generic_expander('-style', '-variant-caps', '-weight', '-stretch', '-size',
                    'line-height', '-family')  # line-height is not a suffix
  def expand_font(name, tokens):
      """Expand the ``font`` shorthand property.
  
      https://www.w3.org/TR/css-fonts-3/#font-prop
  
      """
      expand_font_keyword = get_single_keyword(tokens)
      if expand_font_keyword in ('caption', 'icon', 'menu', 'message-box',
                                 'small-caption', 'status-bar'):
          raise InvalidValues('System fonts are not supported')
  
      # Make `tokens` a stack
      tokens = list(reversed(tokens))
      # Values for font-style, font-variant-caps, font-weight and font-stretch
      # can come in any order and are all optional.
      for _ in range(4):
          token = tokens.pop()
          if get_keyword(token) == 'normal':
              # Just ignore 'normal' keywords. Unspecified properties will get
              # their initial token, which is 'normal' for all four here.
              continue
  
          if font_style([token]) is not None:
              suffix = '-style'
          elif get_keyword(token) in ('normal', 'small-caps'):
              suffix = '-variant-caps'
          elif font_weight([token]) is not None:
              suffix = '-weight'
          elif font_stretch([token]) is not None:
              suffix = '-stretch'
          else:
              # We’re done with these four, continue with font-size
              break
          yield suffix, [token]
  
          if not tokens:
              raise InvalidValues
      else:
          token = tokens.pop()
  
      # Then font-size is mandatory
      # Latest `token` from the loop.
      if font_size([token]) is None:
          raise InvalidValues
      yield '-size', [token]
  
      # Then line-height is optional, but font-family is not so the list
      # must not be empty yet
      if not tokens:
          raise InvalidValues
  
      token = tokens.pop()
      if token.type == 'literal' and token.value == '/':
          token = tokens.pop()
          if line_height([token]) is None:
              raise InvalidValues
          yield 'line-height', [token]
      else:
          # We pop()ed a font-family, add it back
          tokens.append(token)
  
      # Reverse the stack to get normal list
      tokens.reverse()
      if font_family(tokens) is None:
          raise InvalidValues
      yield '-family', tokens
  
  
  @expander('word-wrap')
  def expand_word_wrap(base_url, name, tokens):
      """Expand the ``word-wrap`` legacy property.
  
      See http://http://www.w3.org/TR/css3-text/#overflow-wrap
  
      """
      keyword = overflow_wrap(tokens)
      if keyword is None:
          raise InvalidValues
  
      yield 'overflow-wrap', keyword
  
  
  @expander('flex')
  def expand_flex(base_url, name, tokens):
      """Expand the ``flex`` property."""
      keyword = get_single_keyword(tokens)
      if keyword == 'none':
          yield 'flex-grow', 0
          yield 'flex-shrink', 0
          yield 'flex-basis', 'auto'
      else:
          grow, shrink, basis = 1, 1, Dimension(0, 'px')
          grow_found, shrink_found, basis_found = False, False, False
          for token in tokens:
              # "A unitless zero that is not already preceded by two flex factors
              # must be interpreted as a flex factor."
              forced_flex_factor = (
                  token.type == 'number' and token.int_value == 0 and
                  not all((grow_found, shrink_found)))
              if not basis_found and not forced_flex_factor:
                  new_basis = flex_basis([token])
                  if new_basis is not None:
                      basis = new_basis
                      basis_found = True
                      continue
              if not grow_found:
                  new_grow = flex_grow_shrink([token])
                  if new_grow is None:
                      raise InvalidValues
                  else:
                      grow = new_grow
                      grow_found = True
                      continue
              elif not shrink_found:
                  new_shrink = flex_grow_shrink([token])
                  if new_shrink is None:
                      raise InvalidValues
                  else:
                      shrink = new_shrink
                      shrink_found = True
                      continue
              else:
                  raise InvalidValues
          yield 'flex-grow', grow
          yield 'flex-shrink', shrink
          yield 'flex-basis', basis
  
  
  @expander('flex-flow')
  def expand_flex_flow(base_url, name, tokens):
      """Expand the ``flex-flow`` property."""
      if len(tokens) == 2:
          for sorted_tokens in tokens, tokens[::-1]:
              direction = flex_direction([sorted_tokens[0]])
              wrap = flex_wrap([sorted_tokens[1]])
              if direction and wrap:
                  yield 'flex-direction', direction
                  yield 'flex-wrap', wrap
                  break
          else:
              raise InvalidValues
      elif len(tokens) == 1:
          direction = flex_direction([tokens[0]])
          if direction:
              yield 'flex-direction', direction
          else:
              wrap = flex_wrap([tokens[0]])
              if wrap:
                  yield 'flex-wrap', wrap
              else:
                  raise InvalidValues
      else:
          raise InvalidValues
  
  
  @expander('fill')
  @expander('stroke')
  def expand_fill(base_url, name, tokens):
      """Expand the ``fill`` shorthand property.
  
      See https://www.w3.org/TR/fill-stroke-3/#fill-shorthand
  
      """
      properties = [
          f"{name}_color", f"{name}_image", f"{name}_repeat",
          f"{name}_position", f"{name}_size", f"{name}_origin"]
      keyword = get_single_keyword(tokens)
      if keyword == 'match-parent':
          for name in properties:
              yield name, keyword
          return
  
      def parse_layer(name, tokens, final_layer=False):
          results = {}
  
          def add(name, suffix, value):
              if value is None:
                  return False
              cur_name = f'{name}_{suffix}'
              if cur_name in results:
                  raise InvalidValues
              results[cur_name] = value
              return True
  
          # Make `tokens` a stack
          tokens = tokens[::-1]
          while tokens:
              if add(name, 'repeat',
                     background_repeat.single_value(tokens[-2:][::-1])):
                  del tokens[-2:]
                  continue
              token = tokens[-1:]
              if final_layer and add(name, 'color', other_colors(token)):
                  tokens.pop()
                  continue
              if add(name, 'image', background_image.single_value(token, base_url)):
                  tokens.pop()
                  continue
              if add(name, 'repeat', background_repeat.single_value(token)):
                  tokens.pop()
                  continue
              for n in (4, 3, 2, 1)[-len(tokens):]:
                  n_tokens = tokens[-n:][::-1]
                  position = background_position.single_value(n_tokens)
                  if position is not None:
                      assert add(name, 'position', position)
                      del tokens[-n:]
                      if (tokens and tokens[-1].type == 'literal' and
                              tokens[-1].value == '/'):
                          for n in (3, 2)[-len(tokens):]:
                              # n includes the '/' delimiter.
                              n_tokens = tokens[-n:-1][::-1]
                              size = background_size.single_value(n_tokens)
                              if size is not None:
                                  assert add(name, 'size', size)
                                  del tokens[-n:]
                      break
              if position is not None:
                  continue
              # if add('origin', box.single_value(token)):
              #     tokens.pop()
              #     next_token = tokens[-1:]
              #     if add('clip', box.single_value(next_token)):
              #         tokens.pop()
              #     else:
              #         # The same keyword sets both:
              #         assert add('clip', box.single_value(token))
              #     continue
              raise InvalidValues
  
          color = results.pop(
              f'{name}_color', INITIAL_VALUES[f'{name}_color'])
          for name in properties:
              if name not in results:
                  results[name] = INITIAL_VALUES[name][0]
          return color, results
  
      layers = reversed(split_on_comma(tokens))
      color, last_layer = parse_layer(name, next(layers), final_layer=True)
      results = dict((k, [v]) for k, v in last_layer.items())
      for tokens in layers:
          _, layer = parse_layer(name, tokens)
          for name, value in layer.items():
              results[name].append(value)
      for name, values in results.items():
          yield name, values[::-1]  # "Un-reverse"
      yield f'{name}-color', color
  