When a Word equation contains a delimiter (<m:d>) with more than one argument, MarkItDown keeps only the last argument and drops the rest without any warning. The separator character (<m:sepChr>) is never read either.
Word writes this structure for the bracket templates "with separators" (Insert > Equation > Bracket), for example (a|b), {x|x>0} and bra-ket notation.
Repro
from xml.etree import ElementTree as ET
from markitdown.converter_utils.docx.math.omml import OMML_NS, oMath2Latex
M = f'xmlns:m="{OMML_NS[1:-1]}"'
def latex(xml):
return oMath2Latex(ET.fromstring(f"<m:oMath {M}>{xml}</m:oMath>")).latex
r = lambda t: f"<m:r><m:t>{t}</m:t></m:r>"
# Explicit separator
print(latex(r("f") + '<m:d><m:dPr><m:sepChr m:val=","/></m:dPr>'
f"<m:e>{r('x')}</m:e><m:e>{r('y')}</m:e></m:d>"))
# -> f\left(y\right)
# Set-builder braces, default separator (no sepChr)
print(latex('<m:d><m:dPr><m:begChr m:val="{"/><m:endChr m:val="}"/></m:dPr>'
f"<m:e>{r('x')}</m:e><m:e>{r('x>0')}</m:e></m:d>"))
# -> \left\{x>0\right\}
End to end, adding the first equation to a copy of tests/test_files/equations.docx makes MarkItDown().convert() emit $$f\left(y\right)$$.
Expected vs actual
| Input |
Expected |
Actual |
f + <m:d> with sepChr=",", args x, y |
f\left(x,y\right) |
f\left(y\right) |
<m:d> braces, no sepChr, args x, x>0 |
\left\{x|x>0\right\} |
\left\{x>0\right\} |
<m:d> with sepChr=";", args a, b, c |
\left(a;b;c\right) |
\left(c\right) |
In the OMML schema (ECMA-376 / ISO/IEC 29500-1, CT_D), <m:e> has maxOccurs="unbounded". The sepChr element "specifies the character that separates base arguments e in the delimiter object d". When the element is omitted, the separator is a vertical bar (U+2502). When it is present without m:val, there is no separator. See the Open XML SDK SeparatorChar reference, which reproduces the spec text. Dropping arguments silently loses document content.
Cause
In packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py, oMath2Latex.do_d reads its children with process_children_dict(). That function returns a dict keyed by tag name, so each <m:e> overwrites the previous one and c_dict["e"] holds only the last argument. Separately, Pr.__val_tags (and Pr.tag2meth) do not include sepChr, so pr.sepChr is always None.
Suggested fix
In do_d, collect every <m:e> in order with process_children_list(elm, include=("e",)). Have Pr read sepChr, with an empty value when m:val is missing. Then join the arguments with the separator, falling back to | when sepChr is absent. A single-argument delimiter gives the same output as before.
Environment
- markitdown
main at 1f9530a (0.1.8)
- Python 3.13, macOS
AI assistance: drafted with an AI coding assistant (Claude). The reproduction above was run locally against the current default branch.
When a Word equation contains a delimiter (
<m:d>) with more than one argument, MarkItDown keeps only the last argument and drops the rest without any warning. The separator character (<m:sepChr>) is never read either.Word writes this structure for the bracket templates "with separators" (Insert > Equation > Bracket), for example
(a|b),{x|x>0}and bra-ket notation.Repro
End to end, adding the first equation to a copy of
tests/test_files/equations.docxmakesMarkItDown().convert()emit$$f\left(y\right)$$.Expected vs actual
f+<m:d>withsepChr=",", argsx,yf\left(x,y\right)f\left(y\right)<m:d>braces, nosepChr, argsx,x>0\left\{x|x>0\right\}\left\{x>0\right\}<m:d>withsepChr=";", argsa,b,c\left(a;b;c\right)\left(c\right)In the OMML schema (ECMA-376 / ISO/IEC 29500-1,
CT_D),<m:e>hasmaxOccurs="unbounded". ThesepChrelement "specifies the character that separates base arguments e in the delimiter object d". When the element is omitted, the separator is a vertical bar (U+2502). When it is present withoutm:val, there is no separator. See the Open XML SDKSeparatorCharreference, which reproduces the spec text. Dropping arguments silently loses document content.Cause
In
packages/markitdown/src/markitdown/converter_utils/docx/math/omml.py,oMath2Latex.do_dreads its children withprocess_children_dict(). That function returns a dict keyed by tag name, so each<m:e>overwrites the previous one andc_dict["e"]holds only the last argument. Separately,Pr.__val_tags(andPr.tag2meth) do not includesepChr, sopr.sepChris alwaysNone.Suggested fix
In
do_d, collect every<m:e>in order withprocess_children_list(elm, include=("e",)). HavePrreadsepChr, with an empty value whenm:valis missing. Then join the arguments with the separator, falling back to|whensepChris absent. A single-argument delimiter gives the same output as before.Environment
mainat 1f9530a (0.1.8)AI assistance: drafted with an AI coding assistant (Claude). The reproduction above was run locally against the current default branch.