Skip to content

fix(docx): keep XML comments in NOTES, drop processing instructions - #4584

Open
loickouao wants to merge 4 commits into
docling-project:mainfrom
loickouao:fix/docx-xml-comments
Open

loickouao wants to merge 4 commits into
docling-project:mainfrom
loickouao:fix/docx-xml-comments

Conversation

@loickouao

@loickouao loickouao commented Oct 5, 2026 •

Copy link
Copy Markdown

An XML comment or processing instruction in word/document.xml either fails the whole DOCX conversion with ValueError: Invalid input tag, or silently drops a table's text (see #4583). docx4j writes such a comment at the start of the body in every file it produces.

The backend reads the tag of every child it walks in several places, and a comment does not have one. Guarding each of those places would be fragile, so this PR takes comments and processing instructions out of every XML part (XmlPart) once python-docx has loaded it. The text of the XML comments in the main document part is kept in the NOTES layer, after the body. Processing instructions, and comments in the other parts (styles, settings, …), are dropped. Any text that follows a removed node is kept. Word comments (w:comment elements) are not affected and still go to NOTES as before.

Supersedes #4564 by @chriscoveries, which first fixed the crash by skipping non-element nodes at the top level of the body. This PR also covers comments inside paragraphs, runs, table cells and table rows: 4 of the 6 cases below still fail on #4564's branch. The test from #4564 (a comment and a processing instruction between two body paragraphs) passes unchanged on this branch.

Tests: test_xml_comments_and_processing_instructions_keep_body_text covers six positions:

  • the start of the body;
  • a paragraph;
  • a run;
  • a table cell;
  • a table row;
  • a processing instruction.

Each one checks that every text, table cells included, is in the output. All six fail on main. test_xml_comment_text_goes_to_notes checks that the comment text is in NOTES and not in the body, that the text after an inline comment is kept, and that processing instructions are dropped.

Checks (rebased on main at 6fadbae):

  • No change on existing files: all 36 .docx files under tests/data give byte-identical export_to_dict() on main and on this branch.
  • Real docx4j files: 4 files from one investor-relations site (a 19.6 MB press release and three SEC forms) now convert. Their export_to_dict() is identical to that of the same files with the comments removed by hand, apart from the docx4j banner, which is in NOTES.
  • Existing tests: all tests in tests/test_backend_msword*.py and tests/test_office_image_modes.py pass.
  • Tooling: pre-commit passes (ruff, ty, tach, max lines). ty reports the same diagnostics as on main.

Issue resolved by this Pull Request:
Resolves #4583

Checklist:

  • Documentation has been updated, if necessary.
  • Examples have been added, if necessary.
  • Tests have been added, if necessary.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

✅ DCO Check Passed

Thanks @loickouao, all your commits are properly signed off. 🎉

@mergify

mergify Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\(.+\))?(!)?:

@ceberam ceberam added bug Something isn't working docx issue related to docx backend labels Oct 5, 2026
@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.26087% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
docling/backend/msword_backend.py 78.26% 2 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

Comment thread docling/backend/msword_backend.py Outdated
return etree.tostring(root, xml_declaration=True, encoding="UTF-8", standalone=True)


def _remove_xml_comments(docx_obj: DocxDocument) -> None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments should not be ignored but can be stored with a ContentLayer attribute (eg NOTES)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. The title was ambiguous, my fault: this PR removes XML comment nodes (<!-- ... -->, lxml etree._Comment) and processing instructions, not Word comments. I've retitled the PR and renamed the helper to _remove_xml_comment_nodes to make that clear.

Word comments (w:comment in word/comments.xml, anchored by w:commentRangeStart / w:commentReference) are elements, not comment nodes, so they are untouched: _add_comments still stores them in the NOTES layer with their links, and test_comments_extraction passes. I also checked tests/data/docx/sources/docx_comments.docx with <!-- Created by docx4j 8.2.8 (Apache licensed) --> injected into word/document.xml and inside a w:comment in word/comments.xml:

  • on main, the conversion fails with ValueError: Invalid input tag;
  • with this PR, the 3 reviewer comments are in NOTES, linked to the same 3 items, and the whole output (markdown, NOTES, links) matches main on the unmodified file.

The XML comment nodes themselves are producer metadata, not document content: LibreOffice doesn't show them and drops them on re-save, while it keeps the reviewer comments. Storing them in NOTES would add the docx4j banner to every file docx4j produces. If you'd still like them kept, I can add each one as a text item in the NOTES layer instead.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I saw your comment on #4564 ("not a fan of omitting text, unless it is format related"), so I went ahead. The text of XML comments in the main document part now goes to the NOTES layer, after the body. Processing instructions are still dropped, since they are format related, and so are comments in the other parts (styles, settings, …). test_xml_comment_text_goes_to_notes covers it.

@loickouao loickouao changed the title fix(docx): ignore XML comments and processing instructions fix(docx): drop XML comment nodes and processing instructions Oct 6, 2026
@loickouao loickouao changed the title fix(docx): drop XML comment nodes and processing instructions fix(docx): keep XML comments in NOTES, drop processing instructions Oct 6, 2026
@dolfim-ibm

Copy link
Copy Markdown
Member

@chriscoveries @loickouao @PeterStaar-IBM: #4564 and #4584 fix the same crash. XML comment and processing-instruction nodes in a DOCX part are not elements, so etree.QName(element) fails on them and the conversion stops. Only one of the two PRs can merge, so let's align on one.

#4564 #4584
Comment / PI nodes directly under w:body skipped in _walk_linear removed
Nodes nested in paragraphs, runs, cells, rows not handled removed
Comment text dropped kept in the NOTES content layer
Review request to keep the text in a ContentLayer open addressed

Proposal: continue with #4584 as the single fix, and close #4564, with credit to @chriscoveries for the first fix and the test case.

Before #4584 can merge, it still needs:

  • Replace getattr(part, "_element", None) with isinstance(part, XmlPart) and the public part.element (XmlPart from docx.opc.part). AGENTS.md asks to avoid attribute probing.
  • A rebase on main. The conflict is only in tests/test_backend_msword.py.

@chriscoveries: if the body-level test in #4564 covers a case that #4584 does not, please say so here, and it can move into #4584.
@PeterStaar-IBM: please confirm that this direction matches your review on both PRs.

docx4j writes an XML comment at the start of the document body. The
backend reads the tag of every child it walks, which a comment does not
have, so lxml raised "ValueError: Invalid input tag" and the whole
conversion failed. A comment inside a table cell or row dropped the
table's text instead.

Remove comments and processing instructions from every XML part once the
document is loaded, keeping any text that follows them.

Signed-off-by: loickouao <diegoloic@hotmail.fr>
The helper only removes <!-- --> nodes and processing instructions.
Word comments (w:comment) are elements and still go to the NOTES layer.

Signed-off-by: loickouao <diegoloic@hotmail.fr>
XML comments in the main document part are no longer dropped: their
text is added to the NOTES layer, after the body. Processing
instructions and comments in the other parts are still removed.

Signed-off-by: loickouao <diegoloic@hotmail.fr>
Use the public XmlPart.element instead of probing for the private
_element attribute, as AGENTS.md asks. In python-docx only XmlPart
parts have an element, so the same parts are cleaned.

Signed-off-by: loickouao <diegoloic@hotmail.fr>
@loickouao
loickouao force-pushed the fix/docx-xml-comments branch from 053210d to 543c6c2 Compare October 14, 2026 13:52
@github-actions github-actions Bot added ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem error-handling xml issue related to supported schema-specific XML formats labels Oct 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

AI triage (advisory)

Checked commit 543c6c2211. The analysis read the diff only. It did not run code from this pull request.

The PR adds _remove_xml_comment_nodes() to strip XML comment and processing-instruction nodes from every XML part before the DOCX backend walks the tree. XML comment text from the main document part is saved to the NOTES layer; processing instructions are dropped.

Topic labels added: error-handling, xml.

CI safety: no concern found. A maintainer must still approve CI.

Possible duplicates

PR State Signal Assessment
#4564 open referenced as #4564 duplicate: PR #4564 fixes the same issue (#4583) by guarding the body-level loop with isinstance(element.tag, str); both PRs fix the same crash but use incompatible strategies, so only one can be merged.
#4627 closed also references issue #4583 duplicate: Closed PR #4627 fixes the same issue (#4583) by adding isinstance(...tag, str) guards at multiple call sites; this PR fixes the same root cause with a different (pre-strip) strategy.
1 related pull request(s) in the same code
PR State Signal Assessment
#4393 open changes the same code related: PR #4393 fixes a different DOCX table problem (rows and cells wrapped in w:customXml/w:sdt) and does not touch XML comment or PI handling.

The ai:* labels are advisory. They do not approve CI and they do not add tests:full. A new push runs this check again.

@loickouao

Copy link
Copy Markdown
Author

@dolfim-ibm thanks, both points are done:

  • _remove_xml_comment_nodes now checks isinstance(part, XmlPart) and reads the public part.element (543c6c2). In python-docx only XmlPart parts have an element, so the same parts are cleaned.
  • Rebased on main. The conflict was only where the new tests sit in tests/test_backend_msword.py.

The tests/test_backend_msword*.py modules all pass, and pre-commit is clean. I also ran the test from #4564 on this branch: it passes unchanged, and its comment text goes to NOTES.

This branch has not been deployed

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

Labels

ai:ci-safe AI triage found no CI-safety concern (advisory) ai:possible-duplicate AI triage: another PR may fix the same problem bug Something isn't working docx issue related to docx backend error-handling xml issue related to supported schema-specific XML formats

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOCX conversion fails or drops table text when document.xml contains an XML comment

4 participants