Repository: Kozea/WeasyPrint
Pull request #2219 (2024-08-01 19:39:57)

Don't ignore absolutely positioned elements inside flex
Hi! This PR fixes a long-standing issue (#996) with flexbox support which happened to affect me, at least for my personal use case  🙂  I can't guarantee that it will also fix other people's though...

Indeed the flexbox code is minimally in need of a serious refactoring, and then to be completed.  If I run into more issues (spoiler alert: I already did) then I'll see if I can PR them too.


Patch:
diff --git a/weasyprint/draw/border.py b/weasyprint/draw/border.py
index 79723ded..1c7e21ea 100644
--- a/weasyprint/draw/border.py
+++ b/weasyprint/draw/border.py
@@ -51,10 +51,7 @@ def draw_border(stream, box):
             with stacked(stream):
                 rule_width = box.style['column_rule_width']
                 rule_style = box.style['column_rule_style']
-                if box.style['column_gap'] == 'normal':
-                    gap = box.style['font_size']  # normal equals 1em
-                else:
-                    gap = percentage(box.style['column_gap'], box.width)
+                gap = percentage(box.style['column_gap'], box.width)
                 position_x = (
                     child.position_x - (box.style['column_rule_width'] + gap) / 2)
                 border_box = (position_x, child.position_y, rule_width, child.height)
diff --git a/weasyprint/layout/flex.py b/weasyprint/layout/flex.py
index 13790688..59f75867 100644
--- a/weasyprint/layout/flex.py
+++ b/weasyprint/layout/flex.py
@@ -5,6 +5,7 @@ from math import inf, log10
 
 from ..css.properties import Dimension
 from ..formatting_structure import boxes
+from .absolute import AbsolutePlaceholder, absolute_layout
 from .percent import resolve_one_percentage, resolve_percentages
 from .preferred import max_content_width, min_content_width
 from .table import find_in_flow_baseline
@@ -21,6 +22,10 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
     context.create_block_formatting_context()
     resume_at = None
 
+    if box.style['position'] == 'relative':
+        # New containing block, use a new absolute list
+        absolute_boxes = []
+
     # Step 1 is done in formatting_structure.boxes
     # Step 2
     if box.style['flex_direction'].startswith('row'):
@@ -111,8 +116,19 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
     else:
         skip_stack = None
     child_skip_stack = skip_stack
-    for child in children:
+    for index, child in enumerate(children):
         if not child.is_flex_item:
+            # Absolute child layout: create placeholder.
+            if child.is_absolutely_positioned():
+                child.position_x = box.content_box_x()
+                child.position_y = box.content_box_y()
+                new_child = placeholder = AbsolutePlaceholder(child)
+                placeholder.index = index
+                children[index] = placeholder
+                if child.style['position'] == 'absolute':
+                    absolute_boxes.append(placeholder)
+                else:
+                    fixed_boxes.append(placeholder)
             continue
 
         # See https://www.w3.org/TR/css-flexbox-1/#min-size-auto
@@ -839,8 +855,8 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
 
     # TODO: don't use block_box_layout, see TODOs in Step 14 and
     # build.flex_children.
-    box = box.copy()
-    box.children = []
+    box = box.copy_with_children(
+        [child for child in children if not child.is_flex_item])
     child_skip_stack = skip_stack
     for line in flex_lines:
         for i, child in line:
@@ -876,6 +892,13 @@ def flex_layout(context, box, bottom_space, skip_stack, containing_block,
         if resume_at:
             break
 
+    if box.style['position'] == 'relative':
+        # New containing block, resolve the layout of the absolute descendants
+        for absolute_box in absolute_boxes:
+            absolute_layout(
+                context, absolute_box, box, fixed_boxes, bottom_space,
+                skip_stack=None)
+
     # Set box height
     # TODO: this is probably useless because of step #15
     if axis == 'width' and box.height == 'auto':


Test patch:
diff --git a/tests/draw/test_column.py b/tests/draw/test_column.py
index 741112f8..8dc728b2 100644
--- a/tests/draw/test_column.py
+++ b/tests/draw/test_column.py
@@ -80,25 +80,3 @@ def test_column_rule_span(assert_pixels):
         <img src=blue.jpg>
         <img src=blue.jpg>
       </div>''')
-
-
-@assert_no_logs
-def test_column_rule_normal(assert_pixels):
-    # Test regression: https://github.com/Kozea/WeasyPrint/issues/2217
-    assert_pixels('''
-        a___a
-        a___a
-        _____
-    ''', '''
-      <style>
-        img { display: inline-block; width: 1px; height: 1px }
-        div { columns: 2; column-gap: normal }
-        body { margin: 0; font-size: 3px; line-height: 0 }
-        @page { margin: 0; size: 5px 3px }
-      </style>
-      <div>
-        <img src=blue.jpg>
-        <img src=blue.jpg>
-        <img src=blue.jpg>
-        <img src=blue.jpg>
-      </div>''')
diff --git a/tests/layout/test_flex.py b/tests/layout/test_flex.py
index 1fbcd2ff..30577aa9 100644
--- a/tests/layout/test_flex.py
+++ b/tests/layout/test_flex.py
@@ -597,3 +597,21 @@ def test_flex_break_inside_avoid():
     body, = html.children
     article, = body.children
     div, = article.children
+
+
+@assert_no_logs
+def test_flex_absolute_content():
+    # Regression test for https://github.com/Kozea/WeasyPrint/issues/996
+    page, = render_pages('''
+      <section style="display: flex; position: relative">
+         <h1 style="position: absolute; top: 0; right: 0">TEST</h1>
+         <p>Hello world!</p>
+      </section>''')
+    html, = page.children
+    body, = html.children
+    section, = body.children
+    h1, p = section.children
+    assert h1.position_x != 0
+    assert h1.position_y == 0
+    assert p.position_x == 0
+    assert p.position_y == 0