diff --git a/packages/markitdown/src/markitdown/converter_utils/docx/math/latex_dict.py b/packages/markitdown/src/markitdown/converter_utils/docx/math/latex_dict.py index 5657a6007..a25466d15 100644 --- a/packages/markitdown/src/markitdown/converter_utils/docx/math/latex_dict.py +++ b/packages/markitdown/src/markitdown/converter_utils/docx/math/latex_dict.py @@ -225,6 +225,8 @@ CHR_DEFAULT = { "ACC_VAL": "\\hat{{{0}}}", "GROUP_CHR_VAL": "\\underbrace{{{0}}}", + # Omitting m:chr under m:naryPr means U+222B INTEGRAL (ISO/IEC 29500-1). + "NARY_VAL": "\\int", } POS = { 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 f566f7d3b..3b068e4f2 100644 --- a/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py +++ b/packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py @@ -363,10 +363,12 @@ def do_nary(self, elm): the n-ary object """ res = [] - bo = "" + # m:naryPr is itself optional, so an absent element means every property + # takes its default, the operator included. + bo = CHR_DEFAULT.get("NARY_VAL", "") for stag, t, e in self.process_children_list(elm): if stag == "naryPr": - bo = get_char(t.chr, store=CHR_BO) + bo = get_char(t.chr, default=CHR_DEFAULT.get("NARY_VAL"), store=CHR_BO) else: res.append(t) return bo + BLANK.join(res) diff --git a/packages/markitdown/tests/test_docx_math_nary.py b/packages/markitdown/tests/test_docx_math_nary.py new file mode 100644 index 000000000..d64abb987 --- /dev/null +++ b/packages/markitdown/tests/test_docx_math_nary.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 -m pytest +"""Tests for the default n-ary operator in DOCX math conversion. + +``m:chr`` under ``m:naryPr`` names the n-ary operator. ISO/IEC 29500-1 states +that when the element is omitted the operator is U+222B INTEGRAL, so producers +write ``m:chr`` only for the non-default operators such as the summation sign. +``do_nary`` passed no default to ``get_char``, so an integral yielded ``None`` +and ``None + ""`` raised ``TypeError``. + +That failure is not local to the equation. ``pre_process_docx`` runs +``_pre_process_math`` over the whole of ``word/document.xml`` inside a blanket +``except Exception`` and, on error, writes the *original* unprocessed XML back. +Mammoth does not render OMML, so one integral silently removes every equation +in the document. +""" + +from xml.etree import ElementTree as ET + +from markitdown.converter_utils.docx.math.omml import OMML_NS, oMath2Latex + +MATH_NS_DECL = f'xmlns:m="{OMML_NS[1:-1]}"' + +SUMMATION = "∑" # N-ARY SUMMATION, written out by Word as it is not the default + + +def _nary(nary_pr: str): + xml = ( + f"" + f"{nary_pr}" + "0" + "1" + "x" + "" + ) + return oMath2Latex(ET.fromstring(xml)).latex + + +def test_nary_without_chr_defaults_to_integral(): + # No m:chr, so the operator is U+222B. Previously raised TypeError. + latex = _nary('') + assert latex == "\\int_{0}^{1}x" + + +def test_nary_with_explicit_chr_is_unchanged(): + latex = _nary(f'') + assert latex == "\\sum_{0}^{1}x" + + +def test_nary_without_nary_pr_defaults_to_integral(): + # m:naryPr is optional. Absent, every property takes its default, so the + # operator is still U+222B rather than nothing. + assert _nary("") == "\\int_{0}^{1}x"