Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 4 min read

The continuation cell a DOCX vertical merge leaves behind still owns a grid column

A vertical merge in Word hides the rows it swallows on screen only. Inside the file every one of those rows still carries a cell of its own: a real <w:tc>, with a bare <w:vMerge/> in its properties and an empty paragraph

A vertical merge in Word hides the rows it swallows on screen only. Inside the file every one of those rows still carries a cell of its own: a real <w:tc>, with a bare <w:vMerge/> in its properties and an empty paragraph for a body, sitting below the cell that carries <w:vMerge w:val="restart"/> and opens the group. Nothing in the file marks the continuation as gone. It sits in its row, it keeps its place in the table grid, and it is the one cell a converter has to decide not to emit.

The numbers below come from one pass over one self-made sample, so they describe this document rather than ranking anything. The file is 43,790 bytes, built on purpose with three tables: one merging across, one merging down, and a mixed one at 5 columns by 4 rows. Every tool ran once, on a single Apple-silicon Mac, with the browser side in desktop Chromium.

A vertical merge is written as two different cells

In word/document.xml the pair reads like this, with only the cell text swapped for a placeholder and the structure left alone:

<!-- the merge starts here, and this is the only cell of the group with body text -->
<w:tc><w:tcPr><w:vMerge w:val="restart"/></w:tcPr> <!-- the restart cell, and the only place a rowspan can be counted from -->
  <w:p><w:r><w:t>left column, spans 3 rows</w:t></w:r></w:p></w:tc>

<!-- one row down: same element, no w:val, and an empty paragraph -->
<w:tc><w:tcPr><w:vMerge/></w:tcPr> <!-- the continuation, empty yet still owning one grid column -->
  <w:p/></w:tc> <!-- nothing to emit, the column slot stays taken -->

restart is the only value that means anything in this position. The continuation form leaves w:val out entirely, which is equivalent to continue, so a continuation is identified by a missing attribute rather than by any value. Neither cell holds a number. How many rows the merge covers is only knowable by walking down from the restart and counting continuations until they stop, so one restart can be followed by several. The mixed table in this sample has 2 restarts and 3 bare continuations.

The continuation cell still holds its grid column

Its position matters as much as its emptiness. Row 2 of the mixed table carries four cells in the XML and only three of them should turn into a <td>, because the fourth continues a merge that began a row earlier. Drop continuation cells before deciding which column each surviving cell sits in, and everything after that point shifts one column left without any complaint from the parser. The table's column count lives in the w:gridCol entries of w:tblGrid, which has nothing to do with how many cells a row carries, and continuations are part of why the two numbers drift apart.

Counting the rowspan downward needs one slot per column position

The bookkeeping that worked here is small. Lay the grid out from w:tblGrid, keep one slot per column position holding whichever cell is currently merging down through it, and when a continuation turns up at that position, emit nothing and add one to the rowspan of the cell the slot points at. A forty-line reconstruction script in this round did exactly that and matched the expected grid on all three tables. It is a minimal implementation and not a library: w:tblHeader, nested tables, w:tcBorders and diagonal headers are all left untouched, and whether nesting breaks the slot bookkeeping is something I have not checked.

The outputs put side by side this round came from Mammoth 1.13.0, pandoc 3.13, docx-preview 0.4.1, and the DOCX to HTML converter on ImgIng https://imging.ai/ , which ran in the browser without an account. On the mixed table the first two wrote 2, 5, 3 and 3 cells across the four rows, matching the expected grid. The other two wrote 2, 5, 4 and 5, keeping the continuations as empty <td> elements with display:none. The pure vertical-merge table splits the same way: 3, 3, 2 and 2 against 3, 3, 3 and 3. All four render the table correctly on screen; on this document the difference sits in the markup rather than in what the page looks like.

A merge group with no restart breaks in two opposite ways

The single real misplacement in this round came out of a deliberately malformed variant. In the vertical-merge table, the first cell's <w:vMerge w:val="restart"/> was replaced with a bare <w:vMerge/>, nothing else touched. Mammoth 1.13.0 and pandoc 3.13 then treated the header cell above as the start of the merge and handed it rowspan="4", while the cell that had lost its restart was read as a continuation and its text dropped out completely; that string cannot be found anywhere in the output. The other two did not merge at all and kept the text. No warning came out of either group, and the fault is in the document, which now has a continuation with nothing above it to continue.

What that variant costs is the one piece of information the format never stores. Walking down from a restart is the only way to learn how far a merge reaches, so a group whose restart is gone leaves continuations that belong to nobody, and whatever the converter does with them is a guess. Pairing each bare <w:vMerge/> with a restart sitting above it in the same grid column is worth doing on documents of your own before trusting any rowspan that comes out the other side.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.