Skip to content

Table cell natural width sums stacked blocks instead of taking their maximum #282

Description

@smndvgnc

Hi,

I came across this bug working on a personal project that uses html2text.

Disclaimer: what follows has been written by an agent.

Summary

When a table cell contains several stacked block elements, the cell's natural width is computed as the sum of the blocks' widths instead of their maximum, as if the blocks were laid out side by side. A cell holding three 20-character paragraphs is laid out 60 columns wide.

The bug is invisible whenever the requested width is small enough to clamp the (wrong) natural width, which is probably why it has gone unnoticed. It becomes visible as soon as the rendering width is large — and it is very visible in the wild on HTML emails and newsletters, which nest a lot of stacked blocks inside layout tables.

Reproducer

fn main() {
    let c = "x".repeat(20);
    for (label, html) in [
        ("one paragraph", format!("<table><tr><td><p>{c}</p></td></tr></table>")),
        ("three paragraphs", format!("<table><tr><td><p>{c}</p><p>{c}</p><p>{c}</p></td></tr></table>")),
        ("three rows", format!("<table><tr><td>{c}</td></tr><tr><td>{c}</td></tr><tr><td>{c}</td></tr></table>")),
    ] {
        let out = html2text::from_read(html.as_bytes(), 1000).unwrap();
        let w = out.lines().map(|l| l.chars().count()).max().unwrap_or(0);
        println!("--- {label}: width {w}");
        println!("{out}");
    }
}

Output:

--- one paragraph: width 20
────────────────────
xxxxxxxxxxxxxxxxxxxx
────────────────────
--- three paragraphs: width 60
────────────────────────────────────────────────────────────
xxxxxxxxxxxxxxxxxxxx

xxxxxxxxxxxxxxxxxxxx

xxxxxxxxxxxxxxxxxxxx
────────────────────────────────────────────────────────────
--- three rows: width 20
────────────────────
xxxxxxxxxxxxxxxxxxxx
────────────────────
xxxxxxxxxxxxxxxxxxxx
────────────────────
xxxxxxxxxxxxxxxxxxxx
────────────────────

(The trailing padding of the "three paragraphs" block has been trimmed here for readability; each of those lines is really 60 columns wide.)

Expected: all three cases render 20 columns wide, since no single block is wider than 20 characters.

Measurements

Rendering the same shapes with 100-character blocks at a very large width (from_read(html, 100_000)), to read the natural width directly:

Cell content Computed width Expected
1 paragraph of 100 chars 100 100
3 paragraphs of 100 chars 300 100
3 <div>s of 100 chars 300 100
3 nested tables of 100 chars 300 100
3 lines of 100 chars separated by <br> 302 100
3 table rows of 100 chars 100 100

The last row is the interesting one: widths across rows of a column are correctly maximised. The defect is confined to blocks stacked within a single cell.

Impact

I maintain a terminal mail client that uses html2text to derive the plain-text version of HTML-only messages. On a real newsletter (a layout table nesting many stacked blocks), rendering with an effectively unbounded width produced 146 lines of 5110 columns, including one column 4322 columns wide to display 15 characters of text — while the longest sentence in the whole message is 255 columns. Rendering the same message with tables in raw mode gives 121 lines of 255 columns, which matches the actual content.

Any finite width hides the problem by clamping it, so the practical consequence is narrower than it looks: it makes large or unbounded rendering widths unusable for documents that use layout tables. I have worked around it on my side by capping the width, but the underlying computation still looks wrong.

Versions

Reproduced with 0.16.7 (crates.io) and with main at commit 2fa2c17 (version 0.17.1), with identical numbers on both.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions