Files
Quentin 3271fee5cc 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 默认密钥已清空
2026-09-29 09:43:13 +08:00

169 lines
5.5 KiB
Python

"""
MathML parser for direct MathML input.
This parser handles MathML XML input, performing validation and extraction
of individual math equations.
"""
import sys
from typing import List
from pathlib import Path
from lxml import etree as ET
from ..core.interfaces import Parser
# Import utilities from omml_to_tex package for HTML entity decoding and equation extraction
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from ..utils import decode_html_entities, extract_math_equations
class MathMLParser(Parser):
"""
Parser for MathML XML input.
This parser handles direct MathML XML input, performing basic validation
and extracting individual <math> elements.
Examples:
Single equation:
>>> parser = MathMLParser()
>>> mathml = "<math><mi>x</mi></math>"
>>> results = parser.parse(mathml)
>>> len(results)
1
Multiple equations:
>>> parser = MathMLParser()
>>> mathml = "<root><math><mi>x</mi></math><math><mi>y</mi></math></root>"
>>> results = parser.parse(mathml)
>>> len(results)
2
"""
def __init__(self):
"""Initialize the MathML parser."""
pass
def parse(self, source: str) -> List[str]:
"""
Parse MathML input and return list of MathML equation strings.
Args:
source: MathML XML string or file path
Returns:
List of MathML equation strings (one per <math> element)
Raises:
ValueError: If input is not valid MathML XML
RuntimeError: If parsing fails
Examples:
>>> parser = MathMLParser()
>>> mathml = "<math><mi>x</mi><mo>+</mo><mi>y</mi></math>"
>>> results = parser.parse(mathml)
>>> len(results)
1
>>> '<mi>x</mi>' in results[0]
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 MathML file: {e}")
# STRATEGY 1: Try regex-based extraction first (handles multi-equation files + HTML entities)
# This is the same approach as the original omml_to_tex.py
try:
equations = extract_math_equations(source)
if equations:
return equations
except Exception:
# If regex extraction fails, fall through to XML parsing
pass
# STRATEGY 2: Try XML parsing as fallback (single well-formed document)
try:
root = ET.fromstring(source.encode('utf-8'))
except ET.XMLSyntaxError as e:
raise ValueError(f"Invalid XML syntax: {e}")
# Find all <math> elements
math_elements = []
# Check if root itself is a <math> element
if self._is_math_element(root):
math_elements.append(root)
else:
# Search for <math> elements (with or without namespace)
math_elements.extend(root.findall('.//{http://www.w3.org/1998/Math/MathML}math'))
math_elements.extend(root.findall('.//math'))
if not math_elements:
# No <math> elements found - treat entire document as one equation if it looks like MathML
if self._looks_like_mathml(root):
math_elements.append(root)
else:
raise ValueError("No <math> elements found in MathML input")
# Convert each <math> element to string
results = []
for math_elem in math_elements:
mathml_str = ET.tostring(math_elem, encoding='unicode')
results.append(mathml_str)
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 MathML, False otherwise
Examples:
>>> parser = MathMLParser()
>>> parser.can_handle("<math><mi>x</mi></math>")
True
>>> parser.can_handle("<m:oMath>...</m:oMath>")
False
"""
# Check for MathML markers
content_start = source.strip()[:200]
return '<math' in content_start or '<mml:math' in content_start
@property
def parser_type(self) -> str:
"""Get the parser type identifier."""
return "mathml"
def _is_math_element(self, element: ET.Element) -> bool:
"""Check if element is a <math> element."""
local_name = element.tag.split('}')[-1] if '}' in element.tag else element.tag
return local_name == 'math'
def _looks_like_mathml(self, element: ET.Element) -> bool:
"""Check if element tree looks like MathML content."""
# Check for common MathML element tags
mathml_tags = {'mi', 'mo', 'mn', 'mrow', 'mfrac', 'msup', 'msub', 'mtext'}
# Get all element tags in tree
all_tags = set()
for elem in element.iter():
local_name = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag
all_tags.add(local_name)
# If at least 2 MathML tags present, consider it MathML
return len(all_tags & mathml_tags) >= 2
def __repr__(self) -> str:
"""String representation for debugging."""
return "MathMLParser()"