Skip to content

DOCX: a table row wrapped in w:customXml or w:sdt is dropped, and a cell wrapped in w:customXml with it #4382

Open

Description

@chrikrah

Bug

_handle_tables reads rows through python-docx Table.rows, which yields only direct w:tr children of w:tbl (docx/oxml/table.py declares tr = ZeroOrMore("w:tr")). A row Word wrapped in w:customXml or w:sdt is invisible: its text never reaches the document and num_rows comes out short, so the grid is wrong.

_row_cells already solves the cell half for w:sdt; #3951 added it for #3946. It does not handle w:customXml, and no row-level equivalent exists. Both wrappers are legal there: w:customXml of type CT_CustomXmlRow is a child of w:tbl through w:EG_ContentRowContent.

markup read?
w:customXml around a w:tr no, row lost and num_rows short
w:sdt around a w:tr no, row lost and num_rows short
w:customXml around a w:tc no, cell empty
w:sdt around a w:tc yes, since #3951

Steps to reproduce

Self-contained: it builds its own input and goes through DocumentConverter.convert().

from docx import Document
from docx.oxml import parse_xml
from docling.document_converter import DocumentConverter

W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
cell = lambda t: (f'<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>'
                  f'<w:p><w:r><w:t xml:space="preserve">{t}</w:t></w:r></w:p></w:tc>')

doc = Document()
body = doc.element.body
body.insert(len(body) - 1, parse_xml(f'''<w:tbl {W}>
  <w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>
  <w:tblGrid><w:gridCol w:w="2000"/><w:gridCol w:w="2000"/></w:tblGrid>
  <w:tr>{cell("plain A1")}{cell("plain A2")}</w:tr>
  <w:customXml w:element="Tagged"><w:tr>{cell("customXml row B1")}{cell("customXml row B2")}</w:tr></w:customXml>
  <w:sdt><w:sdtPr/><w:sdtContent><w:tr>{cell("sdt row C1")}{cell("sdt row C2")}</w:tr></w:sdtContent></w:sdt>
  <w:tr>{cell("plain D1")}<w:customXml w:element="Tagged">{cell("customXml cell D2")}</w:customXml></w:tr>
</w:tbl>'''))
doc.save("wrapped_rows.docx")

res = DocumentConverter().convert("wrapped_rows.docx")
print("rows x cols:", res.document.tables[0].data.num_rows, "x", res.document.tables[0].data.num_cols)
print(res.document.export_to_markdown())
$ python repro.py
rows x cols: 2 x 2
| plain A1   | plain A2   |
|------------|------------|
| plain D1   |            |

Expected four rows, and customXml cell D2 in the last one.

#4369 does not cover this. I applied its msword_backend.py hunk with git apply and re-ran the script. The output is byte-identical.

#4369 teaches _walk_linear to descend into customXml at the body level. _handle_tables iterates w:tr and w:tc itself, so it never reaches that code. One control separates the two paths. At 2d5c590 a customXml paragraph at body level is dropped, and #4369 recovers it. Every row and cell case above stays unchanged.

This follows up the note I left on #4369, so the finding survives that pull request resolving.

Docling version

2.128.0 at 2d5c590. docling --version needs torch, absent here; the reproduction uses the Python API.

Python version

3.12.3, Ubuntu 24.04.5 x86_64, python-docx 1.2.0.

Duplicate check: of 127 open pull requests, 14 touch msword_backend.py and none touches _handle_tables. I scanned all 808 open issue titles and searched bodies for customXml, content control, sdt, docx table and missing row; the two hits are false positives. The nearest real neighbour is #3946, closed by #3951, which is this defect for w:sdt cells.

I have no copy of Word here, so the fixture is hand-built markup matching the schema. I did not run tests/test_backend_msword.py.

Send this as an extension of _row_cells plus a row-level equivalent, or wait until #4369 lands so the two do not collide?

Activity

  1. ebrahimeolakey commented on Sep 27, 2026

    @ebrahimeolakey

    Went with the first option, row-level equivalent of _row_cells plus the customXml cell case: #4393. Your reproduction gives 4 x 2 with every cell in place; #4369 is untouched since it only changes _walk_linear.

  2. added
    bugSomething isn't working
    docxissue related to docx backend
    on Sep 28, 2026
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

    bugSomething isn't workingdocxissue related to docx backend

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions