150 lines
3.7 KiB
Python
150 lines
3.7 KiB
Python
"""Best-effort OMML → readable LaTeX-like string for $$ / $ fences."""
|
|
|
|
from __future__ import annotations
|
|
|
|
_OMML_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math"
|
|
|
|
|
|
def _local(tag: str) -> str:
|
|
if "}" in tag:
|
|
return tag.rsplit("}", 1)[-1]
|
|
return tag
|
|
|
|
|
|
def omml_to_latex(element) -> str:
|
|
"""Flatten OMML tree to a readable formula string (not perfect LaTeX)."""
|
|
if element is None:
|
|
return ""
|
|
parts: list[str] = []
|
|
_walk(element, parts)
|
|
text = "".join(parts)
|
|
text = " ".join(text.split())
|
|
return text.strip()
|
|
|
|
|
|
def _walk(node, parts: list[str]) -> None:
|
|
tag = _local(node.tag)
|
|
|
|
if tag == "t":
|
|
parts.append(node.text or "")
|
|
return
|
|
|
|
if tag == "f": # fraction
|
|
num = _child(node, "num")
|
|
den = _child(node, "den")
|
|
parts.append(r"\frac{")
|
|
if num is not None:
|
|
_walk(num, parts)
|
|
parts.append("}{")
|
|
if den is not None:
|
|
_walk(den, parts)
|
|
parts.append("}")
|
|
return
|
|
|
|
if tag == "sSup":
|
|
base = _child(node, "e")
|
|
sup = _child(node, "sup")
|
|
if base is not None:
|
|
_walk(base, parts)
|
|
parts.append("^{")
|
|
if sup is not None:
|
|
_walk(sup, parts)
|
|
parts.append("}")
|
|
return
|
|
|
|
if tag == "sSub":
|
|
base = _child(node, "e")
|
|
sub = _child(node, "sub")
|
|
if base is not None:
|
|
_walk(base, parts)
|
|
parts.append("_{")
|
|
if sub is not None:
|
|
_walk(sub, parts)
|
|
parts.append("}")
|
|
return
|
|
|
|
if tag == "sSubSup":
|
|
base = _child(node, "e")
|
|
sub = _child(node, "sub")
|
|
sup = _child(node, "sup")
|
|
if base is not None:
|
|
_walk(base, parts)
|
|
parts.append("_{")
|
|
if sub is not None:
|
|
_walk(sub, parts)
|
|
parts.append("}^{")
|
|
if sup is not None:
|
|
_walk(sup, parts)
|
|
parts.append("}")
|
|
return
|
|
|
|
if tag == "rad":
|
|
deg = _child(node, "deg")
|
|
e = _child(node, "e")
|
|
if deg is not None and "".join(deg.itertext()).strip():
|
|
parts.append(r"\sqrt[")
|
|
_walk(deg, parts)
|
|
parts.append("]{")
|
|
else:
|
|
parts.append(r"\sqrt{")
|
|
if e is not None:
|
|
_walk(e, parts)
|
|
parts.append("}")
|
|
return
|
|
|
|
if tag == "nary":
|
|
for child in node:
|
|
_walk(child, parts)
|
|
return
|
|
|
|
if tag in ("oMath", "oMathPara", "e", "num", "den", "sup", "sub", "deg",
|
|
"fPr", "ctrlPr", "rPr", "sSupPr", "sSubPr", "sSubSupPr", "radPr",
|
|
"naryPr", "dPr", "boxPr", "argSz"):
|
|
for child in node:
|
|
_walk(child, parts)
|
|
return
|
|
|
|
if tag == "r":
|
|
for child in node:
|
|
_walk(child, parts)
|
|
return
|
|
|
|
if tag == "d": # delimiter
|
|
parts.append("(")
|
|
for child in node:
|
|
if _local(child.tag) != "dPr":
|
|
_walk(child, parts)
|
|
parts.append(")")
|
|
return
|
|
|
|
if tag in ("pPr", "rFonts", "sz", "szCs", "color", "jc"):
|
|
return
|
|
|
|
for child in node:
|
|
_walk(child, parts)
|
|
if node.tail:
|
|
parts.append(node.tail)
|
|
|
|
|
|
def _child(node, local_name: str):
|
|
for child in node:
|
|
if _local(child.tag) == local_name:
|
|
return child
|
|
return None
|
|
|
|
|
|
def find_omml(paragraph_element) -> list:
|
|
"""Return all m:oMath / m:oMathPara elements under a paragraph."""
|
|
if paragraph_element is None:
|
|
return []
|
|
found = []
|
|
for el in paragraph_element.iter():
|
|
tag = _local(el.tag)
|
|
if tag in ("oMath", "oMathPara"):
|
|
found.append(el)
|
|
return found
|
|
|
|
|
|
def paragraph_has_omml(paragraph_element) -> bool:
|
|
return bool(find_omml(paragraph_element))
|