Repository: Kozea/WeasyPrint

Issue:
Table overflow issue
This was tested in `WeasyPrint version 62.3` and `WeasyPrint version 63.0`.
This happens when `overflow-wrap` is set to `break-word` or `normal`. `anywhere` seems to work fine.

Input:
```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width initial-scale=1.0">
  <title>Table with Links</title>
  <style>
    table {
      width: 100%;
      max-width: 10px;
      border: 1px solid black;
      overflow-wrap: break-word; /* or normal */
    }

    th td {
      border: 1px solid black;
      padding: 8px;
      text-align: left;
    }

    a {
      margin-right: 8px;
      text-decoration: none;
      color: blue;
    }
  </style>
</head>
<body>
  <h1>Example 1</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">123.456.78</a>,
          <a href="https://example.org">234.567.89</a>,
          <a href="https://example.org">345.678.90</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 2</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">foooooobar</a>,
          <a href="https://example.org">foooooobar</a>,
          <a href="https://example.org">foooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 3</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">S 123.456.78</a>,
          <a href="https://example.org">S 234.567.89</a>,
          <a href="https://example.org">S 345.678.90</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>Example 3</h1>
  <table>
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">S foooooobar</a>,
          <a href="https://example.org">S foooooobar</a>,
          <a href="https://example.org">S foooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
</body>
</html>


```

Weasyprint PDF rendering:
![Image](https://github.com/user-attachments/assets/f41b826d-e3d5-496d-a370-5bbf977bc1a8)

Firefox rendering:
![Image](https://github.com/user-attachments/assets/c0a20b66-adc1-4887-bff3-d79675475049)

Hints from issue comments:
@kesara: Better example:
```
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width initial-scale=1.0">
  <title>Table with Links</title>
  <style>
    table {
      width: 100%;
      border: 1px solid black;
    }

    .brwd {
      overflow-wrap: break-word;
    }

    .nrml {
      overflow-wrap: normal;
    }

    .anywhere {
      overflow-wrap: anywhere;
    }
  </style>
</head>
<body>
  <h1>overflow-wrap: normal</h1>
  <table class="nrml">
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>overflow-wrap: break-word</h1>
  <table class="brwd">
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
  <h1>overflow-wrap: anywhere</h1>
  <table class="anywhere">
    <thead>
      <tr>
        <th>Header 1</th>
        <th>Header 2</th>
        <th>Header 3</th>
        <th>Links</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>Data foooooooooobar</td>
        <td>
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>,
          <a href="https://example.org">foooooooooooooooobar</a>
        </td>
      </tr>
    </tbody>
  </table>
</body>
</html>
```

[table_1.pdf](https://github.com/user-attachments/files/18059545/table_1.pdf)
@liZe: This bug is caused by a dirty workaround that has been hidden since… 2013.

https://github.com/Kozea/WeasyPrint/blob/3c6f9a5a55b79563e32c766a1b5409e1d20b3601/weasyprint/layout/preferred.py#L317

Here, during minimum size calculation, we break the line if we only have a space, because, well, spaces are a very common way to break lines. So:
- if we have only a space between tags, minimum size works thanks to this hack,
- if we have text with a space inside between tags, it works because the line can be split by `split_first_line`,
- if we have text with a space only at the beginning/end of the text between tags, it fails.

For example, your sample works without the comma between `a` tags, or with "a a" instead of the comma. With `, `, it fails.

It only fails with layouts where the minimum size is required (tables and floats for example), but it works for other full-width layouts thanks to the "real" algorithm:

https://github.com/Kozea/WeasyPrint/blob/3c6f9a5a55b79563e32c766a1b5409e1d20b3601/weasyprint/layout/inline.py#L744-L758

Just as for this code, we should get track of the letters at tags boundaries to detect possible line breaks correctly, or even reuse the real algorithm, but that would much more complicated, and probably much slower.

Or we can just replace the current hack that worked until now by another one that will hopefully work for the next 11 years.@kesara: Yeah, I noticed that this doesn't happen when there's ` ` in-between `a` tags and `a` tag text has spaces.
It is one of those edge cases.

Solution patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2cd1e69fd..6a580d0cf 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -2,6 +2,85 @@ Changelog
 =========
 
 
+Version 63.1
+------------
+
+Released on 2024-12-10.
+
+Dependencies:
+
+* `#2297 <https://github.com/Kozea/WeasyPrint/issues/2297>`_:
+  Remove upper bounds for dependencies
+
+Bug fixes:
+
+* `#2300 <https://github.com/Kozea/WeasyPrint/pull/2300>`_,
+  `#2292 <https://github.com/Kozea/WeasyPrint/issues/2292>`_:
+  Don’t avoid floats for flex items
+* `#2301 <https://github.com/Kozea/WeasyPrint/pull/2301>`_,
+  `#2293 <https://github.com/Kozea/WeasyPrint/issues/2293>`_:
+  Include floats in calculation of minimum cell height
+* `#2303 <https://github.com/Kozea/WeasyPrint/pull/2303>`_,
+  `#2302 <https://github.com/Kozea/WeasyPrint/issues/2302>`_:
+  Set alpha even when current color channels didn’t change
+* `#2306 <https://github.com/Kozea/WeasyPrint/issues/2306>`_:
+  Don’t try to increase column width when there’s no extra width
+* `#2304 <https://github.com/Kozea/WeasyPrint/issues/2304>`_:
+  Don’t forget skip stack when drawing flex items
+* `#2316 <https://github.com/Kozea/WeasyPrint/issues/2316>`_:
+  Don’t crash with SVG symbols
+* `#2320 <https://github.com/Kozea/WeasyPrint/issues/2320>`_:
+  Fix currentcolor detection when parsing gradient color stops
+* `#2322 <https://github.com/Kozea/WeasyPrint/pull/2322>`_,
+  `#2289 <https://github.com/Kozea/WeasyPrint/issues/2289>`_:
+  Don’t add DLL directories when using Windows executable
+* `#2323 <https://github.com/Kozea/WeasyPrint/pull/2323>`_,
+  `#2305 <https://github.com/Kozea/WeasyPrint/issues/2305>`_:
+  Fix different rendering test
+
+Performance:
+
+* `#2319 <https://github.com/Kozea/WeasyPrint/issues/2319>`_:
+  Fix memory leaks
+
+Documentation:
+
+* `#2299 <https://github.com/Kozea/WeasyPrint/pull/2299>`_:
+  Update install instructions for Alpine
+* `#2321 <https://github.com/Kozea/WeasyPrint/pull/2321>`_:
+  Add example invocation of WeasyPrint on the "Contribute" page
+
+Contributors:
+
+* Guillaume Ayoub
+* Jó Ágila Bitsch
+* Lucie Anglade
+* Alexander Gitter
+* Luke Cousins
+
+Backers and sponsors:
+
+* Spacinov
+* Kobalt
+* Grip Angebotssoftware
+* Syslifters
+* Manuel Barkhau
+* SimonSoft
+* Menutech
+* KontextWork
+* Simon Sapin
+* TrainingSparkle
+* Healthchecks.io
+* Hammerbacher
+* Advance Insight
+* Docraptor
+* Method B
+* FieldHub
+* Yanal-Yvez Fargialla
+* Morntag
+* Xavid
+
+
 Version 63.0
 ------------
 
diff --git a/weasyprint/__init__.py b/weasyprint/__init__.py
index 51c3bb27a..40635bf0a 100644
--- a/weasyprint/__init__.py
+++ b/weasyprint/__init__.py
@@ -15,7 +15,7 @@
 import tinycss2
 import tinyhtml5
 
-VERSION = __version__ = '63.0'
+VERSION = __version__ = '63.1'
 
 #: Default values for command-line and Python API options. See
 #: :func:`__main__.main` to learn more about specific options for
diff --git a/weasyprint/layout/preferred.py b/weasyprint/layout/preferred.py
index e3ab421d1..042031879 100644
--- a/weasyprint/layout/preferred.py
+++ b/weasyprint/layout/preferred.py
@@ -13,7 +13,7 @@
 from math import inf
 
 from ..formatting_structure import boxes
-from ..text.line_break import split_first_line
+from ..text.line_break import can_break_text, split_first_line
 from .replaced import default_image_sizing
 
 
@@ -306,6 +306,7 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
         elif isinstance(child, boxes.TextBox):
             space_collapse = child.style['white_space'] in (
                 'normal', 'nowrap', 'pre-line')
+            text_wrap = child.style['white_space'] in ('normal', 'pre-wrap', 'pre-line')
             if skip_stack is None:
                 skip = 0
             else:
@@ -314,25 +315,26 @@ def inline_line_widths(context, box, outer, is_line_start, minimum, skip_stack=N
             child_text = child.text.encode()[(skip or 0):]
             if is_line_start and space_collapse:
                 child_text = child_text.lstrip(b' ')
-            if minimum and child_text == b' ':
-                lines = [0, 0]
-            else:
-                max_width = 0 if minimum else None
-                lines = []
-                resume_index = new_resume_index = 0
-                while new_resume_index is not None:
-                    resume_index += new_resume_index
-                    _, _, new_resume_index, width, _, _ = (
-                        split_first_line(
-                            child_text[resume_index:].decode(), child.style,
-                            context, max_width, child.justification_spacing,
-                            is_line_start=is_line_start, minimum=True))
-                    lines.append(width)
-                    if first_line:
-                        break
-                if first_line and new_resume_index:
-                    current_line += lines[0]
+            max_width = 0 if minimum else None
+            lines = []
+            resume_index = new_resume_index = 0
+            while new_resume_index is not None:
+                resume_index += new_resume_index
+                _, _, new_resume_index, width, _, _ = split_first_line(
+                    child_text[resume_index:].decode(), child.style, context, max_width,
+                    child.justification_spacing, is_line_start=is_line_start,
+                    minimum=True)
+                lines.append(width)
+                if first_line:
                     break
+            if first_line and new_resume_index:
+                current_line += lines[0]
+                break
+            # TODO: use the real next character instead of 'a' to detect line breaks.
+            can_break = can_break_text(
+                child_text.decode()[-1:] + 'a', child.style['lang'])
+            if minimum and text_wrap and can_break:
+                lines.append(0)
         else:
             # https://www.w3.org/TR/css-text-3/#overflow-wrap
             # "The line breaking behavior of a replaced element


Test patch:
diff --git a/tests/layout/test_table.py b/tests/layout/test_table.py
index 721a6e7c6..94601dbd1 100644
--- a/tests/layout/test_table.py
+++ b/tests/layout/test_table.py
@@ -1558,6 +1558,43 @@ def test_layout_table_auto_51():
     assert abs(td_2.width - 70) < 0.1
 
 
+@assert_no_logs
+def test_layout_table_auto_52():
+    # Test regression:
+    # https://github.com/Kozea/WeasyPrint/issues/2325
+    page, = render_pages('''
+      <style>
+        @page { size: 20px }
+      </style>
+      <table style="font-family: weasyprint; border-spacing: 1px;
+                    font-size: 2px; line-height: 1">
+        <tr>
+          <td><img src=pattern.png></td>
+          <td>
+            <span>foo</span>,
+            <span>foo</span>,
+            <span>foo</span>
+          </td>
+        </tr>
+      </table>
+    ''')
+    html, = page.children
+    body, = html.children
+    table_wrapper, = body.children
+    table, = table_wrapper.children
+    row_group, = table.children
+    row, = row_group.children
+    td_1, td_2 = row.children
+    assert table.width == 20
+    assert table_wrapper.position_x == 0
+    assert table.position_x == 0
+    assert td_1.position_x == 1  # spacing
+    assert td_1.width == 4  # image width
+    assert td_2.position_x == td_1.width + 2 * 1  # 2 * spacing
+    assert td_2.width == table.width - td_1.width - 3 * 1  # 3 * spacing
+    assert td_2.height == 3 * 2  # 3 lines * line height
+
+
 @assert_no_logs
 @pytest.mark.parametrize(
     'body_width, table_width, check_width, positions, widths', (