Repository: Kozea/WeasyPrint

Issue:
Bottom margin on flex item ignored at the bottom of flex containers
Here’s a small example:

```html
<section style="display: flex; flex-direction: column /* or row */">
  <div style="background: pink">
    <p style="margin: 1em 0; background: yellow">
      A
    </p>
  </div>
</section>
```

(Discovered while solving #2442.)

Hints from issue comments:

Solution patch:
diff --git a/weasyprint/formatting_structure/build.py b/weasyprint/formatting_structure/build.py
index 237e88af4..fc234a9fb 100644
--- a/weasyprint/formatting_structure/build.py
+++ b/weasyprint/formatting_structure/build.py
@@ -983,6 +983,25 @@ def wrap_table(box, children):
     return wrapper
 
 
+def blockify(box, layout):
+    """Turn an inline box into a block box."""
+    # See https://drafts.csswg.org/css-display-4/#blockify.
+    if isinstance(box, boxes.InlineBlockBox):
+        anonymous = boxes.BlockBox.anonymous_from(box, box.children)
+    elif isinstance(box, boxes.InlineReplacedBox):
+        replacement = box.replacement
+        anonymous = boxes.BlockReplacedBox.anonymous_from(box, replacement)
+    elif isinstance(box, boxes.InlineLevelBox):
+        anonymous = boxes.BlockBox.anonymous_from(box, [box])
+        setattr(box, f'is_{layout}_item', False)
+    else:
+        return box
+    anonymous.style = box.style
+    setattr(anonymous, f'is_{layout}_item', True)
+    return anonymous
+
+
+
 def flex_boxes(box):
     """Remove and add boxes according to the flex model.
 
@@ -1012,18 +1031,7 @@ def flex_children(box, children):
                 # affected by the white-space property"
                 # https://www.w3.org/TR/css-flexbox-1/#flex-items
                 continue
-            if isinstance(child, boxes.InlineBlockBox):
-                anonymous = boxes.BlockBox.anonymous_from(child, child.children)
-                anonymous.style = child.style
-                anonymous.is_flex_item = True
-                flex_children.append(anonymous)
-            elif isinstance(child, boxes.InlineLevelBox):
-                anonymous = boxes.BlockBox.anonymous_from(child, [child])
-                anonymous.style = child.style
-                anonymous.is_flex_item = True
-                flex_children.append(anonymous)
-            else:
-                flex_children.append(child)
+            flex_children.append(blockify(child, 'flex'))
         return flex_children
     else:
         return children
@@ -1057,19 +1065,7 @@ def grid_children(box, children):
                 # affected by the white-space property"
                 # https://drafts.csswg.org/css-grid-2/#grid-item
                 continue
-            if isinstance(child, boxes.InlineBlockBox):
-                anonymous = boxes.BlockBox.anonymous_from(child, child.children)
-                anonymous.style = child.style
-                anonymous.is_grid_item = True
-                grid_children.append(anonymous)
-            elif isinstance(child, boxes.InlineLevelBox):
-                anonymous = boxes.BlockBox.anonymous_from(child, [child])
-                anonymous.style = child.style
-                child.is_grid_item = False
-                anonymous.is_grid_item = True
-                grid_children.append(anonymous)
-            else:
-                grid_children.append(child)
+            grid_children.append(blockify(child, 'grid'))
         return grid_children
     else:
         return children
diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py
index f91b0bb84..73e7f619f 100644
--- a/weasyprint/layout/flex.py
+++ b/weasyprint/layout/flex.py
@@ -172,6 +172,8 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
                     child.style['image_resolution'], child.style['font_size'])
                 if intrinsic_ratio and intrinsic_height:
                     transferred_size = intrinsic_height * intrinsic_ratio
+                    content_size = max(
+                        child.min_width, min(child.max_width, content_size))
             if specified_size != 'auto':
                 child.min_width = min(specified_size, content_size)
             elif transferred_size is not None:
@@ -183,12 +185,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
             specified_size = child.height
             new_child = child.copy()
             new_child.style = child.style.copy()
-            if new_child.style['width'] == 'auto':
-                new_child_width = max_content_width(context, new_child)
-                new_child.style['width'] = Dimension(new_child_width, 'px')
             new_child.style['height'] = 'auto'
             new_child.style['min_height'] = Dimension(0, 'px')
             new_child.style['max_height'] = Dimension(inf, 'px')
+            if new_child.style['width'] == 'auto':
+                new_child_width = max_content_width(context, new_child)
+                new_child.style['width'] = Dimension(new_child_width, 'px')
             new_child = block.block_level_layout(
                 context, new_child, bottom_space, child_skip_stack, parent_box,
                 page_is_empty)[0]
@@ -200,6 +202,12 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
                     child.style['image_resolution'], child.style['font_size'])
                 if intrinsic_ratio and intrinsic_width:
                     transferred_size = intrinsic_width / intrinsic_ratio
+                    content_size = max(
+                        child.min_height, min(child.max_height, content_size))
+                elif not intrinsic_width:
+                    # TODO: wrongly set by block_level_layout, would be OK with
+                    # min_content_height.
+                    content_size = 0
             if specified_size != 'auto':
                 child.min_height = min(specified_size, content_size)
             elif transferred_size is not None:
@@ -242,17 +250,31 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
             pass
         else:
             # 3.E Otherwise…
+            new_child = child.copy()
+            new_child.style = child.style.copy()
             if main == 'width':
-                child.flex_base_size = max_content_width(context, child, outer=False)
+                # … the item’s min and max main sizes are ignored.
+                new_child.style['min_width'] = Dimension(0, 'px')
+                new_child.style['max_width'] = Dimension(inf, 'px')
+
+                child.flex_base_size = max_content_width(
+                    context, new_child, outer=False)
                 child.main_outer_extra = (
                     max_content_width(context, child) - child.flex_base_size)
             else:
-                new_child = child.copy()
+                # … the item’s min and max main sizes are ignored.
+                new_child.style['min_height'] = Dimension(0, 'px')
+                new_child.style['max_height'] = Dimension(inf, 'px')
+
                 new_child.width = inf
-                new_child = block.block_level_layout(
+                new_child, _, _, adjoining_margins, _, _ = block.block_level_layout(
                     context, new_child, bottom_space, child_skip_stack, parent_box,
-                    page_is_empty, absolute_boxes, fixed_boxes)[0]
+                    page_is_empty, absolute_boxes, fixed_boxes)
                 if new_child:
+                    # As flex items margins never collapse (with other flex items or
+                    # with the flex container), we can add the adjoining margins to the
+                    # child height.
+                    new_child.height += block.collapse_margin(adjoining_margins)
                     child.flex_base_size = new_child.height
                     child.main_outer_extra = (
                         new_child.margin_height() - new_child.height)
@@ -477,8 +499,8 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
                 child.height = new_child.height
                 # As flex items margins never collapse (with other flex items or
                 # with the flex container), we can add the adjoining margins to the
-                # child bottom margin.
-                child.margin_bottom += block.collapse_margin(adjoining_margins)
+                # child height.
+                child.height += block.collapse_margin(adjoining_margins)
             else:
                 if child.width == 'auto':
                     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
                 align_self = align_items
             if 'stretch' in align_self and child.style[cross] == 'auto':
                 cross_margins = (
-                    (child.margin_top, child.margin_bottom)
-                    if cross == 'height'
-                    else (child.margin_left, child.margin_right))
+                    (child.style['margin_top'], child.style['margin_bottom'])
+                    if cross == 'height' else
+                    (child.style['margin_left'], child.style['margin_right']))
                 if 'auto' not in cross_margins:
                     cross_size = line.cross_size
                     if cross == 'height':
@@ -733,8 +755,9 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
             line.lower_baseline = line[0][1]._baseline if line else 0
         for index, child in line:
             cross_margins = (
-                (child.margin_top, child.margin_bottom) if cross == 'height'
-                else (child.margin_left, child.margin_right))
+                (child.style['margin_top'], child.style['margin_bottom'])
+                if cross == 'height' else
+                (child.style['margin_left'], child.style['margin_right']))
             auto_margins = sum([margin == 'auto' for margin in cross_margins])
             # If a flex item has auto cross-axis margins…
             if auto_margins:
@@ -755,14 +778,14 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
                     # If its outer cross size is less than the cross size…
                     extra_cross /= auto_margins
                     if cross == 'height':
-                        if child.margin_top == 'auto':
+                        if child.style['margin_top'] == 'auto':
                             child.margin_top = extra_cross
-                        if child.margin_bottom == 'auto':
+                        if child.style['margin_bottom'] == 'auto':
                             child.margin_bottom = extra_cross
                     else:
-                        if child.margin_left == 'auto':
+                        if child.style['margin_left'] == 'auto':
                             child.margin_left = extra_cross
-                        if child.margin_right == 'auto':
+                        if child.style['margin_right'] == 'auto':
                             child.margin_right = extra_cross
                 else:
                     # Otherwise…
@@ -815,9 +838,6 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block, page_i
                                 margins += (
                                     child.border_left_width + child.border_right_width +
                                     child.padding_left + child.padding_right)
-                        # TODO: Don't set style width, find a way to avoid width
-                        # re-calculation after 16.
-                        child.style[cross] = Dimension(line.cross_size - margins, 'px')
         position_cross += line.cross_size
 
     # 15 Determine the flex container’s used cross size.


Test patch:
diff --git a/tests/layout/test_flex.py b/tests/layout/test_flex.py
index b4abfb9b8..bc839c930 100644
--- a/tests/layout/test_flex.py
+++ b/tests/layout/test_flex.py
@@ -965,6 +965,29 @@ def test_flex_item_overflow():
     assert text2.text == 'e'
 
 
+@assert_no_logs
+@pytest.mark.parametrize('direction', ('row', 'column'))
+def test_flex_item_child_bottom_margin(direction):
+    # Regression test for issue #2449.
+    page, = render_pages('''
+      <div style="display: flex; font: 2px weasyprint; flex-direction: %s">
+        <section>
+          <div style="margin: 2px 0">ab</div>
+        </section>
+      </div>''' % direction)
+    html, = page.children
+    body, = html.children
+    flex, = body.children
+    assert flex.content_box_y() == 0
+    assert flex.height == 6
+    section, = flex.children
+    assert section.content_box_y() == 0
+    assert section.height == 6
+    div, = section.children
+    assert div.content_box_y() == 2
+    assert div.height == 2
+
+
 @assert_no_logs
 def test_flex_direction_row_inline_block():
     # Regression test for issue #1652.
@@ -1110,8 +1133,6 @@ def test_flex_item_auto_margin_main():
 
 @assert_no_logs
 def test_flex_item_auto_margin_cross():
-    # TODO: we keep this test because it used to crash, but we have to fix the top
-    # margin.
     page, = render_pages('''
       <article style="display: flex; height: 100px">
         <div style="margin-top: auto; height: 10px; width: 10px"></div>
@@ -1123,7 +1144,7 @@ def test_flex_item_auto_margin_cross():
     div, = article.children
     assert div.height == 10
     assert div.width == 10
-    # assert div.margin_top == 90
+    assert div.margin_top == 90
 
 
 @assert_no_logs
@@ -1831,3 +1852,80 @@ def test_flex_item_replaced():
     div, = body.children
     svg, = div.children
     assert svg.width == svg.height == 100
+
+
+@assert_no_logs
+def test_flex_nested_column():
+    # Regression test for issue #2442.
+    page, = render_pages('''
+      <section style="display: flex; flex-direction: column; width: 200px">
+        <div style="display: flex; flex-direction: column">
+          <p>
+            A
+          </p>
+        </div>
+      </section>
+    ''')
+    html, = page.children
+    body, = html.children
+    section, = body.children
+    div, = section.children
+    p, = div.children
+    assert p.width == 200
+
+
+@assert_no_logs
+def test_flex_blockify_image():
+    page, = render_pages('''
+      <article style="display: flex; line-height: 2">
+        <img src="pattern.png">
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    img, = article.children
+    assert article.height == img.height == 4
+
+
+@assert_no_logs
+def test_flex_image_max_width():
+    page, = render_pages('''
+      <article style="display: flex">
+        <img src="pattern.png" style="max-width: 2px">
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    img, = article.children
+    assert article.height == img.height == img.width == 2
+
+
+@assert_no_logs
+def test_flex_image_max_height():
+    page, = render_pages('''
+      <article style="display: flex">
+        <img src="pattern.png" style="max-height: 2px">
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    img, = article.children
+    assert article.height == img.height == img.width == 2
+
+
+@assert_no_logs
+def test_flex_image_min_width():
+    page, = render_pages('''
+      <article style="display: flex; width: 20px">
+        <img style="min-width: 10px; flex: 1 0 auto" src="pattern.png">
+        <div style="flex: 1 0 1px"></div>
+      </article>
+    ''')
+    html, = page.children
+    body, = html.children
+    article, = body.children
+    img, div = article.children
+    assert article.height == img.height == img.width == div.height == 10