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,16 +1,16 @@
|
|
|
1
1
|
"""Convert specified property values into computed values."""
|
|
2
2
|
|
|
3
3
|
from math import pi
|
|
4
|
-
from urllib.parse import unquote
|
|
5
4
|
|
|
6
|
-
from tinycss2.
|
|
5
|
+
from tinycss2.color5 import parse_color
|
|
7
6
|
|
|
8
7
|
from ..logger import LOGGER
|
|
9
|
-
from ..text.
|
|
10
|
-
from ..
|
|
11
|
-
from
|
|
8
|
+
from ..text.line_break import strut
|
|
9
|
+
from ..urls import get_link_attribute, get_url_tuple
|
|
10
|
+
from .functions import check_math
|
|
12
11
|
from .properties import INITIAL_VALUES, ZERO_PIXELS, Dimension
|
|
13
|
-
from .
|
|
12
|
+
from .units import ANGLE_TO_RADIANS, LENGTH_UNITS, to_pixels
|
|
13
|
+
from .validation import validate_non_shorthand
|
|
14
14
|
|
|
15
15
|
# Value in pixels of font-size for <absolute-size> keywords: 12pt (16px) for
|
|
16
16
|
# medium, and scaling factors given in CSS3 for others:
|
|
@@ -122,35 +122,13 @@ assert all(width.value < height.value for width, height in PAGE_SIZES.values())
|
|
|
122
122
|
|
|
123
123
|
INITIAL_PAGE_SIZE = PAGE_SIZES['a4']
|
|
124
124
|
INITIAL_VALUES['size'] = tuple(
|
|
125
|
-
size
|
|
125
|
+
to_pixels(size, None, 'size') for size in INITIAL_PAGE_SIZE)
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
# Maps property names to functions returning the computed values
|
|
129
129
|
COMPUTER_FUNCTIONS = {}
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def _font_style_cache_key(style, include_size=False):
|
|
133
|
-
key = str((
|
|
134
|
-
style['font_family'],
|
|
135
|
-
style['font_style'],
|
|
136
|
-
style['font_stretch'],
|
|
137
|
-
style['font_weight'],
|
|
138
|
-
style['font_variant_ligatures'],
|
|
139
|
-
style['font_variant_position'],
|
|
140
|
-
style['font_variant_caps'],
|
|
141
|
-
style['font_variant_numeric'],
|
|
142
|
-
style['font_variant_alternates'],
|
|
143
|
-
style['font_variant_east_asian'],
|
|
144
|
-
style['font_feature_settings'],
|
|
145
|
-
style['font_variation_settings'],
|
|
146
|
-
style['font_language_override'],
|
|
147
|
-
style['lang'],
|
|
148
|
-
))
|
|
149
|
-
if include_size:
|
|
150
|
-
key += str(style['font_size']) + str(style['line_height'])
|
|
151
|
-
return key
|
|
152
|
-
|
|
153
|
-
|
|
154
132
|
def register_computer(name):
|
|
155
133
|
"""Decorator registering a property ``name`` for a function."""
|
|
156
134
|
name = name.replace('-', '_')
|
|
@@ -163,7 +141,8 @@ def register_computer(name):
|
|
|
163
141
|
|
|
164
142
|
|
|
165
143
|
def compute_attr(style, values):
|
|
166
|
-
# TODO: use real token parsing instead of casting with Python types
|
|
144
|
+
# TODO: use real token parsing instead of casting with Python types, and follow new
|
|
145
|
+
# syntax. See https://drafts.csswg.org/css-values-5/#attr-notation.
|
|
167
146
|
func_name, value = values
|
|
168
147
|
assert func_name == 'attr()'
|
|
169
148
|
attr_name, type_or_unit, fallback = value
|
|
@@ -172,13 +151,9 @@ def compute_attr(style, values):
|
|
|
172
151
|
if type_or_unit == 'string':
|
|
173
152
|
pass # Keep the string
|
|
174
153
|
elif type_or_unit == 'url':
|
|
175
|
-
|
|
176
|
-
attr_value = ('internal', unquote(attr_value[1:]))
|
|
177
|
-
else:
|
|
178
|
-
attr_value = (
|
|
179
|
-
'external', safe_urljoin(style.base_url, attr_value))
|
|
154
|
+
attr_value = get_url_tuple(attr_value, style.base_url)
|
|
180
155
|
elif type_or_unit == 'color':
|
|
181
|
-
attr_value = parse_color(attr_value.strip())
|
|
156
|
+
attr_value = parse_color(attr_value.strip(), style['color_scheme'])
|
|
182
157
|
elif type_or_unit == 'integer':
|
|
183
158
|
attr_value = int(attr_value.strip())
|
|
184
159
|
elif type_or_unit == 'number':
|
|
@@ -192,6 +167,8 @@ def compute_attr(style, values):
|
|
|
192
167
|
elif type_or_unit in ANGLE_TO_RADIANS:
|
|
193
168
|
attr_value = Dimension(float(attr_value.strip()), type_or_unit)
|
|
194
169
|
type_or_unit = 'angle'
|
|
170
|
+
else:
|
|
171
|
+
return
|
|
195
172
|
except Exception:
|
|
196
173
|
return
|
|
197
174
|
return (type_or_unit, attr_value)
|
|
@@ -205,15 +182,30 @@ def background_image(style, name, values):
|
|
|
205
182
|
value.stop_positions = tuple(
|
|
206
183
|
length(style, name, pos) if pos is not None else None
|
|
207
184
|
for pos in value.stop_positions)
|
|
185
|
+
value.color_hints = tuple(
|
|
186
|
+
length(style, name, hint) if hint is not None else None
|
|
187
|
+
for hint in value.color_hints)
|
|
208
188
|
if type_ == 'radial-gradient':
|
|
209
189
|
value.center, = compute_position(
|
|
210
190
|
style, name, (value.center,))
|
|
211
191
|
if value.size_type == 'explicit':
|
|
212
|
-
value.size = length_or_percentage_tuple(
|
|
213
|
-
style, name, value.size)
|
|
192
|
+
value.size = length_or_percentage_tuple(style, name, value.size)
|
|
214
193
|
return values
|
|
215
194
|
|
|
216
195
|
|
|
196
|
+
@register_computer('color')
|
|
197
|
+
@register_computer('background-color')
|
|
198
|
+
@register_computer('border-top-color')
|
|
199
|
+
@register_computer('border-right-color')
|
|
200
|
+
@register_computer('border-bottom-color')
|
|
201
|
+
@register_computer('border-left-color')
|
|
202
|
+
@register_computer('column-rule-color')
|
|
203
|
+
@register_computer('outline-color')
|
|
204
|
+
@register_computer('text-decoration-color')
|
|
205
|
+
def color(style, name, values):
|
|
206
|
+
return parse_color(values, style['color_scheme'])
|
|
207
|
+
|
|
208
|
+
|
|
217
209
|
@register_computer('background-position')
|
|
218
210
|
@register_computer('object-position')
|
|
219
211
|
def compute_position(style, name, values):
|
|
@@ -235,8 +227,7 @@ def length_or_percentage_tuple(style, name, values):
|
|
|
235
227
|
@register_computer('clip')
|
|
236
228
|
def length_tuple(style, name, values):
|
|
237
229
|
"""Compute the properties with a list of lengths."""
|
|
238
|
-
return tuple(
|
|
239
|
-
length(style, name, value, pixels_only=True) for value in values)
|
|
230
|
+
return tuple(length(style, name, value, pixels_only=True) for value in values)
|
|
240
231
|
|
|
241
232
|
|
|
242
233
|
@register_computer('break-after')
|
|
@@ -271,36 +262,16 @@ def break_before_after(style, name, value):
|
|
|
271
262
|
@register_computer('text-decoration-thickness')
|
|
272
263
|
def length(style, name, value, font_size=None, pixels_only=False):
|
|
273
264
|
"""Compute a length ``value``."""
|
|
274
|
-
if value in ('auto', 'content', 'from-font'):
|
|
265
|
+
if value in ('auto', 'content', 'from-font') or check_math(value):
|
|
275
266
|
return value
|
|
276
|
-
|
|
267
|
+
elif value.value == 0:
|
|
277
268
|
return 0 if pixels_only else ZERO_PIXELS
|
|
278
|
-
|
|
279
|
-
unit = value.unit
|
|
280
|
-
if unit == 'px':
|
|
281
|
-
return value.value if pixels_only else value
|
|
282
|
-
elif unit in LENGTHS_TO_PIXELS:
|
|
283
|
-
# Convert absolute lengths to pixels
|
|
284
|
-
result = value.value * LENGTHS_TO_PIXELS[unit]
|
|
285
|
-
elif unit in ('em', 'ex', 'ch', 'rem'):
|
|
286
|
-
if font_size is None:
|
|
287
|
-
font_size = style['font_size']
|
|
288
|
-
if unit == 'ex':
|
|
289
|
-
# TODO: use context to use @font-face fonts
|
|
290
|
-
ratio = character_ratio(style, 'x')
|
|
291
|
-
result = value.value * font_size * ratio
|
|
292
|
-
elif unit == 'ch':
|
|
293
|
-
ratio = character_ratio(style, '0')
|
|
294
|
-
result = value.value * font_size * ratio
|
|
295
|
-
elif unit == 'em':
|
|
296
|
-
result = value.value * font_size
|
|
297
|
-
elif unit == 'rem':
|
|
298
|
-
result = value.value * style.root_style['font_size']
|
|
299
|
-
else:
|
|
269
|
+
elif value.unit not in LENGTH_UNITS:
|
|
300
270
|
# A percentage or 'auto': no conversion needed.
|
|
301
271
|
return value
|
|
302
272
|
|
|
303
|
-
|
|
273
|
+
pixels = to_pixels(value, style, name, font_size)
|
|
274
|
+
return pixels if pixels_only else Dimension(pixels, 'px')
|
|
304
275
|
|
|
305
276
|
|
|
306
277
|
@register_computer('bleed-left')
|
|
@@ -310,16 +281,14 @@ def length(style, name, value, font_size=None, pixels_only=False):
|
|
|
310
281
|
def bleed(style, name, value):
|
|
311
282
|
if value == 'auto':
|
|
312
283
|
return Dimension(8 if 'crop' in style['marks'] else 0, 'px')
|
|
313
|
-
|
|
314
|
-
return length(style, name, value)
|
|
284
|
+
return length(style, name, value)
|
|
315
285
|
|
|
316
286
|
|
|
317
287
|
@register_computer('letter-spacing')
|
|
318
288
|
def pixel_length(style, name, value):
|
|
319
289
|
if value == 'normal':
|
|
320
290
|
return value
|
|
321
|
-
|
|
322
|
-
return length(style, name, value, pixels_only=True)
|
|
291
|
+
return length(style, name, value, pixels_only=True)
|
|
323
292
|
|
|
324
293
|
|
|
325
294
|
@register_computer('background-size')
|
|
@@ -450,9 +419,7 @@ def border_radius(style, name, values):
|
|
|
450
419
|
@register_computer('row-gap')
|
|
451
420
|
def gap(style, name, value):
|
|
452
421
|
"""Compute the ``*-gap`` properties."""
|
|
453
|
-
if value == 'normal'
|
|
454
|
-
return value
|
|
455
|
-
return length(style, name, value)
|
|
422
|
+
return value if value == 'normal' else length(style, name, value)
|
|
456
423
|
|
|
457
424
|
|
|
458
425
|
def _content_list(style, values):
|
|
@@ -478,7 +445,7 @@ def _content_list(style, values):
|
|
|
478
445
|
if attr is None:
|
|
479
446
|
computed_value = None
|
|
480
447
|
else:
|
|
481
|
-
computed_value = (value[0], (
|
|
448
|
+
computed_value = (value[0], (attr, *value[1][1:]))
|
|
482
449
|
else:
|
|
483
450
|
computed_value = value
|
|
484
451
|
if computed_value is None:
|
|
@@ -523,8 +490,7 @@ def display(style, name, value):
|
|
|
523
490
|
# See https://www.w3.org/TR/CSS21/visuren.html#dis-pos-flo.
|
|
524
491
|
float_ = style.specified['float']
|
|
525
492
|
position = style.specified['position']
|
|
526
|
-
if position in ('absolute', 'fixed') or float_ != 'none' or
|
|
527
|
-
style.is_root_element):
|
|
493
|
+
if position in ('absolute', 'fixed') or float_ != 'none' or style.is_root_element:
|
|
528
494
|
if value == ('inline-table',):
|
|
529
495
|
return ('block', 'table')
|
|
530
496
|
elif len(value) == 1 and value[0].startswith('table-'):
|
|
@@ -572,7 +538,7 @@ def font_size(style, name, value):
|
|
|
572
538
|
return keyword_values[-i - 1]
|
|
573
539
|
else:
|
|
574
540
|
return parent_font_size * 0.8
|
|
575
|
-
elif value.unit == '%':
|
|
541
|
+
elif isinstance(value, Dimension) and value.unit == '%':
|
|
576
542
|
return value.value * parent_font_size / 100
|
|
577
543
|
else:
|
|
578
544
|
return length(
|
|
@@ -602,7 +568,7 @@ def _compute_track_breadth(style, name, value):
|
|
|
602
568
|
if value in ('auto', 'min-content', 'max-content'):
|
|
603
569
|
return value
|
|
604
570
|
elif isinstance(value, Dimension):
|
|
605
|
-
if value.unit == 'fr':
|
|
571
|
+
if value.unit and value.unit.lower() == 'fr':
|
|
606
572
|
return value
|
|
607
573
|
else:
|
|
608
574
|
return length(style, name, value)
|
|
@@ -692,26 +658,23 @@ def anchor(style, name, values):
|
|
|
692
658
|
def link(style, name, values):
|
|
693
659
|
"""Compute the ``link`` property."""
|
|
694
660
|
if values == 'none':
|
|
695
|
-
return
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
else:
|
|
701
|
-
return values
|
|
661
|
+
return
|
|
662
|
+
type_, value = values
|
|
663
|
+
if type_ == 'attr()':
|
|
664
|
+
return get_link_attribute(style.element, value, style.base_url)
|
|
665
|
+
return values
|
|
702
666
|
|
|
703
667
|
|
|
704
668
|
@register_computer('lang')
|
|
705
669
|
def lang(style, name, values):
|
|
706
670
|
"""Compute the ``lang`` property."""
|
|
707
671
|
if values == 'none':
|
|
708
|
-
return
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return key
|
|
672
|
+
return
|
|
673
|
+
name, key = values
|
|
674
|
+
if name == 'attr()':
|
|
675
|
+
return style.element.get(key) or None
|
|
676
|
+
elif name == 'string':
|
|
677
|
+
return key
|
|
715
678
|
|
|
716
679
|
|
|
717
680
|
@register_computer('tab-size')
|
|
@@ -734,17 +697,24 @@ def transform(style, name, value):
|
|
|
734
697
|
@register_computer('vertical-align')
|
|
735
698
|
def vertical_align(style, name, value):
|
|
736
699
|
"""Compute the ``vertical-align`` property."""
|
|
700
|
+
from ..css import resolve_math
|
|
701
|
+
|
|
737
702
|
# Use +/- half an em for super and sub, same as Pango.
|
|
738
703
|
# (See the SUPERSUB_RISE constant in pango-markup.c)
|
|
739
|
-
if value
|
|
740
|
-
|
|
704
|
+
if check_math(value):
|
|
705
|
+
height, _ = strut(style)
|
|
706
|
+
result = resolve_math(value, style, 'vertical_align', height)
|
|
707
|
+
value = validate_non_shorthand((result,), 'vertical-align')[0][1]
|
|
708
|
+
if value is None:
|
|
709
|
+
value = 'baseline'
|
|
710
|
+
if value in ('baseline', 'middle', 'text-top', 'text-bottom', 'top', 'bottom'):
|
|
741
711
|
return value
|
|
742
712
|
elif value == 'super':
|
|
743
713
|
return style['font_size'] * 0.5
|
|
744
714
|
elif value == 'sub':
|
|
745
715
|
return style['font_size'] * -0.5
|
|
746
716
|
elif value.unit == '%':
|
|
747
|
-
height, _ =
|
|
717
|
+
height, _ = strut(style)
|
|
748
718
|
return height * value.value / 100
|
|
749
719
|
else:
|
|
750
720
|
return length(style, name, value, pixels_only=True)
|
|
@@ -753,79 +723,4 @@ def vertical_align(style, name, value):
|
|
|
753
723
|
@register_computer('word-spacing')
|
|
754
724
|
def word_spacing(style, name, value):
|
|
755
725
|
"""Compute the ``word-spacing`` property."""
|
|
756
|
-
if value == 'normal'
|
|
757
|
-
return 0
|
|
758
|
-
else:
|
|
759
|
-
return length(style, name, value, pixels_only=True)
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
def strut_layout(style, context=None):
|
|
763
|
-
"""Return a tuple of the used value of ``line-height`` and the baseline.
|
|
764
|
-
|
|
765
|
-
The baseline is given from the top edge of line height.
|
|
766
|
-
|
|
767
|
-
"""
|
|
768
|
-
if style['font_size'] == 0:
|
|
769
|
-
return 0, 0
|
|
770
|
-
|
|
771
|
-
if context:
|
|
772
|
-
key = _font_style_cache_key(style, include_size=True)
|
|
773
|
-
if key in context.strut_layouts:
|
|
774
|
-
return context.strut_layouts[key]
|
|
775
|
-
|
|
776
|
-
layout = Layout(context, style)
|
|
777
|
-
layout.set_text(' ')
|
|
778
|
-
line, _ = layout.get_first_line()
|
|
779
|
-
_, _, _, _, text_height, baseline = first_line_metrics(
|
|
780
|
-
line, '', layout, resume_at=None, space_collapse=False, style=style)
|
|
781
|
-
if style['line_height'] == 'normal':
|
|
782
|
-
result = text_height, baseline
|
|
783
|
-
if context:
|
|
784
|
-
context.strut_layouts[key] = result
|
|
785
|
-
return result
|
|
786
|
-
type_, line_height = style['line_height']
|
|
787
|
-
if type_ == 'NUMBER':
|
|
788
|
-
line_height *= style['font_size']
|
|
789
|
-
result = line_height, baseline + (line_height - text_height) / 2
|
|
790
|
-
if context:
|
|
791
|
-
context.strut_layouts[key] = result
|
|
792
|
-
return result
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
def character_ratio(style, character):
|
|
796
|
-
"""Return the ratio of 1ex/font_size or 1ch/font_size."""
|
|
797
|
-
# TODO: use context to use @font-face fonts
|
|
798
|
-
|
|
799
|
-
assert character in ('x', '0')
|
|
800
|
-
|
|
801
|
-
cache = style.cache[f'ratio_{"ex" if character == "x" else "ch"}']
|
|
802
|
-
cache_key = _font_style_cache_key(style)
|
|
803
|
-
if cache_key in cache:
|
|
804
|
-
return cache[cache_key]
|
|
805
|
-
|
|
806
|
-
# Avoid recursion for letter-spacing and word-spacing properties
|
|
807
|
-
style = style.copy()
|
|
808
|
-
style['letter_spacing'] = 'normal'
|
|
809
|
-
style['word_spacing'] = 0
|
|
810
|
-
# Random big value
|
|
811
|
-
style['font_size'] = 1000
|
|
812
|
-
|
|
813
|
-
layout = Layout(context=None, style=style)
|
|
814
|
-
layout.set_text(character)
|
|
815
|
-
line, _ = layout.get_first_line()
|
|
816
|
-
|
|
817
|
-
ink_extents = ffi.new('PangoRectangle *')
|
|
818
|
-
logical_extents = ffi.new('PangoRectangle *')
|
|
819
|
-
pango.pango_layout_line_get_extents(line, ink_extents, logical_extents)
|
|
820
|
-
if character == 'x':
|
|
821
|
-
measure = -ink_extents.y * FROM_UNITS
|
|
822
|
-
else:
|
|
823
|
-
measure = logical_extents.width * FROM_UNITS
|
|
824
|
-
ffi.release(ink_extents)
|
|
825
|
-
ffi.release(logical_extents)
|
|
826
|
-
|
|
827
|
-
# Zero means some kind of failure, fallback is 0.5.
|
|
828
|
-
# We round to try keeping exact values that were altered by Pango.
|
|
829
|
-
ratio = round(measure / style['font_size'], 5) or 0.5
|
|
830
|
-
cache[cache_key] = ratio
|
|
831
|
-
return ratio
|
|
726
|
+
return 0 if value == 'normal' else length(style, name, value, pixels_only=True)
|
weasyprint/css/counters.py
CHANGED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""CSS functions parsers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Function:
|
|
5
|
+
"""CSS function."""
|
|
6
|
+
# See https://drafts.csswg.org/css-values-4/#functional-notation.
|
|
7
|
+
|
|
8
|
+
def __init__(self, token):
|
|
9
|
+
"""Create Function from function token."""
|
|
10
|
+
if getattr(token, 'type', None) == 'function':
|
|
11
|
+
self.name = token.lower_name
|
|
12
|
+
self.arguments = token.arguments
|
|
13
|
+
else:
|
|
14
|
+
self.name = self.arguments = None
|
|
15
|
+
|
|
16
|
+
def split_space(self):
|
|
17
|
+
"""Split arguments on spaces."""
|
|
18
|
+
if self.arguments is not None:
|
|
19
|
+
return [
|
|
20
|
+
argument for argument in self.arguments
|
|
21
|
+
if argument.type not in ('whitespace', 'comment')]
|
|
22
|
+
|
|
23
|
+
def split_comma(self, single_tokens=True, trailing=False):
|
|
24
|
+
"""Split arguments on commas.
|
|
25
|
+
|
|
26
|
+
Spaces in parentheses and after commas are removed.
|
|
27
|
+
|
|
28
|
+
If ``single_tokens`` is ``True``, check that only a single token is between
|
|
29
|
+
commas and flatten returned list.
|
|
30
|
+
|
|
31
|
+
If ``trailing`` is ``True``, allow a bare comma at the end.
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
if self.arguments is None:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
parts = [[]]
|
|
38
|
+
for token in self.arguments:
|
|
39
|
+
if token.type == 'literal' and token.value == ',':
|
|
40
|
+
parts.append([])
|
|
41
|
+
continue
|
|
42
|
+
if token.type not in ('comment', 'whitespace'):
|
|
43
|
+
parts[-1].append(token)
|
|
44
|
+
|
|
45
|
+
if trailing:
|
|
46
|
+
if single_tokens:
|
|
47
|
+
if all(len(part) == 1 for part in parts[:-1]):
|
|
48
|
+
if len(parts[-1]) in (0, 1):
|
|
49
|
+
return [part[0] if part else None for part in parts[:-1]]
|
|
50
|
+
elif all(parts[:-1]):
|
|
51
|
+
return parts
|
|
52
|
+
else:
|
|
53
|
+
if single_tokens:
|
|
54
|
+
if all(len(part) == 1 for part in parts):
|
|
55
|
+
return [part[0] for part in parts]
|
|
56
|
+
elif all(parts):
|
|
57
|
+
return parts
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_attr(token, allowed_type=None):
|
|
62
|
+
function = Function(token)
|
|
63
|
+
if function.name != 'attr':
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
parts = function.split_comma(single_tokens=False, trailing=True)
|
|
67
|
+
if len(parts) == 1:
|
|
68
|
+
name_and_type, fallback = parts[0], ''
|
|
69
|
+
elif len(parts) == 2:
|
|
70
|
+
name_and_type, fallback = parts
|
|
71
|
+
else:
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
if any(token.type != 'ident' for token in name_and_type):
|
|
75
|
+
return
|
|
76
|
+
# TODO: follow new syntax, see https://drafts.csswg.org/css-values-5/#attr-notation.
|
|
77
|
+
|
|
78
|
+
name = name_and_type[0].value
|
|
79
|
+
type_or_unit = name_and_type[1].value if len(name_and_type) == 2 else 'string'
|
|
80
|
+
if allowed_type in (None, type_or_unit):
|
|
81
|
+
return ('attr()', (name, type_or_unit, fallback))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_counter(token, allowed_type=None):
|
|
85
|
+
from .validation.properties import list_style_type
|
|
86
|
+
|
|
87
|
+
function = Function(token)
|
|
88
|
+
arguments = function.split_comma()
|
|
89
|
+
if function.name == 'counter':
|
|
90
|
+
if len(arguments) not in (1, 2):
|
|
91
|
+
return
|
|
92
|
+
elif function.name == 'counters':
|
|
93
|
+
if len(arguments) not in (2, 3):
|
|
94
|
+
return
|
|
95
|
+
else:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
result = []
|
|
99
|
+
ident = arguments.pop(0)
|
|
100
|
+
if ident.type != 'ident':
|
|
101
|
+
return
|
|
102
|
+
result.append(ident.value)
|
|
103
|
+
|
|
104
|
+
if function.name == 'counters':
|
|
105
|
+
string = arguments.pop(0)
|
|
106
|
+
if string.type != 'string':
|
|
107
|
+
return
|
|
108
|
+
result.append(string.value)
|
|
109
|
+
|
|
110
|
+
if arguments:
|
|
111
|
+
counter_style = list_style_type((arguments.pop(0),))
|
|
112
|
+
if counter_style is None:
|
|
113
|
+
return
|
|
114
|
+
result.append(counter_style)
|
|
115
|
+
else:
|
|
116
|
+
result.append('decimal')
|
|
117
|
+
|
|
118
|
+
return (f'{function.name}()', tuple(result))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def check_content(token):
|
|
122
|
+
function = Function(token)
|
|
123
|
+
if function.name == 'content':
|
|
124
|
+
arguments = function.split_comma()
|
|
125
|
+
if len(arguments) == 0:
|
|
126
|
+
return ('content()', 'text')
|
|
127
|
+
elif len(arguments) == 1:
|
|
128
|
+
ident = arguments.pop(0)
|
|
129
|
+
values = ('text', 'before', 'after', 'first-letter', 'marker')
|
|
130
|
+
if ident.type == 'ident' and ident.lower_value in values:
|
|
131
|
+
return ('content()', ident.lower_value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def check_string_or_element(string_or_element, token):
|
|
135
|
+
function = Function(token)
|
|
136
|
+
arguments = function.split_comma()
|
|
137
|
+
if function.name == string_or_element and len(arguments) in (1, 2):
|
|
138
|
+
custom_ident = arguments.pop(0)
|
|
139
|
+
if custom_ident.type != 'ident':
|
|
140
|
+
return
|
|
141
|
+
custom_ident = custom_ident.value
|
|
142
|
+
|
|
143
|
+
if arguments:
|
|
144
|
+
ident = arguments.pop(0)
|
|
145
|
+
if ident.type != 'ident':
|
|
146
|
+
return
|
|
147
|
+
if ident.lower_value not in ('first', 'start', 'last', 'first-except'):
|
|
148
|
+
return
|
|
149
|
+
ident = ident.lower_value
|
|
150
|
+
else:
|
|
151
|
+
ident = 'first'
|
|
152
|
+
|
|
153
|
+
return (f'{string_or_element}()', (custom_ident, ident))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def check_var(token):
|
|
157
|
+
if token.type == '() block':
|
|
158
|
+
return any(check_var(item) for item in token.content)
|
|
159
|
+
function = Function(token)
|
|
160
|
+
if function.name is None:
|
|
161
|
+
return
|
|
162
|
+
arguments = function.split_space()
|
|
163
|
+
if function.name == 'var':
|
|
164
|
+
ident = arguments[0]
|
|
165
|
+
# TODO: we should check authorized tokens
|
|
166
|
+
# https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
|
|
167
|
+
return ident.type == 'ident' and ident.value.startswith('--')
|
|
168
|
+
return any(check_var(argument) for argument in arguments)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_math(token):
|
|
172
|
+
# TODO: validate for real.
|
|
173
|
+
function = Function(token)
|
|
174
|
+
if (name := function.name) is None:
|
|
175
|
+
return
|
|
176
|
+
arguments = function.split_comma(single_tokens=False)
|
|
177
|
+
if name == 'calc':
|
|
178
|
+
return len(arguments) == 1
|
|
179
|
+
elif name in ('min', 'max'):
|
|
180
|
+
return len(arguments) >= 1
|
|
181
|
+
elif name == 'clamp':
|
|
182
|
+
return len(arguments) == 3
|
|
183
|
+
elif name == 'round':
|
|
184
|
+
return 1 <= len(arguments) <= 3
|
|
185
|
+
elif name in ('mod', 'rem'):
|
|
186
|
+
return len(arguments) == 2
|
|
187
|
+
elif name in ('sin', 'cos', 'tan'):
|
|
188
|
+
return len(arguments) == 1
|
|
189
|
+
elif name in ('asin', 'acos', 'atan'):
|
|
190
|
+
return len(arguments) == 1
|
|
191
|
+
elif name == 'atan2':
|
|
192
|
+
return len(arguments) == 2
|
|
193
|
+
elif name == 'pow':
|
|
194
|
+
return len(arguments) == 2
|
|
195
|
+
elif name == 'sqrt':
|
|
196
|
+
return len(arguments) == 1
|
|
197
|
+
elif name == 'hypot':
|
|
198
|
+
return len(arguments) >= 1
|
|
199
|
+
elif name == 'log':
|
|
200
|
+
return 1 <= len(arguments) <= 2
|
|
201
|
+
elif name == 'exp':
|
|
202
|
+
return len(arguments) == 1
|
|
203
|
+
elif name in ('abs', 'sign'):
|
|
204
|
+
return len(arguments) == 1
|
|
205
|
+
arguments = function.split_space()
|
|
206
|
+
return any(check_math(argument) for argument in arguments)
|
weasyprint/css/html5_ua.css
CHANGED
|
@@ -47,11 +47,6 @@ h3 { margin-top: 1em; margin-bottom: 1em }
|
|
|
47
47
|
h4 { margin-top: 1.33em; margin-bottom: 1.33em }
|
|
48
48
|
h5 { margin-top: 1.67em; margin-bottom: 1.67em }
|
|
49
49
|
h6 { margin-top: 2.33em; margin-bottom: 2.33em }
|
|
50
|
-
:is(article, aside, nav, section) h1 { font-size: 1.5em; margin-bottom: .83em; margin-top: .83em }
|
|
51
|
-
:is(article, aside, nav, section) :is(article, aside, nav, section) h1 { font-size: 1.17em; margin-bottom: 1em; margin-top: 1em }
|
|
52
|
-
:is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) h1 { font-size: 1em; margin-bottom: 1.33em; margin-top: 1.33em }
|
|
53
|
-
:is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) h1 { font-size: .83em; margin-bottom: 1.67em; margin-top: 1.67em }
|
|
54
|
-
:is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) :is(article, aside, nav, section) h1 { font-size: .67em; margin-bottom: 2.33em; margin-top: 2.33em }
|
|
55
50
|
|
|
56
51
|
blockquote, figure { margin-left: 40px; margin-right: 40px }
|
|
57
52
|
|
|
@@ -87,6 +82,7 @@ sup { vertical-align: super }
|
|
|
87
82
|
|
|
88
83
|
/* Fonts and colors */
|
|
89
84
|
|
|
85
|
+
html { color-scheme: light }
|
|
90
86
|
address, cite, dfn, em, i, var { font-style: italic }
|
|
91
87
|
b, strong, th { font-weight: bold }
|
|
92
88
|
code, kbd, listing, plaintext, pre, samp, tt, xmp { font-family: monospace }
|
|
@@ -174,9 +170,9 @@ input:is([type=button i], [type=reset i], [type=submit i])[value], button[value]
|
|
|
174
170
|
input[type=submit i]:not([value])::before { content: "Submit" }
|
|
175
171
|
input[type=reset i]:not([value])::before { content: "Reset" }
|
|
176
172
|
input:is([type=checkbox i], [type=radio i]) { height: .7em; vertical-align: -.2em; width: .7em }
|
|
177
|
-
input:is([type=checkbox i], [type=radio i])[checked]::before { background: black; content: ""; height: 100% }
|
|
173
|
+
input:is([type=checkbox i], [type=radio i])[checked]::before { background: black; content: ""; display: block; height: 100% }
|
|
178
174
|
input[type=radio i], input[type=radio][checked]:before { border-radius: 50% }
|
|
179
|
-
input[value]::before { content: attr(value); display: block; overflow: hidden }
|
|
175
|
+
input[value]:not([type=checkbox i], [type=radio i])::before { content: attr(value); display: block; overflow: hidden }
|
|
180
176
|
:is(input, input[value=""], input[type=checkbox i], input[type=radio i]) { content: ""; display: block }
|
|
181
177
|
select { background: lightgrey; border-radius: .25em .25em; position: relative; white-space: normal }
|
|
182
178
|
select[multiple] { height: 3.6em }
|
weasyprint/css/html5_ua_form.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* Default stylesheet for PDF forms */
|
|
2
2
|
|
|
3
3
|
button, input, select, textarea { appearance: auto }
|
|
4
|
-
select option, select:not([multiple])::before, input:not([type="submit"])::before {visibility: hidden }
|
|
5
|
-
textarea { text-indent: 10000% } /* Hide text but don’t change color used by PDF form */
|
|
4
|
+
select option, select:not([multiple])::before, input:not([type="submit"])::before { visibility: hidden }
|
|
5
|
+
textarea { white-space: normal; text-indent: 10000% } /* Hide text but don’t change color used by PDF form */
|
weasyprint/css/media_queries.py
CHANGED
|
@@ -7,7 +7,7 @@ https://www.w3.org/TR/mediaqueries-4/
|
|
|
7
7
|
import tinycss2
|
|
8
8
|
|
|
9
9
|
from ..logger import LOGGER
|
|
10
|
-
from .
|
|
10
|
+
from .tokens import remove_whitespace, split_on_comma
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def evaluate_media_query(query_list, device_media_type):
|
|
@@ -28,6 +28,8 @@ def parse_media_query(tokens):
|
|
|
28
28
|
return ['all']
|
|
29
29
|
else:
|
|
30
30
|
media = []
|
|
31
|
+
if tokens[0].type == 'ident' and tokens[0].lower_value == 'only':
|
|
32
|
+
tokens = tokens[1:]
|
|
31
33
|
for part in split_on_comma(tokens):
|
|
32
34
|
types = [token.type for token in part]
|
|
33
35
|
if types == ['ident']:
|