DEV Community

hao jia
hao jia

Posted on

My rowspan overflow check never fired, and a plain text search found the missing cell

Internal process documents live in Word, and sooner or later someone wants them readable on an intranet wiki as HTML. Files like that are not allowed near a third-party converter, which leaves a library on a machine we control or conversion inside the browser. Tables are the part that worries me: those documents are full of merged cells, and a table that quietly loses a column looks broken to everyone who opens the page.

So the question is what check to run on the output, and the way to find out is to have output worth checking. The sample for this round is one DOCX made for the purpose, 43,790 bytes: three tables (horizontal merges, vertical merges, and a 5 by 4 with both), a header, a footer, one footnote, two PNGs. It went through Mammoth 1.13.0, whose JS and Python builds landed byte-identical at 10,167 bytes each, then pandoc 3.13, then docx-preview 0.4.1, and for the browser side I used ImgIng https://imging.ai/ to write the document out as a single offline HTML file. Forty-odd lines of hand-written reconstruction served as a control. Single machine, an Apple silicon Mac with a desktop Chromium, and each tool got exactly one pass.

The first result was that there was nothing to catch: on that sample all four got every colspan and rowspan value right. The HTML differs, the two browser-side outputs carry an extra hidden empty <td> on continuation rows, but on screen the grids matched the document.

Where a bogus rowspan comes from

So something had to be broken on purpose. In OOXML a vertical merge is spread over several cells: the top one declares <w:vMerge w:val="restart"/>, and each cell below it repeats <w:vMerge/> on its own, with no w:val and no text. The variant for this round is that file with one attribute removed — the first cell of the merge group in the vertical-merge table arrives as a bare <w:vMerge/>, a continuation with nothing above it to continue. 43,789 bytes, and that attribute is the only difference. What is malformed here is the document, not any of the converters; the question was what each of them does when it reads one.

Mammoth 1.13.0 and pandoc 3.13 both took the bare vMerge as a continuation of the row above, chose the header cell as the merge anchor, and handed it rowspan="4". The rows under it came out with two cells each. The text that had been sitting in the cell that lost its restart is not in the output at all. docx-preview 0.4.1 failed in the opposite direction, and so did the converter I had run in the browser: the merge disappeared, every row ended up with three cells, two of them empty, and the label was still there. The control script took its orphan-continuation fallback and landed in the same place. Two failures pointing opposite ways, neither of them raising an error.

The overflow check stays quiet when rowspan equals the row count

The structural check that costs the least is also the most obvious. For every cell, the row index plus its rowspan must not exceed the number of rows in the table, because a span reaching past the bottom edge is wrong whatever the document meant. On the malformed variant it said nothing. That table has four rows, the bogus rowspan was 4, and it sat on row 0, so the sum comes to exactly 4 and the comparison is a strict greater-than. Over this whole round it never fired once: not on the six correct outputs, which is how it should be, and not on the planted case, which is the problem.

Pulling every w:t out of document.xml and searching for it

What caught it was the crudest check in the set, the one that knows nothing about tables. Unzip the DOCX, walk word/document.xml, collect the text of every w:t node, then search the output HTML for those strings one at a time and print whatever is missing. On the variant it named that missing string straight away, out of both the Mammoth and the pandoc output. It needs no model of the table, which is why it survived the one case where my model of the table was what got fooled.

It has a hole, and the same run puts it on display. The browser-side outputs threw the merge away but kept every character, so a text search calls them clean. Structure checks miss dropped text, text checks miss dropped structure, and this round landed one failure on each side, which is the argument for running both rather than picking one. One thing I am guessing rather than measuring: the two library readers probably scan upward for an anchor when they meet a bare vMerge, which would account for the header cell inheriting a span. I have not read either source.

Scope: one self-made sample, one Apple silicon Mac, one run per tool, and exactly one shape of malformed input. A gridSpan sum that overruns w:tblGrid, nested tables, continuation cells that do carry text — none of that was put through here. Of the two checks, the text diff is the one I would add first, because it is the one that does not depend on my own model of the table being right.

Top comments (0)