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.
- weasyprint/__init__.py +14 -7
- weasyprint/__main__.py +19 -10
- weasyprint/anchors.py +4 -4
- weasyprint/css/__init__.py +723 -66
- weasyprint/css/computed_values.py +64 -175
- weasyprint/css/counters.py +1 -1
- weasyprint/css/functions.py +206 -0
- weasyprint/css/html5_ua.css +1 -0
- weasyprint/css/html5_ua_form.css +1 -1
- weasyprint/css/media_queries.py +3 -1
- weasyprint/css/properties.py +6 -2
- weasyprint/css/{utils.py → tokens.py} +306 -397
- weasyprint/css/units.py +91 -0
- weasyprint/css/validation/__init__.py +1 -1
- weasyprint/css/validation/descriptors.py +47 -19
- weasyprint/css/validation/expanders.py +7 -8
- weasyprint/css/validation/properties.py +341 -357
- weasyprint/document.py +19 -10
- weasyprint/draw/__init__.py +6 -7
- weasyprint/draw/border.py +3 -5
- weasyprint/draw/color.py +1 -1
- weasyprint/draw/text.py +59 -39
- weasyprint/formatting_structure/boxes.py +21 -3
- weasyprint/formatting_structure/build.py +2 -4
- weasyprint/images.py +68 -47
- weasyprint/layout/__init__.py +27 -24
- weasyprint/layout/absolute.py +3 -5
- weasyprint/layout/background.py +7 -7
- weasyprint/layout/block.py +140 -128
- weasyprint/layout/column.py +18 -24
- weasyprint/layout/flex.py +13 -5
- weasyprint/layout/float.py +4 -6
- weasyprint/layout/grid.py +284 -90
- weasyprint/layout/inline.py +114 -60
- weasyprint/layout/page.py +23 -12
- weasyprint/layout/percent.py +14 -10
- weasyprint/layout/preferred.py +45 -30
- weasyprint/layout/replaced.py +9 -6
- weasyprint/layout/table.py +8 -5
- weasyprint/pdf/__init__.py +46 -13
- weasyprint/pdf/anchors.py +1 -2
- weasyprint/pdf/fonts.py +126 -69
- weasyprint/pdf/metadata.py +36 -4
- weasyprint/pdf/pdfa.py +19 -3
- weasyprint/pdf/pdfx.py +83 -0
- weasyprint/pdf/stream.py +18 -1
- weasyprint/pdf/tags.py +6 -4
- weasyprint/svg/__init__.py +37 -21
- weasyprint/svg/images.py +11 -3
- weasyprint/svg/text.py +11 -2
- weasyprint/svg/utils.py +6 -3
- weasyprint/text/constants.py +1 -1
- weasyprint/text/ffi.py +4 -3
- weasyprint/text/fonts.py +12 -4
- weasyprint/text/line_break.py +101 -17
- weasyprint/urls.py +21 -4
- {weasyprint-66.0.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
- weasyprint-67.0.dist-info/RECORD +77 -0
- weasyprint-66.0.dist-info/RECORD +0 -74
- {weasyprint-66.0.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
- {weasyprint-66.0.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-66.0.dist-info → weasyprint-67.0.dist-info}/licenses/LICENSE +0 -0
weasyprint/css/__init__.py
CHANGED
|
@@ -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 ..
|
|
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 .
|
|
34
|
-
|
|
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,
|
|
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] =
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
592
|
-
token
|
|
593
|
-
token
|
|
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
|
-
|
|
597
|
-
|
|
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
|
|
621
|
+
return [] # endless recursion
|
|
600
622
|
else:
|
|
601
623
|
known_variables.add(variable_name)
|
|
602
|
-
default =
|
|
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
|
-
|
|
626
|
-
|
|
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 =
|
|
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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
1126
|
-
|
|
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)
|