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:
168
scripts/omml_mathml2latex/parsers/mathml_parser.py
Normal file
168
scripts/omml_mathml2latex/parsers/mathml_parser.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
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()"
|
||||
Reference in New Issue
Block a user