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
@@ -15,10 +15,10 @@ from .replaced import block_replaced_box_layout
15
15
  from .table import table_layout, table_wrapper_width
16
16
 
17
17
 
18
- def block_level_layout(context, box, bottom_space, skip_stack,
19
- containing_block, page_is_empty=True,
20
- absolute_boxes=None, fixed_boxes=None,
21
- adjoining_margins=None, discard=False, max_lines=None):
18
+ def block_level_layout(context, box, bottom_space, skip_stack, containing_block,
19
+ page_is_empty=True, absolute_boxes=None, fixed_boxes=None,
20
+ adjoining_margins=None, first_letter_style=None,
21
+ first_line_style=None, discard=False, max_lines=None):
22
22
  """Lay out the block-level ``box``."""
23
23
  absolute_boxes = [] if absolute_boxes is None else absolute_boxes
24
24
  fixed_boxes = [] if fixed_boxes is None else fixed_boxes
@@ -58,14 +58,14 @@ def block_level_layout(context, box, bottom_space, skip_stack,
58
58
 
59
59
  return block_level_layout_switch(
60
60
  context, box, bottom_space, skip_stack, containing_block,
61
- page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins, discard,
62
- max_lines)
61
+ page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
62
+ first_letter_style, first_line_style, discard, max_lines)
63
63
 
64
64
 
65
- def block_level_layout_switch(context, box, bottom_space, skip_stack,
66
- containing_block, page_is_empty, absolute_boxes,
67
- fixed_boxes, adjoining_margins, discard,
68
- max_lines):
65
+ def block_level_layout_switch(context, box, bottom_space, skip_stack, containing_block,
66
+ page_is_empty, absolute_boxes, fixed_boxes,
67
+ adjoining_margins, first_letter_style, first_line_style,
68
+ discard, max_lines):
69
69
  """Call the layout function corresponding to the ``box`` type."""
70
70
  if isinstance(box, boxes.TableBox):
71
71
  result = table_layout(
@@ -75,7 +75,7 @@ def block_level_layout_switch(context, box, bottom_space, skip_stack,
75
75
  return block_box_layout(
76
76
  context, box, bottom_space, skip_stack, containing_block,
77
77
  page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
78
- discard, max_lines)
78
+ first_letter_style, first_line_style, discard, max_lines)
79
79
  elif isinstance(box, boxes.BlockReplacedBox):
80
80
  result = block_replaced_box_layout(context, box, containing_block)
81
81
  elif isinstance(box, boxes.FlexBox):
@@ -93,13 +93,15 @@ def block_level_layout_switch(context, box, bottom_space, skip_stack,
93
93
 
94
94
  def block_box_layout(context, box, bottom_space, skip_stack,
95
95
  containing_block, page_is_empty, absolute_boxes,
96
- fixed_boxes, adjoining_margins, discard, max_lines):
96
+ fixed_boxes, adjoining_margins, first_letter_style,
97
+ first_line_style, discard, max_lines):
97
98
  """Lay out the block ``box``."""
98
99
  if (box.style['column_width'] != 'auto' or
99
100
  box.style['column_count'] != 'auto'):
100
101
  result = columns_layout(
101
- context, box, bottom_space, skip_stack, containing_block,
102
- page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins)
102
+ context, box, bottom_space, skip_stack, containing_block, page_is_empty,
103
+ absolute_boxes, fixed_boxes, adjoining_margins, first_letter_style,
104
+ first_line_style)
103
105
  resume_at = result[1]
104
106
  # TODO: this condition and the whole relayout are probably wrong
105
107
  if resume_at is None:
@@ -112,9 +114,9 @@ def block_box_layout(context, box, bottom_space, skip_stack,
112
114
  context, [new_box], absolute_boxes, fixed_boxes)
113
115
  bottom_space += columns_bottom_space
114
116
  result = columns_layout(
115
- context, box, bottom_space, skip_stack,
116
- containing_block, page_is_empty, absolute_boxes,
117
- fixed_boxes, adjoining_margins)
117
+ context, box, bottom_space, skip_stack, containing_block,
118
+ page_is_empty, absolute_boxes, fixed_boxes, adjoining_margins,
119
+ first_letter_style, first_line_style)
118
120
  return (*result, None)
119
121
  elif box.is_table_wrapper:
120
122
  table_wrapper_width(
@@ -122,8 +124,9 @@ def block_box_layout(context, box, bottom_space, skip_stack,
122
124
  block_level_width(box, containing_block)
123
125
 
124
126
  result = block_container_layout(
125
- context, box, bottom_space, skip_stack, page_is_empty,
126
- absolute_boxes, fixed_boxes, adjoining_margins, discard, max_lines)
127
+ context, box, bottom_space, skip_stack, page_is_empty, absolute_boxes,
128
+ fixed_boxes, adjoining_margins, first_letter_style, first_line_style, discard,
129
+ max_lines)
127
130
  # TODO: columns and flex items shouldn't be block boxes, this condition
128
131
  # would then be useless when this is fixed.
129
132
  if not (new_box := result[0]) or new_box.is_column or new_box.is_flex_item:
@@ -298,9 +301,13 @@ def _out_of_flow_layout(context, box, index, child, new_children,
298
301
  return stop, resume_at, new_child, out_of_flow_resume_at
299
302
 
300
303
 
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):
304
+ def _break_line(context, box, line, new_children, next_lines, page_is_empty, index,
305
+ skip_stack, resume_at, absolute_boxes, fixed_boxes):
306
+ """Break line where allowed by orphans and widows.
307
+
308
+ Return (abort, stop, resume_at).
309
+
310
+ """
304
311
  over_orphans = len(new_children) - box.style['orphans']
305
312
  if over_orphans < 0 and not page_is_empty:
306
313
  # Reached the bottom of the page before we had
@@ -309,12 +316,7 @@ def _break_line(context, box, line, new_children, lines_iterator,
309
316
  return True, False, resume_at
310
317
  # How many lines we need on the next page to satisfy widows
311
318
  # -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
319
+ needed = max(box.style['widows'] - 1 - next_lines, 0)
318
320
  if needed > over_orphans and not page_is_empty:
319
321
  # Total number of lines < orphans + widows
320
322
  remove_placeholders(context, line.children, absolute_boxes, fixed_boxes)
@@ -333,7 +335,7 @@ def _break_line(context, box, line, new_children, lines_iterator,
333
335
  def _linebox_layout(context, box, index, child, new_children, page_is_empty,
334
336
  absolute_boxes, fixed_boxes, adjoining_margins,
335
337
  bottom_space, position_y, skip_stack, first_letter_style,
336
- draw_bottom_decoration, max_lines):
338
+ first_line_style, draw_bottom_decoration, max_lines):
337
339
  abort = stop = False
338
340
  resume_at = None
339
341
  new_footnotes = []
@@ -344,8 +346,8 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
344
346
  position_y += collapse_margin(adjoining_margins)
345
347
  new_containing_block = box
346
348
  lines_iterator = iter_line_boxes(
347
- context, child, position_y, bottom_space, skip_stack,
348
- new_containing_block, absolute_boxes, fixed_boxes, first_letter_style)
349
+ context, child, position_y, bottom_space, skip_stack, new_containing_block,
350
+ absolute_boxes, fixed_boxes, first_letter_style, first_line_style)
349
351
  for i, (line, resume_at) in enumerate(lines_iterator):
350
352
  # Break box if we reached max-lines
351
353
  if max_lines is not None:
@@ -366,24 +368,52 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
366
368
  else:
367
369
  offset_y = 0
368
370
 
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.
371
+ # Allow overflow if the first line of the page is higher than the page itself so
372
+ # that we put *something* on this page and can advance in the context.
372
373
  overflow = (
373
374
  (new_children or not page_is_empty) and
374
375
  context.overflows_page(bottom_space, new_position_y + offset_y))
375
376
  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)
377
+ # If we couldn’t break the line before but can break now, first try to
378
+ # report footnotes and see if we don’t overflow.
379
+ could_break_before = can_break_now = True
380
+ next_lines = len(tuple(lines_iterator))
381
+ if len(new_children) + 1 < box.style['orphans']:
382
+ can_break_now = False
383
+ elif next_lines < box.style['widows']:
384
+ can_break_now = False
385
+ if len(new_children) < box.style['orphans']:
386
+ could_break_before = False
387
+ elif next_lines + 1 < box.style['widows']:
388
+ could_break_before = False
389
+ report = not context.in_column and can_break_now and not could_break_before
390
+ reported_footnotes = 0
391
+ while report and context.current_page_footnotes:
392
+ context.report_footnote(context.current_page_footnotes[-1])
393
+ reported_footnotes += 1
394
+ if not context.overflows_page(bottom_space, new_position_y + offset_y):
395
+ new_children.append(line)
396
+ stop = True
397
+ break
398
+ else:
399
+ abort, stop, resume_at = _break_line(
400
+ context, box, line, new_children, next_lines,
401
+ page_is_empty, index, skip_stack, resume_at, absolute_boxes,
402
+ fixed_boxes)
403
+
404
+ # Revert reported footnotes, as they’ve been reported starting from the last
405
+ # one.
406
+ if reported_footnotes >= 2:
407
+ extra = context.reported_footnotes[-1:-reported_footnotes-1:-1]
408
+ context.reported_footnotes[-reported_footnotes:] = extra
409
+
380
410
  break
381
411
 
382
412
  # TODO: this is incomplete.
383
413
  # See https://drafts.csswg.org/css-page-3/#allowed-pg-brk
384
414
  # "When an unforced page break occurs here, both the adjoining
385
415
  # ‘margin-top’ and ‘margin-bottom’ are set to zero."
386
- # See https://github.com/Kozea/WeasyPrint/issues/115
416
+ # See issue #115.
387
417
  elif page_is_empty and context.overflows_page(
388
418
  bottom_space, new_position_y):
389
419
  # Remove the top border when a page is empty and the box is
@@ -413,9 +443,10 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
413
443
  # even try.
414
444
  if new_children or not page_is_empty:
415
445
  if footnote.style['footnote_policy'] == 'line':
446
+ next_lines = len(tuple(lines_iterator))
416
447
  abort, stop, resume_at = _break_line(
417
448
  context, box, line, new_children,
418
- lines_iterator, page_is_empty, index,
449
+ next_lines, page_is_empty, index,
419
450
  skip_stack, resume_at, absolute_boxes,
420
451
  fixed_boxes)
421
452
  break_linebox = True
@@ -437,15 +468,14 @@ def _linebox_layout(context, box, index, child, new_children, page_is_empty,
437
468
 
438
469
 
439
470
  def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
440
- absolute_boxes, fixed_boxes, adjoining_margins,
441
- bottom_space, position_y, skip_stack, first_letter_style,
442
- draw_bottom_decoration, collapsing_with_children, discard,
443
- next_page, max_lines):
471
+ absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
472
+ position_y, skip_stack, first_letter_style, first_line_style,
473
+ discard, next_page, max_lines):
444
474
  abort = stop = False
445
475
 
476
+ # Find possible page break between in-flow siblings.
446
477
  last_in_flow_child = find_last_in_flow_child(new_children)
447
478
  if last_in_flow_child is not None:
448
- # Between in-flow siblings
449
479
  page_break = block_level_page_break(last_in_flow_child, child)
450
480
  page_name = block_level_page_name(last_in_flow_child, child)
451
481
  if page_name or force_page_break(page_break, context):
@@ -459,16 +489,15 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
459
489
  else:
460
490
  page_break = 'auto'
461
491
 
462
- new_containing_block = box
463
-
464
- if not new_containing_block.is_table_wrapper:
465
- resolve_percentages(child, new_containing_block)
466
- if last_in_flow_child is None and collapsing_with_children:
492
+ # Resolve percentages and collapsing top margins.
493
+ if not box.is_table_wrapper:
494
+ resolve_percentages(child, box)
495
+ if last_in_flow_child is None and box.top_margin_collapses():
467
496
  # TODO: add the adjoining descendants' margin top to
468
- # [child.margin_top]
497
+ # [child.margin_top].
469
498
  old_collapsed_margin = collapse_margin(adjoining_margins)
470
499
  # TODO: the margin-top value is set afterwards in
471
- # block_level_layout, we shouldn’t duplicate this code
500
+ # block_level_layout, we shouldn’t duplicate this code.
472
501
  child_margin_top = child.margin_top
473
502
  if child_margin_top == 'auto':
474
503
  child_margin_top = 0
@@ -502,81 +531,76 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
502
531
  if box.is_table_wrapper: # should not be a special case
503
532
  collapsed_margin = collapse_margin(adjoining_margins)
504
533
  child.position_y += collapsed_margin
505
- position_y += collapsed_margin
506
534
  adjoining_margins = []
507
535
  elif not isinstance(child, boxes.BlockBox): # blocks handle that themselves
508
536
  if child.style['margin_top'] == 'auto':
509
537
  margin_top = 0
510
538
  else:
511
- margin_top = percentage(child.style['margin_top'], box.width)
539
+ margin_top = percentage(
540
+ child.style['margin_top'], child.style, box.width)
512
541
  adjoining_margins.append(margin_top)
513
542
  offset_y = collapse_margin(adjoining_margins) - margin_top
514
543
  child.position_y += offset_y
515
- position_y += offset_y
516
544
  adjoining_margins = []
517
545
 
518
546
  page_is_empty_with_no_children = page_is_empty and not any(
519
547
  child for child in new_children
520
548
  if not isinstance(child, AbsolutePlaceholder))
521
549
 
522
- if not getattr(child, 'first_letter_style', None):
523
- child.first_letter_style = first_letter_style
524
550
  (new_child, resume_at, next_page, next_adjoining_margins,
525
551
  collapsing_through, max_lines) = block_level_layout(
526
- context, child, bottom_space, skip_stack,
527
- new_containing_block, page_is_empty_with_no_children, absolute_boxes,
528
- fixed_boxes, adjoining_margins, discard, max_lines)
552
+ context, child, bottom_space, skip_stack, box, page_is_empty_with_no_children,
553
+ absolute_boxes, fixed_boxes, adjoining_margins, first_letter_style,
554
+ first_line_style, discard, max_lines)
529
555
 
556
+ # Check that child doesn’t overflow and set next position_y.
530
557
  if new_child is not None:
531
558
  if not collapsing_through:
532
- new_content_position_y = (
533
- new_child.content_box_y() + new_child.height)
534
- new_position_y = (
535
- new_child.border_box_y() + new_child.border_height())
559
+ # Find content position and check that it doesn’t overflow.
560
+ new_content_position_y = new_child.content_box_y() + new_child.height
536
561
  content_page_overflow = context.overflows_page(
537
562
  bottom_space, new_content_position_y)
538
- border_page_overflow = context.overflows_page(
539
- bottom_space, new_position_y)
540
- can_break = not (
541
- page_is_empty_with_no_children or box.is_monolithic())
563
+
564
+ # Update bottom space to include new child bottom spacing and check that it
565
+ # doesn’t overflow.
566
+ bottom_space += new_child.padding_bottom + new_child.border_bottom_width
567
+ if not box.bottom_margin_collapses():
568
+ bottom_space += new_child.margin_bottom
569
+ new_position_y = new_child.border_box_y() + new_child.border_height()
570
+ border_page_overflow = context.overflows_page(bottom_space, new_position_y)
571
+
572
+ can_break = not (page_is_empty_with_no_children or box.is_monolithic())
542
573
  if can_break and content_page_overflow:
543
- # The child content overflows the page area, display it on the
544
- # next page.
545
- remove_placeholders(
546
- context, [new_child], absolute_boxes, fixed_boxes)
574
+ # Child content overflows the page area, display it on the next page.
575
+ remove_placeholders(context, [new_child], absolute_boxes, fixed_boxes)
547
576
  new_child = None
548
577
  elif can_break and border_page_overflow:
549
- # The child border/padding overflows the page area, do the
550
- # layout again with a higher bottom_space value.
551
- remove_placeholders(
552
- context, [new_child], absolute_boxes, fixed_boxes)
553
- bottom_space += (
554
- new_child.padding_bottom + new_child.border_bottom_width)
578
+ # Child border/padding/margin overflows the page area, do the layout
579
+ # again with a bottom_space value that includes them.
580
+ remove_placeholders(context, [new_child], absolute_boxes, fixed_boxes)
555
581
  (new_child, resume_at, next_page, next_adjoining_margins,
556
582
  collapsing_through, max_lines) = block_level_layout(
557
- context, child, bottom_space, skip_stack,
558
- new_containing_block, page_is_empty_with_no_children,
559
- absolute_boxes, fixed_boxes, adjoining_margins, discard,
560
- max_lines)
583
+ context, child, bottom_space, skip_stack, box,
584
+ page_is_empty_with_no_children, absolute_boxes, fixed_boxes,
585
+ adjoining_margins, discard, max_lines)
561
586
  if new_child:
562
- position_y = (
563
- new_child.border_box_y() + new_child.border_height())
587
+ position_y = new_child.border_box_y() + new_child.border_height()
564
588
  else:
565
589
  position_y = new_position_y
566
590
 
591
+ # Use the new child adjoining margins.
567
592
  adjoining_margins = next_adjoining_margins
568
593
  if new_child:
569
594
  adjoining_margins.append(new_child.margin_bottom)
570
595
 
596
+ # Handle clearance.
571
597
  if new_child and new_child.clearance:
572
598
  position_y = new_child.border_box_y() + new_child.border_height()
573
599
 
574
- skip_stack = None
575
-
576
600
  if new_child is None:
577
- # Nothing fits in the remaining space of this page: break
601
+ # Nothing fits in the remaining space of this page: break.
578
602
  if avoid_page_break(page_break, context):
579
- # TODO: fill the blank space at the bottom of the page
603
+ # TODO: fill the blank space at the bottom of the page.
580
604
  result = find_earlier_page_break(
581
605
  context, new_children, absolute_boxes, fixed_boxes)
582
606
  if result:
@@ -586,11 +610,10 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
586
610
  abort, stop, resume_at, position_y, adjoining_margins,
587
611
  next_page, new_children, max_lines)
588
612
  else:
589
- # We did not find any page break opportunity
613
+ # We did not find any page break opportunity.
590
614
  if not page_is_empty:
591
- # The page has content *before* this block:
592
- # cancel the block and try to find a break
593
- # in the parent.
615
+ # The page has content *before* this block: cancel the block and try
616
+ # to find a break in the parent.
594
617
  abort = True
595
618
  return (
596
619
  abort, stop, resume_at, position_y, adjoining_margins,
@@ -599,26 +622,26 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
599
622
  # ignore this 'avoid' and break anyway.
600
623
 
601
624
  if all(child.is_absolutely_positioned() for child in new_children):
602
- # This box has only rendered absolute children, keep them
603
- # for the next page. This is for example useful for list
604
- # markers.
605
- remove_placeholders(
606
- context, new_children, absolute_boxes, fixed_boxes)
625
+ # This box has only rendered absolute children, keep them for the next page.
626
+ # This is for example useful for list markers.
627
+ remove_placeholders(context, new_children, absolute_boxes, fixed_boxes)
607
628
  new_children = []
608
629
 
609
630
  if new_children:
631
+ # We already have children, keep them and stop the box rendering.
610
632
  resume_at = {index: None}
611
633
  stop = True
612
634
  else:
613
- # This was the first child of this box, cancel the box completly
635
+ # This was the first child of this box, cancel the box completly.
614
636
  abort = True
615
637
  return (
616
638
  abort, stop, resume_at, position_y, adjoining_margins, next_page,
617
639
  new_children, max_lines)
618
640
 
619
- # index in its non-laid-out parent, not in future new parent
620
- # May be used in find_earlier_page_break()
641
+ # Index in its non-laid-out parent, not in future new parent.
642
+ # May be used in find_earlier_page_break().
621
643
  new_child.index = index
644
+
622
645
  new_children.append(new_child)
623
646
  if resume_at is not None:
624
647
  resume_at = {index: resume_at}
@@ -629,40 +652,34 @@ def _in_flow_layout(context, box, index, child, new_children, page_is_empty,
629
652
  new_children, max_lines)
630
653
 
631
654
 
632
- def block_container_layout(context, box, bottom_space, skip_stack,
633
- page_is_empty, absolute_boxes, fixed_boxes,
634
- adjoining_margins, discard, max_lines):
655
+ def block_container_layout(context, box, bottom_space, skip_stack, page_is_empty,
656
+ absolute_boxes, fixed_boxes, adjoining_margins,
657
+ first_letter_style, first_line_style, discard, max_lines):
635
658
  """Set the ``box`` height."""
636
659
  assert isinstance(box, boxes.BlockContainerBox)
637
660
 
638
661
  if box.establishes_formatting_context():
639
- context.create_block_formatting_context()
662
+ context.create_block_formatting_context(box)
640
663
 
641
664
  # TODO: merge this with _in_flow_layout, flex_layout…
642
665
  is_start = skip_stack is None
643
666
  box.remove_decoration(start=not is_start, end=False)
644
667
 
645
668
  discard |= box.style['continue'] == 'discard'
646
- draw_bottom_decoration = (
647
- discard or box.style['box_decoration_break'] == 'clone')
669
+ draw_bottom_decoration = discard or box.style['box_decoration_break'] == 'clone'
648
670
 
649
671
  if adjoining_margins is None:
650
672
  adjoining_margins = []
651
673
 
652
674
  if draw_bottom_decoration:
653
- bottom_space += (
654
- box.padding_bottom + box.border_bottom_width + box.margin_bottom)
675
+ bottom_space += box.padding_bottom + box.border_bottom_width + box.margin_bottom
655
676
 
656
677
  adjoining_margins.append(box.margin_top)
657
678
  this_box_adjoining_margins = adjoining_margins
658
679
 
659
- collapsing_with_children = not (
660
- box.border_top_width or box.padding_top or box.is_flex_item or
661
- box.is_grid_item or box.establishes_formatting_context() or
662
- box.is_for_root_element)
663
- if collapsing_with_children:
664
- # Not counting margins in adjoining_margins, if any
665
- # (there are not padding or borders, see above)
680
+ if box.top_margin_collapses():
681
+ # Not counting margins in adjoining_margins, if any (there are not padding or
682
+ # borders, see above).
666
683
  position_y = box.position_y
667
684
  else:
668
685
  box.position_y += collapse_margin(adjoining_margins) - box.margin_top
@@ -672,92 +689,118 @@ def block_container_layout(context, box, bottom_space, skip_stack,
672
689
  position_x = box.content_box_x()
673
690
 
674
691
  if box.style['position'] == 'relative':
675
- # New containing block, use a new absolute list
692
+ # New containing block, use a new absolute list.
676
693
  absolute_boxes = []
677
694
 
678
695
  new_children = []
679
696
  next_page = {'break': 'any', 'page': None}
680
- all_footnotes = []
681
- broken_out_of_flow = {}
682
-
683
- last_in_flow_child = None
684
697
 
685
698
  if box.style['max_lines'] != 'none':
686
699
  max_lines = min(box.style['max_lines'], max_lines or inf)
687
700
 
701
+ if box.first_line_style is not None:
702
+ box_first_line_style = {
703
+ key: box.first_line_style[key] for key in box.first_line_style.cascaded}
704
+ if first_line_style is None:
705
+ first_line_style = box_first_line_style
706
+ else:
707
+ first_line_style |= box_first_line_style
708
+
709
+ if box.first_letter_style is not None:
710
+ box_first_letter_style = {
711
+ key: box.first_letter_style[key] for key in box.first_letter_style.cascaded}
712
+ if first_letter_style is None:
713
+ first_letter_style = box_first_letter_style
714
+ else:
715
+ first_letter_style |= box_first_letter_style
716
+
717
+ # Layout box children.
688
718
  if is_start:
689
719
  skip = 0
690
- first_letter_style = getattr(box, 'first_letter_style', None)
691
720
  else:
692
721
  (skip, skip_stack), = skip_stack.items()
693
- first_letter_style = None
694
722
  for index, child in enumerate(box.children[skip:], start=(skip or 0)):
695
723
  child.position_x = position_x
696
724
  child.position_y = position_y # doesn’t count adjoining_margins
697
725
  new_footnotes = []
698
726
 
699
727
  if not child.is_in_normal_flow():
728
+ # Layout out-of-flow child.
700
729
  abort = False
701
730
  stop, resume_at, new_child, out_of_flow_resume_at = (
702
731
  _out_of_flow_layout(
703
732
  context, box, index, child, new_children, page_is_empty,
704
- absolute_boxes, fixed_boxes, adjoining_margins,
705
- bottom_space))
733
+ absolute_boxes, fixed_boxes, adjoining_margins, bottom_space))
706
734
  if out_of_flow_resume_at:
707
- broken_out_of_flow[new_child] = (
708
- child, box, out_of_flow_resume_at)
735
+ context.add_broken_out_of_flow(
736
+ new_child, child, box, out_of_flow_resume_at)
737
+ if child.is_outside_marker:
738
+ new_child.position_x = box.border_box_x()
739
+ if child.style['direction'] == 'rtl':
740
+ new_child.position_x += box.width + box.padding_right
709
741
 
710
742
  elif isinstance(child, boxes.LineBox):
743
+ # Layout line child.
711
744
  (abort, stop, resume_at, position_y,
712
745
  new_footnotes, max_lines) = _linebox_layout(
713
746
  context, box, index, child, new_children, page_is_empty,
714
747
  absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
715
- position_y, skip_stack, first_letter_style,
748
+ position_y, skip_stack, first_letter_style, first_line_style,
716
749
  draw_bottom_decoration, max_lines)
717
750
  draw_bottom_decoration |= resume_at is None
718
751
  adjoining_margins = []
719
- all_footnotes += new_footnotes
752
+ first_letter_style = first_line_style = None
720
753
 
721
754
  else:
755
+ # Layout in-flow child.
722
756
  (abort, stop, resume_at, position_y, adjoining_margins,
723
757
  next_page, new_children, new_max_lines) = _in_flow_layout(
724
758
  context, box, index, child, new_children, page_is_empty,
725
759
  absolute_boxes, fixed_boxes, adjoining_margins, bottom_space,
726
- position_y, skip_stack, first_letter_style,
727
- draw_bottom_decoration, collapsing_with_children, discard,
728
- next_page, max_lines)
760
+ position_y, skip_stack, first_letter_style, first_line_style,
761
+ discard, next_page, max_lines)
729
762
  skip_stack = None
763
+ first_letter_style = first_line_style = None
730
764
 
765
+ # Handle max-lines.
731
766
  if None not in (new_max_lines, max_lines):
732
767
  max_lines = new_max_lines
733
768
  if max_lines <= 0:
769
+ # Maximum number of lines reached, stop box rendering.
734
770
  stop = True
735
771
  last_child = (child == box.children[-1])
736
772
  if not last_child:
773
+ # This is not the last line, recursively find the last line to
774
+ # set ellipsis on it.
737
775
  children = new_children
738
776
  while children:
739
777
  last_child = children[-1]
740
778
  if isinstance(last_child, boxes.LineBox):
741
- last_child.block_ellipsis = (
742
- box.style['block_ellipsis'])
779
+ last_child.block_ellipsis = box.style['block_ellipsis']
743
780
  elif isinstance(last_child, boxes.ParentBox):
744
781
  children = last_child.children
745
782
  continue
746
783
  break
747
784
 
748
785
  if abort:
786
+ # Abort the rendering of box.
749
787
  page = child.page_values()[0]
750
788
  remove_placeholders(
751
789
  context, box.children[skip:], absolute_boxes, fixed_boxes)
752
790
  for footnote in new_footnotes:
753
791
  context.unlayout_footnote(footnote)
792
+ if box.establishes_formatting_context():
793
+ context.finish_block_formatting_context()
754
794
  return (
755
795
  None, None, {'break': 'any', 'page': page}, [], False,
756
796
  max_lines)
757
797
  elif stop:
798
+ # Stop after the rendering of child.
758
799
  if box.height != 'auto':
759
- if context.overflows(box.position_y + box.border_height(), position_y):
760
- # Box heigh is fixed and it doesn’t overflow page, forget
800
+ box_bottom = box.position_y + box.border_height()
801
+ bottom_margin = collapse_margin(adjoining_margins)
802
+ if context.overflows(box_bottom, position_y - bottom_margin):
803
+ # Box height is fixed and it overflows the page, forget
761
804
  # overflowing children.
762
805
  resume_at = None
763
806
  adjoining_margins = []
@@ -766,7 +809,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
766
809
  else:
767
810
  resume_at = None
768
811
 
769
- box_is_fragmented = resume_at is not None
812
+ box_is_fragmented = resume_at is not None or box.force_fragmentation
770
813
  if box.style['continue'] == 'discard':
771
814
  resume_at = None
772
815
 
@@ -775,40 +818,37 @@ def block_container_layout(context, box, bottom_space, skip_stack,
775
818
  not page_is_empty):
776
819
  remove_placeholders(
777
820
  context, [*new_children, *box.children[skip:]], absolute_boxes, fixed_boxes)
821
+ if box.establishes_formatting_context():
822
+ context.finish_block_formatting_context()
778
823
  return None, None, {'break': 'any', 'page': None}, [], False, max_lines
779
824
 
780
- for key, value in broken_out_of_flow.items():
781
- context.broken_out_of_flow[key] = value
782
-
783
- if collapsing_with_children:
784
- box.position_y += (
785
- collapse_margin(this_box_adjoining_margins) - box.margin_top)
825
+ if box.top_margin_collapses():
826
+ box.position_y += collapse_margin(this_box_adjoining_margins) - box.margin_top
786
827
 
828
+ # Detect collapsing-through situation and collapse margins accordingly.
787
829
  last_in_flow_child = find_last_in_flow_child(new_children)
788
830
  collapsing_through = False
789
831
  if last_in_flow_child is None:
832
+ # No in-flow child in box, collapse its top and bottom margins.
790
833
  collapsed_margin = collapse_margin(adjoining_margins)
791
- # Top and bottom margins of this box
792
834
  if (box.height in ('auto', 0) and
793
835
  get_clearance(context, box, collapsed_margin) is None and
794
836
  all(value == 0 for value in (
795
837
  box.min_height, box.border_top_width, box.padding_top,
796
838
  box.border_bottom_width, box.padding_bottom))):
839
+ # Collapse through the box.
797
840
  collapsing_through = True
798
841
  else:
842
+ # Don’t collapse through the box, update position and reset margins.
799
843
  position_y += collapsed_margin
800
844
  adjoining_margins = []
801
845
  else:
802
- # Bottom margin of the last child and bottom margin of this box
846
+ # In-flow children, collapse last child bottom margin and box bottom margin.
803
847
  if box.height != 'auto':
804
- # Not adjoining (position_y is not used afterwards)
848
+ # Not adjoining, reset margins.
805
849
  adjoining_margins = []
806
850
 
807
- if (box.border_bottom_width or
808
- box.padding_bottom or
809
- box.establishes_formatting_context() or
810
- box.is_for_root_element or
811
- box.is_table_wrapper):
851
+ if not box.bottom_margin_collapses():
812
852
  position_y += collapse_margin(adjoining_margins)
813
853
  adjoining_margins = []
814
854
 
@@ -831,7 +871,10 @@ def block_container_layout(context, box, bottom_space, skip_stack,
831
871
  float_box.position_y + float_box.margin_height()
832
872
  for float_box in context.excluded_shapes)
833
873
  position_y = max(max_float_position_y, position_y)
834
- new_box.height = position_y - new_box.content_box_y()
874
+ if position_y == new_box.content_box_y() == inf:
875
+ new_box.height = 0
876
+ else:
877
+ new_box.height = position_y - new_box.content_box_y()
835
878
 
836
879
  if new_box.style['position'] == 'relative':
837
880
  # New containing block, resolve the layout of the absolute descendants
@@ -843,7 +886,7 @@ def block_container_layout(context, box, bottom_space, skip_stack,
843
886
  for child in new_box.children:
844
887
  relative_positioning(child, (new_box.width, new_box.height))
845
888
 
846
- if new_box.establishes_formatting_context():
889
+ if box.establishes_formatting_context():
847
890
  context.finish_block_formatting_context(new_box)
848
891
 
849
892
  if discard or not box_is_fragmented: