From 063bc101b29b46832a416f34e890d5e6d3ed82c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9E=97SO?= <142557582+Linxiushen@users.noreply.github.com> Date: Wed, 4 Nov 2026 07:53:52 +0800 Subject: [PATCH] Fix hidden n-ary limits in DOCX math --- .../converter_utils/docx/math/omml.py | 14 ++- .../tests/test_docx_nary_visibility.py | 99 +++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 packages/markitdown/tests/test_docx_nary_visibility.py diff --git a/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py b/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py index 5a014384a7..92676b343b 100644 --- a/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py +++ b/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py @@ -374,9 +374,21 @@ def do_nary(self, elm): """ the n-ary object """ + include = {"naryPr", "sub", "sup", "e"} + properties = elm.find(OMML_NS + "naryPr") + if properties is not None: + for bound in ("sub", "sup"): + hide = properties.find(OMML_NS + bound + "Hide") + # A present on/off property with no value means enabled. + if hide is not None and hide.get(OMML_NS + "val", "1") in ( + "1", + "true", + "on", + ): + include.remove(bound) res = [] bo = "" - for stag, t, e in self.process_children_list(elm): + for stag, t, e in self.process_children_list(elm, include=include): if stag == "naryPr": bo = get_char(t.chr, store=CHR_BO) else: diff --git a/packages/markitdown/tests/test_docx_nary_visibility.py b/packages/markitdown/tests/test_docx_nary_visibility.py new file mode 100644 index 0000000000..bbfbe7a5ee --- /dev/null +++ b/packages/markitdown/tests/test_docx_nary_visibility.py @@ -0,0 +1,99 @@ +"""Stored hidden n-ary limits must not become visible DOCX math.""" + +import io +import zipfile +from xml.etree import ElementTree as ET + +import pytest + +from markitdown import MarkItDown, StreamInfo +from markitdown.converter_utils.docx.math.omml import OMML_NS, oMath2Latex + + +def _nary_visibility_math(properties, lower="lower", upper="upper"): + return ( + f'' + '' + f"{properties}" + f"{lower}" + f"{upper}" + "x" + "" + ) + + +@pytest.mark.parametrize("bound", ["sub", "sup"]) +@pytest.mark.parametrize("value", [None, "1", "true", "on", "0", "false", "off"]) +def test_nary_limit_visibility(bound, value): + attribute = "" if value is None else f' m:val="{value}"' + properties = f"" + hidden = value in (None, "1", "true", "on") + expected = r"\sum" + if not (bound == "sub" and hidden): + expected += "_{lower}" + if not (bound == "sup" and hidden): + expected += "^{upper}" + expected += "x" + assert ( + oMath2Latex(ET.fromstring(_nary_visibility_math(properties))).latex == expected + ) + + +def test_nary_limits_are_visible_without_hide_properties(): + assert ( + oMath2Latex(ET.fromstring(_nary_visibility_math(""))).latex + == r"\sum_{lower}^{upper}x" + ) + + +def _nary_visibility_docx(properties): + document = ( + '' + + _nary_visibility_math(properties) + + "After equation" + ) + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr( + "[Content_Types].xml", + '' + '' + '' + "", + ) + archive.writestr( + "_rels/.rels", + '' + '' + "", + ) + archive.writestr("word/document.xml", document) + stream.seek(0) + return stream + + +@pytest.mark.parametrize( + "properties,visible,hidden", + [ + ('', "upper", "lower"), + ("", "lower", "upper"), + ('', None, "lower"), + ], +) +def test_nary_visibility_through_public_docx_conversion(properties, visible, hidden): + markdown = ( + MarkItDown() + .convert_stream( + _nary_visibility_docx(properties), stream_info=StreamInfo(extension=".docx") + ) + .markdown + ) + assert hidden not in markdown + if visible is not None: + assert visible in markdown + else: + assert "upper" not in markdown + assert r"\sum" in markdown + assert "x$" in markdown + assert "After equation" in markdown