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