weasyprint 65.1__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 +17 -7
- weasyprint/__main__.py +21 -10
- weasyprint/anchors.py +4 -4
- weasyprint/css/__init__.py +732 -67
- weasyprint/css/computed_values.py +65 -170
- weasyprint/css/counters.py +1 -1
- weasyprint/css/functions.py +206 -0
- weasyprint/css/html5_ua.css +3 -7
- weasyprint/css/html5_ua_form.css +2 -2
- 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 +20 -19
- weasyprint/draw/__init__.py +56 -63
- weasyprint/draw/border.py +121 -69
- weasyprint/draw/color.py +1 -1
- weasyprint/draw/text.py +60 -41
- weasyprint/formatting_structure/boxes.py +24 -5
- weasyprint/formatting_structure/build.py +33 -45
- weasyprint/images.py +76 -62
- weasyprint/layout/__init__.py +32 -26
- weasyprint/layout/absolute.py +7 -6
- weasyprint/layout/background.py +7 -7
- weasyprint/layout/block.py +195 -152
- weasyprint/layout/column.py +19 -24
- weasyprint/layout/flex.py +54 -26
- weasyprint/layout/float.py +12 -7
- weasyprint/layout/grid.py +284 -90
- weasyprint/layout/inline.py +121 -68
- weasyprint/layout/page.py +45 -12
- weasyprint/layout/percent.py +14 -10
- weasyprint/layout/preferred.py +105 -63
- weasyprint/layout/replaced.py +9 -6
- weasyprint/layout/table.py +16 -9
- weasyprint/pdf/__init__.py +58 -18
- weasyprint/pdf/anchors.py +3 -4
- weasyprint/pdf/fonts.py +126 -69
- weasyprint/pdf/metadata.py +36 -4
- weasyprint/pdf/pdfa.py +19 -3
- weasyprint/pdf/pdfua.py +7 -115
- weasyprint/pdf/pdfx.py +83 -0
- weasyprint/pdf/stream.py +57 -49
- weasyprint/pdf/tags.py +307 -0
- weasyprint/stacking.py +14 -15
- weasyprint/svg/__init__.py +59 -32
- weasyprint/svg/bounding_box.py +4 -2
- weasyprint/svg/defs.py +4 -9
- weasyprint/svg/images.py +11 -3
- weasyprint/svg/text.py +11 -2
- weasyprint/svg/utils.py +15 -8
- weasyprint/text/constants.py +1 -1
- weasyprint/text/ffi.py +4 -3
- weasyprint/text/fonts.py +13 -5
- weasyprint/text/line_break.py +146 -43
- weasyprint/urls.py +41 -13
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
- weasyprint-67.0.dist-info/RECORD +77 -0
- weasyprint/draw/stack.py +0 -13
- weasyprint-65.1.dist-info/RECORD +0 -74
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
- {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,48 +1,19 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""CSS tokens parsers."""
|
|
2
2
|
|
|
3
3
|
import functools
|
|
4
|
-
import math
|
|
5
4
|
from abc import ABC, abstractmethod
|
|
6
|
-
from
|
|
5
|
+
from math import e, inf, nan, pi
|
|
7
6
|
|
|
8
|
-
from tinycss2.
|
|
7
|
+
from tinycss2.ast import DimensionToken, IdentToken, NumberToken, PercentageToken
|
|
8
|
+
from tinycss2.color5 import parse_color
|
|
9
9
|
|
|
10
|
-
from .. import LOGGER
|
|
11
|
-
from ..urls import
|
|
10
|
+
from ..logger import LOGGER
|
|
11
|
+
from ..urls import get_url_tuple
|
|
12
|
+
from . import functions
|
|
13
|
+
from .functions import check_math
|
|
12
14
|
from .properties import Dimension
|
|
15
|
+
from .units import ANGLE_TO_RADIANS, LENGTH_UNITS, RESOLUTION_TO_DPPX
|
|
13
16
|
|
|
14
|
-
# https://drafts.csswg.org/css-values-3/#angles
|
|
15
|
-
# 1<unit> is this many radians.
|
|
16
|
-
ANGLE_TO_RADIANS = {
|
|
17
|
-
'rad': 1,
|
|
18
|
-
'turn': 2 * math.pi,
|
|
19
|
-
'deg': math.pi / 180,
|
|
20
|
-
'grad': math.pi / 200,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
# How many CSS pixels is one <unit>?
|
|
24
|
-
# https://www.w3.org/TR/CSS21/syndata.html#length-units
|
|
25
|
-
LENGTHS_TO_PIXELS = {
|
|
26
|
-
'px': 1,
|
|
27
|
-
'pt': 1. / 0.75,
|
|
28
|
-
'pc': 16., # LENGTHS_TO_PIXELS['pt'] * 12
|
|
29
|
-
'in': 96., # LENGTHS_TO_PIXELS['pt'] * 72
|
|
30
|
-
'cm': 96. / 2.54, # LENGTHS_TO_PIXELS['in'] / 2.54
|
|
31
|
-
'mm': 96. / 25.4, # LENGTHS_TO_PIXELS['in'] / 25.4
|
|
32
|
-
'q': 96. / 25.4 / 4, # LENGTHS_TO_PIXELS['mm'] / 4
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
# https://drafts.csswg.org/css-values/#resolution
|
|
36
|
-
RESOLUTION_TO_DPPX = {
|
|
37
|
-
'dppx': 1,
|
|
38
|
-
'dpi': 1 / LENGTHS_TO_PIXELS['in'],
|
|
39
|
-
'dpcm': 1 / LENGTHS_TO_PIXELS['cm'],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
# Sets of possible length units
|
|
43
|
-
LENGTH_UNITS = set(LENGTHS_TO_PIXELS) | set(['ex', 'em', 'ch', 'rem'])
|
|
44
|
-
|
|
45
|
-
# Constants about background positions
|
|
46
17
|
ZERO_PERCENT = Dimension(0, '%')
|
|
47
18
|
FIFTY_PERCENT = Dimension(50, '%')
|
|
48
19
|
HUNDRED_PERCENT = Dimension(100, '%')
|
|
@@ -54,14 +25,13 @@ BACKGROUND_POSITION_PERCENTAGES = {
|
|
|
54
25
|
'right': HUNDRED_PERCENT,
|
|
55
26
|
}
|
|
56
27
|
|
|
57
|
-
# Direction keywords used for gradients
|
|
58
28
|
DIRECTION_KEYWORDS = {
|
|
59
|
-
# ('angle', radians)
|
|
29
|
+
# ('angle', radians), 0 upwards, then clockwise.
|
|
60
30
|
('to', 'top'): ('angle', 0),
|
|
61
|
-
('to', 'right'): ('angle',
|
|
62
|
-
('to', 'bottom'): ('angle',
|
|
63
|
-
('to', 'left'): ('angle',
|
|
64
|
-
# ('corner', keyword)
|
|
31
|
+
('to', 'right'): ('angle', pi / 2),
|
|
32
|
+
('to', 'bottom'): ('angle', pi),
|
|
33
|
+
('to', 'left'): ('angle', pi * 3 / 2),
|
|
34
|
+
# ('corner', keyword).
|
|
65
35
|
('to', 'top', 'left'): ('corner', 'top_left'),
|
|
66
36
|
('to', 'left', 'top'): ('corner', 'top_left'),
|
|
67
37
|
('to', 'top', 'right'): ('corner', 'top_right'),
|
|
@@ -72,29 +42,23 @@ DIRECTION_KEYWORDS = {
|
|
|
72
42
|
('to', 'right', 'bottom'): ('corner', 'bottom_right'),
|
|
73
43
|
}
|
|
74
44
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'integer': ('number', 0),
|
|
81
|
-
'number': ('number', 0),
|
|
82
|
-
'%': ('number', 0),
|
|
83
|
-
}
|
|
84
|
-
for unit in LENGTH_UNITS:
|
|
85
|
-
ATTR_FALLBACKS[unit] = ('length', Dimension('0', unit))
|
|
86
|
-
for unit in ANGLE_TO_RADIANS:
|
|
87
|
-
ATTR_FALLBACKS[unit] = ('angle', Dimension('0', unit))
|
|
45
|
+
E = NumberToken(0, 0, e, None, 'e')
|
|
46
|
+
PI = NumberToken(0, 0, pi, None, 'π')
|
|
47
|
+
PLUS_INFINITY = NumberToken(0, 0, inf, None, '∞')
|
|
48
|
+
MINUS_INFINITY = NumberToken(0, 0, -inf, None, '-∞')
|
|
49
|
+
NAN = NumberToken(0, 0, nan, None, 'NaN')
|
|
88
50
|
|
|
89
51
|
|
|
90
52
|
class InvalidValues(ValueError): # noqa: N818
|
|
91
53
|
"""Invalid or unsupported values for a known CSS property."""
|
|
92
54
|
|
|
93
55
|
|
|
94
|
-
class
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
56
|
+
class PercentageInMath(ValueError): # noqa: N818
|
|
57
|
+
"""Percentage in math function without reference length."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class FontUnitInMath(ValueError): # noqa: N818
|
|
61
|
+
"""Font-relative unit in math function without reference style."""
|
|
98
62
|
|
|
99
63
|
|
|
100
64
|
class Pending(ABC):
|
|
@@ -118,136 +82,65 @@ class Pending(ABC):
|
|
|
118
82
|
# properties and expanders.
|
|
119
83
|
raise InvalidValues('no value')
|
|
120
84
|
return self.validate(tokens, wanted_key)
|
|
121
|
-
except InvalidValues as
|
|
85
|
+
except InvalidValues as exception:
|
|
122
86
|
if self._reported_error:
|
|
123
|
-
raise
|
|
87
|
+
raise exception
|
|
124
88
|
source_line = self.tokens[0].source_line
|
|
125
89
|
source_column = self.tokens[0].source_column
|
|
126
90
|
value = ' '.join(token.serialize() for token in tokens)
|
|
127
|
-
message =
|
|
91
|
+
message = exception.args[0] if exception.args else 'invalid value'
|
|
128
92
|
LOGGER.warning(
|
|
129
93
|
'Ignored `%s: %s` at %d:%d, %s.',
|
|
130
94
|
self.name, value, source_line, source_column, message)
|
|
131
95
|
self._reported_error = True
|
|
132
|
-
raise
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def split_on_comma(tokens):
|
|
136
|
-
"""Split a list of tokens on commas, ie ``LiteralToken(',')``.
|
|
137
|
-
|
|
138
|
-
Only "top-level" comma tokens are splitting points, not commas inside a
|
|
139
|
-
function or blocks.
|
|
140
|
-
|
|
141
|
-
"""
|
|
142
|
-
parts = []
|
|
143
|
-
this_part = []
|
|
144
|
-
for token in tokens:
|
|
145
|
-
if token.type == 'literal' and token.value == ',':
|
|
146
|
-
parts.append(this_part)
|
|
147
|
-
this_part = []
|
|
148
|
-
else:
|
|
149
|
-
this_part.append(token)
|
|
150
|
-
parts.append(this_part)
|
|
151
|
-
return tuple(parts)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def split_on_optional_comma(tokens):
|
|
155
|
-
"""Split a list of tokens on optional commas, ie ``LiteralToken(',')``."""
|
|
156
|
-
parts = []
|
|
157
|
-
for split_part in split_on_comma(tokens):
|
|
158
|
-
if not split_part:
|
|
159
|
-
# Happens when there's a comma at the beginning, at the end, or
|
|
160
|
-
# when two commas are next to each other.
|
|
161
|
-
return
|
|
162
|
-
for part in split_part:
|
|
163
|
-
parts.append(part)
|
|
164
|
-
return parts
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def remove_whitespace(tokens):
|
|
168
|
-
"""Remove any top-level whitespace and comments in a token list."""
|
|
169
|
-
return tuple(
|
|
170
|
-
token for token in tokens
|
|
171
|
-
if token.type not in ('whitespace', 'comment'))
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def safe_urljoin(base_url, url):
|
|
175
|
-
if url_is_absolute(url):
|
|
176
|
-
return iri_to_uri(url)
|
|
177
|
-
elif base_url:
|
|
178
|
-
return iri_to_uri(urljoin(base_url, url))
|
|
179
|
-
else:
|
|
180
|
-
raise InvalidValues(
|
|
181
|
-
f'Relative URI reference without a base URI: {url!r}')
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def comma_separated_list(function):
|
|
185
|
-
"""Decorator for validators that accept a comma separated list."""
|
|
186
|
-
@functools.wraps(function)
|
|
187
|
-
def wrapper(tokens, *args):
|
|
188
|
-
results = []
|
|
189
|
-
for part in split_on_comma(tokens):
|
|
190
|
-
result = function(remove_whitespace(part), *args)
|
|
191
|
-
if result is None:
|
|
192
|
-
return None
|
|
193
|
-
results.append(result)
|
|
194
|
-
return tuple(results)
|
|
195
|
-
wrapper.single_value = function
|
|
196
|
-
return wrapper
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def get_keyword(token):
|
|
200
|
-
"""If ``token`` is a keyword, return its lowercase name.
|
|
201
|
-
|
|
202
|
-
Otherwise return ``None``.
|
|
203
|
-
|
|
204
|
-
"""
|
|
205
|
-
if token.type == 'ident':
|
|
206
|
-
return token.lower_value
|
|
96
|
+
raise exception
|
|
207
97
|
|
|
208
98
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Otherwise return ``None``.
|
|
213
|
-
|
|
214
|
-
"""
|
|
215
|
-
if token.type == 'ident':
|
|
216
|
-
return token.value
|
|
99
|
+
def parse_color_hint(tokens):
|
|
100
|
+
if len(tokens) == 1:
|
|
101
|
+
return get_length(tokens[0], percentage=True)
|
|
217
102
|
|
|
218
103
|
|
|
219
|
-
def
|
|
220
|
-
|
|
104
|
+
def parse_color_stop(tokens):
|
|
105
|
+
if len(tokens) == 1:
|
|
106
|
+
color = parse_color(tokens[0])
|
|
107
|
+
if color == 'currentcolor':
|
|
108
|
+
# TODO: return the current color instead
|
|
109
|
+
return parse_color('black'), None
|
|
110
|
+
if color is not None:
|
|
111
|
+
return color, None
|
|
112
|
+
elif len(tokens) == 2:
|
|
113
|
+
color = parse_color(tokens[0])
|
|
114
|
+
position = get_length(tokens[1], negative=True, percentage=True)
|
|
115
|
+
if color is not None and position is not None:
|
|
116
|
+
return color, position
|
|
117
|
+
raise InvalidValues
|
|
221
118
|
|
|
222
|
-
Otherwise return ``None``.
|
|
223
119
|
|
|
224
|
-
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
if token.type == 'ident':
|
|
228
|
-
return token.lower_value
|
|
120
|
+
def parse_color_stops_and_hints(color_stops_hints):
|
|
121
|
+
if not color_stops_hints:
|
|
122
|
+
raise InvalidValues
|
|
229
123
|
|
|
124
|
+
color_stops = [parse_color_stop(color_stops_hints[0])]
|
|
125
|
+
color_hints = []
|
|
126
|
+
previous_was_color_stop = True
|
|
230
127
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
128
|
+
for tokens in color_stops_hints[1:]:
|
|
129
|
+
if hint := parse_color_hint(tokens):
|
|
130
|
+
color_hints.append(hint)
|
|
131
|
+
previous_was_color_stop = False
|
|
132
|
+
elif previous_was_color_stop:
|
|
133
|
+
color_hints.append(FIFTY_PERCENT)
|
|
134
|
+
color_stops.append(parse_color_stop(tokens))
|
|
135
|
+
previous_was_color_stop = True
|
|
136
|
+
else:
|
|
137
|
+
color_stops.append(parse_color_stop(tokens))
|
|
138
|
+
previous_was_color_stop = True
|
|
240
139
|
|
|
140
|
+
if not previous_was_color_stop:
|
|
141
|
+
raise InvalidValues
|
|
241
142
|
|
|
242
|
-
|
|
243
|
-
"""Decorator for validators that only accept a single token."""
|
|
244
|
-
@functools.wraps(function)
|
|
245
|
-
def single_token_validator(tokens, *args):
|
|
246
|
-
"""Validate a property whose token is single."""
|
|
247
|
-
if len(tokens) == 1:
|
|
248
|
-
return function(tokens[0], *args)
|
|
249
|
-
single_token_validator.__func__ = function
|
|
250
|
-
return single_token_validator
|
|
143
|
+
return color_stops, color_hints
|
|
251
144
|
|
|
252
145
|
|
|
253
146
|
def parse_linear_gradient_parameters(arguments):
|
|
@@ -260,13 +153,13 @@ def parse_linear_gradient_parameters(arguments):
|
|
|
260
153
|
result = DIRECTION_KEYWORDS.get(tuple(map(get_keyword, first_arg)))
|
|
261
154
|
if result is not None:
|
|
262
155
|
return result, arguments[1:]
|
|
263
|
-
return ('angle',
|
|
156
|
+
return ('angle', pi), arguments # Default direction is 'to bottom'
|
|
264
157
|
|
|
265
158
|
|
|
266
159
|
def parse_2d_position(tokens):
|
|
267
160
|
"""Common syntax of background-position and transform-origin."""
|
|
268
161
|
if len(tokens) == 1:
|
|
269
|
-
tokens = [tokens[0],
|
|
162
|
+
tokens = [tokens[0], IdentToken(0, 0, 'center')]
|
|
270
163
|
elif len(tokens) != 2:
|
|
271
164
|
return None
|
|
272
165
|
|
|
@@ -383,166 +276,93 @@ def parse_radial_gradient_parameters(arguments):
|
|
|
383
276
|
arguments[1:])
|
|
384
277
|
|
|
385
278
|
|
|
386
|
-
def
|
|
387
|
-
|
|
388
|
-
color = parse_color(tokens[0])
|
|
389
|
-
if color == 'currentcolor':
|
|
390
|
-
# TODO: return the current color instead
|
|
391
|
-
return parse_color('black'), None
|
|
392
|
-
if color is not None:
|
|
393
|
-
return color, None
|
|
394
|
-
elif len(tokens) == 2:
|
|
395
|
-
color = parse_color(tokens[0])
|
|
396
|
-
position = get_length(tokens[1], negative=True, percentage=True)
|
|
397
|
-
if color is not None and position is not None:
|
|
398
|
-
return color, position
|
|
399
|
-
raise InvalidValues
|
|
279
|
+
def split_on_comma(tokens):
|
|
280
|
+
"""Split a list of tokens on commas, ie ``LiteralToken(',')``.
|
|
400
281
|
|
|
282
|
+
Only "top-level" comma tokens are splitting points, not commas inside a
|
|
283
|
+
function or blocks.
|
|
401
284
|
|
|
402
|
-
|
|
403
|
-
|
|
285
|
+
"""
|
|
286
|
+
parts = []
|
|
287
|
+
this_part = []
|
|
288
|
+
for token in tokens:
|
|
289
|
+
if token.type == 'literal' and token.value == ',':
|
|
290
|
+
parts.append(this_part)
|
|
291
|
+
this_part = []
|
|
292
|
+
else:
|
|
293
|
+
this_part.append(token)
|
|
294
|
+
parts.append(this_part)
|
|
295
|
+
return tuple(parts)
|
|
404
296
|
|
|
405
|
-
Return ``(name, args)`` if the given token is a function with comma- or
|
|
406
|
-
space-separated arguments. Return ``None`` otherwise.
|
|
407
297
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
298
|
+
def remove_whitespace(tokens):
|
|
299
|
+
"""Remove any top-level whitespace and comments in a token list."""
|
|
300
|
+
return tuple(
|
|
301
|
+
token for token in tokens
|
|
302
|
+
if token.type not in ('whitespace', 'comment'))
|
|
411
303
|
|
|
412
|
-
content = list(remove_whitespace(function_token.arguments))
|
|
413
|
-
arguments = []
|
|
414
|
-
last_is_comma = False
|
|
415
|
-
while content:
|
|
416
|
-
token = content.pop(0)
|
|
417
|
-
is_comma = token.type == 'literal' and token.value == ','
|
|
418
|
-
if last_is_comma and is_comma:
|
|
419
|
-
return
|
|
420
|
-
if is_comma:
|
|
421
|
-
last_is_comma = True
|
|
422
|
-
else:
|
|
423
|
-
last_is_comma = False
|
|
424
|
-
if token.type == 'function':
|
|
425
|
-
argument_function = parse_function(token)
|
|
426
|
-
if argument_function is None:
|
|
427
|
-
return
|
|
428
|
-
arguments.append(token)
|
|
429
|
-
if last_is_comma:
|
|
430
|
-
return
|
|
431
|
-
return function_token.lower_name, arguments
|
|
432
304
|
|
|
305
|
+
def get_keyword(token):
|
|
306
|
+
"""If ``token`` is a keyword, return its lowercase name.
|
|
433
307
|
|
|
434
|
-
|
|
435
|
-
function = parse_function(token)
|
|
436
|
-
if function is None:
|
|
437
|
-
return
|
|
438
|
-
name, args = function
|
|
439
|
-
if name == 'attr' and len(args) in (1, 2, 3):
|
|
440
|
-
if args[0].type != 'ident':
|
|
441
|
-
return
|
|
442
|
-
attr_name = args[0].value
|
|
443
|
-
if len(args) == 1:
|
|
444
|
-
type_or_unit = 'string'
|
|
445
|
-
fallback = ''
|
|
446
|
-
else:
|
|
447
|
-
if args[1].type != 'ident':
|
|
448
|
-
return
|
|
449
|
-
type_or_unit = args[1].value
|
|
450
|
-
if type_or_unit not in ATTR_FALLBACKS:
|
|
451
|
-
return
|
|
452
|
-
if len(args) == 2:
|
|
453
|
-
fallback = ATTR_FALLBACKS[type_or_unit]
|
|
454
|
-
else:
|
|
455
|
-
fallback_type = args[2].type
|
|
456
|
-
if fallback_type == 'string':
|
|
457
|
-
fallback = args[2].value
|
|
458
|
-
else:
|
|
459
|
-
# TODO: handle other fallback types
|
|
460
|
-
return
|
|
461
|
-
if allowed_type in (None, type_or_unit):
|
|
462
|
-
return ('attr()', (attr_name, type_or_unit, fallback))
|
|
308
|
+
Otherwise return ``None``.
|
|
463
309
|
|
|
310
|
+
"""
|
|
311
|
+
if token.type == 'ident':
|
|
312
|
+
return token.lower_value
|
|
464
313
|
|
|
465
|
-
def check_counter_function(token, allowed_type=None):
|
|
466
|
-
from .validation.properties import list_style_type
|
|
467
314
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return
|
|
471
|
-
name, args = function
|
|
472
|
-
arguments = []
|
|
473
|
-
if (name == 'counter' and len(args) in (1, 2)) or (
|
|
474
|
-
name == 'counters' and len(args) in (2, 3)):
|
|
475
|
-
ident = args.pop(0)
|
|
476
|
-
if ident.type != 'ident':
|
|
477
|
-
return
|
|
478
|
-
arguments.append(ident.value)
|
|
315
|
+
def get_custom_ident(token):
|
|
316
|
+
"""If ``token`` is a keyword, return its name.
|
|
479
317
|
|
|
480
|
-
|
|
481
|
-
string = args.pop(0)
|
|
482
|
-
if string.type != 'string':
|
|
483
|
-
return
|
|
484
|
-
arguments.append(string.value)
|
|
318
|
+
Otherwise return ``None``.
|
|
485
319
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return
|
|
490
|
-
arguments.append(counter_style)
|
|
491
|
-
else:
|
|
492
|
-
arguments.append('decimal')
|
|
320
|
+
"""
|
|
321
|
+
if token.type == 'ident':
|
|
322
|
+
return token.value
|
|
493
323
|
|
|
494
|
-
return (f'{name}()', tuple(arguments))
|
|
495
324
|
|
|
325
|
+
def get_single_keyword(tokens):
|
|
326
|
+
"""If ``values`` is a 1-element list of keywords, return its name.
|
|
496
327
|
|
|
497
|
-
|
|
498
|
-
function = parse_function(token)
|
|
499
|
-
if function is None:
|
|
500
|
-
return
|
|
501
|
-
name, args = function
|
|
502
|
-
if name == 'content':
|
|
503
|
-
if len(args) == 0:
|
|
504
|
-
return ('content()', 'text')
|
|
505
|
-
elif len(args) == 1:
|
|
506
|
-
ident = args.pop(0)
|
|
507
|
-
if ident.type == 'ident' and ident.lower_value in (
|
|
508
|
-
'text', 'before', 'after', 'first-letter', 'marker'):
|
|
509
|
-
return ('content()', ident.lower_value)
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
def check_string_or_element_function(string_or_element, token):
|
|
513
|
-
function = parse_function(token)
|
|
514
|
-
if function is None:
|
|
515
|
-
return
|
|
516
|
-
name, args = function
|
|
517
|
-
if name == string_or_element and len(args) in (1, 2):
|
|
518
|
-
custom_ident = args.pop(0)
|
|
519
|
-
if custom_ident.type != 'ident':
|
|
520
|
-
return
|
|
521
|
-
custom_ident = custom_ident.value
|
|
328
|
+
Otherwise return ``None``.
|
|
522
329
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
ident = ident.lower_value
|
|
529
|
-
else:
|
|
530
|
-
ident = 'first'
|
|
330
|
+
"""
|
|
331
|
+
if len(tokens) == 1:
|
|
332
|
+
token = tokens[0]
|
|
333
|
+
if token.type == 'ident':
|
|
334
|
+
return token.lower_value
|
|
531
335
|
|
|
532
|
-
return (f'{string_or_element}()', (custom_ident, ident))
|
|
533
336
|
|
|
337
|
+
def get_number(token, negative=True, integer=False):
|
|
338
|
+
"""Parse a <number> token."""
|
|
339
|
+
from . import resolve_math
|
|
534
340
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
341
|
+
if check_math(token):
|
|
342
|
+
try:
|
|
343
|
+
resolved = resolve_math(token)
|
|
344
|
+
except (PercentageInMath, FontUnitInMath):
|
|
345
|
+
return
|
|
346
|
+
else:
|
|
347
|
+
if resolved is None:
|
|
348
|
+
return
|
|
349
|
+
if resolved.type != 'number':
|
|
350
|
+
return
|
|
351
|
+
value = resolved.value
|
|
352
|
+
if not negative and value < 0:
|
|
353
|
+
value = 0
|
|
354
|
+
if integer:
|
|
355
|
+
# TODO: always round x.5 to +inf, see
|
|
356
|
+
# https://drafts.csswg.org/css-values-4/#combine-integers.
|
|
357
|
+
value = round(value)
|
|
358
|
+
return Dimension(value, None)
|
|
359
|
+
elif token.type == 'number':
|
|
360
|
+
if integer:
|
|
361
|
+
if token.int_value is not None:
|
|
362
|
+
if negative or token.int_value >= 0:
|
|
363
|
+
return Dimension(token.int_value, None)
|
|
364
|
+
elif negative or token.value >= 0:
|
|
365
|
+
return Dimension(token.value, None)
|
|
546
366
|
|
|
547
367
|
|
|
548
368
|
def get_string(token):
|
|
@@ -551,39 +371,83 @@ def get_string(token):
|
|
|
551
371
|
return ('string', token.value)
|
|
552
372
|
if token.type == 'function':
|
|
553
373
|
if token.name == 'attr':
|
|
554
|
-
return
|
|
374
|
+
return functions.check_attr(token, 'string')
|
|
555
375
|
elif token.name in ('counter', 'counters'):
|
|
556
|
-
return
|
|
376
|
+
return functions.check_counter(token)
|
|
557
377
|
elif token.name == 'content':
|
|
558
|
-
return
|
|
378
|
+
return functions.check_content(token)
|
|
559
379
|
elif token.name == 'string':
|
|
560
|
-
return
|
|
380
|
+
return functions.check_string_or_element('string', token)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def get_percentage(token, negative=True):
|
|
384
|
+
"""Parse a <percentage> token."""
|
|
385
|
+
from . import resolve_math
|
|
386
|
+
|
|
387
|
+
if check_math(token):
|
|
388
|
+
try:
|
|
389
|
+
token = resolve_math(token) or token
|
|
390
|
+
except (PercentageInMath, FontUnitInMath):
|
|
391
|
+
return
|
|
392
|
+
else:
|
|
393
|
+
# Range clamp.
|
|
394
|
+
if not negative:
|
|
395
|
+
token.value = max(0, token.value)
|
|
396
|
+
if token.type == 'percentage' and (negative or token.value >= 0):
|
|
397
|
+
return Dimension(token.value, '%')
|
|
561
398
|
|
|
562
399
|
|
|
563
400
|
def get_length(token, negative=True, percentage=False):
|
|
564
401
|
"""Parse a <length> token."""
|
|
402
|
+
from . import resolve_math
|
|
403
|
+
|
|
404
|
+
if check_math(token):
|
|
405
|
+
try:
|
|
406
|
+
token = resolve_math(token) or token
|
|
407
|
+
except PercentageInMath:
|
|
408
|
+
# PercentageInMath is raised in priority to help discarding percentages for
|
|
409
|
+
# properties that don’t allow them.
|
|
410
|
+
return token if percentage else None
|
|
411
|
+
except FontUnitInMath:
|
|
412
|
+
return token
|
|
413
|
+
else:
|
|
414
|
+
# Range clamp.
|
|
415
|
+
if not negative and token.type not in ('function', 'number'):
|
|
416
|
+
token.value = max(0, token.value)
|
|
565
417
|
if percentage and token.type == 'percentage':
|
|
566
418
|
if negative or token.value >= 0:
|
|
567
419
|
return Dimension(token.value, '%')
|
|
568
|
-
if token.type == 'dimension' and token.unit in LENGTH_UNITS:
|
|
420
|
+
if token.type == 'dimension' and token.unit.lower() in LENGTH_UNITS:
|
|
569
421
|
if negative or token.value >= 0:
|
|
570
|
-
return Dimension(token.value, token.unit)
|
|
422
|
+
return Dimension(token.value, token.unit.lower())
|
|
571
423
|
if token.type == 'number' and token.value == 0:
|
|
572
424
|
return Dimension(0, None)
|
|
573
425
|
|
|
574
426
|
|
|
575
427
|
def get_angle(token):
|
|
576
428
|
"""Parse an <angle> token in radians."""
|
|
429
|
+
from . import resolve_math
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
token = resolve_math(token) or token
|
|
433
|
+
except (PercentageInMath, FontUnitInMath):
|
|
434
|
+
return
|
|
577
435
|
if token.type == 'dimension':
|
|
578
|
-
factor = ANGLE_TO_RADIANS.get(token.unit)
|
|
436
|
+
factor = ANGLE_TO_RADIANS.get(token.unit.lower())
|
|
579
437
|
if factor is not None:
|
|
580
438
|
return token.value * factor
|
|
581
439
|
|
|
582
440
|
|
|
583
441
|
def get_resolution(token):
|
|
584
|
-
"""Parse a <resolution> token in
|
|
442
|
+
"""Parse a <resolution> token in dppx."""
|
|
443
|
+
from . import resolve_math
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
token = resolve_math(token) or token
|
|
447
|
+
except (PercentageInMath, FontUnitInMath):
|
|
448
|
+
return
|
|
585
449
|
if token.type == 'dimension':
|
|
586
|
-
factor = RESOLUTION_TO_DPPX.get(token.unit)
|
|
450
|
+
factor = RESOLUTION_TO_DPPX.get(token.unit.lower())
|
|
587
451
|
if factor is not None:
|
|
588
452
|
return token.value * factor
|
|
589
453
|
|
|
@@ -592,22 +456,21 @@ def get_image(token, base_url):
|
|
|
592
456
|
"""Parse an <image> token."""
|
|
593
457
|
from ..images import LinearGradient, RadialGradient
|
|
594
458
|
|
|
595
|
-
parsed_url
|
|
596
|
-
if parsed_url:
|
|
459
|
+
if parsed_url := get_url(token, base_url):
|
|
597
460
|
assert parsed_url[0] == 'url'
|
|
598
461
|
if parsed_url[1][0] == 'external':
|
|
599
462
|
return 'url', parsed_url[1][1]
|
|
600
|
-
|
|
463
|
+
function = functions.Function(token)
|
|
464
|
+
arguments = function.split_comma(single_tokens=False)
|
|
465
|
+
if not arguments:
|
|
601
466
|
return
|
|
602
|
-
|
|
603
|
-
name
|
|
604
|
-
if name in ('linear-gradient', 'repeating-linear-gradient'):
|
|
467
|
+
repeating = function.name.startswith('repeating-')
|
|
468
|
+
if function.name in ('linear-gradient', 'repeating-linear-gradient'):
|
|
605
469
|
direction, color_stops = parse_linear_gradient_parameters(arguments)
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
elif name in ('radial-gradient', 'repeating-radial-gradient'):
|
|
470
|
+
color_stops, color_hints = parse_color_stops_and_hints(color_stops)
|
|
471
|
+
return 'linear-gradient', LinearGradient(
|
|
472
|
+
color_stops, direction, repeating, color_hints)
|
|
473
|
+
elif function.name in ('radial-gradient', 'repeating-radial-gradient'):
|
|
611
474
|
result = parse_radial_gradient_parameters(arguments)
|
|
612
475
|
if result is not None:
|
|
613
476
|
shape, size, position, color_stops = result
|
|
@@ -616,30 +479,31 @@ def get_image(token, base_url):
|
|
|
616
479
|
size = 'keyword', 'farthest-corner'
|
|
617
480
|
position = 'left', FIFTY_PERCENT, 'top', FIFTY_PERCENT
|
|
618
481
|
color_stops = arguments
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
shape, size, position, 'repeating' in name)
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
def _get_url_tuple(string, base_url):
|
|
626
|
-
if string.startswith('#'):
|
|
627
|
-
return ('url', ('internal', unquote(string[1:])))
|
|
628
|
-
else:
|
|
629
|
-
return ('url', ('external', safe_urljoin(base_url, string)))
|
|
482
|
+
color_stops, color_hints = parse_color_stops_and_hints(color_stops)
|
|
483
|
+
return 'radial-gradient', RadialGradient(
|
|
484
|
+
color_stops, shape, size, position, repeating, color_hints)
|
|
630
485
|
|
|
631
486
|
|
|
632
487
|
def get_url(token, base_url):
|
|
633
488
|
"""Parse an <url> token."""
|
|
634
489
|
if token.type == 'url':
|
|
635
|
-
|
|
490
|
+
url = get_url_tuple(token.value, base_url)
|
|
636
491
|
elif token.type == 'function':
|
|
637
492
|
if token.name == 'attr':
|
|
638
|
-
return
|
|
493
|
+
return functions.check_attr(token, 'url')
|
|
639
494
|
elif token.name == 'url' and len(token.arguments) in (1, 2):
|
|
640
495
|
# Ignore url modifiers
|
|
641
496
|
# See https://drafts.csswg.org/css-values-3/#urls
|
|
642
|
-
|
|
497
|
+
url = get_url_tuple(token.arguments[0].value, base_url)
|
|
498
|
+
else:
|
|
499
|
+
return
|
|
500
|
+
else:
|
|
501
|
+
return
|
|
502
|
+
|
|
503
|
+
if url is None:
|
|
504
|
+
raise InvalidValues(f'Relative URI reference without a base URI: {url!r}')
|
|
505
|
+
|
|
506
|
+
return ('url', url)
|
|
643
507
|
|
|
644
508
|
|
|
645
509
|
def get_quote(token):
|
|
@@ -653,29 +517,23 @@ def get_quote(token):
|
|
|
653
517
|
|
|
654
518
|
def get_target(token, base_url):
|
|
655
519
|
"""Parse a <target> token."""
|
|
656
|
-
function =
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
args = split_on_optional_comma(args)
|
|
661
|
-
if not args:
|
|
662
|
-
return
|
|
663
|
-
|
|
664
|
-
if name == 'target-counter':
|
|
665
|
-
if len(args) not in (2, 3):
|
|
520
|
+
function = functions.Function(token)
|
|
521
|
+
arguments = function.split_comma()
|
|
522
|
+
if function.name == 'target-counter':
|
|
523
|
+
if len(arguments) not in (2, 3):
|
|
666
524
|
return
|
|
667
|
-
elif name == 'target-counters':
|
|
668
|
-
if len(
|
|
525
|
+
elif function.name == 'target-counters':
|
|
526
|
+
if len(arguments) not in (3, 4):
|
|
669
527
|
return
|
|
670
|
-
elif name == 'target-text':
|
|
671
|
-
if len(
|
|
528
|
+
elif function.name == 'target-text':
|
|
529
|
+
if len(arguments) not in (1, 2):
|
|
672
530
|
return
|
|
673
531
|
else:
|
|
674
532
|
return
|
|
675
533
|
|
|
676
534
|
values = []
|
|
677
535
|
|
|
678
|
-
link =
|
|
536
|
+
link = arguments.pop(0)
|
|
679
537
|
string_link = get_string(link)
|
|
680
538
|
if string_link is None:
|
|
681
539
|
url = get_url(link, base_url)
|
|
@@ -685,54 +543,49 @@ def get_target(token, base_url):
|
|
|
685
543
|
else:
|
|
686
544
|
values.append(string_link)
|
|
687
545
|
|
|
688
|
-
if name.startswith('target-counter'):
|
|
689
|
-
|
|
690
|
-
return
|
|
691
|
-
|
|
692
|
-
ident = args.pop(0)
|
|
546
|
+
if function.name.startswith('target-counter'):
|
|
547
|
+
ident = arguments.pop(0)
|
|
693
548
|
if ident.type != 'ident':
|
|
694
549
|
return
|
|
695
550
|
values.append(ident.value)
|
|
696
551
|
|
|
697
|
-
if name == 'target-counters':
|
|
698
|
-
string = get_string(
|
|
552
|
+
if function.name == 'target-counters':
|
|
553
|
+
string = get_string(arguments.pop(0))
|
|
699
554
|
if string is None:
|
|
700
555
|
return
|
|
701
556
|
values.append(string)
|
|
702
557
|
|
|
703
|
-
if
|
|
704
|
-
counter_style = get_keyword(
|
|
558
|
+
if arguments:
|
|
559
|
+
counter_style = get_keyword(arguments.pop(0))
|
|
705
560
|
else:
|
|
706
561
|
counter_style = 'decimal'
|
|
707
562
|
values.append(counter_style)
|
|
708
563
|
else:
|
|
709
|
-
if
|
|
710
|
-
content = get_keyword(
|
|
564
|
+
if arguments:
|
|
565
|
+
content = get_keyword(arguments.pop(0))
|
|
711
566
|
if content not in ('content', 'before', 'after', 'first-letter'):
|
|
712
567
|
return
|
|
713
568
|
else:
|
|
714
569
|
content = 'content'
|
|
715
570
|
values.append(content)
|
|
716
571
|
|
|
717
|
-
return (f'{name}()', tuple(values))
|
|
572
|
+
return (f'{function.name}()', tuple(values))
|
|
718
573
|
|
|
719
574
|
|
|
720
575
|
def get_content_list(tokens, base_url):
|
|
721
576
|
"""Parse <content-list> tokens."""
|
|
722
577
|
# See https://www.w3.org/TR/css-content-3/#typedef-content-list
|
|
723
|
-
parsed_tokens = [
|
|
724
|
-
get_content_list_token(token, base_url) for token in tokens]
|
|
578
|
+
parsed_tokens = [get_content_list_token(token, base_url) for token in tokens]
|
|
725
579
|
if None not in parsed_tokens:
|
|
726
580
|
return parsed_tokens
|
|
727
581
|
|
|
728
582
|
|
|
729
583
|
def get_content_list_token(token, base_url):
|
|
730
584
|
"""Parse one of the <content-list> tokens."""
|
|
731
|
-
# See https://
|
|
585
|
+
# See https://drafts.csswg.org/css-content-3/#content-values.
|
|
732
586
|
|
|
733
587
|
# <string>
|
|
734
|
-
string
|
|
735
|
-
if string is not None:
|
|
588
|
+
if (string := get_string(token)) is not None:
|
|
736
589
|
return string
|
|
737
590
|
|
|
738
591
|
# contents
|
|
@@ -740,30 +593,25 @@ def get_content_list_token(token, base_url):
|
|
|
740
593
|
return ('content()', 'text')
|
|
741
594
|
|
|
742
595
|
# <uri>
|
|
743
|
-
url
|
|
744
|
-
if url is not None:
|
|
596
|
+
if (url := get_url(token, base_url)) is not None:
|
|
745
597
|
return url
|
|
746
598
|
|
|
747
599
|
# <quote>
|
|
748
|
-
quote
|
|
749
|
-
if quote is not None:
|
|
600
|
+
if (quote := get_quote(token)) is not None:
|
|
750
601
|
return ('quote', quote)
|
|
751
602
|
|
|
752
603
|
# <target>
|
|
753
|
-
target
|
|
754
|
-
if target is not None:
|
|
604
|
+
if (target := get_target(token, base_url)) is not None:
|
|
755
605
|
return target
|
|
756
606
|
|
|
757
|
-
function =
|
|
758
|
-
|
|
759
|
-
return
|
|
760
|
-
name, args = function
|
|
607
|
+
function = functions.Function(token)
|
|
608
|
+
arguments = function.split_comma()
|
|
761
609
|
|
|
762
610
|
# <leader()>
|
|
763
|
-
if name == 'leader':
|
|
764
|
-
if len(
|
|
611
|
+
if function.name == 'leader':
|
|
612
|
+
if len(arguments) != 1:
|
|
765
613
|
return
|
|
766
|
-
arg, =
|
|
614
|
+
arg, = arguments
|
|
767
615
|
if arg.type == 'ident':
|
|
768
616
|
if arg.value == 'dotted':
|
|
769
617
|
string = '.'
|
|
@@ -778,5 +626,66 @@ def get_content_list_token(token, base_url):
|
|
|
778
626
|
return ('leader()', ('string', string))
|
|
779
627
|
|
|
780
628
|
# <element()>
|
|
781
|
-
elif name == 'element':
|
|
782
|
-
return
|
|
629
|
+
elif function.name == 'element':
|
|
630
|
+
return functions.check_string_or_element('element', token)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def single_keyword(function):
|
|
634
|
+
"""Decorator for validators that only accept a single keyword."""
|
|
635
|
+
@functools.wraps(function)
|
|
636
|
+
def keyword_validator(tokens):
|
|
637
|
+
"""Wrap a validator to call get_single_keyword on tokens."""
|
|
638
|
+
keyword = get_single_keyword(tokens)
|
|
639
|
+
if function(keyword):
|
|
640
|
+
return keyword
|
|
641
|
+
return keyword_validator
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def single_token(function):
|
|
645
|
+
"""Decorator for validators that only accept a single token."""
|
|
646
|
+
@functools.wraps(function)
|
|
647
|
+
def single_token_validator(tokens, *args):
|
|
648
|
+
"""Validate a property whose token is single."""
|
|
649
|
+
if len(tokens) == 1:
|
|
650
|
+
return function(tokens[0], *args)
|
|
651
|
+
single_token_validator.__func__ = function
|
|
652
|
+
return single_token_validator
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def comma_separated_list(function):
|
|
656
|
+
"""Decorator for validators that accept a comma separated list."""
|
|
657
|
+
@functools.wraps(function)
|
|
658
|
+
def wrapper(tokens, *args):
|
|
659
|
+
results = []
|
|
660
|
+
for part in split_on_comma(tokens):
|
|
661
|
+
result = function(remove_whitespace(part), *args)
|
|
662
|
+
if result is None:
|
|
663
|
+
return None
|
|
664
|
+
results.append(result)
|
|
665
|
+
return tuple(results)
|
|
666
|
+
wrapper.single_value = function
|
|
667
|
+
return wrapper
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def tokenize(item, function=None, unit=None):
|
|
671
|
+
"""Transform a computed value result into a token."""
|
|
672
|
+
if isinstance(item, (DimensionToken, Dimension)):
|
|
673
|
+
value = function(item.value) if function else item.value
|
|
674
|
+
return DimensionToken(0, 0, value, None, str(value), item.unit.lower())
|
|
675
|
+
elif isinstance(item, PercentageToken):
|
|
676
|
+
value = function(item.value) if function else item.value
|
|
677
|
+
return PercentageToken(0, 0, value, None, str(value))
|
|
678
|
+
elif isinstance(item, (NumberToken, int, float)):
|
|
679
|
+
if isinstance(item, NumberToken):
|
|
680
|
+
value = item.value
|
|
681
|
+
else:
|
|
682
|
+
value = item
|
|
683
|
+
value = function(value) if function else value
|
|
684
|
+
int_value = round(value) if float(value).is_integer() else None
|
|
685
|
+
representation = str(int_value if float(value).is_integer() else value)
|
|
686
|
+
if unit is None:
|
|
687
|
+
return NumberToken(0, 0, value, int_value, representation)
|
|
688
|
+
elif unit == '%':
|
|
689
|
+
return PercentageToken(0, 0, value, int_value, representation)
|
|
690
|
+
else:
|
|
691
|
+
return DimensionToken(0, 0, value, int_value, representation, unit)
|