"""
OMML parser for Office Math Markup Language input.
This parser handles OMML XML input from Microsoft Office, converting it
to MathML format using the existing OMML processing pipeline.
"""
from typing import List
from lxml import etree as ET
from ..core.interfaces import Parser
class OmmlParser(Parser):
"""
Parser for OMML (Office Math Markup Language) XML input.
This parser handles OMML XML from Microsoft Office documents, converting
it to MathML using the existing OMML transformation pipeline.
Examples:
Single OMML equation:
>>> parser = OmmlParser()
>>> omml = 'x'
>>> results = parser.parse(omml)
>>> len(results) >= 1
True
"""
def __init__(self):
"""Initialize the OMML parser."""
pass
def parse(self, source: str) -> List[str]:
"""
Parse OMML input and return list of MathML equation strings.
Args:
source: OMML XML string or file path
Returns:
List of MathML equation strings (converted from OMML)
Raises:
ValueError: If input is not valid OMML XML
RuntimeError: If OMML to MathML conversion fails
Examples:
>>> parser = OmmlParser()
>>> omml = '...'
>>> results = parser.parse(omml)
>>> isinstance(results, list)
True
"""
import os
# Check if source is a file path
if os.path.exists(source):
try:
with open(source, 'r', encoding='utf-8') as f:
source = f.read()
except (IOError, UnicodeDecodeError) as e:
raise ValueError(f"Failed to read OMML file: {e}")
# Parse XML
try:
root = ET.fromstring(source.encode('utf-8'))
except ET.XMLSyntaxError as e:
raise ValueError(f"Invalid XML syntax: {e}")
# Find all OMML math elements
# OMML uses or elements
omml_ns = {'m': 'http://schemas.openxmlformats.org/officeDocument/2006/math'}
# Find oMath elements (with or without namespace)
omath_elements = []
omath_elements.extend(root.findall('.//m:oMath', omml_ns))
omath_elements.extend(root.findall('.//m:oMathPara', omml_ns))
# Try without namespace (in case it's not declared)
if not omath_elements:
for elem in root.iter():
local_name = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag
if local_name in ('oMath', 'oMathPara'):
omath_elements.append(elem)
if not omath_elements:
# Check if root itself is an OMML element
root_local = root.tag.split('}')[-1] if '}' in root.tag else root.tag
if root_local in ('oMath', 'oMathPara'):
omath_elements.append(root)
else:
raise ValueError("No OMML math elements ( or ) found in input")
# Convert each OMML element to MathML
results = []
for omml_elem in omath_elements:
try:
mathml_str = self._omml_to_mathml(omml_elem)
if mathml_str and mathml_str.strip():
results.append(mathml_str)
except Exception as e:
# Log error but continue with other equations
print(f"Warning: Failed to convert OMML element: {e}")
continue
if not results:
raise RuntimeError("Failed to convert any OMML elements to MathML")
return results
def can_handle(self, source: str) -> bool:
"""
Check if this parser can handle the given input source.
Args:
source: Input source to check
Returns:
True if source appears to be OMML, False otherwise
Examples:
>>> parser = OmmlParser()
>>> parser.can_handle("...")
True
>>> parser.can_handle("")
False
"""
# Check for OMML markers
content_start = source.strip()[:200]
return ' str:
"""Get the parser type identifier."""
return "omml"
def _omml_to_mathml(self, omml_element: ET.Element) -> str:
"""
Convert OMML element to MathML string with styling preservation.
This wraps the existing OMML to MathML conversion from the core module,
and adds post-processing to preserve styling (bold, fonts, colors) that
the XSLT stylesheet doesn't preserve.
Args:
omml_element: OMML XML element
Returns:
MathML string with styling attributes
Raises:
RuntimeError: If conversion fails
"""
# Import here to avoid circular dependencies
import os
from ..core import omml_to_mathml
from ..utils.styling_postprocessor import apply_styling_to_mathml_from_omml
try:
# Convert OMML element to string
omml_str = ET.tostring(omml_element, encoding='unicode')
# Get XSL path (in core module)
current_dir = os.path.dirname(os.path.abspath(__file__))
core_dir = os.path.join(os.path.dirname(current_dir), 'core')
xsl_path = os.path.join(core_dir, 'OMML2MML.xsl')
# Step 1: Use XSLT to convert OMML to MathML
mathml_str = omml_to_mathml(omml_str, xsl_path)
# Step 2: Post-process to add styling (bold, fonts, colors)
# The XSLT doesn't preserve m:sty, m:scr, w:color, etc.
# This step extracts styling from OMML and applies to MathML
mathml_str = apply_styling_to_mathml_from_omml(omml_str, mathml_str)
return mathml_str
except Exception as e:
raise RuntimeError(f"OMML to MathML conversion failed: {e}")
def __repr__(self) -> str:
"""String representation for debugging."""
return "OmmlParser()"