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
@@ -7,17 +7,17 @@ See https://www.w3.org/TR/CSS21/propidx.html and various CSS3 modules.
7
7
  from math import inf
8
8
 
9
9
  from tinycss2 import parse_component_value_list
10
- from tinycss2.color4 import parse_color
10
+ from tinycss2.color5 import parse_color
11
11
 
12
12
  from .. import computed_values
13
+ from ..functions import Function, check_var
13
14
  from ..properties import KNOWN_PROPERTIES, ZERO_PIXELS, Dimension
14
15
 
15
- from ..utils import ( # isort:skip
16
- InvalidValues, Pending, check_var_function, comma_separated_list,
17
- get_angle, get_content_list, get_content_list_token, get_custom_ident,
18
- get_image, get_keyword, get_length, get_resolution, get_single_keyword,
19
- get_url, parse_2d_position, parse_function, parse_position,
20
- remove_whitespace, single_keyword, single_token)
16
+ from ..tokens import ( # isort:skip
17
+ InvalidValues, Pending, comma_separated_list, get_angle, get_content_list,
18
+ get_content_list_token, get_custom_ident, get_image, get_keyword, get_length,
19
+ get_number, get_percentage, get_resolution, get_single_keyword, get_url,
20
+ parse_2d_position, parse_position, remove_whitespace, single_keyword, single_token)
21
21
 
22
22
  PREFIX = '-weasy-'
23
23
  PROPRIETARY = set()
@@ -94,7 +94,7 @@ def validate_non_shorthand(tokens, name, base_url=None, required=False):
94
94
 
95
95
  function = PROPERTIES[name]
96
96
  for token in tokens:
97
- if check_var_function(token):
97
+ if check_var(token):
98
98
  # Found CSS variable, return pending-substitution values.
99
99
  return ((name, PendingProperty(tokens, name)),)
100
100
 
@@ -128,7 +128,8 @@ def background_attachment(keyword):
128
128
  @property('text-decoration-color')
129
129
  @single_token
130
130
  def other_colors(token):
131
- return parse_color(token)
131
+ if parse_color(token):
132
+ return token
132
133
 
133
134
 
134
135
  @property()
@@ -137,7 +138,7 @@ def outline_color(token):
137
138
  if get_keyword(token) == 'invert':
138
139
  return 'currentcolor'
139
140
  else:
140
- return parse_color(token)
141
+ return token
141
142
 
142
143
 
143
144
  @property()
@@ -161,7 +162,7 @@ def color(token):
161
162
  if result == 'currentcolor':
162
163
  return 'inherit'
163
164
  else:
164
- return result
165
+ return token
165
166
 
166
167
 
167
168
  @property('background-image', wants_base_url=True)
@@ -278,8 +279,7 @@ def border_spacing(tokens):
278
279
  @property('border-top-left-radius')
279
280
  def border_corner_radius(tokens):
280
281
  """Validator for the `border-*-radius` properties."""
281
- lengths = [
282
- get_length(token, negative=False, percentage=True) for token in tokens]
282
+ lengths = [get_length(token, negative=False, percentage=True) for token in tokens]
283
283
  if all(lengths):
284
284
  if len(lengths) == 1:
285
285
  return (lengths[0], lengths[0])
@@ -344,12 +344,10 @@ def continue_(keyword):
344
344
  @property(unstable=True)
345
345
  @single_token
346
346
  def max_lines(token):
347
- if token.type == 'number' and token.int_value is not None:
348
- if token.int_value >= 1:
349
- return token.int_value
350
- keyword = get_keyword(token)
351
- if keyword == 'none':
352
- return keyword
347
+ if number := get_number(token, negative=False, integer=True):
348
+ return number.value
349
+ elif get_keyword(token) == 'none':
350
+ return 'none'
353
351
 
354
352
 
355
353
  @property(unstable=True)
@@ -374,8 +372,7 @@ def page(token):
374
372
  @single_token
375
373
  def bleed(token):
376
374
  """``bleed`` property validation."""
377
- keyword = get_keyword(token)
378
- if keyword == 'auto':
375
+ if get_keyword(token) == 'auto':
379
376
  return 'auto'
380
377
  else:
381
378
  return get_length(token)
@@ -389,8 +386,7 @@ def marks(tokens):
389
386
  if 'crop' in keywords and 'cross' in keywords:
390
387
  return keywords
391
388
  elif len(tokens) == 1:
392
- keyword = get_keyword(tokens[0])
393
- if keyword in ('crop', 'cross'):
389
+ if (keyword := get_keyword(tokens[0])) in ('crop', 'cross'):
394
390
  return (keyword,)
395
391
  elif keyword == 'none':
396
392
  return ()
@@ -413,11 +409,9 @@ def outline_style(keyword):
413
409
  @single_token
414
410
  def border_width(token):
415
411
  """Border, column rule and outline widths properties validation."""
416
- length = get_length(token, negative=False)
417
- if length:
412
+ if length := get_length(token, negative=False):
418
413
  return length
419
- keyword = get_keyword(token)
420
- if keyword in ('thin', 'medium', 'thick'):
414
+ if (keyword := get_keyword(token)) in ('thin', 'medium', 'thick'):
421
415
  return keyword
422
416
 
423
417
 
@@ -437,13 +431,13 @@ def border_image_slice(tokens):
437
431
  fill = False
438
432
  for i, token in enumerate(tokens):
439
433
  # Don't use get_length() because a dimension with a unit is disallowed.
440
- if token.type == 'percentage' and token.value >= 0:
441
- values.append(Dimension(token.value, '%'))
442
- elif token.type == 'number' and token.value >= 0:
443
- values.append(Dimension(token.value, None))
434
+ if percentage := get_percentage(token, negative=False):
435
+ values.append(percentage)
444
436
  elif get_keyword(token) == 'fill' and not fill and i in (0, len(tokens) - 1):
445
437
  fill = True
446
438
  values.append('fill')
439
+ elif number := get_number(token, negative=False):
440
+ values.append(number)
447
441
  else:
448
442
  return
449
443
 
@@ -458,13 +452,12 @@ def border_image_width(tokens):
458
452
  for token in tokens:
459
453
  if get_keyword(token) == 'auto':
460
454
  values.append('auto')
461
- elif token.type == 'number' and token.value >= 0:
462
- values.append(Dimension(token.value, None))
455
+ elif number := get_number(token, negative=False):
456
+ values.append(number)
457
+ elif length := get_length(token, negative=False, percentage=True):
458
+ values.append(length)
463
459
  else:
464
- if length := get_length(token, negative=False, percentage=True):
465
- values.append(length)
466
- else:
467
- return
460
+ return
468
461
 
469
462
  if 1 <= len(values) <= 4:
470
463
  return tuple(values)
@@ -475,13 +468,12 @@ def border_image_width(tokens):
475
468
  def border_image_outset(tokens):
476
469
  values = []
477
470
  for token in tokens:
478
- if token.type == 'number' and token.value >= 0:
479
- values.append(Dimension(token.value, None))
471
+ if number := get_number(token, negative=False):
472
+ values.append(number)
473
+ elif length := get_length(token, negative=False):
474
+ values.append(length)
480
475
  else:
481
- if length := get_length(token, negative=False):
482
- values.append(length)
483
- else:
484
- return
476
+ return
485
477
 
486
478
  if 1 <= len(values) <= 4:
487
479
  return tuple(values)
@@ -506,8 +498,7 @@ def mask_border_mode(keyword):
506
498
  @single_token
507
499
  def column_width(token):
508
500
  """``column-width`` property validation."""
509
- length = get_length(token, negative=False)
510
- if length:
501
+ if length := get_length(token, negative=False):
511
502
  return length
512
503
  keyword = get_keyword(token)
513
504
  if keyword == 'auto':
@@ -546,21 +537,19 @@ def clear(keyword):
546
537
  @single_token
547
538
  def clip(token):
548
539
  """Validation for the ``clip`` property."""
549
- function = parse_function(token)
550
- if function:
551
- name, args = function
552
- if name == 'rect' and len(args) == 4:
553
- values = []
554
- for arg in args:
555
- if get_keyword(arg) == 'auto':
556
- values.append('auto')
557
- else:
558
- length = get_length(arg)
559
- if length:
560
- values.append(length)
561
- if len(values) == 4:
562
- return tuple(values)
563
- if get_keyword(token) == 'auto':
540
+ function = Function(token)
541
+ arguments = function.split_comma()
542
+ if function.name == 'rect' and len(arguments) == 4:
543
+ values = []
544
+ for argument in arguments:
545
+ if get_keyword(argument) == 'auto':
546
+ values.append('auto')
547
+ elif length := get_length(argument):
548
+ values.append(length)
549
+ else:
550
+ return
551
+ return tuple(values)
552
+ elif get_keyword(token) == 'auto':
564
553
  return ()
565
554
 
566
555
 
@@ -571,12 +560,9 @@ def content(tokens, base_url):
571
560
  tokens = list(tokens)
572
561
  parsed_tokens = []
573
562
  while tokens:
574
- if len(tokens) >= 2 and (
575
- tokens[1].type == 'literal' and tokens[1].value == ','):
563
+ if len(tokens) >= 2 and tokens[1].type == 'literal' and tokens[1].value == ',':
576
564
  token, tokens = tokens[0], tokens[2:]
577
- parsed_token = (
578
- get_image(token, base_url) or get_url(token, base_url))
579
- if parsed_token:
565
+ if parsed_token := get_image(token, base_url) or get_url(token, base_url):
580
566
  parsed_tokens.append(parsed_token)
581
567
  else:
582
568
  return
@@ -627,14 +613,13 @@ def counter(tokens, default_integer):
627
613
  if counter_name in ('none', 'initial', 'inherit'):
628
614
  raise InvalidValues(f'Invalid counter name: {counter_name}')
629
615
  token = next(tokens, None)
630
- if token is not None and (
631
- token.type == 'number' and token.int_value is not None):
632
- # Found an integer. Use it and get the next token
633
- integer = token.int_value
616
+ if token and (number := get_number(token, integer=True)):
617
+ # Found an integer. Use it and get the next token.
618
+ integer = number.value
634
619
  token = next(tokens, None)
635
620
  else:
636
- # Not an integer. Might be the next counter name.
637
- # Keep `token` for the next loop iteration.
621
+ # Not an integer. Might be the next counter name. Keep `token` for the next
622
+ # loop iteration.
638
623
  integer = default_integer
639
624
  results.append((counter_name, integer))
640
625
  return tuple(results)
@@ -652,8 +637,7 @@ def counter(tokens, default_integer):
652
637
  @single_token
653
638
  def lenght_precentage_or_auto(token):
654
639
  """``margin-*`` and various other properties validation."""
655
- length = get_length(token, percentage=True)
656
- if length:
640
+ if length := get_length(token, percentage=True):
657
641
  return length
658
642
  if get_keyword(token) == 'auto':
659
643
  return 'auto'
@@ -664,8 +648,7 @@ def lenght_precentage_or_auto(token):
664
648
  @single_token
665
649
  def width_height(token):
666
650
  """Validation for the ``width`` and ``height`` properties."""
667
- length = get_length(token, negative=False, percentage=True)
668
- if length:
651
+ if length := get_length(token, negative=False, percentage=True):
669
652
  return length
670
653
  if get_keyword(token) == 'auto':
671
654
  return 'auto'
@@ -676,8 +659,7 @@ def width_height(token):
676
659
  @single_token
677
660
  def gap(token):
678
661
  """Validation for the ``column-gap`` and ``row-gap`` properties."""
679
- length = get_length(token, percentage=True, negative=False)
680
- if length:
662
+ if length := get_length(token, percentage=True, negative=False):
681
663
  return length
682
664
  keyword = get_keyword(token)
683
665
  if keyword == 'normal':
@@ -867,9 +849,8 @@ def font_feature_settings(tokens):
867
849
  tokens, token = tokens[:-1], tokens[-1]
868
850
  if token.type == 'ident':
869
851
  value = {'on': 1, 'off': 0}.get(token.value)
870
- elif (token.type == 'number' and
871
- token.int_value is not None and token.int_value >= 0):
872
- value = token.int_value
852
+ elif number := get_number(token, negative=False):
853
+ value = number.value
873
854
  elif len(tokens) == 1:
874
855
  value = 1
875
856
 
@@ -943,8 +924,7 @@ def font_variation_settings(tokens):
943
924
  @single_token
944
925
  def font_size(token):
945
926
  """``font-size`` property validation."""
946
- length = get_length(token, negative=False, percentage=True)
947
- if length:
927
+ if length := get_length(token, negative=False, percentage=True):
948
928
  return length
949
929
  font_size_keyword = get_keyword(token)
950
930
  if font_size_keyword in ('smaller', 'larger'):
@@ -1005,8 +985,7 @@ def spacing(token):
1005
985
  """Validation for ``letter-spacing`` and ``word-spacing``."""
1006
986
  if get_keyword(token) == 'normal':
1007
987
  return 'normal'
1008
- length = get_length(token)
1009
- if length:
988
+ if length := get_length(token):
1010
989
  return length
1011
990
 
1012
991
 
@@ -1014,8 +993,7 @@ def spacing(token):
1014
993
  @single_token
1015
994
  def outline_offset(token):
1016
995
  """Validation for ``outline-offset``."""
1017
- length = get_length(token)
1018
- if length:
996
+ if length := get_length(token):
1019
997
  return length
1020
998
 
1021
999
 
@@ -1025,12 +1003,10 @@ def line_height(token):
1025
1003
  """``line-height`` property validation."""
1026
1004
  if get_keyword(token) == 'normal':
1027
1005
  return 'normal'
1028
- if token.type == 'number' and token.value >= 0:
1029
- return Dimension(token.value, None)
1030
- if token.type == 'percentage' and token.value >= 0:
1031
- return Dimension(token.value, '%')
1032
- elif token.type == 'dimension' and token.value >= 0:
1033
- return get_length(token)
1006
+ elif number := get_number(token, negative=False):
1007
+ return number
1008
+ elif length := get_length(token, negative=False, percentage=True):
1009
+ return length
1034
1010
 
1035
1011
 
1036
1012
  @property()
@@ -1049,30 +1025,29 @@ def list_style_type(token):
1049
1025
  elif token.type == 'string':
1050
1026
  return ('string', token.value)
1051
1027
  elif token.type == 'function' and token.name == 'symbols':
1052
- allowed_types = (
1053
- 'cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')
1054
- function_arguments = remove_whitespace(token.arguments)
1055
- if len(function_arguments) >= 1:
1056
- arguments = []
1057
- if function_arguments[0].type == 'ident':
1058
- if function_arguments[0].value in allowed_types:
1059
- index = 1
1060
- arguments.append(function_arguments[0].value)
1061
- else:
1062
- return
1028
+ allowed_types = ('cyclic', 'numeric', 'alphabetic', 'symbolic', 'fixed')
1029
+ if not (function_arguments := remove_whitespace(token.arguments)):
1030
+ return
1031
+ arguments = []
1032
+ if function_arguments[0].type == 'ident':
1033
+ if function_arguments[0].value in allowed_types:
1034
+ index = 1
1035
+ arguments.append(function_arguments[0].value)
1063
1036
  else:
1064
- arguments.append('symbolic')
1065
- index = 0
1066
- if len(function_arguments) < index + 1:
1067
1037
  return
1068
- for i in range(index, len(function_arguments)):
1069
- if function_arguments[i].type != 'string':
1070
- return
1071
- arguments.append(function_arguments[i].value)
1072
- if arguments[0] in ('alphabetic', 'numeric'):
1073
- if len(arguments) < 3:
1074
- return
1075
- return ('symbols()', tuple(arguments))
1038
+ else:
1039
+ arguments.append('symbolic')
1040
+ index = 0
1041
+ if len(function_arguments) < index + 1:
1042
+ return
1043
+ for i in range(index, len(function_arguments)):
1044
+ if function_arguments[i].type != 'string':
1045
+ return
1046
+ arguments.append(function_arguments[i].value)
1047
+ if arguments[0] in ('alphabetic', 'numeric'):
1048
+ if len(arguments) < 3:
1049
+ return
1050
+ return ('symbols()', tuple(arguments))
1076
1051
 
1077
1052
 
1078
1053
  @property('min-width')
@@ -1081,9 +1056,8 @@ def list_style_type(token):
1081
1056
  def min_width_height(token):
1082
1057
  """``min-width`` and ``min-height`` properties validation."""
1083
1058
  # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
1084
- keyword = get_keyword(token)
1085
- if keyword == 'auto':
1086
- return keyword
1059
+ if get_keyword(token) == 'auto':
1060
+ return 'auto'
1087
1061
  else:
1088
1062
  return length_or_precentage([token])
1089
1063
 
@@ -1095,8 +1069,7 @@ def min_width_height(token):
1095
1069
  @single_token
1096
1070
  def length_or_precentage(token):
1097
1071
  """``padding-*`` properties validation."""
1098
- length = get_length(token, negative=False, percentage=True)
1099
- if length:
1072
+ if length := get_length(token, negative=False, percentage=True):
1100
1073
  return length
1101
1074
 
1102
1075
 
@@ -1105,8 +1078,7 @@ def length_or_precentage(token):
1105
1078
  @single_token
1106
1079
  def max_width_height(token):
1107
1080
  """Validation for max-width and max-height"""
1108
- length = get_length(token, negative=False, percentage=True)
1109
- if length:
1081
+ if length := get_length(token, negative=False, percentage=True):
1110
1082
  return length
1111
1083
  if get_keyword(token) == 'none':
1112
1084
  return Dimension(inf, 'px')
@@ -1116,10 +1088,10 @@ def max_width_height(token):
1116
1088
  @single_token
1117
1089
  def opacity(token):
1118
1090
  """Validation for the ``opacity`` property."""
1119
- if token.type == 'number':
1120
- return min(1, max(0, token.value))
1121
- if token.type == 'percentage':
1122
- return min(1, max(0, token.value / 100))
1091
+ if number := get_number(token):
1092
+ return min(1, max(0, number.value))
1093
+ elif percentage := get_percentage(token):
1094
+ return min(1, max(0, percentage.value / 100))
1123
1095
 
1124
1096
 
1125
1097
  @property()
@@ -1128,8 +1100,8 @@ def z_index(token):
1128
1100
  """Validation for the ``z-index`` property."""
1129
1101
  if get_keyword(token) == 'auto':
1130
1102
  return 'auto'
1131
- if token.type == 'number' and token.int_value is not None:
1132
- return token.int_value
1103
+ elif number := get_number(token, integer=True):
1104
+ return number.value
1133
1105
 
1134
1106
 
1135
1107
  @property('orphans')
@@ -1137,21 +1109,19 @@ def z_index(token):
1137
1109
  @single_token
1138
1110
  def orphans_widows(token):
1139
1111
  """Validation for the ``orphans`` and ``widows`` properties."""
1140
- if token.type == 'number' and token.int_value is not None:
1141
- value = token.int_value
1142
- if value >= 1:
1143
- return value
1112
+ if number := get_number(token, negative=False, integer=True):
1113
+ if number.value >= 1:
1114
+ return number.value
1144
1115
 
1145
1116
 
1146
1117
  @property(unstable=True)
1147
1118
  @single_token
1148
1119
  def column_count(token):
1149
1120
  """Validation for the ``column-count`` property."""
1150
- if token.type == 'number' and token.int_value is not None:
1151
- value = token.int_value
1152
- if value >= 1:
1153
- return value
1154
- if get_keyword(token) == 'auto':
1121
+ if number := get_number(token, negative=False, integer=True):
1122
+ if number.value >= 1:
1123
+ return number.value
1124
+ elif get_keyword(token) == 'auto':
1155
1125
  return 'auto'
1156
1126
 
1157
1127
 
@@ -1214,15 +1184,13 @@ def text_align_all(keyword):
1214
1184
  @single_keyword
1215
1185
  def text_align_last(keyword):
1216
1186
  """``text-align-last`` property validation."""
1217
- return keyword in (
1218
- 'auto', 'left', 'right', 'center', 'justify', 'start', 'end')
1187
+ return keyword in ('auto', 'left', 'right', 'center', 'justify', 'start', 'end')
1219
1188
 
1220
1189
 
1221
1190
  @property()
1222
1191
  def text_decoration_line(tokens):
1223
1192
  """``text-decoration-line`` property validation."""
1224
- keywords = {get_keyword(token) for token in tokens}
1225
- if keywords == {'none'}:
1193
+ if (keywords := {get_keyword(token) for token in tokens}) == {'none'}:
1226
1194
  return 'none'
1227
1195
  allowed_values = {'underline', 'overline', 'line-through', 'blink'}
1228
1196
  if len(tokens) == len(keywords) and keywords.issubset(allowed_values):
@@ -1241,10 +1209,9 @@ def text_decoration_style(keyword):
1241
1209
  @single_token
1242
1210
  def text_decoration_thickness(token):
1243
1211
  """``text-decoration-thickness`` property validation."""
1244
- length = get_length(token, percentage=True)
1245
- if length:
1212
+ if length := get_length(token, percentage=True):
1246
1213
  return length
1247
- if keyword := get_keyword(token) in ('auto', 'from-font'):
1214
+ elif keyword := get_keyword(token) in ('auto', 'from-font'):
1248
1215
  return keyword
1249
1216
 
1250
1217
 
@@ -1252,8 +1219,7 @@ def text_decoration_thickness(token):
1252
1219
  @single_token
1253
1220
  def text_indent(token):
1254
1221
  """``text-indent`` property validation."""
1255
- length = get_length(token, percentage=True)
1256
- if length:
1222
+ if length := get_length(token, percentage=True):
1257
1223
  return length
1258
1224
 
1259
1225
 
@@ -1261,16 +1227,14 @@ def text_indent(token):
1261
1227
  @single_keyword
1262
1228
  def text_transform(keyword):
1263
1229
  """``text-align`` property validation."""
1264
- return keyword in (
1265
- 'none', 'uppercase', 'lowercase', 'capitalize', 'full-width')
1230
+ return keyword in ('none', 'uppercase', 'lowercase', 'capitalize', 'full-width')
1266
1231
 
1267
1232
 
1268
1233
  @property()
1269
1234
  @single_token
1270
1235
  def vertical_align(token):
1271
1236
  """Validation for the ``vertical-align`` property"""
1272
- length = get_length(token, percentage=True)
1273
- if length:
1237
+ if length := get_length(token, percentage=True):
1274
1238
  return length
1275
1239
  keyword = get_keyword(token)
1276
1240
  if keyword in ('baseline', 'middle', 'sub', 'super',
@@ -1310,10 +1274,9 @@ def word_break(keyword):
1310
1274
  @single_token
1311
1275
  def flex_basis(token):
1312
1276
  """``flex-basis`` property validation."""
1313
- basis = width_height([token])
1314
- if basis is not None:
1277
+ if (basis := width_height([token])) is not None:
1315
1278
  return basis
1316
- if get_keyword(token) == 'content':
1279
+ elif get_keyword(token) == 'content':
1317
1280
  return 'content'
1318
1281
 
1319
1282
 
@@ -1328,77 +1291,68 @@ def flex_direction(keyword):
1328
1291
  @property('flex-shrink')
1329
1292
  @single_token
1330
1293
  def flex_grow_shrink(token):
1331
- if token.type == 'number':
1332
- return token.value
1294
+ if number := get_number(token):
1295
+ return number.value
1333
1296
 
1334
1297
 
1335
1298
  def _inflexible_breadth(token):
1336
1299
  """Parse ``inflexible-breadth``."""
1337
- keyword = get_keyword(token)
1338
- if keyword in ('auto', 'min-content', 'max-content'):
1300
+ if (keyword := get_keyword(token)) in ('auto', 'min-content', 'max-content'):
1339
1301
  return keyword
1340
1302
  elif keyword:
1341
1303
  return
1342
- length = get_length(token, negative=False, percentage=True)
1343
- if length:
1304
+ elif length := get_length(token, negative=False, percentage=True):
1344
1305
  return length
1345
1306
 
1346
1307
 
1347
1308
  def _track_breadth(token):
1348
1309
  """Parse ``track-breadth``."""
1349
- if token.type == 'dimension' and token.value >= 0 and token.unit == 'fr':
1350
- return Dimension(token.value, token.unit)
1310
+ if token.type == 'dimension' and token.value >= 0 and token.unit.lower() == 'fr':
1311
+ return Dimension(token.value, token.unit.lower())
1351
1312
  return _inflexible_breadth(token)
1352
1313
 
1353
1314
 
1354
1315
  def _track_size(token):
1355
1316
  """Parse ``track-size``."""
1356
- track_breadth = _track_breadth(token)
1357
- if track_breadth:
1317
+ if track_breadth := _track_breadth(token):
1358
1318
  return track_breadth
1359
- function = parse_function(token)
1360
- if function:
1361
- name, args = function
1362
- if name == 'minmax':
1363
- if len(args) == 2:
1364
- inflexible_breadth = _inflexible_breadth(args[0])
1365
- track_breadth = _track_breadth(args[1])
1366
- if inflexible_breadth and track_breadth:
1367
- return ('minmax()', inflexible_breadth, track_breadth)
1368
- elif name == 'fit-content':
1369
- if len(args) == 1:
1370
- length = get_length(args[0], negative=False, percentage=True)
1371
- if length:
1372
- return ('fit-content()', length)
1319
+ function = Function(token)
1320
+ arguments = function.split_comma()
1321
+ if function.name == 'minmax':
1322
+ if len(arguments) == 2:
1323
+ inflexible_breadth = _inflexible_breadth(arguments[0])
1324
+ track_breadth = _track_breadth(arguments[1])
1325
+ if inflexible_breadth and track_breadth:
1326
+ return ('minmax()', inflexible_breadth, track_breadth)
1327
+ elif function.name == 'fit-content':
1328
+ if len(arguments) == 1:
1329
+ if length := get_length(arguments[0], negative=False, percentage=True):
1330
+ return ('fit-content()', length)
1373
1331
 
1374
1332
 
1375
1333
  def _fixed_size(token):
1376
1334
  """Parse ``fixed-size``."""
1377
- length = get_length(token, negative=False, percentage=True)
1378
- if length:
1335
+ if length := get_length(token, negative=False, percentage=True):
1379
1336
  return length
1380
- function = parse_function(token)
1381
- if function:
1382
- name, args = function
1383
- if name == 'minmax' and len(args) == 2:
1384
- length = get_length(args[0], negative=False, percentage=True)
1385
- if length:
1386
- track_breadth = _track_breadth(args[1])
1387
- if track_breadth:
1388
- return ('minmax()', length, track_breadth)
1389
- keyword = get_keyword(args[0])
1390
- if keyword in ('min-content', 'max-content', 'auto') or length:
1391
- fixed_breadth = get_length(
1392
- args[1], negative=False, percentage=True)
1393
- if fixed_breadth:
1394
- return ('minmax()', length or keyword, fixed_breadth)
1395
-
1396
-
1397
- def _line_names(arg):
1337
+ function = Function(token)
1338
+ arguments = function.split_comma()
1339
+ if function.name == 'minmax' and len(arguments) == 2:
1340
+ if length := get_length(arguments[0], negative=False, percentage=True):
1341
+ track_breadth = _track_breadth(arguments[1])
1342
+ if track_breadth:
1343
+ return ('minmax()', length, track_breadth)
1344
+ keyword = get_keyword(arguments[0])
1345
+ if keyword in ('min-content', 'max-content', 'auto') or length:
1346
+ fixed_breadth = get_length(arguments[1], negative=False, percentage=True)
1347
+ if fixed_breadth:
1348
+ return ('minmax()', length or keyword, fixed_breadth)
1349
+
1350
+
1351
+ def _line_names(token):
1398
1352
  """Parse ``line-names``."""
1399
1353
  return_line_names = []
1400
- if arg.type == '[] block':
1401
- for token in arg.content:
1354
+ if token.type == '[] block':
1355
+ for token in token.content:
1402
1356
  if token.type == 'ident':
1403
1357
  return_line_names.append(token.value)
1404
1358
  elif token.type != 'whitespace':
@@ -1412,8 +1366,7 @@ def grid_auto(tokens):
1412
1366
  """``grid-auto-columns`` and ``grid-auto-rows`` properties validation."""
1413
1367
  return_tokens = []
1414
1368
  for token in tokens:
1415
- track_size = _track_size(token)
1416
- if track_size:
1369
+ if track_size := _track_size(token):
1417
1370
  return_tokens.append(track_size)
1418
1371
  continue
1419
1372
  return
@@ -1450,25 +1403,29 @@ def grid_template(tokens):
1450
1403
  if line_names is not None:
1451
1404
  subgrid_tokens.append(line_names)
1452
1405
  continue
1453
- function = parse_function(token)
1454
- if function:
1455
- name, args = function
1456
- if name == 'repeat' and len(args) >= 2:
1457
- if (args[0].type == 'number' and
1458
- args[0].is_integer and args[0].value >= 1):
1459
- number = args[0].int_value
1460
- elif get_keyword(args[0]) == 'auto-fill':
1461
- number = 'auto-fill'
1462
- else:
1463
- return
1464
- line_names_list = []
1465
- for arg in args[1:]:
1466
- line_names = _line_names(arg)
1467
- if line_names is not None:
1468
- line_names_list.append(line_names)
1469
- subgrid_tokens.append(
1470
- ('repeat()', number, tuple(line_names_list)))
1471
- continue
1406
+ function = Function(token)
1407
+ arguments = function.split_comma(single_tokens=False)
1408
+ if arguments is None or len(arguments) != 2:
1409
+ return
1410
+ repeat, tracks = arguments
1411
+ if len(repeat) != 1 or not tracks:
1412
+ return
1413
+ repeat, = repeat
1414
+ if function.name == 'repeat' and len(arguments) >= 2:
1415
+ if (repeat.type == 'number' and float(repeat.value).is_integer() and
1416
+ repeat.value >= 1):
1417
+ number = repeat.int_value
1418
+ elif get_keyword(repeat) == 'auto-fill':
1419
+ number = 'auto-fill'
1420
+ else:
1421
+ return
1422
+ line_names_list = []
1423
+ for argument in tracks:
1424
+ line_names = _line_names(argument)
1425
+ if line_names is not None:
1426
+ line_names_list.append(line_names)
1427
+ subgrid_tokens.append(('repeat()', number, tuple(line_names_list)))
1428
+ continue
1472
1429
  return
1473
1430
  return_tokens.append(tuple(subgrid_tokens))
1474
1431
  else:
@@ -1498,57 +1455,61 @@ def grid_template(tokens):
1498
1455
  return_tokens.append(track_size)
1499
1456
  includes_track = True
1500
1457
  continue
1501
- function = parse_function(token)
1502
- if function:
1503
- name, args = function
1504
- if name == 'repeat' and len(args) >= 2:
1505
- if (args[0].type == 'number' and
1506
- args[0].is_integer and args[0].value >= 1):
1507
- number = args[0].int_value
1508
- elif get_keyword(args[0]) in ('auto-fill', 'auto-fit'):
1509
- # auto-repeat
1510
- if includes_auto_repeat:
1511
- return
1512
- number = args[0].value
1513
- includes_auto_repeat = True
1514
- else:
1515
- return
1516
- names_and_sizes = []
1517
- repeat_last_is_line_name = False
1518
- for arg in args[1:]:
1519
- line_names = _line_names(arg)
1520
- if line_names is not None:
1521
- if repeat_last_is_line_name:
1522
- return
1523
- names_and_sizes.append(line_names)
1524
- repeat_last_is_line_name = True
1525
- continue
1526
- # fixed-repead
1527
- fixed_size = _fixed_size(arg)
1528
- if fixed_size:
1529
- if not repeat_last_is_line_name:
1530
- names_and_sizes.append(())
1531
- repeat_last_is_line_name = False
1532
- names_and_sizes.append(fixed_size)
1533
- continue
1534
- # track-repeat
1535
- track_size = _track_size(arg)
1536
- if track_size:
1537
- includes_track = True
1538
- if not repeat_last_is_line_name:
1539
- names_and_sizes.append(())
1540
- repeat_last_is_line_name = False
1541
- names_and_sizes.append(track_size)
1542
- continue
1458
+ function = Function(token)
1459
+ arguments = function.split_comma(single_tokens=False)
1460
+ if arguments is None or len(arguments) != 2:
1461
+ return
1462
+ repeat, tracks = arguments
1463
+ if len(repeat) != 1 or not tracks:
1464
+ return
1465
+ repeat, = repeat
1466
+ if function.name == 'repeat' and len(arguments) >= 2:
1467
+ if number := get_number(repeat, negative=False, integer=True):
1468
+ if number.value >= 1:
1469
+ number = number.value
1470
+ elif get_keyword(repeat) in ('auto-fill', 'auto-fit'):
1471
+ # auto-repeat
1472
+ if includes_auto_repeat:
1543
1473
  return
1544
- if not last_is_line_name:
1545
- return_tokens.append(())
1546
- last_is_line_name = False
1547
- if not repeat_last_is_line_name:
1548
- names_and_sizes.append(())
1549
- return_tokens.append(
1550
- ('repeat()', number, tuple(names_and_sizes)))
1551
- continue
1474
+ number = repeat.value
1475
+ includes_auto_repeat = True
1476
+ else:
1477
+ return
1478
+ names_and_sizes = []
1479
+ repeat_last_is_line_name = False
1480
+ for arg in tracks:
1481
+ line_names = _line_names(arg)
1482
+ if line_names is not None:
1483
+ if repeat_last_is_line_name:
1484
+ return
1485
+ names_and_sizes.append(line_names)
1486
+ repeat_last_is_line_name = True
1487
+ continue
1488
+ # fixed-repeat
1489
+ fixed_size = _fixed_size(arg)
1490
+ if fixed_size:
1491
+ if not repeat_last_is_line_name:
1492
+ names_and_sizes.append(())
1493
+ repeat_last_is_line_name = False
1494
+ names_and_sizes.append(fixed_size)
1495
+ continue
1496
+ # track-repeat
1497
+ track_size = _track_size(arg)
1498
+ if track_size:
1499
+ includes_track = True
1500
+ if not repeat_last_is_line_name:
1501
+ names_and_sizes.append(())
1502
+ repeat_last_is_line_name = False
1503
+ names_and_sizes.append(track_size)
1504
+ continue
1505
+ return
1506
+ if not last_is_line_name:
1507
+ return_tokens.append(())
1508
+ last_is_line_name = False
1509
+ if not repeat_last_is_line_name:
1510
+ names_and_sizes.append(())
1511
+ return_tokens.append(('repeat()', number, tuple(names_and_sizes)))
1512
+ continue
1552
1513
  return
1553
1514
  if includes_auto_repeat and includes_track:
1554
1515
  return
@@ -1627,8 +1588,9 @@ def grid_line(tokens):
1627
1588
  return keyword
1628
1589
  elif keyword != 'span':
1629
1590
  return (None, None, token.value)
1630
- elif token.type == 'number' and token.is_integer and token.value:
1631
- return (None, token.int_value, None)
1591
+ elif number := get_number(token, integer=True):
1592
+ if number.value != 0:
1593
+ return (None, number.value, None)
1632
1594
  return
1633
1595
  number = ident = span = None
1634
1596
  for token in tokens:
@@ -1642,13 +1604,13 @@ def grid_line(tokens):
1642
1604
  elif keyword and ident is None:
1643
1605
  ident = token.value
1644
1606
  continue
1645
- elif token.type == 'number' and token.is_integer and token.value:
1646
- if number is None:
1647
- number = token.int_value
1607
+ elif item := get_number(token, integer=True):
1608
+ if item.value != 0 and number is None:
1609
+ number = item.value
1648
1610
  continue
1649
1611
  return
1650
1612
  if span:
1651
- if number and number < 0:
1613
+ if isinstance(number, int) and number < 0:
1652
1614
  return
1653
1615
  elif ident or number:
1654
1616
  return (span, number, ident)
@@ -1805,8 +1767,8 @@ def align_content(tokens):
1805
1767
  @property()
1806
1768
  @single_token
1807
1769
  def order(token):
1808
- if token.type == 'number' and token.int_value is not None:
1809
- return token.int_value
1770
+ if number := get_number(token, integer=True):
1771
+ return number.value
1810
1772
 
1811
1773
 
1812
1774
  @property(unstable=True)
@@ -1886,12 +1848,11 @@ def anchor(token):
1886
1848
  """Validation for ``anchor``."""
1887
1849
  if get_keyword(token) == 'none':
1888
1850
  return 'none'
1889
- function = parse_function(token)
1890
- if function:
1891
- name, args = function
1892
- prototype = (name, [arg.type for arg in args])
1851
+ function = Function(token)
1852
+ if arguments := function.split_space():
1853
+ prototype = (function.name, [argument.type for argument in arguments])
1893
1854
  if prototype == ('attr', ['ident']):
1894
- return ('attr()', args[0].value)
1855
+ return ('attr()', arguments[0].value)
1895
1856
 
1896
1857
 
1897
1858
  @property(proprietary=True, wants_base_url=True)
@@ -1903,12 +1864,11 @@ def link(token, base_url):
1903
1864
  parsed_url = get_url(token, base_url)
1904
1865
  if parsed_url:
1905
1866
  return parsed_url
1906
- function = parse_function(token)
1907
- if function:
1908
- name, args = function
1909
- prototype = (name, [arg.type for arg in args])
1867
+ function = Function(token)
1868
+ if arguments := function.split_space():
1869
+ prototype = (function.name, [argument.type for argument in arguments])
1910
1870
  if prototype == ('attr', ['ident']):
1911
- return ('attr()', args[0].value)
1871
+ return ('attr()', arguments[0].value)
1912
1872
 
1913
1873
 
1914
1874
  @property()
@@ -1920,8 +1880,7 @@ def tab_size(token):
1920
1880
 
1921
1881
  """
1922
1882
  if token.type == 'number' and token.int_value is not None:
1923
- value = token.int_value
1924
- if value >= 0:
1883
+ if value := token.int_value:
1925
1884
  return value
1926
1885
  return get_length(token, negative=False)
1927
1886
 
@@ -1930,8 +1889,7 @@ def tab_size(token):
1930
1889
  @single_token
1931
1890
  def hyphens(token):
1932
1891
  """Validation for ``hyphens``."""
1933
- keyword = get_keyword(token)
1934
- if keyword in ('none', 'manual', 'auto'):
1892
+ if (keyword := get_keyword(token)) in ('none', 'manual', 'auto'):
1935
1893
  return keyword
1936
1894
 
1937
1895
 
@@ -1939,8 +1897,7 @@ def hyphens(token):
1939
1897
  @single_token
1940
1898
  def hyphenate_character(token):
1941
1899
  """Validation for ``hyphenate-character``."""
1942
- keyword = get_keyword(token)
1943
- if keyword == 'auto':
1900
+ if get_keyword(token) == 'auto':
1944
1901
  return '‐'
1945
1902
  elif token.type == 'string':
1946
1903
  return token.value
@@ -1961,36 +1918,33 @@ def hyphenate_limit_chars(tokens):
1961
1918
  keyword = get_keyword(token)
1962
1919
  if keyword == 'auto':
1963
1920
  return (5, 2, 2)
1964
- elif token.type == 'number' and token.int_value is not None:
1965
- return (token.int_value, 2, 2)
1921
+ elif number := get_number(token, integer=True):
1922
+ return (number.value, 2, 2)
1966
1923
  elif len(tokens) == 2:
1967
1924
  total, left = tokens
1968
1925
  total_keyword = get_keyword(total)
1969
1926
  left_keyword = get_keyword(left)
1970
- if total.type == 'number' and total.int_value is not None:
1971
- if left.type == 'number' and left.int_value is not None:
1972
- return (total.int_value, left.int_value, left.int_value)
1927
+ if total_number := get_number(total, integer=True):
1928
+ if left_number := get_number(left, integer=True):
1929
+ return (total_number.value, left_number.value, left_number.value)
1973
1930
  elif left_keyword == 'auto':
1974
- return (total.value, 2, 2)
1931
+ return (total_number.value, 2, 2)
1975
1932
  elif total_keyword == 'auto':
1976
- if left.type == 'number' and left.int_value is not None:
1977
- return (5, left.int_value, left.int_value)
1933
+ if left_number := get_number(left, integer=True):
1934
+ return (5, left_number.value, left_number.value)
1978
1935
  elif left_keyword == 'auto':
1979
1936
  return (5, 2, 2)
1980
1937
  elif len(tokens) == 3:
1981
1938
  total, left, right = tokens
1982
- if (
1983
- (get_keyword(total) == 'auto' or
1984
- (total.type == 'number' and total.int_value is not None)) and
1985
- (get_keyword(left) == 'auto' or
1986
- (left.type == 'number' and left.int_value is not None)) and
1987
- (get_keyword(right) == 'auto' or
1988
- (right.type == 'number' and right.int_value is not None))
1989
- ):
1990
- total = total.int_value if total.type == 'number' else 5
1991
- left = left.int_value if left.type == 'number' else 2
1992
- right = right.int_value if right.type == 'number' else 2
1993
- return (total, left, right)
1939
+ result = []
1940
+ for token in total, left, right:
1941
+ if get_keyword(token) == 'auto':
1942
+ result.append(5 if token is total else 2)
1943
+ elif number := get_number(token, integer=True):
1944
+ result.append(number.value)
1945
+ else:
1946
+ return
1947
+ return tuple(result)
1994
1948
 
1995
1949
 
1996
1950
  @property(proprietary=True)
@@ -1999,12 +1953,11 @@ def lang(token):
1999
1953
  """Validation for ``lang``."""
2000
1954
  if get_keyword(token) == 'none':
2001
1955
  return 'none'
2002
- function = parse_function(token)
2003
- if function:
2004
- name, args = function
2005
- prototype = (name, [arg.type for arg in args])
1956
+ function = Function(token)
1957
+ if arguments := function.split_space():
1958
+ prototype = (function.name, [argument.type for argument in arguments])
2006
1959
  if prototype == ('attr', ['ident']):
2007
- return ('attr()', args[0].value)
1960
+ return ('attr()', arguments[0].value)
2008
1961
  elif token.type == 'string':
2009
1962
  return ('string', token.value)
2010
1963
 
@@ -2012,8 +1965,7 @@ def lang(token):
2012
1965
  @property(unstable=True, wants_base_url=True)
2013
1966
  def bookmark_label(tokens, base_url):
2014
1967
  """Validation for ``bookmark-label``."""
2015
- parsed_tokens = tuple(
2016
- get_content_list_token(token, base_url) for token in tokens)
1968
+ parsed_tokens = tuple(get_content_list_token(token, base_url) for token in tokens)
2017
1969
  if None not in parsed_tokens:
2018
1970
  return parsed_tokens
2019
1971
 
@@ -2022,10 +1974,9 @@ def bookmark_label(tokens, base_url):
2022
1974
  @single_token
2023
1975
  def bookmark_level(token):
2024
1976
  """Validation for ``bookmark-level``."""
2025
- if token.type == 'number' and token.int_value is not None:
2026
- value = token.int_value
2027
- if value >= 1:
2028
- return value
1977
+ if number := get_number(token, negative=False, integer=True):
1978
+ if number.value >= 1:
1979
+ return number.value
2029
1980
  elif get_keyword(token) == 'none':
2030
1981
  return 'none'
2031
1982
 
@@ -2076,53 +2027,55 @@ def transform(tokens):
2076
2027
  else:
2077
2028
  transforms = []
2078
2029
  for token in tokens:
2079
- function = parse_function(token)
2080
- if not function:
2030
+ function = Function(token)
2031
+ arguments = function.split_comma()
2032
+ if arguments is None:
2081
2033
  return
2082
- name, args = function
2083
-
2084
- if len(args) == 1:
2085
- angle = get_angle(args[0])
2086
- length = get_length(args[0], percentage=True)
2087
- if name == 'rotate' and angle is not None:
2088
- transforms.append((name, angle))
2089
- elif name in ('skewx', 'skew') and angle is not None:
2034
+
2035
+ all_numbers = {argument.type for argument in arguments} == {'number'}
2036
+ if len(arguments) == 1:
2037
+ angle = get_angle(arguments[0])
2038
+ length = get_length(arguments[0], percentage=True)
2039
+ if function.name == 'rotate' and angle is not None:
2040
+ transforms.append((function.name, angle))
2041
+ elif function.name in ('skewx', 'skew') and angle is not None:
2090
2042
  transforms.append(('skew', (angle, 0)))
2091
- elif name == 'skewy' and angle is not None:
2043
+ elif function.name == 'skewy' and angle is not None:
2092
2044
  transforms.append(('skew', (0, angle)))
2093
- elif name in ('translatex', 'translate') and length:
2045
+ elif function.name in ('translatex', 'translate') and length:
2094
2046
  transforms.append(('translate', (length, ZERO_PIXELS)))
2095
- elif name == 'translatey' and length:
2047
+ elif function.name == 'translatey' and length:
2096
2048
  transforms.append(('translate', (ZERO_PIXELS, length)))
2097
- elif name == 'scalex' and args[0].type == 'number':
2098
- transforms.append(('scale', (args[0].value, 1)))
2099
- elif name == 'scaley' and args[0].type == 'number':
2100
- transforms.append(('scale', (1, args[0].value)))
2101
- elif name == 'scale' and args[0].type == 'number':
2102
- transforms.append(('scale', (args[0].value,) * 2))
2049
+ elif function.name == 'scalex' and all_numbers:
2050
+ transforms.append(('scale', (arguments[0].value, 1)))
2051
+ elif function.name == 'scaley' and all_numbers:
2052
+ transforms.append(('scale', (1, arguments[0].value)))
2053
+ elif function.name == 'scale' and all_numbers:
2054
+ transforms.append(('scale', (arguments[0].value,) * 2))
2103
2055
  else:
2104
2056
  return
2105
- elif len(args) == 2:
2106
- if name == 'scale' and all(a.type == 'number' for a in args):
2107
- transforms.append((name, tuple(arg.value for arg in args)))
2108
- elif name == 'translate':
2057
+ elif len(arguments) == 2:
2058
+ if function.name == 'scale' and all_numbers:
2059
+ values = tuple(argument.value for argument in arguments)
2060
+ transforms.append((function.name, values))
2061
+ elif function.name == 'translate':
2109
2062
  lengths = tuple(
2110
- get_length(token, percentage=True) for token in args)
2063
+ get_length(token, percentage=True) for token in arguments)
2111
2064
  if all(lengths):
2112
- transforms.append((name, lengths))
2065
+ transforms.append((function.name, lengths))
2113
2066
  else:
2114
2067
  return
2115
- elif name == 'skew':
2116
- angles = tuple(get_angle(token) for token in args)
2068
+ elif function.name == 'skew':
2069
+ angles = tuple(get_angle(token) for token in arguments)
2117
2070
  if all(angle is not None for angle in angles):
2118
- transforms.append((name, angles))
2071
+ transforms.append((function.name, angles))
2119
2072
  else:
2120
2073
  return
2121
2074
  else:
2122
2075
  return
2123
- elif len(args) == 6 and name == 'matrix' and all(
2124
- a.type == 'number' for a in args):
2125
- transforms.append((name, tuple(arg.value for arg in args)))
2076
+ elif len(arguments) == 6 and function.name == 'matrix' and all_numbers:
2077
+ transforms.append(
2078
+ (function.name, tuple(argument.value for argument in arguments)))
2126
2079
  else:
2127
2080
  return
2128
2081
  return tuple(transforms)
@@ -2132,6 +2085,37 @@ def transform(tokens):
2132
2085
  @single_token
2133
2086
  def appearance(token):
2134
2087
  """``appearance`` property validation."""
2135
- keyword = get_keyword(token)
2136
- if keyword in ('none', 'auto'):
2088
+ if (keyword := get_keyword(token)) in ('none', 'auto'):
2137
2089
  return keyword
2090
+
2091
+
2092
+ @property()
2093
+ def color_scheme(tokens):
2094
+ """``color-scheme`` property validation."""
2095
+ if len(tokens) == 1:
2096
+ keyword = get_single_keyword(tokens)
2097
+ if keyword == 'normal':
2098
+ return keyword
2099
+ elif keyword != 'only':
2100
+ return (keyword,)
2101
+ else:
2102
+ return
2103
+ else:
2104
+ keywords = []
2105
+ only = False
2106
+ for i, token in enumerate(tokens):
2107
+ keyword = get_keyword(token)
2108
+ if keyword == 'only':
2109
+ if only or i not in (0, len(tokens) - 1):
2110
+ return
2111
+ else:
2112
+ only = True
2113
+ elif keyword == 'normal':
2114
+ return
2115
+ elif keyword:
2116
+ keywords.append(keyword)
2117
+ else:
2118
+ return
2119
+ if only:
2120
+ keywords.append('only')
2121
+ return tuple(keywords)