""" DOCX file processing and end-to-end conversion. This module provides the DocxProcessor class which orchestrates the complete conversion pipeline from DOCX to LaTeX, including OMML extraction, MathML transformation, and LaTeX conversion. """ import logging import time from typing import List from .result import ConversionResult from .omml_extractor import OMMLExtractor logger = logging.getLogger(__name__) class DocxProcessor: """Process DOCX files and convert all OMML equations to LaTeX. This class coordinates: - OMML extraction from DOCX (via OMMLExtractor) - OMML → MathML transformation (via omml_to_mathml function) - MathML → LaTeX conversion (via MathMLToTeXConverter) Example: >>> from omml2tex.compat import LegacyMathMLToTeXConverter >>> processor = DocxProcessor( ... xsl_path='OMML2MML.xsl', ... converter=MathMLToTeXConverter() ... ) >>> results = processor.process_docx('document.docx') >>> for result in results: ... if result.success: ... print(result.latex) """ def __init__(self, xsl_path: str, converter, subject_mode: str = "auto"): """Initialize DOCX processor. Args: xsl_path: Path to OMML2MML.xsl transformation file converter: MathMLToTeXConverter instance subject_mode: Subject mode for conversion ('auto', 'math', 'chemistry', 'physics') """ self.extractor = OMMLExtractor() self.xsl_path = xsl_path self.converter = converter self.subject_mode = subject_mode def process_docx(self, docx_path: str, omml_to_mathml_func=None) -> List[ConversionResult]: """Process a DOCX file and convert all equations. Args: docx_path: Path to DOCX file omml_to_mathml_func: Function to convert OMML to MathML (for dependency injection) Returns: List of ConversionResult objects Example: >>> results = processor.process_docx('test.docx') >>> successful = [r for r in results if r.success] >>> print(f"{len(successful)}/{len(results)} equations converted") """ results = [] try: # Extract OMML equations equations = self.extractor.extract_from_docx(docx_path) logger.info(f"Found {len(equations)} equations in {docx_path}") for eq_data in equations: result = ConversionResult() result.omml = eq_data['cleaned_omml'] result.context = eq_data['context'] result.position = eq_data['position'] result.source_file = eq_data['source_file'] result.display_mode = eq_data.get('display_mode', False) try: start_time = time.time() # Convert OMML to MathML if omml_to_mathml_func is None: # Import here to avoid circular dependency from ..core.xslt_transformer import omml_to_mathml omml_to_mathml_func = omml_to_mathml mathml_xml = omml_to_mathml_func(eq_data['cleaned_omml'], self.xsl_path) result.mathml = mathml_xml # Convert MathML to LaTeX latex = self.converter.convert(mathml_xml) result.latex = latex result.processing_time = time.time() - start_time result.success = True except Exception as e: result.error = str(e) result.success = False logger.error(f"Failed to convert equation {result.position}: {e}") results.append(result) except Exception as e: logger.error(f"Failed to process DOCX file: {e}") # Return empty results with error result = ConversionResult() result.error = f"DOCX processing failed: {e}" results.append(result) return results def detect_subject_mode(self, results: List[ConversionResult]) -> str: """Auto-detect subject mode based on content. Analyzes context text from equations to determine if document is primarily chemistry, physics, or general math. Args: results: List of conversion results with context Returns: Detected subject mode: 'chemistry', 'physics', or 'math' Example: >>> results = processor.process_docx('chem_doc.docx') >>> mode = processor.detect_subject_mode(results) >>> print(f"Detected mode: {mode}") """ chemistry_indicators = 0 physics_indicators = 0 total_context = "" for result in results: if result.success and result.context: total_context += " " + result.context.lower() # Simple heuristic detection chemistry_words = ['reaction', 'equation', 'chemical', 'compound', 'element', 'molecule', 'ion', 'acid', 'base'] physics_words = ['force', 'energy', 'velocity', 'acceleration', 'wave', 'field', 'quantum', 'particle'] for word in chemistry_words: chemistry_indicators += total_context.count(word) for word in physics_words: physics_indicators += total_context.count(word) if chemistry_indicators > physics_indicators and chemistry_indicators > 2: return "chemistry" elif physics_indicators > chemistry_indicators and physics_indicators > 2: return "physics" else: return "math"