Repository: Kozea/WeasyPrint (language: python)

Issue:
Problem with named pages and out of normal page flow elements
## Problem

When rendering following HTML with weasyprint, the CSS for `named-page` is not applied on the first page. The first page is blue (from the default `@page` rule), but it contains content of the named page.

```html
<html>
    <head>
        <style>
            #header { position: running(header); }
            #watermark { position: fixed; top: 10cm; left: 5cm; font-size: 30pt; }
            
            @page {
                size: A4;
                background-color: blue;
                @top-center { content: element(header); }
            }
            
            #named-page { page: named-page; }
            @page named-page { background-color: red; }
            
            .pagebreak { break-after: page; }
        </style>
    </head>
    <body>
        <div id="header">HEADER</div>
        <div id="watermark">WATERMARK</div>
        <div id="named-page">
            NAMED PAGE 1
            <div class="pagebreak"></div>
            NAMED PAGE 2
        </div>
    </body>
</html>
```

![Screenshot_20230620_090329](https://github.com/Kozea/WeasyPrint/assets/29510012/7eefeb2c-30c6-4e49-be9d-a6e8d3327d5c)


You can get the expected result (both pages in red) by moving the HTML elements for the header and watermark below the named page. However, I think this is unexpected behavior.

## Suggestion

I think the problem is in calculating the start page value of the first page. Currently the start page value of the first element (`#header`) is used which results in an unnamed page. I think weasyprint should exclude elements that are out of the normal page flow from the start/end page value calculation (such as `position: running()` and `position: fixed`).

This should be compliant with the CSS specification for `page` (https://drafts.csswg.org/css-page-3/#propdef-page):

> 2. Next, a start [page](https://drafts.csswg.org/css-page-3/#propdef-page) value and end [page](https://drafts.csswg.org/css-page-3/#propdef-page) value is determined for each box as the value (if any) propagated from its first or last child box (respectively), else the used value on the box itself. A child propagates its own [start](https://drafts.csswg.org/css-page-3/#start-page-value) or [end page value](https://drafts.csswg.org/css-page-3/#end-page-value) if and only if the [page](https://drafts.csswg.org/css-page-3/#propdef-page) property applies to it.
>
> > NOTE: A first or last child _box_ is not always generated by a first or last child _element_. For example, an element could only have a previous sibling with 'display: none' which does not generate any box.

What is your opinion on that?


Discussion / hints:
Hi!

According to the specification, the 'page' names should be inherited from:
- elements that create a box, AND
- elements that allow 'page' to be applied to it, ie. [boxes that create class A break points](https://drafts.csswg.org/css-page-3/#propdef-page).

The second point is slightly different from "elements in the flow" (because floats create class A break points for exemple even if they’re not in the flow), but your overall diagnostic seems to be the right one.

The code to fix this is probably in the `page_values` methods in `boxes.py` and in the places where they’re called.

Fix patch:
diff --git a/weasyprint/formatting_structure/boxes.py b/weasyprint/formatting_structure/boxes.py
index 93c378ffcc..b31540790a 100644
--- a/weasyprint/formatting_structure/boxes.py
+++ b/weasyprint/formatting_structure/boxes.py
@@ -377,13 +377,17 @@ def get_wrapped_table(self):
 
     def page_values(self):
         start_value, end_value = super().page_values()
-        if self.children:
-            if len(self.children) == 1:
-                page_values = self.children[0].page_values()
+        children = [
+            c for c in self.children
+            if not (c.is_absolutely_positioned() or c.is_running())
+        ]
+        if children:
+            if len(children) == 1:
+                page_values = children[0].page_values()
                 start_value = page_values[0] or start_value
                 end_value = page_values[1] or end_value
             else:
-                start_box, end_box = self.children[0], self.children[-1]
+                start_box, end_box = children[0], children[-1]
                 start_value = start_box.page_values()[0] or start_value
                 end_value = end_box.page_values()[1] or end_value
         return start_value, end_value


Test patch:
diff --git a/tests/layout/test_page.py b/tests/layout/test_page.py
index 4e9f0d277d..04c7aa047d 100644
--- a/tests/layout/test_page.py
+++ b/tests/layout/test_page.py
@@ -817,6 +817,42 @@ def test_page_names_9():
     assert article.element_tag == 'article'
 
 
+@assert_no_logs
+def test_page_names_10():
+    pages = render_pages('''
+      <style>
+        #running { position: running(running); }
+        #fixed { position: fixed; }
+        @page { size: 200px 200px; @top-center { content: element(header); }}
+        section { page: small; }
+        @page small { size: 100px 100px; }
+        .pagebreak { break-after: page; }
+      </style>
+      <div id="running">running</div>
+      <div id="fixed">fixed</div>
+      <section>
+        <h1>text</h1>
+        <div class="pagebreak"></div>
+        <article>text</article>
+      </section>
+    ''')
+    page1, page2 = pages
+
+    assert (page1.width, page1.height) == (100, 100)
+    html, runing = page1.children
+    body, = html.children
+    fixed, section, = body.children
+    h1, pagebreak = section.children
+    assert h1.element_tag == 'h1'
+
+    assert (page2.width, page2.height) == (100, 100)
+    html, running = page2.children
+    fixed, body = html.children
+    section, = body.children
+    article, = section.children
+    assert article.element_tag == 'article'
+
+
 @assert_no_logs
 @pytest.mark.parametrize('style, line_counts', (
     ('orphans: 2; widows: 2', [4, 3]),