From 4719af75f02953596c41502daa2f22814e7dc3fb Mon Sep 17 00:00:00 2001 From: Taran Mamidala Date: Fri, 16 Oct 2026 18:22:08 -0400 Subject: [PATCH] fix(docx): normalize style type list to numbering to avoid load failure Signed-off-by: Taran Mamidala --- docling/backend/msword_backend.py | 34 ++++++++++++++++++++++++++++--- tests/test_backend_msword.py | 17 ++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/docling/backend/msword_backend.py b/docling/backend/msword_backend.py index c0324f6fb1..04e0c62a7e 100644 --- a/docling/backend/msword_backend.py +++ b/docling/backend/msword_backend.py @@ -879,9 +879,13 @@ def __init__( if self.docx_obj: self.valid = True self.current_part = self.docx_obj.part - self._default_paragraph_style = self.docx_obj.styles.default( - WD_STYLE_TYPE.PARAGRAPH - ) + self._normalize_style_types() + try: + self._default_paragraph_style = self.docx_obj.styles.default( + WD_STYLE_TYPE.PARAGRAPH + ) + except Exception: + self._default_paragraph_style = None # Build comment mappings after loading document self._extract_comment_ranges() @@ -4834,3 +4838,27 @@ def _get_comment_ids_for_element(self, element: BaseOxmlElement) -> set[str]: comment_ids.add(comment_id) return comment_ids + + def _normalize_style_types(self) -> None: + """Normalize non-standard style types in styles.xml. + + Some third-party DOCX generators write ``w:type="list"`` instead of + ``w:type="numbering"``. In python-docx, ``WD_STYLE_TYPE.LIST`` maps to + the XML attribute value ``"numbering"``. An unmapped XML value causes + ``WD_STYLE_TYPE.from_xml`` to raise a ``ValueError`` when resolving + default styles or accessing unstyled paragraphs. + """ + if not self.docx_obj or not hasattr(self.docx_obj, "styles"): + return + styles_element = getattr(self.docx_obj.styles, "element", None) + if styles_element is None: + return + type_key = f"{_W_NS_CLARK}type" + for style_el in styles_element: + if style_el.tag == f"{_W_NS_CLARK}style" or style_el.tag.endswith("style"): + val = style_el.get(type_key) + if val == "list": + style_el.set(type_key, "numbering") + elif val is None and "type" in style_el.attrib: + if style_el.attrib["type"] == "list": + style_el.attrib["type"] = "numbering" diff --git a/tests/test_backend_msword.py b/tests/test_backend_msword.py index 314fa1c6f3..997bd2b165 100644 --- a/tests/test_backend_msword.py +++ b/tests/test_backend_msword.py @@ -2640,3 +2640,20 @@ def test_body_level_alternate_content_with_no_usable_branch_adds_nothing(tmp_pat "Paragraph before AlternateContent", "Paragraph after AlternateContent", ] + + +def test_docx_styles_xml_with_type_list_converts(tmp_path): + """A style with w:type='list' does not crash default style lookup or conversion.""" + document = Document() + document.add_heading("Quarterly results", level=1) + document.add_paragraph("Revenue grew 3%.") # paragraph without w:pStyle + + style = OxmlElement("w:style") + style.set(qn("w:type"), "list") + style.set(qn("w:styleId"), "OutlineList") + document.styles.element.append(style) + + doc = _convert_built(document, tmp_path) + + texts = [item.text for item in doc.texts] + assert texts == ["Quarterly results", "Revenue grew 3%."]