weasyprint 65.1__py3-none-any.whl → 66.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 (45) hide show
  1. weasyprint/__init__.py +4 -1
  2. weasyprint/__main__.py +2 -0
  3. weasyprint/css/__init__.py +12 -4
  4. weasyprint/css/computed_values.py +8 -2
  5. weasyprint/css/html5_ua.css +2 -7
  6. weasyprint/css/html5_ua_form.css +1 -1
  7. weasyprint/css/utils.py +1 -1
  8. weasyprint/document.py +2 -10
  9. weasyprint/draw/__init__.py +51 -57
  10. weasyprint/draw/border.py +120 -66
  11. weasyprint/draw/text.py +1 -2
  12. weasyprint/formatting_structure/boxes.py +3 -2
  13. weasyprint/formatting_structure/build.py +32 -42
  14. weasyprint/images.py +8 -15
  15. weasyprint/layout/__init__.py +5 -2
  16. weasyprint/layout/absolute.py +4 -1
  17. weasyprint/layout/block.py +60 -29
  18. weasyprint/layout/column.py +1 -0
  19. weasyprint/layout/flex.py +41 -21
  20. weasyprint/layout/float.py +8 -1
  21. weasyprint/layout/grid.py +1 -1
  22. weasyprint/layout/inline.py +7 -8
  23. weasyprint/layout/page.py +23 -1
  24. weasyprint/layout/preferred.py +59 -32
  25. weasyprint/layout/table.py +8 -4
  26. weasyprint/pdf/__init__.py +13 -6
  27. weasyprint/pdf/anchors.py +2 -2
  28. weasyprint/pdf/pdfua.py +7 -115
  29. weasyprint/pdf/stream.py +40 -49
  30. weasyprint/pdf/tags.py +305 -0
  31. weasyprint/stacking.py +14 -15
  32. weasyprint/svg/__init__.py +22 -11
  33. weasyprint/svg/bounding_box.py +4 -2
  34. weasyprint/svg/defs.py +4 -9
  35. weasyprint/svg/utils.py +9 -5
  36. weasyprint/text/fonts.py +1 -1
  37. weasyprint/text/line_break.py +45 -26
  38. weasyprint/urls.py +21 -10
  39. {weasyprint-65.1.dist-info → weasyprint-66.0.dist-info}/METADATA +1 -1
  40. weasyprint-66.0.dist-info/RECORD +74 -0
  41. weasyprint/draw/stack.py +0 -13
  42. weasyprint-65.1.dist-info/RECORD +0 -74
  43. {weasyprint-65.1.dist-info → weasyprint-66.0.dist-info}/WHEEL +0 -0
  44. {weasyprint-65.1.dist-info → weasyprint-66.0.dist-info}/entry_points.txt +0 -0
  45. {weasyprint-65.1.dist-info → weasyprint-66.0.dist-info}/licenses/LICENSE +0 -0
@@ -298,9 +298,13 @@ def _out_of_flow_layout(context, box, index, child, new_children,
298
298
  return stop, resume_at, new_child, out_of_flow_resume_at
299
299
 
300
300
 
301
- def _break_line(context, box, line, new_children, lines_iterator,
302
- page_is_empty, index, skip_stack, resume_at, absolute_boxes,
303
- fixed_boxes):
301
+ def _break_line(context, box, line, new_children, next_lines, page_is_empty, index,
302
+ skip_stack, resume_at, absolute_boxes, fixed_boxes):
303
+ """Break line where allowed by orphans and widows.
304
+
305
+ Return (abort, stop, resume_at).
306
+
307
+ """
304
308
  over_orphans = len(new_children) - box.style['orphans']
305
309
  if over_orphans < 0 and not page_is_empty:
306
310
  # Reached the bottom of the page before we had
@@ -309,12 +313,7 @@ def _break_line(context, box, line, new_children, lines_iterator,
309
313
  return True, False, resume_at
310
314
  # How many lines we need on the next page to satisfy widows
311
315
  # -1 for the current line.
312
- needed = box.style['widows'] - 1
313
- if needed:
314
- for _ in lines_iterator:
315
- needed -= 1
316
- if needed == 0:
317
- break
316
+ needed = max(box.style['widows'] - 1 - next_lines, 0)
318
317
  if needed > over_orphans and not page_is_empty:
319
318
  # Total number of lines < orphans + widows
320
319
  remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)
@@ -366,24 +365,52 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
366
365
  else:
367
366
  offset_y = 0
368
367
 
369
- # Allow overflow if the first line of the page is higher
370
- # than the page itself so that we put *something* on this
371
- # page and can advance in the context.
368
+ # Allow overflow if the first line of the page is higher than the page itself so
369
+ # that we put *something* on this page and can advance in the context.
372
370
  overflow = (
373
371
  (new_children or not page_is_empty) and
374
372
  context.overflows_page(bottom_space, new_position_y + offset_y))
375
373
  if overflow:
376
- abort, stop, resume_at = _break_line(
377
- context, box, line, new_children, lines_iterator,
378
- page_is_empty, index, skip_stack, resume_at, absolute_boxes,
379
- fixed_boxes)
374
+ # If we couldn’t break the line before but can break now, first try to
375
+ # report footnotes and see if we don’t overflow.
376
+ could_break_before = can_break_now = True
377
+ next_lines = len(tuple(lines_iterator))
378
+ if len(new_children) + 1 < box.style['orphans']:
379
+ can_break_now = False
380
+ elif next_lines < box.style['widows']:
381
+ can_break_now = False
382
+ if len(new_children) < box.style['orphans']:
383
+ could_break_before = False
384
+ elif next_lines + 1 < box.style['widows']:
385
+ could_break_before = False
386
+ report = not context.in_column and can_break_now and not could_break_before
387
+ reported_footnotes = 0
388
+ while report and context.current_page_footnotes:
389
+ context.report_footnote(context.current_page_footnotes[-1])
390
+ reported_footnotes += 1
391
+ if not context.overflows_page(bottom_space, new_position_y + offset_y):
392
+ new_children.append(line)
393
+ stop = True
394
+ break
395
+ else:
396
+ abort, stop, resume_at = _break_line(
397
+ context, box, line, new_children, next_lines,
398
+ page_is_empty, index, skip_stack, resume_at, absolute_boxes,
399
+ fixed_boxes)
400
+
401
+ # Revert reported footnotes, as they’ve been reported starting from the last
402
+ # one.
403
+ if reported_footnotes >= 2:
404
+ extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]
405
+ context.reported_footnotes[-reported_footnotes:] = extra
406
+
380
407
  break
381
408
 
382
409
  # TODO: this is incomplete.
383
410
  # See https://drafts.csswg.org/css-page-3/#allowed-pg-brk
384
411
  # "When an unforced page break occurs here, both the adjoining
385
412
  # ‘margin-top’ and ‘margin-bottom’ are set to zero."
386
- # See https://github.com/Kozea/WeasyPrint/issues/115
413
+ # See issue #115.
387
414
  elif page_is_empty and context.overflows_page(
388
415
  bottom_space, new_position_y):
389
416
  # Remove the top border when a page is empty and the box is
@@ -413,9 +440,10 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
413
440
  # even try.
414
441
  if new_children or not page_is_empty:
415
442
  if footnote.style['footnote_policy'] == 'line':
443
+ next_lines = len(tuple(lines_iterator))
416
444
  abort, stop, resume_at = _break_line(
417
445
  context, box, line, new_children,
418
- lines_iterator, page_is_empty, index,
446
+ next_lines, page_is_empty, index,
419
447
  skip_stack, resume_at, absolute_boxes,
420
448
  fixed_boxes)
421
449
  break_linebox = True
@@ -502,7 +530,6 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
502
530
  if box.is_table_wrapper: # should not be a special case
503
531
  collapsed_margin = collapse_margin(adjoining_margins)
504
532
  child.position_y += collapsed_margin
505
- position_y += collapsed_margin
506
533
  adjoining_margins = []
507
534
  elif not isinstance(child, boxes.BlockBox): # blocks handle that themselves
508
535
  if child.style['margin_top'] == 'auto':
@@ -512,7 +539,6 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
512
539
  adjoining_margins.append(margin_top)
513
540
  offset_y = collapse_margin(adjoining_margins) - margin_top
514
541
  child.position_y += offset_y
515
- position_y += offset_y
516
542
  adjoining_margins = []
517
543
 
518
544
  page_is_empty_with_no_children = page_is_empty and not any(
@@ -677,8 +703,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
677
703
 
678
704
  new_children = []
679
705
  next_page = {'break': 'any', 'page': None}
680
- all_footnotes = []
681
- broken_out_of_flow = {}
706
+ broken_out_of_flow = []
682
707
 
683
708
  last_in_flow_child = None
684
709
 
@@ -704,8 +729,13 @@ def block_container_layout(context, box, bottom_space, skip_stack,
704
729
  absolute_boxes, fixed_boxes, adjoining_margins,
705
730
  bottom_space))
706
731
  if out_of_flow_resume_at:
707
- broken_out_of_flow[new_child] = (
732
+ context.broken_out_of_flow[new_child] = (
708
733
  child, box, out_of_flow_resume_at)
734
+ broken_out_of_flow.append(new_child)
735
+ if child.is_outside_marker:
736
+ new_child.position_x = box.border_box_x()
737
+ if child.style['direction'] == 'rtl':
738
+ new_child.position_x += box.width + box.padding_right
709
739
 
710
740
  elif isinstance(child, boxes.LineBox):
711
741
  (abort, stop, resume_at, position_y,
@@ -716,7 +746,6 @@ def block_container_layout(context, box, bottom_space, skip_stack,
716
746
  draw_bottom_decoration, max_lines)
717
747
  draw_bottom_decoration |= resume_at is None
718
748
  adjoining_margins = []
719
- all_footnotes += new_footnotes
720
749
 
721
750
  else:
722
751
  (abort, stop, resume_at, position_y, adjoining_margins,
@@ -766,7 +795,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
766
795
  else:
767
796
  resume_at = None
768
797
 
769
- box_is_fragmented = resume_at is not None
798
+ box_is_fragmented = resume_at is not None or box.force_fragmentation
770
799
  if box.style['continue'] == 'discard':
771
800
  resume_at = None
772
801
 
@@ -775,11 +804,10 @@ def block_container_layout(context, box, bottom_space, skip_stack,
775
804
  not page_is_empty):
776
805
  remove_placeholders(
777
806
  context, [*new_children, *box.children[skip:]], absolute_boxes, fixed_boxes)
807
+ for child in broken_out_of_flow:
808
+ del context.broken_out_of_flow[child]
778
809
  return None, None, {'break': 'any', 'page': None}, [], False, max_lines
779
810
 
780
- for key, value in broken_out_of_flow.items():
781
- context.broken_out_of_flow[key] = value
782
-
783
811
  if collapsing_with_children:
784
812
  box.position_y += (
785
813
  collapse_margin(this_box_adjoining_margins) - box.margin_top)
@@ -831,7 +859,10 @@ def block_container_layout(context, box, bottom_space, skip_stack,
831
859
  float_box.position_y + float_box.margin_height()
832
860
  for float_box in context.excluded_shapes)
833
861
  position_y = max(max_float_position_y, position_y)
834
- new_box.height = position_y - new_box.content_box_y()
862
+ if position_y == new_box.content_box_y() == inf:
863
+ new_box.height = 0
864
+ else:
865
+ new_box.height = position_y - new_box.content_box_y()
835
866
 
836
867
  if new_box.style['position'] == 'relative':
837
868
  # New containing block, resolve the layout of the absolute descendants
@@ -190,6 +190,7 @@ def columns_layout(context, box, bottom_space, skip_stack, containing_block,
190
190
  in_flow_children[-1].margin_height() +
191
191
  in_flow_children[-1].position_y - current_position_y)
192
192
  empty_space = height - consumed_height
193
+ consumed_height -= in_flow_children[-1].margin_bottom
193
194
 
194
195
  # Get the minimum size needed to render the next box
195
196
  if column_skip_stack:
weasyprint/layout/flex.py CHANGED
@@ -172,6 +172,8 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
172
172
  child.style['image_resolution'], child.style['font_size'])
173
173
  if intrinsic_ratio and intrinsic_height:
174
174
  transferred_size = intrinsic_height * intrinsic_ratio
175
+ content_size = max(
176
+ child.min_width, min(child.max_width, content_size))
175
177
  if specified_size != 'auto':
176
178
  child.min_width = min(specified_size, content_size)
177
179
  elif transferred_size is not None:
@@ -183,12 +185,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
183
185
  specified_size = child.height
184
186
  new_child = child.copy()
185
187
  new_child.style = child.style.copy()
186
- if new_child.style['width'] == 'auto':
187
- new_child_width = max_content_width(context, new_child)
188
- new_child.style['width'] = Dimension(new_child_width, 'px')
189
188
  new_child.style['height'] = 'auto'
190
189
  new_child.style['min_height'] = Dimension(0, 'px')
191
190
  new_child.style['max_height'] = Dimension(inf, 'px')
191
+ if new_child.style['width'] == 'auto':
192
+ new_child_width = max_content_width(context, new_child)
193
+ new_child.style['width'] = Dimension(new_child_width, 'px')
192
194
  new_child = block.block_level_layout(
193
195
  context, new_child, bottom_space, child_skip_stack, parent_box,
194
196
  page_is_empty)[0]
@@ -200,6 +202,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
200
202
  child.style['image_resolution'], child.style['font_size'])
201
203
  if intrinsic_ratio and intrinsic_width:
202
204
  transferred_size = intrinsic_width / intrinsic_ratio
205
+ content_size = max(
206
+ child.min_height, min(child.max_height, content_size))
207
+ elif not intrinsic_width:
208
+ # TODO: wrongly set by block_level_layout, would be OK with
209
+ # min_content_height.
210
+ content_size = 0
203
211
  if specified_size != 'auto':
204
212
  child.min_height = min(specified_size, content_size)
205
213
  elif transferred_size is not None:
@@ -242,17 +250,31 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
242
250
  pass
243
251
  else:
244
252
  # 3.E Otherwise…
253
+ new_child = child.copy()
254
+ new_child.style = child.style.copy()
245
255
  if main == 'width':
246
- child.flex_base_size = max_content_width(context, child, outer=False)
256
+ # the item’s min and max main sizes are ignored.
257
+ new_child.style['min_width'] = Dimension(0, 'px')
258
+ new_child.style['max_width'] = Dimension(inf, 'px')
259
+
260
+ child.flex_base_size = max_content_width(
261
+ context, new_child, outer=False)
247
262
  child.main_outer_extra = (
248
263
  max_content_width(context, child) - child.flex_base_size)
249
264
  else:
250
- new_child = child.copy()
265
+ # the item’s min and max main sizes are ignored.
266
+ new_child.style['min_height'] = Dimension(0, 'px')
267
+ new_child.style['max_height'] = Dimension(inf, 'px')
268
+
251
269
  new_child.width = inf
252
- new_child = block.block_level_layout(
270
+ new_child, _, _, adjoining_margins, _, _ = block.block_level_layout(
253
271
  context, new_child, bottom_space, child_skip_stack, parent_box,
254
- page_is_empty, absolute_boxes, fixed_boxes)[0]
272
+ page_is_empty, absolute_boxes, fixed_boxes)
255
273
  if new_child:
274
+ # As flex items margins never collapse (with other flex items or
275
+ # with the flex container), we can add the adjoining margins to the
276
+ # child height.
277
+ new_child.height += block.collapse_margin(adjoining_margins)
256
278
  child.flex_base_size = new_child.height
257
279
  child.main_outer_extra = (
258
280
  new_child.margin_height() - new_child.height)
@@ -477,8 +499,8 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
477
499
  child.height = new_child.height
478
500
  # As flex items margins never collapse (with other flex items or
479
501
  # with the flex container), we can add the adjoining margins to the
480
- # child bottom margin.
481
- child.margin_bottom += block.collapse_margin(adjoining_margins)
502
+ # child height.
503
+ child.height += block.collapse_margin(adjoining_margins)
482
504
  else:
483
505
  if child.width == 'auto':
484
506
  min_width = min_content_width(context, child, outer=False)
@@ -591,9 +613,9 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
591
613
  align_self = align_items
592
614
  if 'stretch' in align_self and child.style[cross] == 'auto':
593
615
  cross_margins = (
594
- (child.margin_top, child.margin_bottom)
595
- if cross == 'height'
596
- else (child.margin_left, child.margin_right))
616
+ (child.style['margin_top'], child.style['margin_bottom'])
617
+ if cross == 'height' else
618
+ (child.style['margin_left'], child.style['margin_right']))
597
619
  if 'auto' not in cross_margins:
598
620
  cross_size = line.cross_size
599
621
  if cross == 'height':
@@ -733,8 +755,9 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
733
755
  line.lower_baseline = line[0][1]._baseline if line else 0
734
756
  for index, child in line:
735
757
  cross_margins = (
736
- (child.margin_top, child.margin_bottom) if cross == 'height'
737
- else (child.margin_left, child.margin_right))
758
+ (child.style['margin_top'], child.style['margin_bottom'])
759
+ if cross == 'height' else
760
+ (child.style['margin_left'], child.style['margin_right']))
738
761
  auto_margins = sum([margin == 'auto' for margin in cross_margins])
739
762
  # If a flex item has auto cross-axis margins…
740
763
  if auto_margins:
@@ -755,14 +778,14 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
755
778
  # If its outer cross size is less than the cross size…
756
779
  extra_cross /= auto_margins
757
780
  if cross == 'height':
758
- if child.margin_top == 'auto':
781
+ if child.style['margin_top'] == 'auto':
759
782
  child.margin_top = extra_cross
760
- if child.margin_bottom == 'auto':
783
+ if child.style['margin_bottom'] == 'auto':
761
784
  child.margin_bottom = extra_cross
762
785
  else:
763
- if child.margin_left == 'auto':
786
+ if child.style['margin_left'] == 'auto':
764
787
  child.margin_left = extra_cross
765
- if child.margin_right == 'auto':
788
+ if child.style['margin_right'] == 'auto':
766
789
  child.margin_right = extra_cross
767
790
  else:
768
791
  # Otherwise…
@@ -815,9 +838,6 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
815
838
  margins += (
816
839
  child.border_left_width + child.border_right_width +
817
840
  child.padding_left + child.padding_right)
818
- # TODO: Don't set style width, find a way to avoid width
819
- # re-calculation after 16.
820
- child.style[cross] = Dimension(line.cross_size - margins, 'px')
821
841
  position_cross += line.cross_size
822
842
 
823
843
  # 15 Determine the flex container’s used cross size.
@@ -1,5 +1,7 @@
1
1
  """Layout for floating boxes."""
2
2
 
3
+ from math import inf
4
+
3
5
  from ..formatting_structure import boxes
4
6
  from .min_max import handle_min_max_width
5
7
  from .percent import resolve_percentages, resolve_position_percentages
@@ -115,9 +117,14 @@ def find_float_position(context, box, containing_block):
115
117
 
116
118
  def get_clearance(context, box, collapsed_margin=0):
117
119
  """Return None if there is no clearance, otherwise the clearance value."""
120
+ # Box should be after shape that’s broken on this page.
121
+ for broken_shape in context.broken_out_of_flow:
122
+ if broken_shape.is_floated():
123
+ if box.style['clear'] in (broken_shape.style['float'], 'both'):
124
+ return inf
125
+ # Hypothetical position is the position of the top border edge
118
126
  clearance = None
119
127
  hypothetical_position = box.position_y + collapsed_margin
120
- # Hypothetical position is the position of the top border edge
121
128
  for excluded_shape in context.excluded_shapes:
122
129
  if box.style['clear'] in (excluded_shape.style['float'], 'both'):
123
130
  y, h = excluded_shape.position_y, excluded_shape.margin_height()
weasyprint/layout/grid.py CHANGED
@@ -1115,7 +1115,7 @@ def grid_layout(context, box, bottom_space, skip_stack, containing_block,
1115
1115
  sum(size for size, _ in rows_sizes[skip_row:]) +
1116
1116
  (len(rows_sizes[skip_row:]) - 1) * row_gap)
1117
1117
  row_lines_positions = (
1118
- rows_positions[skip_row + 1:] + [box.content_box_y() + total_height])
1118
+ [*rows_positions[skip_row + 1:], box.content_box_y() + total_height])
1119
1119
  for i, row_y in enumerate(row_lines_positions, start=skip_row + 1):
1120
1120
  if context.overflows_page(bottom_space, row_y - skip_height):
1121
1121
  if not page_is_empty:
@@ -30,7 +30,7 @@ def iter_line_boxes(context, box, position_y, bottom_space, skip_stack,
30
30
  """
31
31
  resolve_percentages(box, containing_block)
32
32
  if skip_stack is None:
33
- # TODO: wrong, see https://github.com/Kozea/WeasyPrint/issues/679
33
+ # TODO: wrong, see issue #679.
34
34
  resolve_one_percentage(box, 'text_indent', containing_block.width)
35
35
  else:
36
36
  box.text_indent = 0
@@ -126,7 +126,7 @@ def get_next_linebox(context, linebox, position_y, bottom_space, skip_stack,
126
126
  line.translate(offset_x, offset_y)
127
127
  # Avoid floating point errors, as position_y - top + top != position_y
128
128
  # Removing this line breaks the position == linebox.position test below
129
- # See https://github.com/Kozea/WeasyPrint/issues/583
129
+ # See issue #583.
130
130
  line.position_y = position_y
131
131
 
132
132
  if line.height <= candidate_height:
@@ -767,15 +767,14 @@ def split_inline_box(context, box, position_x, max_x, bottom_space, skip_stack,
767
767
  # May be None where we have an empty TextBox.
768
768
  assert isinstance(child, boxes.TextBox)
769
769
  else:
770
+ # Store lines to get previous break points.
770
771
  if isinstance(box, boxes.LineBox):
771
772
  line_children.append((index, new_child))
772
- trailing_whitespace = (
773
- isinstance(new_child, boxes.TextBox) and
774
- new_child.text and
775
- unicodedata.category(new_child.text[-1]) == 'Zs')
776
- new_position_x = new_child.position_x + new_child.margin_width()
777
773
 
778
- if new_position_x > max_x and not trailing_whitespace:
774
+ # Check that text doesn’t overflow.
775
+ new_position_x = new_child.position_x + new_child.margin_width()
776
+ if new_position_x - trailing_whitespace_size(context, new_child) > max_x:
777
+ # Text overflows, find previous break point.
779
778
  previous_resume_at = _break_waiting_children(
780
779
  context, containing_block, max_x, bottom_space, initial_skip_stack,
781
780
  absolute_boxes, fixed_boxes, line_placeholders,
weasyprint/layout/page.py CHANGED
@@ -622,6 +622,8 @@ def make_page(context, root_box, page_type, resume_at, page_number,
622
622
  context.reported_footnotes = reported_footnotes[i:]
623
623
  break
624
624
 
625
+ # Display out-of-flow boxes broken on the previous page.
626
+ # TODO: we shouldn’t separate broken in-flow and out-of-flow layout.
625
627
  page_is_empty = True
626
628
  adjoining_margins = []
627
629
  positioned_boxes = [] # Mixed absolute and fixed
@@ -641,13 +643,33 @@ def make_page(context, root_box, page_type, resume_at, page_number,
641
643
  context, box, containing_block, positioned_boxes, 0,
642
644
  skip_stack)
643
645
  out_of_flow_boxes.append(out_of_flow_box)
646
+ page_is_empty = False
644
647
  if out_of_flow_resume_at:
645
648
  broken_out_of_flow[out_of_flow_box] = (
646
649
  box, containing_block, out_of_flow_resume_at)
650
+
651
+ # Display in-flow content.
652
+ initial_root_box = root_box
653
+ initial_resume_at = resume_at
647
654
  root_box, resume_at, next_page, _, _, _ = block_level_layout(
648
655
  context, root_box, 0, resume_at, initial_containing_block,
649
656
  page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins)
650
- assert root_box
657
+ if not root_box:
658
+ # In-flow page rendering didn’t progress, only out-of-flow did. Render empty box
659
+ # at skip_stack and force fragmentation to make the root box and its descendants
660
+ # cover the whole page height.
661
+ assert not page_is_empty
662
+ box = parent = initial_root_box = initial_root_box.deepcopy()
663
+ skip_stack = initial_resume_at
664
+ while skip_stack and len(skip_stack) == 1:
665
+ (skip, skip_stack), = skip_stack.items()
666
+ box, parent = box.children[skip], box
667
+ parent.children = []
668
+ parent.force_fragmentation = True
669
+ root_box, _, _, _, _, _ = block_level_layout(
670
+ context, initial_root_box, 0, initial_resume_at, initial_containing_block,
671
+ page_is_empty, positioned_boxes, positioned_boxes, adjoining_margins)
672
+ resume_at = initial_resume_at
651
673
  root_box.children = out_of_flow_boxes + root_box.children
652
674
 
653
675
  footnote_area = build.create_anonymous_boxes(footnote_area.deepcopy())
@@ -270,37 +270,43 @@ def table_cell_min_max_content_width(context, box, outer=True):
270
270
 
271
271
  def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=None,
272
272
  first_line=False):
273
+ """Yield line width for each line."""
274
+
275
+ # Set text indent.
276
+ text_indent = 0
273
277
  if isinstance(box, boxes.LineBox) and box.style['text_indent'].unit != '%':
274
278
  text_indent = box.style['text_indent'].value
275
- else:
276
- text_indent = 0
277
279
 
280
+ # Yield widths for each line.
278
281
  current_line = 0
279
282
  if skip_stack is None:
280
283
  skip = 0
281
284
  else:
282
285
  (skip, skip_stack), = skip_stack.items()
283
286
  for child in box.children[skip:]:
287
+ # Skip absolutely positioned elements.
284
288
  if child.is_absolutely_positioned():
285
- continue # Skip
289
+ continue
286
290
 
291
+ # None is used in "lines" to track line breaks, transformed to 0 when yielded.
287
292
  if isinstance(child, boxes.InlineBox):
293
+ # Inline box, call function recursively.
288
294
  lines = inline_line_widths(
289
- context, child, outer, is_line_start, minimum, skip_stack,
290
- first_line)
295
+ context, child, outer, is_line_start, minimum, skip_stack, first_line)
291
296
  if first_line:
292
- lines = [next(lines)]
297
+ lines = [next(lines) or None]
293
298
  else:
294
- lines = list(lines)
299
+ lines = [line or None for line in lines]
295
300
  if len(lines) == 1:
296
- lines[0] = adjust(child, outer, lines[0])
301
+ lines[0] = adjust(child, outer, lines[0] or 0)
297
302
  else:
298
- lines[0] = adjust(child, outer, lines[0], right=False)
299
- lines[-1] = adjust(child, outer, lines[-1], left=False)
303
+ lines[0] = adjust(child, outer, lines[0] or 0, right=False) or None
304
+ lines[-1] = adjust(child, outer, lines[-1] or 0, left=False) or None
300
305
  elif isinstance(child, boxes.TextBox):
301
- space_collapse = child.style['white_space'] in (
302
- 'normal', 'nowrap', 'pre-line')
303
- text_wrap = child.style['white_space'] in ('normal', 'pre-wrap', 'pre-line')
306
+ # Text box, split into lines.
307
+ white_space = child.style['white_space']
308
+ space_collapse = white_space in ('normal', 'nowrap', 'pre-line')
309
+ text_wrap = white_space in ('normal', 'pre-wrap', 'pre-line')
304
310
  if skip_stack is None:
305
311
  skip = 0
306
312
  else:
@@ -318,18 +324,21 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
318
324
  child_text[resume_index:].decode(), child.style, context, max_width,
319
325
  child.justification_spacing, is_line_start=is_line_start,
320
326
  minimum=True)
321
- lines.append(width)
327
+ lines.append(width or None)
322
328
  if first_line:
323
329
  break
324
330
  if first_line and new_resume_index:
325
- current_line += lines[0]
331
+ # We only need the first line, break early.
332
+ current_line += lines[0] or 0
326
333
  break
327
334
  # TODO: use the real next character instead of 'a' to detect line breaks.
328
- can_break = can_break_text(
329
- child_text.decode()[-1:] + 'a', child.style['lang'])
335
+ last_letter = child_text.decode()[-1:]
336
+ can_break = can_break_text(last_letter + 'a', child.style['lang'])
330
337
  if minimum and text_wrap and can_break:
331
- lines.append(0)
338
+ # Add all possible line breaks for minimal width.
339
+ lines.append(None)
332
340
  else:
341
+ # Replaced elements, inline blocks…
333
342
  # https://www.w3.org/TR/css-text-3/#overflow-wrap
334
343
  # "The line breaking behavior of a replaced element
335
344
  # or other atomic inline is equivalent to that
@@ -338,20 +347,20 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
338
347
  # "By default, there is a break opportunity
339
348
  # both before and after any inline object."
340
349
  if minimum:
341
- lines = [0, min_content_width(context, child), 0]
350
+ lines = [None, min_content_width(context, child), None]
342
351
  else:
343
352
  lines = [max_content_width(context, child)]
344
- # The first text line goes on the current line
345
- current_line += lines[0]
353
+ # The first text line goes on the current line.
354
+ current_line += lines[0] or 0
346
355
  if len(lines) > 1:
347
- # Forced line break
356
+ # Forced line break(s).
348
357
  yield current_line + text_indent
349
358
  text_indent = 0
350
359
  if len(lines) > 2:
351
360
  for line in lines[1:-1]:
352
- yield line
353
- current_line = lines[-1]
354
- is_line_start = lines[-1] == 0
361
+ yield line or 0
362
+ current_line = lines[-1] or 0
363
+ is_line_start = lines[-1] is None
355
364
  skip_stack = None
356
365
  yield current_line + text_indent
357
366
 
@@ -743,17 +752,30 @@ def trailing_whitespace_size(context, box):
743
752
  """Return the size of the trailing whitespace of ``box``."""
744
753
  from .inline import split_first_line, split_text_box
745
754
 
755
+ # Find last box child, keep last parent to remove nested trailing spaces.
756
+ last_parent = None
746
757
  while isinstance(box, (boxes.InlineBox, boxes.LineBox)):
747
758
  if not box.children:
748
759
  return 0
749
- box = box.children[-1]
750
- if not (isinstance(box, boxes.TextBox) and box.text and
751
- box.style['white_space'] in ('normal', 'nowrap', 'pre-line')):
760
+ last_parent, box = box, box.children[-1]
761
+
762
+ # Return early if possible.
763
+ if not isinstance(box, boxes.TextBox) or not box.text:
764
+ # There’s no text in last child.
752
765
  return 0
753
- stripped_text = box.text.rstrip(' ')
754
- if box.style['font_size'] == 0 or len(stripped_text) == len(box.text):
766
+ elif box.style['white_space'] not in ('normal', 'nowrap', 'pre-line'):
767
+ # Spaces don’t collapse.
755
768
  return 0
756
- if stripped_text:
769
+ elif box.style['font_size'] == 0:
770
+ # Trailing spaces take no space.
771
+ return 0
772
+ elif not box.text.endswith(' '):
773
+ # No trailing space.
774
+ return 0
775
+
776
+ # Strip text.
777
+ if stripped_text := box.text.rstrip(' '):
778
+ # Stripped text is not empty, calculate width difference.
757
779
  resume = 0
758
780
  while resume is not None:
759
781
  old_resume = resume
@@ -763,12 +785,17 @@ def trailing_whitespace_size(context, box):
763
785
  stripped_box, resume, _ = split_text_box(
764
786
  context, stripped_box, None, old_resume)
765
787
  if stripped_box is None:
766
- # old_box split just before the trailing spaces
788
+ # Old box is split just before the trailing spaces.
767
789
  return old_box.width
768
790
  else:
791
+ # Return difference between old width and stripped width.
769
792
  assert resume is None
770
793
  return old_box.width - stripped_box.width
771
794
  else:
795
+ # Stripped text is empty, render spaces to get width.
772
796
  _, _, _, width, _, _ = split_first_line(
773
797
  box.text, box.style, context, None, box.justification_spacing)
798
+ # Remove possible trailing spaces from previous child.
799
+ if last_parent and len(last_parent.children) >= 2:
800
+ width += trailing_whitespace_size(context, last_parent.children[-2])
774
801
  return width
@@ -414,9 +414,13 @@ def table_layout(context, table, bottom_space, skip_stack, containing_block,
414
414
  if avoid_page_break(page_break, context):
415
415
  earlier_page_break = find_earlier_page_break(
416
416
  context, new_table_children, absolute_boxes, fixed_boxes)
417
- if earlier_page_break is not None:
418
- new_table_children, resume_at = earlier_page_break
419
- break
417
+ if earlier_page_break is None:
418
+ remove_placeholders(
419
+ context, new_table_children, absolute_boxes,
420
+ fixed_boxes)
421
+ return None, None, next_page, position_y
422
+ new_table_children, resume_at = earlier_page_break
423
+ break
420
424
  resume_at = {index_group: None}
421
425
  else:
422
426
  return None, None, next_page, position_y
@@ -782,7 +786,7 @@ def auto_table_layout(context, box, containing_block):
782
786
 
783
787
  if assignable_width < sum(max_content_guess):
784
788
  # Default values shouldn't be used, but we never know.
785
- # See https://github.com/Kozea/WeasyPrint/issues/770
789
+ # See issue #770.
786
790
  lower_guess = guesses[0]
787
791
  upper_guess = guesses[-1]
788
792