服务: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 默认密钥已清空
169 lines
5.5 KiB
Python
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()"
|