weasyprint 66.0__py3-none-any.whl → 68.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. weasyprint/__init__.py +47 -108
  2. weasyprint/__main__.py +120 -84
  3. weasyprint/anchors.py +4 -4
  4. weasyprint/css/__init__.py +719 -68
  5. weasyprint/css/computed_values.py +64 -175
  6. weasyprint/css/counters.py +1 -1
  7. weasyprint/css/functions.py +211 -0
  8. weasyprint/css/html5_ua.css +2 -1
  9. weasyprint/css/html5_ua_form.css +1 -1
  10. weasyprint/css/media_queries.py +3 -1
  11. weasyprint/css/properties.py +6 -2
  12. weasyprint/css/{utils.py → tokens.py} +310 -398
  13. weasyprint/css/units.py +91 -0
  14. weasyprint/css/validation/__init__.py +1 -1
  15. weasyprint/css/validation/descriptors.py +47 -19
  16. weasyprint/css/validation/expanders.py +7 -8
  17. weasyprint/css/validation/properties.py +343 -359
  18. weasyprint/document.py +22 -73
  19. weasyprint/draw/__init__.py +6 -7
  20. weasyprint/draw/border.py +3 -5
  21. weasyprint/draw/color.py +1 -1
  22. weasyprint/draw/text.py +62 -40
  23. weasyprint/formatting_structure/boxes.py +24 -3
  24. weasyprint/formatting_structure/build.py +113 -41
  25. weasyprint/images.py +94 -78
  26. weasyprint/layout/__init__.py +29 -25
  27. weasyprint/layout/absolute.py +3 -5
  28. weasyprint/layout/background.py +7 -7
  29. weasyprint/layout/block.py +140 -128
  30. weasyprint/layout/column.py +18 -24
  31. weasyprint/layout/flex.py +13 -5
  32. weasyprint/layout/float.py +4 -6
  33. weasyprint/layout/grid.py +304 -99
  34. weasyprint/layout/inline.py +114 -60
  35. weasyprint/layout/page.py +27 -16
  36. weasyprint/layout/percent.py +14 -10
  37. weasyprint/layout/preferred.py +79 -31
  38. weasyprint/layout/replaced.py +9 -6
  39. weasyprint/layout/table.py +8 -5
  40. weasyprint/pdf/__init__.py +58 -14
  41. weasyprint/pdf/anchors.py +11 -18
  42. weasyprint/pdf/fonts.py +135 -69
  43. weasyprint/pdf/metadata.py +155 -68
  44. weasyprint/pdf/pdfa.py +20 -6
  45. weasyprint/pdf/pdfua.py +1 -3
  46. weasyprint/pdf/pdfx.py +81 -0
  47. weasyprint/pdf/stream.py +18 -3
  48. weasyprint/pdf/tags.py +6 -4
  49. weasyprint/svg/__init__.py +85 -48
  50. weasyprint/svg/css.py +21 -4
  51. weasyprint/svg/defs.py +5 -3
  52. weasyprint/svg/images.py +11 -3
  53. weasyprint/svg/text.py +11 -2
  54. weasyprint/svg/utils.py +6 -3
  55. weasyprint/text/constants.py +1 -1
  56. weasyprint/text/ffi.py +4 -3
  57. weasyprint/text/fonts.py +14 -7
  58. weasyprint/text/line_break.py +101 -17
  59. weasyprint/urls.py +288 -95
  60. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/METADATA +6 -6
  61. weasyprint-68.0.dist-info/RECORD +77 -0
  62. weasyprint-66.0.dist-info/RECORD +0 -74
  63. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/WHEEL +0 -0
  64. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/entry_points.txt +0 -0
  65. {weasyprint-66.0.dist-info → weasyprint-68.0.dist-info}/licenses/LICENSE +0 -0
@@ -12,27 +12,34 @@ on other functions in this module.
12
12
 
13
13
  """
14
14
 
15
+ import math
15
16
  from collections import namedtuple
16
17
  from itertools import groupby
17
18
  from logging import DEBUG, WARNING
19
+ from math import inf
18
20
 
19
21
  import cssselect2
20
22
  import tinycss2
21
23
  import tinycss2.ast
22
24
  import tinycss2.nth
25
+ from PIL.ImageCms import ImageCmsProfile
23
26
 
24
27
  from .. import CSS
25
28
  from ..logger import LOGGER, PROGRESS_LOGGER
26
- from ..urls import URLFetchingError, get_url_attribute, url_join
29
+ from ..text.fonts import FontConfiguration
30
+ from ..urls import URLFetchingError, fetch, get_url_attribute, url_join
27
31
  from . import counters, media_queries
28
32
  from .computed_values import COMPUTER_FUNCTIONS
33
+ from .functions import Function, check_math, check_var
29
34
  from .properties import INHERITED, INITIAL_NOT_COMPUTED, INITIAL_VALUES, ZERO_PIXELS
35
+ from .units import ANGLE_UNITS, FONT_UNITS, LENGTH_UNITS, to_pixels, to_radians
30
36
  from .validation import preprocess_declarations
31
37
  from .validation.descriptors import preprocess_descriptors
38
+ from .validation.properties import validate_non_shorthand
32
39
 
33
- from .utils import ( # isort:skip
34
- InvalidValues, Pending, check_var_function, get_url, parse_function,
35
- remove_whitespace)
40
+ from .tokens import ( # isort:skip
41
+ E, MINUS_INFINITY, NAN, PI, PLUS_INFINITY, FontUnitInMath, InvalidValues, Pending,
42
+ PercentageInMath, get_angle, get_url, remove_whitespace, split_on_comma, tokenize)
36
43
 
37
44
  # Reject anything not in here:
38
45
  PSEUDO_ELEMENTS = (
@@ -45,7 +52,8 @@ PageSelectorType = namedtuple(
45
52
 
46
53
  class StyleFor:
47
54
  """Convenience function to get the computed styles for an element."""
48
- def __init__(self, html, sheets, presentational_hints, target_collector):
55
+ def __init__(self, html, sheets, presentational_hints, font_config,
56
+ target_collector):
49
57
  # keys: (element, pseudo_element_type)
50
58
  # element: an ElementTree Element or the '@page' string
51
59
  # pseudo_element_type: a string such as 'first' (for @page) or
@@ -65,8 +73,10 @@ class StyleFor:
65
73
  self._computed_styles = {}
66
74
 
67
75
  self._sheets = sheets
76
+ self.font_config = font_config
68
77
 
69
78
  PROGRESS_LOGGER.info('Step 3 - Applying CSS')
79
+ layer_order = inf
70
80
  for specificity, attributes in find_style_attributes(
71
81
  html.etree_element, presentational_hints, html.base_url):
72
82
  element, declarations, base_url = attributes
@@ -74,7 +84,7 @@ class StyleFor:
74
84
  for name, values, importance in preprocess_declarations(
75
85
  base_url, declarations):
76
86
  precedence = declaration_precedence('author', importance)
77
- weight = (precedence, specificity)
87
+ weight = (precedence, layer_order, specificity)
78
88
  old_weight = style.get(name, (None, None))[1]
79
89
  if old_weight is None or old_weight <= weight:
80
90
  style[name] = values, weight
@@ -88,13 +98,14 @@ class StyleFor:
88
98
  for sheet, origin, sheet_specificity in sheets:
89
99
  # Add declarations for matched elements
90
100
  for selector in sheet.matcher.match(element):
91
- specificity, order, pseudo_type, declarations = selector
101
+ specificity, order, pseudo_type, (declarations, layer) = selector
102
+ layer_order = inf if layer is None else sheet.layers.index(layer)
92
103
  specificity = sheet_specificity or specificity
93
104
  style = cascaded_styles.setdefault(
94
105
  (element.etree_element, pseudo_type), {})
95
106
  for name, values, importance in declarations:
96
107
  precedence = declaration_precedence(origin, importance)
97
- weight = (precedence, specificity)
108
+ weight = (precedence, layer_order, specificity)
98
109
  old_weight = style.get(name, (None, None))[1]
99
110
  if old_weight is None or old_weight <= weight:
100
111
  style[name] = values, weight
@@ -151,22 +162,22 @@ class StyleFor:
151
162
  if element == root and pseudo_type is None:
152
163
  assert parent is None
153
164
  parent_style = None
154
- root_style = {
155
- # When specified on the font-size property of the root element,
156
- # the rem units refer to the property’s initial value.
157
- 'font_size': INITIAL_VALUES['font_size'],
158
- }
165
+ root_style = InitialStyle(self.font_config)
159
166
  else:
160
167
  assert parent is not None
161
168
  parent_style = computed_styles[parent, None]
162
169
  root_style = computed_styles[root, None]
163
170
 
164
171
  cascaded = cascaded_styles.get((element, pseudo_type), {})
165
- computed_styles[element, pseudo_type] = computed_from_cascaded(
166
- element, cascaded, parent_style, pseudo_type, root_style, base_url,
167
- target_collector)
172
+ computed = computed_styles[element, pseudo_type] = ComputedStyle(
173
+ parent_style, cascaded, element, pseudo_type, root_style, base_url,
174
+ self.font_config)
175
+ if target_collector and computed['anchor']:
176
+ target_collector.collect_anchor(computed['anchor'])
168
177
 
169
178
  def add_page_declarations(self, page_type):
179
+ # TODO: use real layer order.
180
+ layer_order = None
170
181
  for sheet, origin, sheet_specificity in self._sheets:
171
182
  for _rule, selector_list, declarations in sheet.page_rules:
172
183
  for selector in selector_list:
@@ -177,7 +188,7 @@ class StyleFor:
177
188
  (page_type, pseudo_type), {})
178
189
  for name, values, importance in declarations:
179
190
  precedence = declaration_precedence(origin, importance)
180
- weight = (precedence, specificity)
191
+ weight = (precedence, layer_order, specificity)
181
192
  old_weight = style.get(name, (None, None))[1]
182
193
  if old_weight is None or old_weight <= weight:
183
194
  style[name] = values, weight
@@ -245,7 +256,7 @@ def text_decoration(key, value, parent_value, cascaded):
245
256
 
246
257
 
247
258
  def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
248
- font_config, counter_style, page_rules):
259
+ font_config, counter_style, color_profiles, page_rules, layers):
249
260
  """Yield the stylesheets in ``element_tree``.
250
261
 
251
262
  The output order is the same as the source order.
@@ -273,7 +284,7 @@ def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
273
284
  string=content, base_url=base_url,
274
285
  url_fetcher=url_fetcher, media_type=device_media_type,
275
286
  font_config=font_config, counter_style=counter_style,
276
- page_rules=page_rules)
287
+ page_rules=page_rules, color_profiles=color_profiles, layers=layers)
277
288
  yield css
278
289
  elif element.tag == 'link' and element.get('href'):
279
290
  if not element_has_link_type(element, 'stylesheet') or \
@@ -283,10 +294,10 @@ def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
283
294
  if href is not None:
284
295
  try:
285
296
  yield CSS(
286
- url=href, url_fetcher=url_fetcher,
287
- _check_mime_type=True, media_type=device_media_type,
297
+ url=href, url_fetcher=url_fetcher, media_type=device_media_type,
288
298
  font_config=font_config, counter_style=counter_style,
289
- page_rules=page_rules)
299
+ color_profiles=color_profiles, page_rules=page_rules,
300
+ layers=layers, _check_mime_type=True)
290
301
  except URLFetchingError as exception:
291
302
  LOGGER.error('Failed to load stylesheet at %s: %s', href, exception)
292
303
  LOGGER.debug('Error while loading stylesheet:', exc_info=exception)
@@ -575,38 +586,460 @@ def declaration_precedence(origin, importance):
575
586
 
576
587
  def resolve_var(computed, token, parent_style, known_variables=None):
577
588
  """Return token with resolved CSS variables."""
578
- if not check_var_function(token):
589
+ if not check_var(token):
579
590
  return
580
591
 
581
592
  if known_variables is None:
582
593
  known_variables = set()
583
594
 
584
- if token.lower_name != 'var':
585
- arguments = []
586
- for i, argument in enumerate(token.arguments):
587
- if argument.type == 'function':
588
- arguments.extend(resolve_var(
589
- computed, argument, parent_style, known_variables))
595
+ if token.type == '() block' or token.lower_name != 'var':
596
+ items = []
597
+ token_items = token.arguments if token.type == 'function' else token.content
598
+ for i, argument in enumerate(token_items):
599
+ if argument.type in ('function', '() block'):
600
+ resolved = resolve_var(
601
+ computed, argument, parent_style, known_variables.copy())
602
+ items.extend((argument,) if resolved is None else resolved)
590
603
  else:
591
- arguments.append(argument)
592
- token = tinycss2.ast.FunctionBlock(
593
- token.source_line, token.source_column, token.name, arguments)
604
+ items.append(argument)
605
+ if token.type == '() block':
606
+ token = tinycss2.ast.ParenthesesBlock(
607
+ token.source_line, token.source_column, items)
608
+ else:
609
+ token = tinycss2.ast.FunctionBlock(
610
+ token.source_line, token.source_column, token.name, items)
594
611
  return resolve_var(computed, token, parent_style, known_variables) or (token,)
595
612
 
596
- args = parse_function(token)[1]
597
- variable_name = args.pop(0).value.replace('-', '_') # first arg is name
613
+ function = Function(token)
614
+ arguments = function.split_comma(single_tokens=False, trailing=True)
615
+ if not arguments or len(arguments[0]) != 1:
616
+ return []
617
+ variable_name = arguments[0][0].value.replace('-', '_') # first arg is name
598
618
  if variable_name in known_variables:
599
- return [] # endless recursion, returned value is nothing
619
+ return [] # endless recursion
600
620
  else:
601
621
  known_variables.add(variable_name)
602
- default = args # next args are default value
622
+ default = arguments[1] if len(arguments) > 1 else []
603
623
  computed_value = []
604
624
  for value in (computed[variable_name] or default):
605
- resolved = resolve_var(computed, value, parent_style, known_variables)
625
+ resolved = resolve_var(computed, value, parent_style, known_variables.copy())
606
626
  computed_value.extend((value,) if resolved is None else resolved)
607
627
  return computed_value
608
628
 
609
629
 
630
+ def _resolve_calc_sum(computed, tokens, property_name, refer_to):
631
+ groups = [[]]
632
+ for token in tokens:
633
+ if token.type == 'literal' and token.value in '+-':
634
+ groups.append(token.value)
635
+ groups.append([])
636
+ elif token.type == '() block':
637
+ content = remove_whitespace(token.content)
638
+ result = _resolve_calc_sum(computed, content, property_name, refer_to)
639
+ if result is None:
640
+ return
641
+ groups[-1].append(result)
642
+ else:
643
+ groups[-1].append(token)
644
+
645
+ value, sign, unit = 0, '+', None
646
+ exception = None
647
+ while groups:
648
+ if sign is None:
649
+ sign = groups.pop(0)
650
+ assert sign in '+-'
651
+ else:
652
+ group = groups.pop(0)
653
+ assert group
654
+ assert isinstance(group, list)
655
+ try:
656
+ product = _resolve_calc_product(
657
+ computed, group, property_name, refer_to)
658
+ except FontUnitInMath as font_exception:
659
+ # FontUnitInMath raised, assume that we got pixels and continue to find
660
+ # if we have to raise PercentageInMath first.
661
+ if unit == '%':
662
+ raise PercentageInMath
663
+ exception = font_exception
664
+ unit = 'px'
665
+ sign = None
666
+ continue
667
+ else:
668
+ if product is None:
669
+ return
670
+ if product.type == 'dimension':
671
+ if unit is None:
672
+ unit = product.unit.lower()
673
+ elif unit == '%':
674
+ raise PercentageInMath
675
+ elif unit != product.unit.lower():
676
+ return
677
+ elif product.type == 'percentage':
678
+ if refer_to is not None:
679
+ product.value = product.value / 100 * refer_to
680
+ unit = 'px'
681
+ else:
682
+ if unit is None or unit == '%':
683
+ unit = '%'
684
+ else:
685
+ raise PercentageInMath
686
+ if sign == '+':
687
+ value += product.value
688
+ else:
689
+ value -= product.value
690
+ sign = None
691
+
692
+ # Raise FontUnitInMath, only if we didn’t raise PercentageInMath before.
693
+ if exception:
694
+ raise exception
695
+
696
+ return tokenize(value, unit=unit)
697
+
698
+
699
+ def _resolve_calc_product(computed, tokens, property_name, refer_to):
700
+ groups = [[]]
701
+ for token in tokens:
702
+ if token.type == 'literal' and token.value in '*/':
703
+ groups.append(token.value)
704
+ groups.append([])
705
+ elif token.type == 'number':
706
+ groups[-1].append(token)
707
+ elif token.type == 'dimension' and token.unit.lower() in LENGTH_UNITS:
708
+ if computed is None and token.unit.lower() in FONT_UNITS:
709
+ raise FontUnitInMath
710
+ pixels = to_pixels(token, computed, property_name)
711
+ groups[-1].append(tokenize(pixels, unit='px'))
712
+ elif token.type == 'dimension' and token.unit.lower() in ANGLE_UNITS:
713
+ groups[-1].append(tokenize(to_radians(token), unit='rad'))
714
+ elif token.type == 'percentage':
715
+ groups[-1].append(tokenize(token.value, unit='%'))
716
+ elif token.type == 'ident':
717
+ groups[-1].append(token)
718
+ else:
719
+ return
720
+
721
+ value, sign, unit = 1, '*', None
722
+ while groups:
723
+ if sign is None:
724
+ sign = groups.pop(0)
725
+ assert sign in '*/'
726
+ else:
727
+ group = groups.pop(0)
728
+ assert isinstance(group, list)
729
+ calc = _resolve_calc_value(computed, group)
730
+ if calc is None:
731
+ return
732
+ if calc.type == 'dimension':
733
+ if unit is None or unit == '%':
734
+ unit = calc.unit.lower()
735
+ else:
736
+ return
737
+ elif calc.type == 'percentage':
738
+ if unit is None:
739
+ unit = '%'
740
+ if sign == '*':
741
+ value *= calc.value
742
+ else:
743
+ value /= calc.value
744
+ sign = None
745
+
746
+ return tokenize(value, unit=unit)
747
+
748
+
749
+ def _resolve_calc_value(computed, tokens):
750
+ if len(tokens) == 1:
751
+ token, = tokens
752
+ if token.type in ('number', 'dimension', 'percentage'):
753
+ return token
754
+ elif token.type == 'ident':
755
+ if token.lower_value == 'e':
756
+ return E
757
+ elif token.lower_value == 'pi':
758
+ return PI
759
+ elif token.lower_value == 'infinity':
760
+ return PLUS_INFINITY
761
+ elif token.lower_value == '-infinity':
762
+ return MINUS_INFINITY
763
+ elif token.lower_value == 'nan':
764
+ return NAN
765
+
766
+
767
+ def resolve_math(token, computed=None, property_name=None, refer_to=None):
768
+ """Return token with resolved math functions.
769
+
770
+ Raise, in order of priority, ``PercentageInMath`` if percentages are mixed with
771
+ other values with no ``refer_to`` size, or ``FontUnitInMath`` if no ``computed``
772
+ style is available to get font size.
773
+
774
+ ``PercentageInMath`` has to be raised before FontUnitInMath so that it can be used
775
+ to discard validation of properties that don’t accept percentages.
776
+
777
+ """
778
+ if not check_math(token):
779
+ return
780
+
781
+ args = []
782
+ original_token = token
783
+ function = Function(token)
784
+ if function.name is None:
785
+ return
786
+ for part in function.split_comma(single_tokens=False):
787
+ args.append([])
788
+ for arg in part:
789
+ if check_math(arg):
790
+ arg = resolve_math(arg, computed, property_name, refer_to)
791
+ if arg is None:
792
+ return
793
+ args[-1].append(arg)
794
+
795
+ if function.name == 'calc':
796
+ result = _resolve_calc_sum(computed, args[0], property_name, refer_to)
797
+ if result is None:
798
+ return original_token
799
+ else:
800
+ return tokenize(result)
801
+
802
+ elif function.name in ('min', 'max'):
803
+ target_value = target_token = unit = None
804
+ for tokens in args:
805
+ token = _resolve_calc_sum(computed, tokens, property_name, refer_to)
806
+ if token is None:
807
+ return
808
+ if token.type == 'percentage':
809
+ if refer_to is None:
810
+ if unit in ('px', ''):
811
+ raise PercentageInMath
812
+ unit = '%'
813
+ value = token
814
+ else:
815
+ unit = 'px'
816
+ token = value = tokenize(token.value / 100 * refer_to, unit='px')
817
+ elif token.type == 'number':
818
+ if unit == '%':
819
+ raise PercentageInMath
820
+ elif unit == 'px':
821
+ return
822
+ unit = ''
823
+ value = tokenize(token.value, unit='px')
824
+ else:
825
+ if unit == '%':
826
+ raise PercentageInMath
827
+ elif unit == '':
828
+ return
829
+ unit = 'px'
830
+ value = tokenize(to_pixels(token, computed, property_name), unit='px')
831
+ update_condition = (
832
+ target_value is None or
833
+ (function.name == 'min' and value.value < target_value.value) or
834
+ (function.name == 'max' and value.value > target_value.value))
835
+ if update_condition:
836
+ target_value, target_token = value, token
837
+ return tokenize(target_token)
838
+
839
+ elif function.name == 'round':
840
+ strategy, multiple = 'nearest', 1
841
+ if len(args) == 1:
842
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
843
+ elif len(args) == 2:
844
+ strategies = ('nearest', 'up', 'down', 'to-zero')
845
+ if len(args[0]) == 1 and args[0][0].value in strategies:
846
+ strategy = args[0][0].value
847
+ number_token = _resolve_calc_sum(
848
+ computed, args[1], property_name, refer_to)
849
+ if number_token is None:
850
+ return
851
+ else:
852
+ number_token = _resolve_calc_sum(
853
+ computed, args[0], property_name, refer_to)
854
+ multiple_token = _resolve_calc_sum(
855
+ computed, args[1], property_name, refer_to)
856
+ if None in (number_token, multiple_token):
857
+ return
858
+ if number_token.type != multiple_token.type:
859
+ return
860
+ multiple = multiple_token.value
861
+ elif len(args) == 3:
862
+ strategy = args[0][0].value
863
+ number_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)
864
+ multiple_token = _resolve_calc_sum(
865
+ computed, args[2], property_name, refer_to)
866
+ if None in (number_token, multiple_token):
867
+ return
868
+ if number_token.type != multiple_token.type:
869
+ return
870
+ multiple = multiple_token.value
871
+ if strategy == 'nearest':
872
+ # TODO: always round x.5 to +inf, see
873
+ # https://drafts.csswg.org/css-values-4/#combine-integers.
874
+ function = round
875
+ elif strategy == 'up':
876
+ function = math.ceil
877
+ elif strategy == 'down':
878
+ function = math.floor
879
+ elif strategy == 'to-zero':
880
+ function = math.floor if number_token.value > 0 else math.ceil
881
+ else:
882
+ return
883
+ return tokenize(number_token, lambda x: function(x / multiple) * multiple)
884
+
885
+ elif function.name in ('mod', 'rem'):
886
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
887
+ parameter_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)
888
+ if None in (number_token, parameter_token):
889
+ return
890
+ if number_token.type != parameter_token.type:
891
+ return
892
+ number = number_token.value
893
+ parameter = parameter_token.value
894
+ value = number % parameter
895
+ if function.name == 'rem' and number * parameter < 0:
896
+ value += abs(parameter)
897
+ return tokenize(number_token, lambda x: value)
898
+
899
+ elif function.name in ('sin', 'cos', 'tan'):
900
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
901
+ if number_token is None:
902
+ return
903
+ if number_token.type == 'number':
904
+ angle = number_token.value
905
+ elif (angle := get_angle(number_token)) is None:
906
+ return
907
+ value = getattr(math, function.name)(angle)
908
+ return tokenize(value)
909
+
910
+ elif function.name in ('asin', 'acos', 'atan'):
911
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
912
+ if number_token is None or number_token.type != 'number':
913
+ return
914
+ try:
915
+ value = getattr(math, function.name)(number_token.value)
916
+ except ValueError:
917
+ return
918
+ return tokenize(value, unit='rad')
919
+
920
+ elif function.name == 'atan2':
921
+ y_token, x_token = [
922
+ _resolve_calc_sum(computed, arg, property_name, refer_to) for arg in args]
923
+ if None in (y_token, x_token):
924
+ return
925
+ if {y_token.type, x_token.type} != {'number'}:
926
+ return
927
+ y, x = y_token.value, x_token.value
928
+ return tokenize(math.atan2(y, x), unit='rad')
929
+
930
+ elif function.name == 'clamp':
931
+ pixels_list = []
932
+ unit = None
933
+ for tokens in args:
934
+ token = _resolve_calc_sum(computed, tokens, property_name, refer_to)
935
+ if token is None:
936
+ return
937
+ if token.type == 'percentage':
938
+ if refer_to is None:
939
+ if unit == 'px':
940
+ raise PercentageInMath
941
+ unit = '%'
942
+ value = token
943
+ else:
944
+ unit = 'px'
945
+ token = tokenize(token.value / 100 * refer_to, unit='px')
946
+ else:
947
+ if unit == '%':
948
+ raise PercentageInMath
949
+ unit = 'px'
950
+ pixels = to_pixels(token, computed, property_name)
951
+ value = tokenize(pixels, unit='px')
952
+ pixels_list.append(value)
953
+ min_token, token, max_token = pixels_list
954
+ if token.value < min_token.value:
955
+ token = min_token
956
+ if token.value > max_token.value:
957
+ token = max_token
958
+ return tokenize(token)
959
+
960
+ elif function.name == 'pow':
961
+ number_token, power_token = [
962
+ _resolve_calc_sum(computed, arg, property_name, refer_to) for arg in args]
963
+ if None in (number_token, power_token):
964
+ return
965
+ if {number_token.type, power_token.type} != {'number'}:
966
+ return
967
+ return tokenize(number_token, lambda x: x ** power_token.value)
968
+
969
+ elif function.name == 'sqrt':
970
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
971
+ if number_token is None or number_token.type != 'number':
972
+ return
973
+ return tokenize(number_token, lambda x: x ** 0.5)
974
+
975
+ elif function.name == 'hypot':
976
+ resolved = [
977
+ _resolve_calc_sum(computed, tokens, property_name, refer_to)
978
+ for tokens in args]
979
+ if None in resolved:
980
+ return
981
+ value = math.hypot(*[token.value for token in resolved])
982
+ return tokenize(resolved[0], lambda x: value)
983
+
984
+ elif function.name == 'log':
985
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
986
+ if number_token is None or number_token.type != 'number':
987
+ return
988
+ if len(args) == 2:
989
+ base_token = _resolve_calc_sum(computed, args[1], property_name, refer_to)
990
+ if base_token is None or base_token.type != 'number':
991
+ return
992
+ base = base_token.value
993
+ else:
994
+ base = math.e
995
+ return tokenize(number_token, lambda x: math.log(x, base))
996
+
997
+ elif function.name == 'exp':
998
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
999
+ if number_token is None or number_token.type != 'number':
1000
+ return
1001
+ return tokenize(number_token, math.exp)
1002
+
1003
+ elif function.name == 'abs':
1004
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
1005
+ if number_token is None:
1006
+ return
1007
+ return tokenize(number_token, abs)
1008
+
1009
+ elif function.name == 'sign':
1010
+ number_token = _resolve_calc_sum(computed, args[0], property_name, refer_to)
1011
+ if number_token is None:
1012
+ return
1013
+ return tokenize(
1014
+ number_token.value, lambda x: 0 if x == 0 else 1 if x > 0 else -1)
1015
+
1016
+ arguments = []
1017
+ for i, argument in enumerate(token.arguments):
1018
+ if argument.type == 'function':
1019
+ result = resolve_math(argument, computed, property_name, refer_to)
1020
+ if result is None:
1021
+ return
1022
+ arguments.append(result)
1023
+ else:
1024
+ arguments.append(argument)
1025
+ token = tinycss2.ast.FunctionBlock(
1026
+ token.source_line, token.source_column, token.name, arguments)
1027
+ return resolve_math(token, computed, property_name, refer_to) or token
1028
+
1029
+
1030
+ class InitialStyle(dict):
1031
+ """Dummy computed style used to store initial values."""
1032
+ def __init__(self, font_config):
1033
+ self.parent_style = None
1034
+ self.specified = self
1035
+ self.cache = {}
1036
+ self.font_config = font_config
1037
+
1038
+ def __missing__(self, key):
1039
+ value = self[key] = INITIAL_VALUES[key]
1040
+ return value
1041
+
1042
+
610
1043
  class AnonymousStyle(dict):
611
1044
  """Computed style used for anonymous boxes."""
612
1045
  def __init__(self, parent_style):
@@ -621,11 +1054,10 @@ class AnonymousStyle(dict):
621
1054
  'outline_width': 0,
622
1055
  })
623
1056
  self.parent_style = parent_style
1057
+ self.is_root_element = False
624
1058
  self.specified = self
625
- if parent_style:
626
- self.cache = parent_style.cache
627
- else:
628
- self.cache = {'ratio_ch': {}, 'ratio_ex': {}}
1059
+ self.cache = parent_style.cache
1060
+ self.font_config = parent_style.font_config
629
1061
 
630
1062
  def copy(self):
631
1063
  copy = AnonymousStyle(self.parent_style)
@@ -642,14 +1074,20 @@ class AnonymousStyle(dict):
642
1074
  value = self[key] = text_decoration(
643
1075
  key, INITIAL_VALUES[key], self.parent_style[key], cascaded=False)
644
1076
  else:
645
- value = self[key] = INITIAL_VALUES[key]
1077
+ value = INITIAL_VALUES[key]
1078
+ if key in INITIAL_NOT_COMPUTED:
1079
+ # Value not computed yet: compute.
1080
+ value = self[key] = COMPUTER_FUNCTIONS[key](self, key, value)
1081
+ else:
1082
+ # The value is the same as when computed.
1083
+ self[key] = value
646
1084
  return value
647
1085
 
648
1086
 
649
1087
  class ComputedStyle(dict):
650
1088
  """Computed style used for non-anonymous boxes."""
651
1089
  def __init__(self, parent_style, cascaded, element, pseudo_type,
652
- root_style, base_url):
1090
+ root_style, base_url, font_config):
653
1091
  self.specified = {}
654
1092
  self.parent_style = parent_style
655
1093
  self.cascaded = cascaded
@@ -658,15 +1096,13 @@ class ComputedStyle(dict):
658
1096
  self.pseudo_type = pseudo_type
659
1097
  self.root_style = root_style
660
1098
  self.base_url = base_url
661
- if parent_style:
662
- self.cache = parent_style.cache
663
- else:
664
- self.cache = {'ratio_ch': {}, 'ratio_ex': {}}
1099
+ self.font_config = font_config
1100
+ self.cache = parent_style.cache if parent_style else {}
665
1101
 
666
1102
  def copy(self):
667
1103
  copy = ComputedStyle(
668
1104
  self.parent_style, self.cascaded, self.element, self.pseudo_type,
669
- self.root_style, self.base_url)
1105
+ self.root_style, self.base_url, self.font_config)
670
1106
  copy.update(self)
671
1107
  copy.specified = self.specified.copy()
672
1108
  return copy
@@ -732,9 +1168,10 @@ class ComputedStyle(dict):
732
1168
  if key[:16] == 'text_decoration_' and parent_style is not None:
733
1169
  # Text decorations are not inherited but propagated. See
734
1170
  # https://www.w3.org/TR/css-text-decor-3/#line-decoration.
735
- value = text_decoration(key, value, parent_style[key], key in self.cascaded)
736
- if key in self:
737
- del self[key]
1171
+ if key in COMPUTER_FUNCTIONS:
1172
+ value = COMPUTER_FUNCTIONS[key](self, key, value)
1173
+ self[key] = text_decoration(
1174
+ key, value, parent_style[key], key in self.cascaded)
738
1175
  elif key == 'page' and value == 'auto':
739
1176
  # The page property does not inherit. However, if the page value on
740
1177
  # an element is auto, then its used value is the value specified on
@@ -750,6 +1187,33 @@ class ComputedStyle(dict):
750
1187
  # https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.
751
1188
  self.specified[key] = value
752
1189
 
1190
+ if check_math(value):
1191
+ function = value
1192
+ solved_tokens = []
1193
+ try:
1194
+ try:
1195
+ token = resolve_math(function, self, key)
1196
+ except PercentageInMath:
1197
+ token = None
1198
+ if token is None:
1199
+ solved_tokens.append(function)
1200
+ else:
1201
+ solved_tokens.append(token)
1202
+ original_key = key.replace('_', '-')
1203
+ value = validate_non_shorthand(solved_tokens, original_key)[0][1]
1204
+ except Exception:
1205
+ LOGGER.warning(
1206
+ 'Invalid math function at %d:%d: %s',
1207
+ function.source_line, function.source_column, function.serialize())
1208
+ if key in INHERITED and parent_style is not None:
1209
+ # Values in parent_style are already computed.
1210
+ self[key] = value = parent_style[key]
1211
+ else:
1212
+ value = INITIAL_VALUES[key]
1213
+ if key not in INITIAL_NOT_COMPUTED:
1214
+ # The value is the same as when computed.
1215
+ self[key] = value
1216
+
753
1217
  if key in self:
754
1218
  # Value already computed and saved: return.
755
1219
  return self[key]
@@ -762,6 +1226,44 @@ class ComputedStyle(dict):
762
1226
  return value
763
1227
 
764
1228
 
1229
+ class ColorProfile:
1230
+ def __init__(self, file_object, descriptors):
1231
+ self.src = descriptors['src'][1]
1232
+ self.renderingintent = descriptors['rendering-intent']
1233
+ self.components = descriptors['components']
1234
+ self._profile = ImageCmsProfile(file_object)
1235
+
1236
+ @property
1237
+ def name(self):
1238
+ return (
1239
+ self._profile.profile.model or
1240
+ self._profile.profile.profile_description)
1241
+
1242
+ @property
1243
+ def content(self):
1244
+ return self._profile.tobytes()
1245
+
1246
+
1247
+ def _add_layer(layer, layers):
1248
+ """Add layer to list of layers, handling order."""
1249
+ index = None
1250
+ parts = layer.split('.')
1251
+ full_layer = ''
1252
+ for part in parts:
1253
+ if full_layer:
1254
+ full_layer += '.'
1255
+ full_layer += part
1256
+ if full_layer in layers:
1257
+ index = layers.index(full_layer)
1258
+ continue
1259
+ if index is None:
1260
+ layers.append(full_layer)
1261
+ index = len(layers) - 1
1262
+ else:
1263
+ layers.insert(index, full_layer)
1264
+ index -= 1
1265
+
1266
+
765
1267
  def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
766
1268
  root_style=None, base_url=None,
767
1269
  target_collector=None):
@@ -769,11 +1271,38 @@ def computed_from_cascaded(element, cascaded, parent_style, pseudo_type=None,
769
1271
  if not cascaded and parent_style is not None:
770
1272
  return AnonymousStyle(parent_style)
771
1273
 
772
- style = ComputedStyle(
773
- parent_style, cascaded, element, pseudo_type, root_style, base_url)
774
- if target_collector and style['anchor']:
775
- target_collector.collect_anchor(style['anchor'])
776
- return style
1274
+
1275
+ def _parse_layer(tokens):
1276
+ """Parse tokens representing a layer name."""
1277
+ if not tokens:
1278
+ return
1279
+ new_layer = ''
1280
+ last_dot = True
1281
+ for token in tokens:
1282
+ if token.type == 'ident' and last_dot:
1283
+ new_layer += token.value
1284
+ last_dot = False
1285
+ elif token.type == 'literal' and token.value == '.' and not last_dot:
1286
+ new_layer += '.'
1287
+ last_dot = True
1288
+ else:
1289
+ return
1290
+ if not last_dot:
1291
+ return new_layer
1292
+
1293
+
1294
+ def parse_color_profile_name(prelude):
1295
+ tokens = list(remove_whitespace(prelude))
1296
+
1297
+ if len(tokens) != 1:
1298
+ return
1299
+
1300
+ token = tokens[0]
1301
+ if token.type != 'ident':
1302
+ return
1303
+
1304
+ if token.value.startswith('--') or token.value == 'device-cmyk':
1305
+ return token.value
777
1306
 
778
1307
 
779
1308
  def parse_page_selectors(rule):
@@ -894,8 +1423,8 @@ def parse_page_selectors(rule):
894
1423
 
895
1424
 
896
1425
  def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fetcher,
897
- matcher, page_rules, font_config, counter_style,
898
- ignore_imports=False):
1426
+ matcher, page_rules, layers, font_config, counter_style,
1427
+ color_profiles, ignore_imports=False, layer=None):
899
1428
  """Do what can be done early on stylesheet, before being in a document."""
900
1429
  for rule in stylesheet_rules:
901
1430
  if getattr(rule, 'content', None) is None:
@@ -905,7 +1434,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
905
1434
  rule.source_line, rule.source_column, rule.message)
906
1435
  if rule.type != 'at-rule':
907
1436
  continue
908
- if rule.lower_at_keyword != 'import':
1437
+ if rule.lower_at_keyword not in ('import', 'layer'):
909
1438
  LOGGER.warning(
910
1439
  "Unknown empty rule %s at %d:%d",
911
1440
  rule, rule.source_line, rule.source_column)
@@ -925,7 +1454,7 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
925
1454
  declarations = [
926
1455
  declaration[1] for declaration in declarations]
927
1456
  for selector in selectors:
928
- matcher.add_selector(selector, declarations)
1457
+ matcher.add_selector(selector, (declarations, layer))
929
1458
  if selector.pseudo_element not in PSEUDO_ELEMENTS:
930
1459
  prelude = tinycss2.serialize(rule.prelude)
931
1460
  if selector.pseudo_element.startswith('-'):
@@ -969,7 +1498,28 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
969
1498
  url = url_tuple[1][1]
970
1499
  if url is None:
971
1500
  continue
972
- media = media_queries.parse_media_query(tokens[1:])
1501
+
1502
+ new_layer = None
1503
+ next_tokens = list(tokens[1:])
1504
+ if next_tokens:
1505
+ if next_tokens[0].type == 'function' and next_tokens[0].name == 'layer':
1506
+ function = next_tokens.pop(0)
1507
+ if not (new_layer := _parse_layer(function.arguments)):
1508
+ LOGGER.warning(
1509
+ 'Invalid layer name %r '
1510
+ 'the whole @import rule was ignored at %d:%d.',
1511
+ tinycss2.serialize(function),
1512
+ rule.source_line, rule.source_column)
1513
+ continue
1514
+ elif next_tokens[0].type == 'ident' and next_tokens[0].value == 'layer':
1515
+ next_tokens.pop(0)
1516
+ new_layer = f'@anonymous{len(layers)}'
1517
+ if new_layer:
1518
+ if layer is not None:
1519
+ new_layer = f'{layer}.{new_layer}'
1520
+ _add_layer(new_layer, layers)
1521
+
1522
+ media = media_queries.parse_media_query(next_tokens)
973
1523
  if media is None:
974
1524
  LOGGER.warning(
975
1525
  'Invalid media type %r '
@@ -984,7 +1534,8 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
984
1534
  CSS(
985
1535
  url=url, url_fetcher=url_fetcher, media_type=device_media_type,
986
1536
  font_config=font_config, counter_style=counter_style,
987
- matcher=matcher, page_rules=page_rules)
1537
+ color_profiles=color_profiles, matcher=matcher,
1538
+ page_rules=page_rules, layers=layers, layer=new_layer)
988
1539
  except URLFetchingError as exception:
989
1540
  LOGGER.error('Failed to load stylesheet at %s : %s', url, exception)
990
1541
  LOGGER.debug('Error while loading stylesheet:', exc_info=exception)
@@ -998,13 +1549,13 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
998
1549
  tinycss2.serialize(rule.prelude),
999
1550
  rule.source_line, rule.source_column)
1000
1551
  continue
1001
- ignore_imports = True
1002
1552
  if not media_queries.evaluate_media_query(media, device_media_type):
1003
1553
  continue
1004
1554
  content_rules = tinycss2.parse_rule_list(rule.content)
1005
1555
  preprocess_stylesheet(
1006
1556
  device_media_type, base_url, content_rules, url_fetcher, matcher,
1007
- page_rules, font_config, counter_style, ignore_imports=True)
1557
+ page_rules, layers, font_config, counter_style, color_profiles,
1558
+ ignore_imports=True)
1008
1559
 
1009
1560
  elif rule.type == 'at-rule' and rule.lower_at_keyword == 'page':
1010
1561
  data = parse_page_selectors(rule)
@@ -1055,6 +1606,56 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
1055
1606
  if font_config is not None:
1056
1607
  font_config.add_font_face(rule_descriptors, url_fetcher)
1057
1608
 
1609
+ elif rule.type == 'at-rule' and rule.lower_at_keyword == 'color-profile':
1610
+ ignore_imports = True
1611
+
1612
+ if (name := parse_color_profile_name(rule.prelude)) is None:
1613
+ LOGGER.warning(
1614
+ 'Invalid color profile name %r, the whole '
1615
+ '@color-profile rule was ignored at %d:%d.',
1616
+ tinycss2.serialize(rule.prelude), rule.source_line,
1617
+ rule.source_column)
1618
+ continue
1619
+
1620
+ content = tinycss2.parse_blocks_contents(rule.content)
1621
+ rule_descriptors = preprocess_descriptors(
1622
+ 'color-profile', base_url, content)
1623
+
1624
+ descriptors = {
1625
+ 'src': None,
1626
+ 'rendering-intent': 'relative-colorimetric',
1627
+ 'components': None,
1628
+ }
1629
+ for descriptor_name, descriptor_value in rule_descriptors:
1630
+ if descriptor_name in descriptors:
1631
+ descriptors[descriptor_name] = descriptor_value
1632
+ else:
1633
+ LOGGER.warning(
1634
+ 'Unknown descriptor %r for profile named %r at %d:%d.',
1635
+ descriptor_name, tinycss2.serialize(rule.prelude),
1636
+ rule.source_line, rule.source_column)
1637
+
1638
+ if descriptors['src'] is None:
1639
+ LOGGER.warning(
1640
+ 'No source for profile named %r, the whole '
1641
+ '@color-profile rule was ignored at %d:%d.',
1642
+ tinycss2.serialize(rule.prelude), rule.source_line,
1643
+ rule.source_column)
1644
+ continue
1645
+
1646
+ with fetch(url_fetcher, descriptors['src'][1]) as response:
1647
+ try:
1648
+ color_profile = ColorProfile(response, descriptors)
1649
+ except BaseException:
1650
+ LOGGER.warning(
1651
+ 'Invalid profile file for profile named %r, the whole '
1652
+ '@color-profile rule was ignored at %d:%d.',
1653
+ tinycss2.serialize(rule.prelude), rule.source_line,
1654
+ rule.source_column)
1655
+ continue
1656
+ else:
1657
+ color_profiles[name] = color_profile
1658
+
1058
1659
  elif rule.type == 'at-rule' and rule.lower_at_keyword == 'counter-style':
1059
1660
  name = counters.parse_counter_style_name(rule.prelude, counter_style)
1060
1661
  if name is None:
@@ -1115,6 +1716,52 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
1115
1716
 
1116
1717
  counter_style[name] = counter
1117
1718
 
1719
+ elif rule.type == 'at-rule' and rule.lower_at_keyword == 'layer':
1720
+ new_layers = []
1721
+ prelude = remove_whitespace(rule.prelude)
1722
+ comma_separated_tokens = split_on_comma(prelude) if prelude else ()
1723
+ for tokens in comma_separated_tokens:
1724
+ if new_layer := _parse_layer(tokens):
1725
+ if layer is not None:
1726
+ new_layer = f'{layer}.{new_layer}'
1727
+ new_layers.append(new_layer)
1728
+ else:
1729
+ new_layers = None
1730
+ break
1731
+ if new_layers is None:
1732
+ LOGGER.warning(
1733
+ 'Unsupported @layer selector %r, '
1734
+ 'the whole @layer rule was ignored at %d:%d.',
1735
+ tinycss2.serialize(rule.prelude),
1736
+ rule.source_line, rule.source_column)
1737
+ continue
1738
+ elif len(new_layers) > 1:
1739
+ if rule.content:
1740
+ LOGGER.warning(
1741
+ '@layer rule with multiple layer names, '
1742
+ 'the whole @layer rule was ignored at %d:%d.',
1743
+ rule.source_line, rule.source_column)
1744
+ continue
1745
+ for new_layer in new_layers:
1746
+ _add_layer(new_layer, layers)
1747
+ continue
1748
+
1749
+ if new_layers:
1750
+ new_layer, = new_layers
1751
+ else:
1752
+ new_layer = f'@anonymous{len(layers)}'
1753
+ if layer is not None:
1754
+ new_layer = f'{layer}.{new_layer}'
1755
+ _add_layer(new_layer, layers)
1756
+
1757
+ if rule.content is None:
1758
+ continue
1759
+ content_rules = tinycss2.parse_rule_list(rule.content)
1760
+ preprocess_stylesheet(
1761
+ device_media_type, base_url, content_rules, url_fetcher, matcher,
1762
+ page_rules, layers, font_config, counter_style, color_profiles,
1763
+ ignore_imports=True, layer=new_layer)
1764
+
1118
1765
  else:
1119
1766
  LOGGER.warning(
1120
1767
  "Unknown rule %s at %d:%d",
@@ -1122,8 +1769,9 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
1122
1769
 
1123
1770
 
1124
1771
  def get_all_computed_styles(html, user_stylesheets=None, presentational_hints=False,
1125
- font_config=None, counter_style=None, page_rules=None,
1126
- target_collector=None, forms=False):
1772
+ font_config=None, counter_style=None, color_profiles=None,
1773
+ page_rules=None, layers=None, target_collector=None,
1774
+ forms=False):
1127
1775
  """Compute all the computed styles of all elements in ``html`` document.
1128
1776
 
1129
1777
  Do everything from finding author stylesheets to parsing and applying them.
@@ -1136,6 +1784,8 @@ def get_all_computed_styles(html, user_stylesheets=None, presentational_hints=Fa
1136
1784
  sheets = []
1137
1785
  if counter_style is None:
1138
1786
  counter_style = counters.CounterStyle()
1787
+ if font_config is None:
1788
+ font_config = FontConfiguration()
1139
1789
  for style in html._ua_counter_style():
1140
1790
  for key, value in style.items():
1141
1791
  counter_style[key] = value
@@ -1146,9 +1796,10 @@ def get_all_computed_styles(html, user_stylesheets=None, presentational_hints=Fa
1146
1796
  sheets.append((sheet, 'author', (0, 0, 0)))
1147
1797
  for sheet in find_stylesheets(
1148
1798
  html.wrapper_element, html.media_type, html.url_fetcher,
1149
- html.base_url, font_config, counter_style, page_rules):
1799
+ html.base_url, font_config, counter_style, color_profiles, page_rules,
1800
+ layers):
1150
1801
  sheets.append((sheet, 'author', None))
1151
1802
  for sheet in (user_stylesheets or []):
1152
1803
  sheets.append((sheet, 'user', None))
1153
1804
 
1154
- return StyleFor(html, sheets, presentational_hints, target_collector)
1805
+ return StyleFor(html, sheets, presentational_hints, font_config, target_collector)