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