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.
Files changed (67) hide show
  1. weasyprint/__init__.py +17 -7
  2. weasyprint/__main__.py +21 -10
  3. weasyprint/anchors.py +4 -4
  4. weasyprint/css/__init__.py +732 -67
  5. weasyprint/css/computed_values.py +65 -170
  6. weasyprint/css/counters.py +1 -1
  7. weasyprint/css/functions.py +206 -0
  8. weasyprint/css/html5_ua.css +3 -7
  9. weasyprint/css/html5_ua_form.css +2 -2
  10. weasyprint/css/media_queries.py +3 -1
  11. weasyprint/css/properties.py +6 -2
  12. weasyprint/css/{utils.py → tokens.py} +306 -397
  13. weasyprint/css/units.py +91 -0
  14. weasyprint/css/validation/__init__.py +1 -1
  15. weasyprint/css/validation/descriptors.py +47 -19
  16. weasyprint/css/validation/expanders.py +7 -8
  17. weasyprint/css/validation/properties.py +341 -357
  18. weasyprint/document.py +20 -19
  19. weasyprint/draw/__init__.py +56 -63
  20. weasyprint/draw/border.py +121 -69
  21. weasyprint/draw/color.py +1 -1
  22. weasyprint/draw/text.py +60 -41
  23. weasyprint/formatting_structure/boxes.py +24 -5
  24. weasyprint/formatting_structure/build.py +33 -45
  25. weasyprint/images.py +76 -62
  26. weasyprint/layout/__init__.py +32 -26
  27. weasyprint/layout/absolute.py +7 -6
  28. weasyprint/layout/background.py +7 -7
  29. weasyprint/layout/block.py +195 -152
  30. weasyprint/layout/column.py +19 -24
  31. weasyprint/layout/flex.py +54 -26
  32. weasyprint/layout/float.py +12 -7
  33. weasyprint/layout/grid.py +284 -90
  34. weasyprint/layout/inline.py +121 -68
  35. weasyprint/layout/page.py +45 -12
  36. weasyprint/layout/percent.py +14 -10
  37. weasyprint/layout/preferred.py +105 -63
  38. weasyprint/layout/replaced.py +9 -6
  39. weasyprint/layout/table.py +16 -9
  40. weasyprint/pdf/__init__.py +58 -18
  41. weasyprint/pdf/anchors.py +3 -4
  42. weasyprint/pdf/fonts.py +126 -69
  43. weasyprint/pdf/metadata.py +36 -4
  44. weasyprint/pdf/pdfa.py +19 -3
  45. weasyprint/pdf/pdfua.py +7 -115
  46. weasyprint/pdf/pdfx.py +83 -0
  47. weasyprint/pdf/stream.py +57 -49
  48. weasyprint/pdf/tags.py +307 -0
  49. weasyprint/stacking.py +14 -15
  50. weasyprint/svg/__init__.py +59 -32
  51. weasyprint/svg/bounding_box.py +4 -2
  52. weasyprint/svg/defs.py +4 -9
  53. weasyprint/svg/images.py +11 -3
  54. weasyprint/svg/text.py +11 -2
  55. weasyprint/svg/utils.py +15 -8
  56. weasyprint/text/constants.py +1 -1
  57. weasyprint/text/ffi.py +4 -3
  58. weasyprint/text/fonts.py +13 -5
  59. weasyprint/text/line_break.py +146 -43
  60. weasyprint/urls.py +41 -13
  61. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/METADATA +5 -6
  62. weasyprint-67.0.dist-info/RECORD +77 -0
  63. weasyprint/draw/stack.py +0 -13
  64. weasyprint-65.1.dist-info/RECORD +0 -74
  65. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/WHEEL +0 -0
  66. {weasyprint-65.1.dist-info → weasyprint-67.0.dist-info}/entry_points.txt +0 -0
  67. {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.color4 import parse_color
5
+ from tinycss2.color5 import parse_color
7
6
 
8
7
  from ..logger import LOGGER
9
- from ..text.ffi import FROM_UNITS, ffi, pango
10
- from ..text.line_break import Layout, first_line_metrics
11
- from ..urls import get_link_attribute
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 .utils import ANGLE_TO_RADIANS, LENGTH_UNITS, LENGTHS_TO_PIXELS, safe_urljoin
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.value * LENGTHS_TO_PIXELS[size.unit] for size in INITIAL_PAGE_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
- if attr_value.startswith('#'):
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
- if value.value == 0:
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
- return result if pixels_only else Dimension(result, 'px')
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
- else:
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
- else:
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], ((attr,) + value[1][1:]))
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 None
696
- else:
697
- type_, value = values
698
- if type_ == 'attr()':
699
- return get_link_attribute(style.element, value, style.base_url)
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 None
709
- else:
710
- name, key = values
711
- if name == 'attr()':
712
- return style.element.get(key) or None
713
- elif name == 'string':
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 in (
740
- 'baseline', 'middle', 'text-top', 'text-bottom', 'top', 'bottom'):
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, _ = strut_layout(style)
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)
@@ -7,7 +7,7 @@ https://www.w3.org/TR/css-counter-styles-3/#counter-style-system
7
7
 
8
8
  from math import inf
9
9
 
10
- from .utils import remove_whitespace
10
+ from .tokens import remove_whitespace
11
11
 
12
12
 
13
13
  def symbol(string_or_url):
@@ -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)
@@ -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 }
@@ -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 */
@@ -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 .utils import remove_whitespace, split_on_comma
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']: