weasyprint 67.0__py3-none-any.whl → 68.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
weasyprint/__init__.py CHANGED
@@ -5,7 +5,6 @@ importing sub-modules.
5
5
 
6
6
  """
7
7
 
8
- import contextlib
9
8
  from datetime import datetime
10
9
  from os.path import getctime, getmtime
11
10
  from pathlib import Path
@@ -15,9 +14,9 @@ import cssselect2
15
14
  import tinycss2
16
15
  import tinyhtml5
17
16
 
18
- VERSION = __version__ = '67.0'
17
+ VERSION = __version__ = '68.0'
19
18
 
20
- #: Default values for command-line and Python API options. See
19
+ #: Default values for command-line and Python API rendering options. See
21
20
  #: :func:`__main__.main` to learn more about specific options for
22
21
  #: command-line.
23
22
  #:
@@ -67,14 +66,15 @@ VERSION = __version__ = '67.0'
67
66
  #: images are temporarily stored.
68
67
  DEFAULT_OPTIONS = {
69
68
  'stylesheets': None,
70
- 'media_type': 'print',
71
69
  'attachments': None,
70
+ 'attachment_relationships': None,
72
71
  'pdf_identifier': None,
73
72
  'pdf_variant': None,
74
73
  'pdf_version': None,
75
74
  'pdf_forms': None,
76
75
  'pdf_tags': False,
77
76
  'uncompressed_pdf': False,
77
+ 'xmp_metadata': None,
78
78
  'custom_metadata': False,
79
79
  'presentational_hints': False,
80
80
  'srgb': False,
@@ -92,8 +92,7 @@ __all__ = [
92
92
 
93
93
 
94
94
  # Import after setting the version, as the version is used in other modules
95
- from .urls import ( # noqa: I001, E402
96
- fetch, default_url_fetcher, path2url, ensure_url, url_is_absolute)
95
+ from .urls import URLFetcher, default_url_fetcher, select_source # noqa: I001, E402
97
96
  from .logger import LOGGER, PROGRESS_LOGGER # noqa: E402
98
97
  # Some imports are at the end of the file (after the CSS class)
99
98
  # to work around circular imports.
@@ -150,9 +149,7 @@ class HTML:
150
149
  :term:`file objects <file object>`.
151
150
  :type url_fetcher: :term:`callable`
152
151
  :param url_fetcher:
153
- A function or other callable with the same signature as
154
- :func:`default_url_fetcher` called to fetch external resources such as
155
- stylesheets and images. (See :ref:`URL Fetchers`.)
152
+ An instance of :class:`urls.URLFetcher`. (See :ref:`URL Fetchers`.)
156
153
  :param str media_type:
157
154
  The media type to use for ``@media``. Defaults to ``'print'``.
158
155
  **Note:** In some cases like ``HTML(string=foo)`` relative URLs will be
@@ -161,25 +158,24 @@ class HTML:
161
158
  """
162
159
  def __init__(self, guess=None, filename=None, url=None, file_obj=None,
163
160
  string=None, encoding=None, base_url=None,
164
- url_fetcher=default_url_fetcher, media_type='print'):
161
+ url_fetcher=None, media_type='print'):
165
162
  PROGRESS_LOGGER.info(
166
163
  'Step 1 - Fetching and parsing HTML - %s',
167
164
  guess or filename or url or
168
165
  getattr(file_obj, 'name', 'HTML string'))
169
166
  if isinstance(base_url, Path):
170
167
  base_url = str(base_url)
171
- result = _select_source(
168
+ if url_fetcher is None:
169
+ url_fetcher = URLFetcher()
170
+ result = select_source(
172
171
  guess, filename, url, file_obj, string, base_url, url_fetcher)
173
- with result as (source_type, source, base_url, protocol_encoding):
174
- if isinstance(source, str):
175
- result = tinyhtml5.parse(source, namespace_html_elements=False)
176
- else:
177
- kwargs = {'namespace_html_elements': False}
178
- if protocol_encoding is not None:
179
- kwargs['transport_encoding'] = protocol_encoding
180
- if encoding is not None:
181
- kwargs['override_encoding'] = encoding
182
- result = tinyhtml5.parse(source, **kwargs)
172
+ with result as (file_obj, base_url, protocol_encoding, _):
173
+ kwargs = {'namespace_html_elements': False}
174
+ if protocol_encoding is not None:
175
+ kwargs['transport_encoding'] = protocol_encoding
176
+ if encoding is not None:
177
+ kwargs['override_encoding'] = encoding
178
+ result = tinyhtml5.parse(file_obj, **kwargs)
183
179
  self.base_url = _find_base_url(result, base_url)
184
180
  self.url_fetcher = url_fetcher
185
181
  self.media_type = media_type
@@ -286,28 +282,26 @@ class CSS:
286
282
  of :class:`HTML` objects.
287
283
 
288
284
  """
289
- def __init__(self, guess=None, filename=None, url=None, file_obj=None,
290
- string=None, encoding=None, base_url=None,
291
- url_fetcher=default_url_fetcher, _check_mime_type=False,
285
+ def __init__(self, guess=None, filename=None, url=None, file_obj=None, string=None,
286
+ encoding=None, base_url=None, url_fetcher=None, _check_mime_type=False,
292
287
  media_type='print', font_config=None, counter_style=None,
293
288
  color_profiles=None, matcher=None, page_rules=None, layers=None,
294
289
  layer=None):
295
290
  PROGRESS_LOGGER.info(
296
291
  'Step 2 - Fetching and parsing CSS - %s',
297
292
  filename or url or getattr(file_obj, 'name', 'CSS string'))
298
- result = _select_source(
299
- guess, filename, url, file_obj, string,
300
- base_url=base_url, url_fetcher=url_fetcher,
301
- check_css_mime_type=_check_mime_type)
302
- with result as (source_type, source, base_url, protocol_encoding):
303
- if source_type == 'file_obj':
304
- source = source.read()
305
- if isinstance(source, str):
306
- # unicode, no encoding
307
- stylesheet = tinycss2.parse_stylesheet(source)
293
+ if url_fetcher is None:
294
+ url_fetcher = URLFetcher()
295
+ result = select_source(
296
+ guess, filename, url, file_obj, string, base_url=base_url,
297
+ url_fetcher=url_fetcher, check_css_mime_type=_check_mime_type)
298
+ with result as (file_obj, base_url, protocol_encoding, mime_type):
299
+ css = file_obj.read()
300
+ if isinstance(css, str):
301
+ stylesheet = tinycss2.parse_stylesheet(css)
308
302
  else:
309
- stylesheet, encoding = tinycss2.parse_stylesheet_bytes(
310
- source, environment_encoding=encoding,
303
+ stylesheet, _ = tinycss2.parse_stylesheet_bytes(
304
+ css, environment_encoding=encoding,
311
305
  protocol_encoding=protocol_encoding)
312
306
  self.base_url = base_url
313
307
  self.matcher = matcher or cssselect2.Matcher()
@@ -347,10 +341,12 @@ class Attachment:
347
341
 
348
342
  """
349
343
  def __init__(self, guess=None, filename=None, url=None, file_obj=None,
350
- string=None, base_url=None, url_fetcher=default_url_fetcher,
351
- name=None, description=None, created=None, modified=None,
344
+ string=None, base_url=None, url_fetcher=None, name=None,
345
+ description=None, created=None, modified=None,
352
346
  relationship='Unspecified'):
353
- self.source = _select_source(
347
+ if url_fetcher is None:
348
+ url_fetcher = URLFetcher()
349
+ self.source = select_source(
354
350
  guess, filename, url, file_obj, string, base_url=base_url,
355
351
  url_fetcher=url_fetcher)
356
352
  self.name = name
@@ -372,70 +368,6 @@ class Attachment:
372
368
  self.modified = modified
373
369
 
374
370
 
375
- @contextlib.contextmanager
376
- def _select_source(guess=None, filename=None, url=None, file_obj=None,
377
- string=None, base_url=None, url_fetcher=default_url_fetcher,
378
- check_css_mime_type=False):
379
- """If only one input is given, return it with normalized ``base_url``."""
380
- if base_url is not None:
381
- base_url = ensure_url(base_url)
382
-
383
- selected_params = [
384
- param for param in (guess, filename, url, file_obj, string) if
385
- param is not None]
386
- if len(selected_params) != 1:
387
- source = ', '.join(selected_params) or 'nothing'
388
- raise TypeError(f'Expected exactly one source, got {source}')
389
- elif guess is not None:
390
- if hasattr(guess, 'read'):
391
- type_ = 'file_obj'
392
- elif isinstance(guess, Path):
393
- type_ = 'filename'
394
- elif url_is_absolute(guess):
395
- type_ = 'url'
396
- else:
397
- type_ = 'filename'
398
- result = _select_source(
399
- base_url=base_url, url_fetcher=url_fetcher,
400
- check_css_mime_type=check_css_mime_type,
401
- **{type_: guess})
402
- with result as result:
403
- yield result
404
- elif filename is not None:
405
- if base_url is None:
406
- base_url = path2url(filename)
407
- with open(filename, 'rb') as file_obj:
408
- yield 'file_obj', file_obj, base_url, None
409
- elif url is not None:
410
- with fetch(url_fetcher, url) as result:
411
- if check_css_mime_type and result['mime_type'] != 'text/css':
412
- LOGGER.error(
413
- 'Unsupported stylesheet type %s for %s',
414
- result['mime_type'], result['redirected_url'])
415
- yield 'string', '', base_url, None
416
- else:
417
- proto_encoding = result.get('encoding')
418
- if base_url is None:
419
- base_url = result.get('redirected_url', url)
420
- if 'string' in result:
421
- yield 'string', result['string'], base_url, proto_encoding
422
- else:
423
- yield (
424
- 'file_obj', result['file_obj'], base_url,
425
- proto_encoding)
426
- elif file_obj is not None:
427
- if base_url is None:
428
- # filesystem file-like objects have a 'name' attribute.
429
- name = getattr(file_obj, 'name', None)
430
- # Some streams have a .name like '<stdin>', not a filename.
431
- if name and not name.startswith('<'):
432
- base_url = ensure_url(name)
433
- yield 'file_obj', file_obj, base_url, None
434
- else:
435
- assert string is not None
436
- yield 'string', string, base_url, None
437
-
438
-
439
371
  # Work around circular imports.
440
372
  from .css import preprocess_stylesheet # noqa: I001, E402
441
373
  from .html import ( # noqa: E402
weasyprint/__main__.py CHANGED
@@ -4,14 +4,13 @@ import argparse
4
4
  import logging
5
5
  import platform
6
6
  import sys
7
- from functools import partial
8
7
 
9
8
  import pydyf
10
9
 
11
10
  from . import DEFAULT_OPTIONS, HTML, LOGGER, __version__
12
11
  from .pdf import VARIANTS
13
12
  from .text.ffi import pango
14
- from .urls import default_url_fetcher
13
+ from .urls import URLFetcher
15
14
 
16
15
 
17
16
  class PrintInfo(argparse.Action):
@@ -33,117 +32,141 @@ class PrintInfo(argparse.Action):
33
32
 
34
33
  class Parser(argparse.ArgumentParser):
35
34
  def __init__(self, *args, **kwargs):
36
- self._arguments = {}
35
+ self._groups = {None: {}}
37
36
  super().__init__(*args, **kwargs)
38
37
 
39
- def add_argument(self, *args, **kwargs):
40
- super().add_argument(*args, **kwargs)
38
+ def add_argument(self, *args, _group_name=None, **kwargs):
39
+ if _group_name is None:
40
+ super().add_argument(*args, **kwargs)
41
41
  key = args[-1].lstrip('-')
42
42
  kwargs['flags'] = args
43
43
  kwargs['positional'] = args[-1][0] != '-'
44
- self._arguments[key] = kwargs
44
+ self._groups[_group_name][key] = kwargs
45
+
46
+ def add_argument_group(self, name, *args, **kwargs):
47
+ group = super().add_argument_group(name, *args, **kwargs)
48
+ self._groups[name] = {}
49
+ def add_argument(*args, **kwargs):
50
+ group._add_argument(*args, **kwargs)
51
+ self.add_argument(*args, _group_name=name, **kwargs)
52
+ group._add_argument = group.add_argument
53
+ group.add_argument = add_argument
54
+ return group
45
55
 
46
56
  @property
47
57
  def docstring(self):
48
- self._arguments['help'] = self._arguments.pop('help')
58
+ self._groups[None].pop('help')
49
59
  data = []
50
- for key, args in self._arguments.items():
51
- data.append('.. option:: ')
52
- action = args.get('action', 'store')
53
- for flag in args['flags']:
54
- data.append(flag)
55
- if not args['positional'] and action in ('store', 'append'):
56
- data.append(f' <{key}>')
57
- data.append(', ')
58
- data[-1] = '\n\n'
59
- data.append(f' {args["help"][0].upper()}{args["help"][1:]}.\n\n')
60
- if 'choices' in args:
61
- choices = ", ".join(args['choices'])
62
- data.append(f' Possible choices: {choices}.\n\n')
63
- if action == 'append':
64
- data.append(' This option can be passed multiple times.\n\n')
60
+ for group, arguments in self._groups.items():
61
+ if not arguments:
62
+ continue
63
+ if group:
64
+ data.append(f'{group[0].title()}{group[1:]}\n')
65
+ data.append(f'{"~" * len(group)}\n\n')
66
+ for key, args in arguments.items():
67
+ data.append('.. option:: ')
68
+ action = args.get('action', 'store')
69
+ for flag in args['flags']:
70
+ data.append(flag)
71
+ if not args['positional'] and action in ('store', 'append'):
72
+ data.append(f' <{key}>')
73
+ data.append(', ')
74
+ data[-1] = '\n\n'
75
+ data.append(f' {args["help"][0].upper()}{args["help"][1:]}.\n\n')
76
+ if 'choices' in args:
77
+ choices = ", ".join(args['choices'])
78
+ data.append(f' Possible choices: {choices}.\n\n')
79
+ if action == 'append':
80
+ data.append(' This option can be passed multiple times.\n\n')
65
81
  return ''.join(data)
66
82
 
67
83
 
68
84
  PARSER = Parser(prog='weasyprint', description='Render web pages to PDF.')
85
+ PARSER.add_argument('input', help='URL or filename of the HTML input, or - for stdin')
86
+ PARSER.add_argument('output', help='filename where output is written, or - for stdout')
69
87
  PARSER.add_argument(
70
- 'input', help='URL or filename of the HTML input, or - for stdin')
71
- PARSER.add_argument(
72
- 'output', help='filename where output is written, or - for stdout')
73
- PARSER.add_argument(
74
- '-e', '--encoding', help='force the input character encoding')
88
+ '-i', '--info', action=PrintInfo, nargs=0, help='print system information and exit')
75
89
  PARSER.add_argument(
90
+ '--version', action='version', version=f'WeasyPrint version {__version__}',
91
+ help='print WeasyPrint’s version number and exit')
92
+
93
+ group = PARSER.add_argument_group('rendering options')
94
+ group.add_argument(
76
95
  '-s', '--stylesheet', action='append', dest='stylesheets',
77
96
  help='URL or filename for a user CSS stylesheet')
78
- PARSER.add_argument(
79
- '-m', '--media-type',
80
- help='media type to use for @media, defaults to print')
81
- PARSER.add_argument(
82
- '-u', '--base-url',
83
- help='base for relative URLs in the HTML input, defaults to the '
84
- 'input’s own filename or URL or the current directory for stdin')
85
- PARSER.add_argument(
97
+ group.add_argument(
86
98
  '-a', '--attachment', action='append', dest='attachments',
87
99
  help='URL or filename of a file to attach to the PDF document')
88
- PARSER.add_argument('--pdf-identifier', help='PDF file identifier')
89
- PARSER.add_argument(
90
- '--pdf-variant', choices=VARIANTS, help='PDF variant to generate')
91
- PARSER.add_argument('--pdf-version', help='PDF version number')
92
- PARSER.add_argument(
93
- '--pdf-forms', action='store_true', help='include PDF forms')
94
- PARSER.add_argument(
95
- '--pdf-tags', action='store_true', help='tag PDF for accessibility')
96
- PARSER.add_argument(
100
+ group.add_argument(
101
+ '--attachment-relationship', action='append', dest='attachment_relationships',
102
+ help='Relationship of the attachment file to attach to the PDF')
103
+ group.add_argument('--pdf-identifier', help='PDF file identifier')
104
+ group.add_argument('--pdf-variant', choices=VARIANTS, help='PDF variant to generate')
105
+ group.add_argument('--pdf-version', help='PDF version number')
106
+ group.add_argument('--pdf-forms', action='store_true', help='include PDF forms')
107
+ group.add_argument('--pdf-tags', action='store_true', help='tag PDF for accessibility')
108
+ group.add_argument(
97
109
  '--uncompressed-pdf', action='store_true',
98
110
  help='do not compress PDF content, mainly for debugging purpose')
99
- PARSER.add_argument(
111
+ group.add_argument(
112
+ '--xmp-metadata', action='append',
113
+ help='URL or filename of a file to include into the XMP metadata')
114
+ group.add_argument(
100
115
  '--custom-metadata', action='store_true',
101
116
  help='include custom HTML meta tags in PDF metadata')
102
- PARSER.add_argument(
117
+ group.add_argument(
103
118
  '-p', '--presentational-hints', action='store_true',
104
119
  help='follow HTML presentational hints')
105
- PARSER.add_argument(
106
- '--srgb', action='store_true',
107
- help='include sRGB color profile')
108
- PARSER.add_argument(
120
+ group.add_argument('--srgb', action='store_true', help='include sRGB color profile')
121
+ group.add_argument(
109
122
  '--optimize-images', action='store_true',
110
123
  help='optimize size of embedded images with no quality loss')
111
- PARSER.add_argument(
124
+ group.add_argument(
112
125
  '-j', '--jpeg-quality', type=int,
113
126
  help='JPEG quality between 0 (worst) to 95 (best)')
114
- PARSER.add_argument(
127
+ group.add_argument(
128
+ '-D', '--dpi', type=int,
129
+ help='set maximum resolution of images embedded in the PDF')
130
+ group.add_argument(
115
131
  '--full-fonts', action='store_true',
116
132
  help='embed unmodified font files when possible')
117
- PARSER.add_argument(
118
- '--hinting', action='store_true',
119
- help='keep hinting information in embedded fonts')
120
- PARSER.add_argument(
133
+ group.add_argument(
134
+ '--hinting', action='store_true', help='keep hinting information in embedded fonts')
135
+ group.add_argument(
121
136
  '-c', '--cache-folder', dest='cache',
122
137
  help='store cache on disk instead of memory, folder is '
123
138
  'created if needed and cleaned after the PDF is generated')
124
- PARSER.add_argument(
125
- '-D', '--dpi', type=int,
126
- help='set maximum resolution of images embedded in the PDF')
127
- PARSER.add_argument(
139
+
140
+ group = PARSER.add_argument_group('HTML options')
141
+ group.add_argument('-e', '--encoding', help='force the input character encoding')
142
+ group.add_argument(
143
+ '-m', '--media-type', help='media type to use for @media, defaults to print',
144
+ default='print')
145
+ group.add_argument(
146
+ '-u', '--base-url',
147
+ help='base for relative URLs in the HTML input, defaults to the '
148
+ 'input’s own filename or URL or the current directory for stdin')
149
+
150
+ group = PARSER.add_argument_group('URL fetcher options')
151
+ group.add_argument(
152
+ '-t', '--timeout', type=int, help='set timeout in seconds for HTTP requests')
153
+ group.add_argument(
154
+ '--allowed-protocols', dest='allowed_protocols',
155
+ help='only authorize comma-separated list of protocols for fetching URLs')
156
+ group.add_argument(
157
+ '--no-http-redirects', action='store_true', help='do not follow HTTP redirects')
158
+ group.add_argument(
159
+ '--fail-on-http-errors', action='store_true',
160
+ help='abort document rendering on any HTTP error')
161
+
162
+ group = PARSER.add_argument_group('command-line logging options')
163
+ group.add_argument(
128
164
  '-v', '--verbose', action='store_true',
129
165
  help='show warnings and information messages')
130
- PARSER.add_argument(
166
+ group.add_argument(
131
167
  '-d', '--debug', action='store_true', help='show debugging messages')
132
- PARSER.add_argument(
133
- '-q', '--quiet', action='store_true', help='hide logging messages')
134
- PARSER.add_argument(
135
- '--version', action='version',
136
- version=f'WeasyPrint version {__version__}',
137
- help='print WeasyPrint’s version number and exit')
138
- PARSER.add_argument(
139
- '-i', '--info', action=PrintInfo, nargs=0,
140
- help='print system information and exit')
141
- PARSER.add_argument(
142
- '-t', '--timeout', type=int,
143
- help='set timeout in seconds for HTTP requests')
144
- PARSER.add_argument(
145
- '--allowed-protocols', dest='allowed_protocols',
146
- help='only authorize comma-separated list of protocols for fetching URLs')
168
+ group.add_argument('-q', '--quiet', action='store_true', help='hide logging messages')
169
+
147
170
  PARSER.set_defaults(**DEFAULT_OPTIONS)
148
171
 
149
172
 
@@ -171,13 +194,17 @@ def main(argv=None, stdout=None, stdin=None, HTML=HTML): # noqa: N803
171
194
  else:
172
195
  output = args.output
173
196
 
174
- url_fetcher = default_url_fetcher
197
+ fetcher_args = {}
175
198
  if args.timeout is not None:
176
- url_fetcher = partial(default_url_fetcher, timeout=args.timeout)
199
+ fetcher_args['timeout'] = args.timeout
177
200
  if args.allowed_protocols is not None:
178
- protocols = {
201
+ fetcher_args['allowed_protocols'] = {
179
202
  protocol.strip().lower() for protocol in args.allowed_protocols.split(',')}
180
- url_fetcher = partial(url_fetcher, allowed_protocols=protocols)
203
+ if args.no_http_redirects:
204
+ fetcher_args['allow_redirects'] = False
205
+ if args.fail_on_http_errors:
206
+ fetcher_args['fail_on_errors'] = True
207
+ url_fetcher = URLFetcher(**fetcher_args)
181
208
 
182
209
  options = {
183
210
  key: value for key, value in vars(args).items() if key in DEFAULT_OPTIONS}
@@ -14,7 +14,6 @@ on other functions in this module.
14
14
 
15
15
  import math
16
16
  from collections import namedtuple
17
- from io import BytesIO
18
17
  from itertools import groupby
19
18
  from logging import DEBUG, WARNING
20
19
  from math import inf
@@ -295,11 +294,10 @@ def find_stylesheets(wrapper_element, device_media_type, url_fetcher, base_url,
295
294
  if href is not None:
296
295
  try:
297
296
  yield CSS(
298
- url=href, url_fetcher=url_fetcher,
299
- _check_mime_type=True, media_type=device_media_type,
297
+ url=href, url_fetcher=url_fetcher, media_type=device_media_type,
300
298
  font_config=font_config, counter_style=counter_style,
301
299
  color_profiles=color_profiles, page_rules=page_rules,
302
- layers=layers)
300
+ layers=layers, _check_mime_type=True)
303
301
  except URLFetchingError as exception:
304
302
  LOGGER.error('Failed to load stylesheet at %s: %s', href, exception)
305
303
  LOGGER.debug('Error while loading stylesheet:', exc_info=exception)
@@ -1645,13 +1643,9 @@ def preprocess_stylesheet(device_media_type, base_url, stylesheet_rules, url_fet
1645
1643
  rule.source_column)
1646
1644
  continue
1647
1645
 
1648
- with fetch(url_fetcher, descriptors['src'][1]) as result:
1649
- if 'string' in result:
1650
- file_object = BytesIO(result['string'])
1651
- else:
1652
- file_object = result['file_obj']
1646
+ with fetch(url_fetcher, descriptors['src'][1]) as response:
1653
1647
  try:
1654
- color_profile = ColorProfile(file_object, descriptors)
1648
+ color_profile = ColorProfile(response, descriptors)
1655
1649
  except BaseException:
1656
1650
  LOGGER.warning(
1657
1651
  'Invalid profile file for profile named %r, the whole '
@@ -68,6 +68,11 @@ def check_attr(token, allowed_type=None):
68
68
  name_and_type, fallback = parts[0], ''
69
69
  elif len(parts) == 2:
70
70
  name_and_type, fallback = parts
71
+ # TODO: support fallbacks with multiple tokens and follow type.
72
+ if len(fallback) >= 1 and fallback[0].type == 'string':
73
+ fallback = fallback[0].value
74
+ else:
75
+ fallback = ''
71
76
  else:
72
77
  return
73
78
 
@@ -124,7 +124,7 @@ dir, menu, ul { list-style-type: disc }
124
124
  :is(dir, menu, ol, ul) ul { list-style-type: circle }
125
125
  :is(dir, menu, ol, ul) :is(dir, menu, ol, ul) ul { list-style-type: square }
126
126
 
127
- ::marker { font-variant-numeric: tabular-nums }
127
+ ::marker { font-variant-numeric: tabular-nums; white-space: pre; text-transform: none }
128
128
 
129
129
  [dir=ltr i] { direction: ltr }
130
130
  [dir=rtl i] { direction: rtl }
weasyprint/css/tokens.py CHANGED
@@ -432,7 +432,10 @@ def get_angle(token):
432
432
  token = resolve_math(token) or token
433
433
  except (PercentageInMath, FontUnitInMath):
434
434
  return
435
- if token.type == 'dimension':
435
+ if token.type == 'number' and token.value == 0:
436
+ # Legacy syntax: https://drafts.csswg.org/css-values-4/#angles.
437
+ return 0
438
+ elif token.type == 'dimension':
436
439
  factor = ANGLE_TO_RADIANS.get(token.unit.lower())
437
440
  if factor is not None:
438
441
  return token.value * factor
@@ -137,7 +137,7 @@ def other_colors(token):
137
137
  def outline_color(token):
138
138
  if get_keyword(token) == 'invert':
139
139
  return 'currentcolor'
140
- else:
140
+ elif parse_color(token):
141
141
  return token
142
142
 
143
143
 
@@ -161,7 +161,7 @@ def color(token):
161
161
  result = parse_color(token)
162
162
  if result == 'currentcolor':
163
163
  return 'inherit'
164
- else:
164
+ elif result:
165
165
  return token
166
166
 
167
167
 
@@ -849,7 +849,7 @@ def font_feature_settings(tokens):
849
849
  tokens, token = tokens[:-1], tokens[-1]
850
850
  if token.type == 'ident':
851
851
  value = {'on': 1, 'off': 0}.get(token.value)
852
- elif number := get_number(token, negative=False):
852
+ elif number := get_number(token, negative=False, integer=True):
853
853
  value = number.value
854
854
  elif len(tokens) == 1:
855
855
  value = 1
@@ -1211,7 +1211,7 @@ def text_decoration_thickness(token):
1211
1211
  """``text-decoration-thickness`` property validation."""
1212
1212
  if length := get_length(token, percentage=True):
1213
1213
  return length
1214
- elif keyword := get_keyword(token) in ('auto', 'from-font'):
1214
+ elif (keyword := get_keyword(token)) in ('auto', 'from-font'):
1215
1215
  return keyword
1216
1216
 
1217
1217