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
weasyprint/layout/grid.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Layout for grid containers and grid-items."""
2
2
 
3
+ from collections import defaultdict
3
4
  from itertools import count, cycle
4
5
  from math import inf
5
6
 
@@ -12,11 +13,11 @@ from .table import find_in_flow_baseline
12
13
 
13
14
 
14
15
  def _is_length(sizing):
15
- return isinstance(sizing, Dimension) and sizing.unit != 'fr'
16
+ return isinstance(sizing, Dimension) and sizing.unit.lower() != 'fr'
16
17
 
17
18
 
18
19
  def _is_fr(sizing):
19
- return isinstance(sizing, Dimension) and sizing.unit == 'fr'
20
+ return isinstance(sizing, Dimension) and sizing.unit.lower() == 'fr'
20
21
 
21
22
 
22
23
  def _intersect(position_1, size_1, position_2, size_2):
@@ -227,10 +228,9 @@ def _get_template_tracks(tracks):
227
228
  return tracks_list
228
229
 
229
230
 
230
- def _distribute_extra_space(affected_sizes, affected_tracks_types,
231
- size_contribution, tracks_children,
232
- sizing_functions, tracks_sizes, span, direction,
233
- context, containing_block):
231
+ def _distribute_extra_space(affected_sizes, affected_tracks_types, size_contribution,
232
+ tracks_children, sizing_functions, tracks_sizes, span,
233
+ direction, context):
234
234
  assert affected_sizes in ('min', 'max')
235
235
  assert affected_tracks_types in (
236
236
  'intrinsic', 'content-based', 'max-content')
@@ -243,7 +243,14 @@ def _distribute_extra_space(affected_sizes, affected_tracks_types,
243
243
  # 2. Distribute space.
244
244
  affected_tracks = []
245
245
  affected_size_index = 0 if affected_sizes == 'min' else 1
246
- for functions in sizing_functions:
246
+ current_span = 0
247
+ for children, functions in zip(tracks_children, sizing_functions):
248
+ if children:
249
+ current_span = span
250
+ if not current_span:
251
+ affected_tracks.append(False)
252
+ continue
253
+ current_span -= 1
247
254
  function = functions[affected_size_index]
248
255
  if affected_tracks_types == 'intrinsic':
249
256
  if (function in ('min-content', 'max-content', 'auto') or
@@ -262,7 +269,7 @@ def _distribute_extra_space(affected_sizes, affected_tracks_types,
262
269
  for i, children in enumerate(tracks_children):
263
270
  if not children:
264
271
  continue
265
- for item in children:
272
+ for item, parent in children:
266
273
  # 2.1 Find the space distribution.
267
274
  # TODO: Differenciate minimum and min-content values.
268
275
  # TODO: Find a better way to get height.
@@ -278,8 +285,7 @@ def _distribute_extra_space(affected_sizes, affected_tracks_types,
278
285
  item.position_y = 0
279
286
  item, _, _, _, _, _ = block_level_layout(
280
287
  context, item, bottom_space=-inf, skip_stack=None,
281
- containing_block=containing_block, page_is_empty=True,
282
- absolute_boxes=[], fixed_boxes=[])
288
+ containing_block=parent)
283
289
  space = item.margin_height()
284
290
  for sizes in tracks_sizes[i:i+span]:
285
291
  space -= sizes[affected_size_index]
@@ -329,17 +335,21 @@ def _distribute_extra_space(affected_sizes, affected_tracks_types,
329
335
  for k, extra in enumerate(item_incurred_increases):
330
336
  if extra > planned_increases[k]:
331
337
  planned_increases[k] = extra
338
+
332
339
  # 3. Update the tracks’ affected size.
333
- for i, increase in enumerate(planned_increases):
334
- if affected_sizes == 'max' and tracks_sizes[i][1] is inf:
335
- tracks_sizes[i][1] = tracks_sizes[i][0] + increase
340
+ iterator = zip(affected_tracks, tracks_sizes, planned_increases)
341
+ for affected, track_sizes, increase in iterator:
342
+ if not affected:
343
+ continue
344
+ if affected_sizes == 'max' and track_sizes[1] is inf:
345
+ track_sizes[1] = track_sizes[0] + increase
336
346
  else:
337
- tracks_sizes[i][affected_size_index] += increase
347
+ track_sizes[affected_size_index] += increase
338
348
 
339
349
 
340
350
  def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
341
- implicit_start, direction, gap, context,
342
- containing_block, orthogonal_sizes=None):
351
+ implicit_start, direction, gap, context, containing_block,
352
+ orthogonal_sizes=None):
343
353
  assert direction in 'xy'
344
354
  tracks_sizes = []
345
355
  # TODO: Check that auto box size is 0 for percentages.
@@ -348,13 +358,15 @@ def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
348
358
  for min_function, max_function in sizing_functions:
349
359
  base_size = None
350
360
  if _is_length(min_function):
351
- base_size = percentage(min_function, percent_box_size)
361
+ base_size = percentage(
362
+ min_function, containing_block.style, percent_box_size)
352
363
  elif (min_function in ('min-content', 'max-content', 'auto') or
353
364
  min_function[0] == 'fit-content()'):
354
365
  base_size = 0
355
366
  growth_limit = None
356
367
  if _is_length(max_function):
357
- growth_limit = percentage(max_function, percent_box_size)
368
+ growth_limit = percentage(
369
+ max_function, containing_block.style, percent_box_size)
358
370
  elif (max_function in ('min-content', 'max-content', 'auto') or
359
371
  max_function[0] == 'fit-content()' or _is_fr(max_function)):
360
372
  growth_limit = inf
@@ -381,13 +393,12 @@ def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
381
393
  from .block import block_level_layout
382
394
  height = 0
383
395
  for child in children:
384
- x, _, width, _ = children_positions[child]
396
+ x, y, width, _ = children_positions[child]
385
397
  width = sum(orthogonal_sizes[x:x+width])
386
398
  child = child.deepcopy()
387
399
  child.position_x = 0
388
400
  child.position_y = 0
389
- parent = boxes.BlockContainerBox.anonymous_from(
390
- containing_block, ())
401
+ parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
391
402
  resolve_percentages(parent, containing_block)
392
403
  parent.position_x = child.position_x
393
404
  parent.position_y = child.position_y
@@ -396,8 +407,7 @@ def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
396
407
  bottom_space = -inf
397
408
  child, _, _, _, _, _ = block_level_layout(
398
409
  context, child, bottom_space, skip_stack=None,
399
- containing_block=parent, page_is_empty=True,
400
- absolute_boxes=[], fixed_boxes=[])
410
+ containing_block=parent)
401
411
  height = max(height, child.margin_height())
402
412
  if min_function in ('min-content', 'max_content', 'auto'):
403
413
  sizes[0] = height
@@ -442,48 +452,35 @@ def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
442
452
  if _is_fr(max_function):
443
453
  break
444
454
  else:
445
- tracks_children[coord - implicit_start].append(child)
455
+ parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
456
+ resolve_percentages(parent, containing_block)
457
+ if direction == 'y':
458
+ parent.width = sum(orthogonal_sizes[x:x+width])
459
+ tracks_children[coord - implicit_start].append((child, parent))
446
460
  # 1.2.3.1 For intrinsic minimums.
447
461
  # TODO: Respect min-/max-content constraint.
448
462
  _distribute_extra_space(
449
463
  'min', 'intrinsic', 'minimum', tracks_children,
450
- sizing_functions, tracks_sizes, span, direction, context,
451
- containing_block)
464
+ sizing_functions, tracks_sizes, span, direction, context)
452
465
  # 1.2.3.2 For content-based minimums.
453
466
  _distribute_extra_space(
454
467
  'min', 'content-based', 'min-content', tracks_children,
455
- sizing_functions, tracks_sizes, span, direction, context,
456
- containing_block)
468
+ sizing_functions, tracks_sizes, span, direction, context)
457
469
  # 1.2.3.3 For max-content minimums.
458
470
  # TODO: Respect max-content constraint.
459
471
  _distribute_extra_space(
460
472
  'min', 'max-content', 'max-content', tracks_children,
461
- sizing_functions, tracks_sizes, span, direction, context,
462
- containing_block)
473
+ sizing_functions, tracks_sizes, span, direction, context)
463
474
  # 1.2.3.4 Increase growth limit.
464
- for sizes in tracks_sizes:
465
- if None not in sizes:
466
- sizes[1] = max(sizes)
467
- iterable = enumerate(children_positions.items())
468
- for i, (child, (x, y, width, height)) in iterable:
469
- coord, size = (x, width) if direction == 'x' else (y, height)
470
- if size != span:
471
- continue
472
- for _, max_function in sizing_functions[i:i+span+1]:
473
- if _is_fr(max_function):
474
- break
475
- else:
476
- tracks_children[coord - implicit_start].append(child)
475
+ # TODO: Increase growth limit.
477
476
  # 1.2.3.5 For intrinsic maximums.
478
477
  _distribute_extra_space(
479
478
  'max', 'intrinsic', 'min-content', tracks_children,
480
- sizing_functions, tracks_sizes, span, direction, context,
481
- containing_block)
479
+ sizing_functions, tracks_sizes, span, direction, context)
482
480
  # 1.2.3.6 For max-content maximums.
483
481
  _distribute_extra_space(
484
482
  'max', 'max-content', 'max-content', tracks_children,
485
- sizing_functions, tracks_sizes, span, direction, context,
486
- containing_block)
483
+ sizing_functions, tracks_sizes, span, direction, context)
487
484
  # 1.2.4 Increase sizes to accommodate items spanning flexible tracks.
488
485
  # TODO: Support spans for flexible tracks.
489
486
  # 1.2.5 Fix infinite growth limits.
@@ -577,7 +574,14 @@ def _resolve_tracks_sizes(sizing_functions, box_size, children_positions,
577
574
 
578
575
  def grid_layout(context, box, bottom_space, skip_stack, containing_block,
579
576
  page_is_empty, absolute_boxes, fixed_boxes):
580
- context.create_block_formatting_context()
577
+ context.create_block_formatting_context(box)
578
+
579
+ if skip_stack and box.style['box_decoration_break'] != 'clone':
580
+ box.remove_decoration(start=True, end=False)
581
+
582
+ if box.style['position'] == 'relative':
583
+ # New containing block, use a new absolute list
584
+ absolute_boxes = []
581
585
 
582
586
  # Define explicit grid
583
587
  grid_areas = box.style['grid_template_areas']
@@ -591,13 +595,13 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
591
595
  column_gap = 0
592
596
  else:
593
597
  refer_to = containing_block.width if box.width == 'auto' else box.width
594
- column_gap = percentage(column_gap, refer_to)
598
+ column_gap = percentage(column_gap, box.style, refer_to)
595
599
  row_gap = box.style['row_gap']
596
600
  if row_gap == 'normal':
597
601
  row_gap = 0
598
602
  else:
599
603
  refer_to = 0 if box.height == 'auto' else box.height
600
- row_gap = percentage(row_gap, refer_to)
604
+ row_gap = percentage(row_gap, box.style, refer_to)
601
605
 
602
606
  if grid_areas == 'none':
603
607
  grid_areas = ((None,),)
@@ -660,8 +664,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
660
664
  second_tracks = rows if second_flow == 'row' else columns
661
665
 
662
666
  # 1.1 Position anything that’s not auto-positioned.
667
+ children = sorted(box.children, key=lambda item: item.style['order'])
663
668
  children_positions = {}
664
- for child in box.children:
669
+ for child in children:
665
670
  column_start = child.style['grid_column_start']
666
671
  column_end = child.style['grid_column_end']
667
672
  row_start = child.style['grid_row_start']
@@ -677,7 +682,6 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
677
682
  children_positions[child] = (x, y, width, height)
678
683
 
679
684
  # 1.2 Process the items locked to a given row (resp. column).
680
- children = sorted(box.children, key=lambda item: item.style['order'])
681
685
  for child in children:
682
686
  if child in children_positions:
683
687
  continue
@@ -980,6 +984,21 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
980
984
  rows.append(next(auto_rows))
981
985
  rows.append([])
982
986
 
987
+ page_breaks_by_row = defaultdict(
988
+ lambda: {'before': 'auto', 'after': 'auto', 'inside': 'auto'})
989
+ for child, (x, y, width, height) in children_positions.items():
990
+ row_page_break = page_breaks_by_row[y]
991
+ if child.style['break_inside'] in ('avoid', 'avoid-page'):
992
+ row_page_break['inside'] = 'avoid'
993
+ for side in ('before', 'after'):
994
+ if child.style[f'break_{side}'] in ('avoid', 'avoid-page'):
995
+ if row_page_break[side] != 'page':
996
+ row_page_break[side] = 'avoid'
997
+ elif child.style[f'break_{side}'] in ('page', 'always'):
998
+ row_page_break[side] = 'page'
999
+ elif child.style[f'break_{side}'] in ('left', 'right', 'recto', 'verso'):
1000
+ row_page_break[side] = child.style[f'break_{side}']
1001
+
983
1002
  # 2. Find the size of the grid container.
984
1003
 
985
1004
  if isinstance(box, boxes.GridBox):
@@ -1006,9 +1025,10 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1006
1025
  'x', column_gap, context, box)
1007
1026
 
1008
1027
  # 3.2 Resolve the sizes of the grid rows.
1028
+ vertical_sizes = [size for size, _ in columns_sizes]
1009
1029
  rows_sizes = _resolve_tracks_sizes(
1010
1030
  row_sizing_functions, box.height, children_positions, implicit_y1, 'y',
1011
- row_gap, context, box, [size for size, _ in columns_sizes])
1031
+ row_gap, context, box, vertical_sizes)
1012
1032
 
1013
1033
  # 3.3 Re-resolve the sizes of the grid columns with min-/max-content.
1014
1034
  # TODO: Re-resolve.
@@ -1099,57 +1119,132 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1099
1119
  y += size + row_gap
1100
1120
 
1101
1121
  # 4. Lay out the grid items into their respective containing blocks.
1122
+
1102
1123
  # Find resume_at row.
1103
1124
  this_page_children = []
1104
1125
  resume_row = None
1105
1126
  if skip_stack:
1106
- skip_row = next(iter(skip_stack))
1127
+ from .block import block_level_layout
1128
+ first_skip_row = min(skip_stack)
1129
+ last_skip_row = max(skip_stack)
1107
1130
  skip_height = (
1108
- sum(size for size, _ in rows_sizes[:skip_row]) +
1109
- (len(rows_sizes[:skip_row]) - 1) * row_gap)
1131
+ sum(size for size, _ in rows_sizes[:last_skip_row]) +
1132
+ (len(rows_sizes[:last_skip_row]) - 1) * row_gap)
1133
+ extra_skip_height = 0
1134
+ for child, (x, y, width, height) in children_positions.items():
1135
+ if (advancement := box.advancements.get((x, y))) is None:
1136
+ continue
1137
+ span = _get_span(child.style['grid_row_start'])
1138
+ span_height = (
1139
+ sum(size for size, _ in rows_sizes[y:y+span]) +
1140
+ (span - 1) * row_gap)
1141
+ index = tuple(children_positions).index(child)
1142
+ width = sum(vertical_sizes[x:x+width])
1143
+ child = child.deepcopy()
1144
+ child.position_x = 0
1145
+ child.position_y = 0
1146
+ if y in skip_stack:
1147
+ child_skip_stack = skip_stack[y].get(index)
1148
+ else:
1149
+ child_skip_stack = None
1150
+ parent = boxes.BlockContainerBox.anonymous_from(containing_block, ())
1151
+ resolve_percentages(parent, containing_block)
1152
+ parent.position_x = 0
1153
+ parent.position_y = 0
1154
+ parent.width = width
1155
+ parent.height = 0
1156
+ child, _, _, _, _, _ = block_level_layout(
1157
+ context, child, bottom_space=-inf, skip_stack=child_skip_stack,
1158
+ containing_block=parent)
1159
+ skip_stack_advancement = span_height - child.margin_height()
1160
+ if skip_stack_advancement < advancement:
1161
+ extra_skip_height = max(
1162
+ extra_skip_height, advancement - skip_stack_advancement)
1163
+ for (x, y), advancement in box.advancements.items():
1164
+ if y != last_skip_row:
1165
+ continue
1166
+ skip_height += advancement
1167
+ break
1168
+ else:
1169
+ extra_skip_height = 0
1170
+ skip_height -= extra_skip_height
1110
1171
  else:
1111
- skip_row = 0
1112
- skip_height = 0
1172
+ first_skip_row = last_skip_row = skip_height = 0
1113
1173
  resume_at = None
1114
1174
  total_height = (
1115
- sum(size for size, _ in rows_sizes[skip_row:]) +
1116
- (len(rows_sizes[skip_row:]) - 1) * row_gap)
1175
+ sum(size for size, _ in rows_sizes[last_skip_row:]) +
1176
+ (len(rows_sizes[last_skip_row:]) - 1) * row_gap)
1177
+
1178
+ def _add_page_children(max_row=inf):
1179
+ for j, child in enumerate(children):
1180
+ _, y, _, _ = children_positions[child]
1181
+ if not first_skip_row <= y < max_row:
1182
+ # Item in previous or next rows.
1183
+ continue
1184
+ if skip_stack is None:
1185
+ # No skip stack, draw on this page.
1186
+ this_page_children.append((j, child))
1187
+ elif y > last_skip_row:
1188
+ # Row after the skip stack, draw on this page.
1189
+ this_page_children.append((j, child))
1190
+ elif y in skip_stack:
1191
+ child_skip_stack = skip_stack[y]
1192
+ if child_skip_stack is None or j in child_skip_stack:
1193
+ # Child in skip stack, draw on this page.
1194
+ this_page_children.append((j, child))
1195
+
1117
1196
  row_lines_positions = (
1118
- rows_positions[skip_row + 1:] + [box.content_box_y() + total_height])
1119
- for i, row_y in enumerate(row_lines_positions, start=skip_row + 1):
1120
- if context.overflows_page(bottom_space, row_y - skip_height):
1121
- if not page_is_empty:
1122
- if i == 1:
1123
- return None, None, {'break': 'any', 'page': None}, [], False
1124
- resume_row = i - 1
1125
- resume_at = {i-1: None}
1126
- for child in children:
1127
- _, y, _, _ = children_positions[child]
1128
- if skip_row <= y <= i-2:
1129
- this_page_children.append(child)
1130
- break
1131
- page_is_empty = False
1197
+ [*rows_positions[first_skip_row + 1:], box.content_box_y() + total_height])
1198
+ for i, row_y in enumerate(row_lines_positions, start=first_skip_row):
1199
+ # TODO: handle break-before and break-after for rows.
1200
+ if not context.overflows_page(bottom_space, row_y - skip_height):
1201
+ page_is_empty = False
1202
+ continue
1203
+ resume_row = i
1204
+ if box.style['break_inside'] == 'avoid' and not page_is_empty:
1205
+ # Avoid breaks inside grid container, break before.
1206
+ context.finish_block_formatting_context(box)
1207
+ return None, None, {'break': 'any', 'page': None}, [], False
1208
+ if page_breaks_by_row[i]['inside'] == 'avoid' and not page_is_empty:
1209
+ # Break before current row.
1210
+ if resume_row == 0:
1211
+ # First row, break before grid container.
1212
+ context.finish_block_formatting_context(box)
1213
+ return None, None, {'break': 'any', 'page': None}, [], False
1214
+ # Mark all children before and in current row as drawn on the page.
1215
+ _add_page_children(resume_row)
1216
+ resume_at = {resume_row: None}
1217
+ else:
1218
+ # Break inside current row.
1219
+ # Mark all children before current row as drawn on the page.
1220
+ page_is_empty = False
1221
+ _add_page_children(resume_row + 1)
1222
+ break
1132
1223
  else:
1133
- for child in children:
1134
- _, y, _, _ = children_positions[child]
1135
- if skip_row <= y:
1136
- this_page_children.append(child)
1224
+ # Mark all children as drawn on the page.
1225
+ _add_page_children()
1137
1226
  if box.height == 'auto':
1138
1227
  box.height = (
1139
- sum(size for size, _ in rows_sizes[skip_row:resume_row]) +
1140
- (len(rows_sizes[skip_row:resume_row]) - 1) * row_gap)
1228
+ sum(size for size, _ in rows_sizes[:resume_row]) +
1229
+ (len(rows_sizes[:resume_row]) - 1) * row_gap) - skip_height
1230
+
1141
1231
  # Lay out grid items.
1142
1232
  justify_items = set(box.style['justify_items'])
1143
1233
  align_items = set(box.style['align_items'])
1144
1234
  new_children = []
1235
+ new_children_by_rows = defaultdict(list)
1145
1236
  baseline = None
1146
1237
  next_page = {'break': 'any', 'page': None}
1147
1238
  from .block import block_level_layout
1148
- for child in this_page_children:
1239
+ for i, child in this_page_children:
1149
1240
  x, y, width, height = children_positions[child]
1150
- index = box.children.index(child)
1151
- if skip_stack and skip_stack.get(y) and index in skip_stack[y]:
1152
- child_skip_stack = skip_stack[y][index]
1241
+ index = children.index(child)
1242
+ if skip_stack and skip_stack.get(y):
1243
+ if index in skip_stack[y]:
1244
+ child_skip_stack = skip_stack[y][index]
1245
+ else:
1246
+ assert isinstance(child, boxes.ParentBox)
1247
+ child_skip_stack = {len(child.children): None}
1153
1248
  else:
1154
1249
  child_skip_stack = None
1155
1250
  child = child.deepcopy()
@@ -1162,6 +1257,9 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1162
1257
  height = (
1163
1258
  sum(size for size, _ in rows_sizes[y:y+height]) +
1164
1259
  (height - 1) * row_gap)
1260
+ if skip_stack and (x, y) in box.advancements:
1261
+ child.position_y += box.advancements[x, y] - extra_skip_height
1262
+ height -= box.advancements[x, y] - extra_skip_height
1165
1263
 
1166
1264
  # TODO: Apply auto margin.
1167
1265
  if child.margin_top == 'auto':
@@ -1177,20 +1275,24 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1177
1275
  child.margin_left + child.border_left_width + child.padding_left +
1178
1276
  child.margin_right + child.border_right_width + child.padding_right)
1179
1277
  child_height = height - (
1180
- child.margin_top + child.border_top_width + child.padding_top +
1181
1278
  child.margin_bottom + child.border_bottom_width + child.padding_bottom)
1279
+ if not child_skip_stack or child.style['box_decoration_break'] == 'clone':
1280
+ child_height -= (
1281
+ child.margin_top + child.border_top_width + child.padding_top)
1182
1282
 
1183
1283
  justify_self = set(child.style['justify_self'])
1184
1284
  if justify_self & {'auto'}:
1185
1285
  justify_self = justify_items
1186
1286
  if justify_self & {'normal', 'stretch'}:
1187
1287
  if child.style['width'] == 'auto':
1288
+ child.style = child.style.copy()
1188
1289
  child.style['width'] = Dimension(child_width, 'px')
1189
1290
  align_self = set(child.style['align_self'])
1190
1291
  if align_self & {'auto'}:
1191
1292
  align_self = align_items
1192
1293
  if align_self & {'normal', 'stretch'}:
1193
1294
  if child.style['height'] == 'auto':
1295
+ child.style = child.style.copy()
1194
1296
  child.style['height'] = Dimension(child_height, 'px')
1195
1297
 
1196
1298
  # TODO: Find a better solution for the layout.
@@ -1205,15 +1307,41 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1205
1307
  page_is_empty, absolute_boxes, fixed_boxes)[:3]
1206
1308
  if new_child:
1207
1309
  page_is_empty = False
1208
- # TODO: Support fragmentation in grid items.
1310
+ broken_child = False
1311
+ span = _get_span(child.style['grid_row_start'])
1312
+ if child_resume_at:
1313
+ broken_child = True
1314
+ elif resume_row is not None and y + span >= resume_row + 1:
1315
+ broken_child = True
1316
+ if broken_child:
1317
+ # Child is broken, add row to resume_at.
1318
+ if resume_at is None:
1319
+ resume_at = {}
1320
+ if y not in resume_at:
1321
+ resume_at[y] = {}
1322
+ if child_resume_at:
1323
+ # There is some content left for next page, save the cell’s resume_at.
1324
+ resume_at[y][i] = child_resume_at
1325
+ elif broken_child:
1326
+ # Everything fits but the cell overflows. Only display the bottom of an
1327
+ # empty cell on next page, set the cell’s resume_at after the cell’s
1328
+ # last child.
1329
+ assert isinstance(new_child, boxes.ParentBox)
1330
+ previous_skip_child = max(child_skip_stack) if child_skip_stack else 0
1331
+ resume_at[y][i] = {previous_skip_child + len(new_child.children): None}
1209
1332
  else:
1210
- # TODO: Support fragmentation in grid rows.
1333
+ if resume_at is None:
1334
+ resume_at = {}
1335
+ if y not in resume_at:
1336
+ resume_at[y] = {}
1337
+ resume_at[y][i] = None
1211
1338
  continue
1212
1339
 
1213
1340
  if justify_self & {'normal', 'stretch'}:
1214
1341
  new_child.width = max(child_width, new_child.width)
1215
1342
  else:
1216
- new_child.width = max_content_width(context, new_child)
1343
+ if new_child.style['width'] == 'auto':
1344
+ new_child.width = max_content_width(context, new_child, outer=False)
1217
1345
  diff = child_width - new_child.width
1218
1346
  if justify_self & {'center'}:
1219
1347
  new_child.translate(diff / 2, 0)
@@ -1230,17 +1358,83 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1230
1358
  elif align_self & {'end', 'flex-end', 'self-end'}:
1231
1359
  new_child.translate(0, diff)
1232
1360
 
1233
- # TODO: Take care of page fragmentation.
1234
1361
  new_children.append(new_child)
1362
+ new_children_by_rows[y].append((x, new_child))
1235
1363
  if baseline is None and y == implicit_y1:
1236
1364
  baseline = find_in_flow_baseline(new_child)
1237
1365
 
1366
+ # Abort whole grid rendering if no child fits.
1367
+ if this_page_children and not new_children:
1368
+ context.finish_block_formatting_context(box)
1369
+ return None, None, {'break': 'any', 'page': None}, [], False
1370
+
1371
+ old_advancements = box.advancements or {}
1372
+ advancements = box.advancements = {}
1238
1373
  box = box.copy_with_children(new_children)
1239
1374
  if isinstance(box, boxes.InlineGridBox):
1240
1375
  # TODO: Synthetize a real baseline value.
1241
1376
  LOGGER.warning('Inline grids are not supported')
1242
1377
  box.baseline = baseline or 0
1243
1378
 
1244
- context.finish_block_formatting_context(box)
1379
+ from .absolute import absolute_layout
1380
+ from .block import relative_positioning
1245
1381
 
1382
+ if box.style['position'] == 'relative':
1383
+ # New containing block, resolve the layout of the absolute descendants
1384
+ for absolute_box in absolute_boxes:
1385
+ absolute_layout(
1386
+ context, absolute_box, box, fixed_boxes, bottom_space,
1387
+ skip_stack=None)
1388
+
1389
+ for child in box.children:
1390
+ relative_positioning(child, (box.width, box.height))
1391
+
1392
+ # Resume early when there’s no resume_at.
1393
+ if not resume_at:
1394
+ context.finish_block_formatting_context(box)
1395
+ return box, resume_at, next_page, [], False
1396
+
1397
+ # Set broken rows’ bottom at the bottom of the page.
1398
+ last_page_row = max(new_children_by_rows)
1399
+ next_page_first_row = min(resume_at)
1400
+ next_page_last_row = max(resume_at)
1401
+ extra_advancement = 0
1402
+ for y in range(last_page_row, next_page_first_row - 1, -1):
1403
+ for x, child in new_children_by_rows[y]:
1404
+ span = _get_span(child.style['grid_row_start'])
1405
+ if y + span < last_page_row + 1:
1406
+ # Child finishing before the last row, do nothing.
1407
+ continue
1408
+ broken_child = y + span >= next_page_last_row + 1
1409
+ if broken_child and child.style['box_decoration_break'] != 'clone':
1410
+ child.remove_decoration(start=False, end=True)
1411
+ child.height = (
1412
+ context.page_bottom - bottom_space - child.position_y -
1413
+ child.margin_top - child.border_top_width - child.padding_top -
1414
+ child.margin_bottom - child.border_bottom_width - child.padding_bottom)
1415
+ if broken_child:
1416
+ # Child not fully drawn, keep advancement.
1417
+ advancements[x, y] = child.margin_height()
1418
+ if (x, y) in old_advancements:
1419
+ advancements[x, y] += old_advancements[x, y] - extra_skip_height
1420
+ else:
1421
+ # Child fully drawn, save the extra height added to reach the bottom of
1422
+ # the page to substract it from the advancements.
1423
+ extra_advancement = max(extra_advancement, child.height - child_height)
1424
+
1425
+ # Substract the extra height added to reach the bottom of the page from all the
1426
+ # advancements.
1427
+ if extra_advancement:
1428
+ for x, y in advancements:
1429
+ advancements[x, y] -= extra_advancement
1430
+
1431
+ # Set box height and remove bottom decoration.
1432
+ box.height = (
1433
+ context.page_bottom - bottom_space - box.position_y -
1434
+ box.margin_top - box.border_top_width - box.padding_top)
1435
+ if box.style['box_decoration_break'] != 'clone':
1436
+ box.remove_decoration(start=False, end=True)
1437
+ box.height -= box.margin_bottom + box.border_bottom_width + box.padding_bottom
1438
+
1439
+ context.finish_block_formatting_context(box)
1246
1440
  return box, resume_at, next_page, [], False