init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)
服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)
公式支持三类来源:
- OMML(Word 2007+ 公式) → Python omml_mathml2latex
- MathType OLE 对象 → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力) → EqToLatexUtil(本次新增)
本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
移植自 jby backend-services office-core,EQ 语法 → LaTeX
(\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
\a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
(原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)
修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留
安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
OssService 的 @Value 默认密钥已清空
This commit is contained in:
27
scripts/omml_mathml2latex/processing/__init__.py
Normal file
27
scripts/omml_mathml2latex/processing/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Input/output processing module for OMML and DOCX files.
|
||||
|
||||
This module handles:
|
||||
- DOCX file processing and equation extraction
|
||||
- OMML extraction from Word documents
|
||||
- LaTeX output formatting (inline, display, various delimiters)
|
||||
- Result data structures
|
||||
"""
|
||||
|
||||
from .result import ConversionResult
|
||||
from .omml_extractor import OMMLExtractor
|
||||
from .docx_processor import DocxProcessor
|
||||
from .output_formatter import (
|
||||
format_latex,
|
||||
save_results,
|
||||
generate_html_preview,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ConversionResult",
|
||||
"OMMLExtractor",
|
||||
"DocxProcessor",
|
||||
"format_latex",
|
||||
"save_results",
|
||||
"generate_html_preview",
|
||||
]
|
||||
156
scripts/omml_mathml2latex/processing/docx_processor.py
Normal file
156
scripts/omml_mathml2latex/processing/docx_processor.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
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"
|
||||
273
scripts/omml_mathml2latex/processing/omml_extractor.py
Normal file
273
scripts/omml_mathml2latex/processing/omml_extractor.py
Normal file
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
OMML equation extraction from DOCX files.
|
||||
|
||||
This module provides the OMMLExtractor class which extracts OMML (Office Math Markup Language)
|
||||
equations from Microsoft Word DOCX files, including main document, headers, and footers.
|
||||
"""
|
||||
|
||||
import zipfile
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from lxml import etree as ET
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OMMLExtractor:
|
||||
"""Extract OMML equations from DOCX files.
|
||||
|
||||
Handles extraction from:
|
||||
- Main document body (word/document.xml)
|
||||
- Headers (word/header*.xml)
|
||||
- Footers (word/footer*.xml)
|
||||
|
||||
Preserves:
|
||||
- Non-breaking spaces (NBSP, U+00A0)
|
||||
- Display mode detection (inline vs display equations)
|
||||
- Context from surrounding paragraphs
|
||||
- Position tracking
|
||||
|
||||
Example:
|
||||
>>> extractor = OMMLExtractor()
|
||||
>>> equations = extractor.extract_from_docx('document.docx')
|
||||
>>> print(f"Found {len(equations)} equations")
|
||||
>>> print(extractor.stats)
|
||||
"""
|
||||
|
||||
# OMML and Word namespace definitions
|
||||
NAMESPACES = {
|
||||
'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
|
||||
'm': 'http://schemas.openxmlformats.org/officeDocument/2006/math',
|
||||
'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize extractor with empty statistics."""
|
||||
self.stats = {
|
||||
'equations_found': 0,
|
||||
'files_processed': 0,
|
||||
'errors': 0
|
||||
}
|
||||
|
||||
def extract_from_docx(self, docx_path: str) -> List[Dict[str, Any]]:
|
||||
"""Extract all OMML equations from a DOCX file.
|
||||
|
||||
Args:
|
||||
docx_path: Path to DOCX file
|
||||
|
||||
Returns:
|
||||
List of equation dictionaries with keys:
|
||||
- omml: Raw OMML XML string
|
||||
- cleaned_omml: Cleaned OMML ready for transformation
|
||||
- position: Position/index in document
|
||||
- context: Surrounding text context
|
||||
- source_file: XML file within DOCX (e.g., 'word/document.xml')
|
||||
- display_mode: Boolean indicating display (True) vs inline (False)
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If DOCX file doesn't exist
|
||||
Exception: For other processing errors
|
||||
|
||||
Example:
|
||||
>>> extractor = OMMLExtractor()
|
||||
>>> equations = extractor.extract_from_docx('test.docx')
|
||||
>>> for eq in equations:
|
||||
... print(f"Position {eq['position']}: {eq['display_mode']}")
|
||||
"""
|
||||
equations = []
|
||||
docx_path = Path(docx_path)
|
||||
|
||||
if not docx_path.exists():
|
||||
raise FileNotFoundError(f"DOCX file not found: {docx_path}")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(docx_path, 'r') as docx:
|
||||
# Extract main document
|
||||
equations.extend(self._extract_from_document(docx, 'word/document.xml'))
|
||||
|
||||
# Check for equations in headers/footers
|
||||
header_files = [f for f in docx.namelist() if f.startswith('word/header')]
|
||||
footer_files = [f for f in docx.namelist() if f.startswith('word/footer')]
|
||||
|
||||
for header_file in header_files:
|
||||
equations.extend(self._extract_from_document(docx, header_file))
|
||||
|
||||
for footer_file in footer_files:
|
||||
equations.extend(self._extract_from_document(docx, footer_file))
|
||||
|
||||
self.stats['files_processed'] += 1
|
||||
self.stats['equations_found'] += len(equations)
|
||||
|
||||
except Exception as e:
|
||||
self.stats['errors'] += 1
|
||||
logger.error(f"Error processing {docx_path}: {e}")
|
||||
raise
|
||||
|
||||
return equations
|
||||
|
||||
def _extract_from_document(self, docx: zipfile.ZipFile, xml_path: str) -> List[Dict[str, Any]]:
|
||||
"""Extract equations from a specific XML document within the DOCX.
|
||||
|
||||
Args:
|
||||
docx: Open zipfile.ZipFile object
|
||||
xml_path: Path to XML file within DOCX
|
||||
|
||||
Returns:
|
||||
List of equation dictionaries
|
||||
"""
|
||||
equations = []
|
||||
|
||||
try:
|
||||
if xml_path not in docx.namelist():
|
||||
return equations
|
||||
|
||||
doc_xml = docx.read(xml_path)
|
||||
|
||||
# IMPORTANT: Preserve NBSP characters before XML parsing
|
||||
# lxml normalizes U+00A0 (NBSP) to regular space, so we replace it
|
||||
# with a placeholder that we'll restore later
|
||||
# UTF-8 encoding of NBSP is \xc2\xa0
|
||||
doc_xml = doc_xml.replace(b'\xc2\xa0', b' ') # Convert NBSP to decimal entity
|
||||
|
||||
# Use lxml for parsing to enable getparent() method
|
||||
root = ET.fromstring(doc_xml)
|
||||
|
||||
# Find all OMML equations
|
||||
math_elements = root.findall('.//m:oMath', self.NAMESPACES)
|
||||
|
||||
for i, math_elem in enumerate(math_elements):
|
||||
try:
|
||||
# Extract raw OMML (use lxml for consistency)
|
||||
raw_omml = ET.tostring(math_elem, encoding='unicode', method='xml')
|
||||
|
||||
# Clean and prepare OMML
|
||||
cleaned_omml = self._clean_omml(raw_omml)
|
||||
|
||||
# Get surrounding context
|
||||
context = self._get_context(math_elem)
|
||||
|
||||
# Detect if this is a display equation (wrapped in oMathPara)
|
||||
display_mode = self._is_display_equation(math_elem)
|
||||
|
||||
equation_data = {
|
||||
'omml': raw_omml,
|
||||
'cleaned_omml': cleaned_omml,
|
||||
'position': i,
|
||||
'context': context,
|
||||
'source_file': xml_path,
|
||||
'display_mode': display_mode
|
||||
}
|
||||
|
||||
equations.append(equation_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error extracting equation {i} from {xml_path}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading {xml_path}: {e}")
|
||||
|
||||
return equations
|
||||
|
||||
def _clean_omml(self, omml: str) -> str:
|
||||
"""Clean and prepare OMML for transformation.
|
||||
|
||||
Fixes:
|
||||
- Missing namespace declarations
|
||||
- Word formatting properties
|
||||
- Whitespace normalization between tags
|
||||
|
||||
Args:
|
||||
omml: Raw OMML XML string
|
||||
|
||||
Returns:
|
||||
Cleaned OMML string
|
||||
"""
|
||||
import re
|
||||
|
||||
# Add proper namespace declarations if missing
|
||||
if 'xmlns:m=' not in omml:
|
||||
omml = omml.replace(
|
||||
'<m:oMath',
|
||||
'<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"'
|
||||
)
|
||||
|
||||
if 'xmlns:w=' not in omml and 'w:' in omml:
|
||||
omml = omml.replace(
|
||||
'<m:oMath',
|
||||
'<m:oMath xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
|
||||
)
|
||||
|
||||
# Remove Word formatting properties that might interfere (but preserve font information)
|
||||
omml = re.sub(r'<w:szCs[^>]*?/>', '', omml)
|
||||
# Keep w:color elements for color preservation
|
||||
# omml = re.sub(r'<w:color[^>]*?/>', '', omml)
|
||||
|
||||
# Normalize whitespace BETWEEN XML tags only (not inside text content)
|
||||
# Collapse multiple whitespace chars between tags into single space
|
||||
# Pattern: ">[ \t\n\r]+<" → "> <"
|
||||
omml = re.sub(r'>([ \t\n\r]+)<', r'> <', omml)
|
||||
omml = omml.strip()
|
||||
|
||||
return omml
|
||||
|
||||
def _get_context(self, math_elem: ET._Element) -> str:
|
||||
"""Get surrounding text context for the equation.
|
||||
|
||||
Args:
|
||||
math_elem: OMML math element
|
||||
|
||||
Returns:
|
||||
Surrounding paragraph text (max 200 characters)
|
||||
"""
|
||||
try:
|
||||
# Find the parent paragraph
|
||||
current = math_elem
|
||||
while current is not None:
|
||||
if current.tag.endswith('p'): # Word paragraph
|
||||
break
|
||||
parent = current.find('..')
|
||||
current = parent
|
||||
|
||||
if current is None:
|
||||
return ""
|
||||
|
||||
# Extract all text from the paragraph
|
||||
text_parts = []
|
||||
for elem in current.iter():
|
||||
if elem.tag.endswith('t') and elem.text: # Word text element
|
||||
text_parts.append(elem.text)
|
||||
|
||||
context = ' '.join(text_parts).strip()
|
||||
|
||||
# Limit context length
|
||||
if len(context) > 200:
|
||||
context = context[:200] + "..."
|
||||
|
||||
return context
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error getting context: {e}")
|
||||
return ""
|
||||
|
||||
def _is_display_equation(self, math_elem: ET._Element) -> bool:
|
||||
"""Determine if an equation is display mode ($$) or inline mode ($).
|
||||
|
||||
Display equations are wrapped in <m:oMathPara>, inline equations are not.
|
||||
|
||||
Args:
|
||||
math_elem: OMML math element
|
||||
|
||||
Returns:
|
||||
True for display mode, False for inline mode
|
||||
"""
|
||||
try:
|
||||
# Check if the parent is oMathPara
|
||||
parent = math_elem.getparent()
|
||||
if parent is not None and parent.tag.endswith('oMathPara'):
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.debug(f"Error detecting display mode: {e}")
|
||||
return False # Default to inline if unsure
|
||||
221
scripts/omml_mathml2latex/processing/output_formatter.py
Normal file
221
scripts/omml_mathml2latex/processing/output_formatter.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Output formatting for conversion results.
|
||||
|
||||
This module provides functions to format conversion results in various output
|
||||
formats including LaTeX, JSON, and HTML with MathJax rendering.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List
|
||||
from .result import ConversionResult
|
||||
from ..utils.text_processing import write_text
|
||||
|
||||
|
||||
def format_latex(results: List[ConversionResult], format_type: str = "display",
|
||||
delimiter: str = "dollar") -> str:
|
||||
"""Format LaTeX results for output.
|
||||
|
||||
Args:
|
||||
results: List of conversion results
|
||||
format_type: 'inline', 'display', or 'both' (auto-detect)
|
||||
delimiter: 'dollar', 'bracket', 'mathjax', or 'katex'
|
||||
|
||||
Returns:
|
||||
Formatted LaTeX string with appropriate delimiters
|
||||
|
||||
Example:
|
||||
>>> results = [ConversionResult()]
|
||||
>>> results[0].success = True
|
||||
>>> results[0].latex = 'x + y'
|
||||
>>> results[0].display_mode = False
|
||||
>>> output = format_latex(results, 'both', 'dollar')
|
||||
>>> print(output)
|
||||
'$x + y$'
|
||||
"""
|
||||
if delimiter == "dollar":
|
||||
inline_start, inline_end = "$", "$"
|
||||
display_start, display_end = "$$", "$$"
|
||||
elif delimiter == "bracket":
|
||||
inline_start, inline_end = "\\(", "\\)"
|
||||
display_start, display_end = "\\[", "\\]"
|
||||
elif delimiter == "mathjax":
|
||||
inline_start, inline_end = "\\(", "\\)"
|
||||
display_start, display_end = "$$", "$$"
|
||||
elif delimiter == "katex":
|
||||
inline_start, inline_end = "$", "$"
|
||||
display_start, display_end = "$$", "$$"
|
||||
else:
|
||||
inline_start, inline_end = "$", "$"
|
||||
display_start, display_end = "$$", "$$"
|
||||
|
||||
output_lines = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
if not result.success:
|
||||
output_lines.append(f"% Equation {i+1}: ERROR - {result.error}")
|
||||
continue
|
||||
|
||||
# Determine if equation should be inline or display
|
||||
if hasattr(result, 'display_mode') and format_type == "both":
|
||||
# Use the detected display mode from OMML structure
|
||||
if result.display_mode:
|
||||
equation = f"{display_start}{result.latex}{display_end}"
|
||||
else:
|
||||
equation = f"{inline_start}{result.latex}{inline_end}"
|
||||
elif format_type == "inline":
|
||||
equation = f"{inline_start}{result.latex}{inline_end}"
|
||||
elif format_type == "display":
|
||||
equation = f"{display_start}{result.latex}{display_end}"
|
||||
else: # format_type == "both" but no display_mode set
|
||||
# Use heuristic: short equations inline, complex ones display
|
||||
if len(result.latex) < 50 and not any(cmd in result.latex for cmd in ["\\frac", "\\sum", "\\int", "\\begin"]):
|
||||
equation = f"{inline_start}{result.latex}{inline_end}"
|
||||
else:
|
||||
equation = f"{display_start}{result.latex}{display_end}"
|
||||
|
||||
output_lines.append(equation)
|
||||
output_lines.append("") # Empty line between equations
|
||||
|
||||
return "\n".join(output_lines)
|
||||
|
||||
|
||||
def save_results(results: List[ConversionResult], output_path: str,
|
||||
format_type: str = "display", delimiter: str = "dollar",
|
||||
include_context: bool = False, output_format: str = None):
|
||||
"""Save conversion results in various formats.
|
||||
|
||||
Args:
|
||||
results: List of conversion results
|
||||
output_path: Output file path
|
||||
format_type: For LaTeX output - 'inline', 'display', or 'both'
|
||||
delimiter: For LaTeX output - delimiter style
|
||||
include_context: Whether to include context in JSON output
|
||||
output_format: 'latex', 'json', or 'html' (auto-detected from output_path if not provided)
|
||||
|
||||
Example:
|
||||
>>> results = [ConversionResult()]
|
||||
>>> save_results(results, 'output.json')
|
||||
>>> save_results(results, 'output.tex', format_type='inline', delimiter='dollar')
|
||||
"""
|
||||
# Auto-detect output format from file extension if not provided
|
||||
if output_format is None:
|
||||
if output_path.endswith('.json'):
|
||||
output_format = 'json'
|
||||
elif output_path.endswith('.html'):
|
||||
output_format = 'html'
|
||||
else:
|
||||
# Default to LaTeX for .tex, .md, and any other extension
|
||||
output_format = 'latex'
|
||||
|
||||
if output_format == "latex":
|
||||
latex_content = format_latex(results, format_type, delimiter)
|
||||
write_text(output_path, latex_content)
|
||||
|
||||
elif output_format == "json":
|
||||
json_data = {
|
||||
"metadata": {
|
||||
"total_equations": len(results),
|
||||
"successful_conversions": sum(1 for r in results if r.success),
|
||||
"failed_conversions": sum(1 for r in results if not r.success),
|
||||
"format_type": format_type,
|
||||
"delimiter": delimiter,
|
||||
"include_context": include_context
|
||||
},
|
||||
"equations": [result.to_dict() for result in results]
|
||||
}
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
elif output_format == "html":
|
||||
html_content = generate_html_preview(results, format_type, delimiter)
|
||||
write_text(output_path, html_content)
|
||||
|
||||
|
||||
def generate_html_preview(results: List[ConversionResult], format_type: str, delimiter: str) -> str:
|
||||
"""Generate HTML preview with MathJax rendering.
|
||||
|
||||
Args:
|
||||
results: List of conversion results
|
||||
format_type: 'inline', 'display', or 'both'
|
||||
delimiter: Delimiter style
|
||||
|
||||
Returns:
|
||||
HTML document string with embedded MathJax
|
||||
|
||||
Example:
|
||||
>>> results = [ConversionResult()]
|
||||
>>> html = generate_html_preview(results, 'display', 'dollar')
|
||||
>>> '<!DOCTYPE html>' in html
|
||||
True
|
||||
"""
|
||||
html_template = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OMML to LaTeX Conversion Results</title>
|
||||
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
|
||||
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; margin: 40px; }}
|
||||
.equation {{ margin: 20px 0; padding: 15px; border: 1px solid #ddd; background: #f9f9f9; }}
|
||||
.context {{ font-style: italic; color: #666; margin-bottom: 10px; }}
|
||||
.latex-code {{ font-family: monospace; background: #f0f0f0; padding: 10px; margin: 5px 0; }}
|
||||
.error {{ color: red; background: #ffe6e6; }}
|
||||
.success {{ border-left: 4px solid #4CAF50; }}
|
||||
.failure {{ border-left: 4px solid #f44336; }}
|
||||
.stats {{ background: #e3f2fd; padding: 15px; margin-bottom: 20px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>OMML to LaTeX Conversion Results</h1>
|
||||
|
||||
<div class="stats">
|
||||
<h3>Conversion Statistics</h3>
|
||||
<p>Total equations: {total}</p>
|
||||
<p>Successful: {successful}</p>
|
||||
<p>Failed: {failed}</p>
|
||||
<p>Success rate: {rate:.1f}%</p>
|
||||
</div>
|
||||
|
||||
{equations_html}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
successful = sum(1 for r in results if r.success)
|
||||
failed = len(results) - successful
|
||||
success_rate = (successful / len(results)) * 100 if results else 0
|
||||
|
||||
equations_html = []
|
||||
|
||||
for i, result in enumerate(results):
|
||||
equation_class = "success" if result.success else "failure"
|
||||
|
||||
if result.success:
|
||||
latex_formatted = format_latex([result], format_type, delimiter).strip()
|
||||
|
||||
equation_html = f"""
|
||||
<div class="equation {equation_class}">
|
||||
<h4>Equation {i+1}</h4>
|
||||
{f'<div class="context">Context: {result.context}</div>' if result.context else ''}
|
||||
<div class="latex-code">LaTeX: {result.latex}</div>
|
||||
<div>Rendered: {latex_formatted}</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
equation_html = f"""
|
||||
<div class="equation {equation_class}">
|
||||
<h4>Equation {i+1} - ERROR</h4>
|
||||
{f'<div class="context">Context: {result.context}</div>' if result.context else ''}
|
||||
<div class="error">Error: {result.error}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
equations_html.append(equation_html)
|
||||
|
||||
return html_template.format(
|
||||
total=len(results),
|
||||
successful=successful,
|
||||
failed=failed,
|
||||
rate=success_rate,
|
||||
equations_html="\n".join(equations_html)
|
||||
)
|
||||
96
scripts/omml_mathml2latex/processing/result.py
Normal file
96
scripts/omml_mathml2latex/processing/result.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""
|
||||
Conversion result data structure.
|
||||
|
||||
This module provides the ConversionResult class which represents the result
|
||||
of an OMML to LaTeX conversion operation.
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
class ConversionResult:
|
||||
"""Result of OMML conversion process.
|
||||
|
||||
Attributes:
|
||||
success: Whether conversion was successful
|
||||
omml: Original OMML XML string
|
||||
mathml: Intermediate MathML XML string
|
||||
latex: Final LaTeX output
|
||||
context: Surrounding text context from source document
|
||||
position: Position/index of equation in source document
|
||||
source_file: Source file name or path
|
||||
error: Error message if conversion failed
|
||||
warnings: List of warning messages
|
||||
processing_time: Time taken for conversion in seconds
|
||||
display_mode: True for display equations ($$), False for inline ($)
|
||||
|
||||
Example:
|
||||
>>> result = ConversionResult()
|
||||
>>> result.success = True
|
||||
>>> result.latex = '\\\\frac{1}{2}'
|
||||
>>> result.display_mode = False
|
||||
>>> print(result.to_dict())
|
||||
{'success': True, 'latex': '\\\\frac{1}{2}', ...}
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize conversion result with default values."""
|
||||
self.success: bool = False
|
||||
self.omml: str = ""
|
||||
self.mathml: str = ""
|
||||
self.latex: str = ""
|
||||
self.context: str = ""
|
||||
self.position: int = 0
|
||||
self.source_file: str = ""
|
||||
self.error: Optional[str] = None
|
||||
self.warnings: List[str] = []
|
||||
self.processing_time: float = 0.0
|
||||
self.display_mode: bool = False # True for display equations ($$), False for inline ($)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert result to dictionary for JSON serialization.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of conversion result
|
||||
|
||||
Example:
|
||||
>>> result = ConversionResult()
|
||||
>>> result.success = True
|
||||
>>> result.latex = 'x + y'
|
||||
>>> data = result.to_dict()
|
||||
>>> print(data['latex'])
|
||||
'x + y'
|
||||
"""
|
||||
return {
|
||||
'success': self.success,
|
||||
'omml': self.omml,
|
||||
'mathml': self.mathml,
|
||||
'latex': self.latex,
|
||||
'context': self.context,
|
||||
'position': self.position,
|
||||
'source_file': self.source_file,
|
||||
'error': self.error,
|
||||
'warnings': self.warnings,
|
||||
'processing_time': self.processing_time,
|
||||
'display_mode': self.display_mode
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return detailed string representation.
|
||||
|
||||
Returns:
|
||||
String representation showing key attributes
|
||||
"""
|
||||
status = "Success" if self.success else f"Failed: {self.error}"
|
||||
return f"ConversionResult(position={self.position}, status={status}, latex='{self.latex[:50]}...')"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return human-readable string representation.
|
||||
|
||||
Returns:
|
||||
User-friendly string representation
|
||||
"""
|
||||
if self.success:
|
||||
return f"Equation {self.position}: {self.latex}"
|
||||
else:
|
||||
return f"Equation {self.position}: ERROR - {self.error}"
|
||||
Reference in New Issue
Block a user