Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/visitors/mn_visitor.py
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

137 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Visitor for <mn> (number) MathML elements.
The <mn> element represents numeric literals in MathML, including:
- Integers (1, 2, 3, ...)
- Decimals (3.14, 2.718, ...)
- Scientific notation (1.5×10⁻³)
- Fractions in decimal form (0.5)
"""
from lxml import etree as ET
from ..core.interfaces import Visitor, Strategy
from ..utils import text_content as _text_content, convert_spaces_to_latex, escape_latex_special_chars
from ..data import CHEMICAL_ELEMENTS, COMMON_UNITS
from ..converters.post_processor import PostProcessor
class MnVisitor(Visitor):
"""
Handles <mn> (number) MathML elements.
This visitor converts numbers to LaTeX format. Most numbers
are returned as-is, but the strategy can customize formatting
for specific subjects (e.g., chemistry, physics).
Examples:
Standard numbers:
<mn>123</mn> → 123
<mn>3.14</mn> → 3.14
<mn>0.5</mn> → 0.5
Scientific notation (handled at operator level):
<mn>1.5</mn><mo>×</mo><mn>10</mn><msup><mn>-3</mn></msup>
→ 1.5 \\times 10^{-3}
"""
def __init__(self, strategy: Strategy):
"""
Initialize the Mn visitor.
Args:
strategy: The formatting strategy to use
"""
super().__init__(strategy)
self._converter_context = None
def set_converter_context(self, converter):
"""Set converter context for color suppression checking."""
self._converter_context = converter
def visit(self, node: ET.Element) -> str:
"""
Convert <mn> element to LaTeX.
Args:
node: The <mn> element to convert
Returns:
LaTeX string representation
Raises:
ValueError: If node is not an <mn> element
"""
if not self.can_handle(node):
raise ValueError(f"MnVisitor cannot handle element '{node.tag}'")
# Get text content
content = _text_content(node)
# Handle empty content
if not content:
return ""
# Escape LaTeX special characters before any other processing
content = escape_latex_special_chars(content)
# Handle spaces (NBSP → ~, space → \ )
if '\xa0' in content or ' ' in content:
converted = convert_spaces_to_latex(content)
# If content was ONLY spaces, return directly
if not content.strip():
return self._wrap_with_color(converted, node)
# Update content with converted spaces
content = converted
# Delegate to strategy for subject-specific number formatting
result = self.strategy.format_number(content, node)
return self._wrap_with_color(result, node)
def _wrap_with_color(self, latex: str, node: ET.Element) -> str:
"""
Wrap LaTeX with color command if node has color attribute.
Args:
latex: The LaTeX string to wrap
node: The element node
Returns:
LaTeX string, optionally wrapped with color command
"""
# Use the centralized color wrapping utility with suppression support
from ..utils.color_helpers import wrap_element_with_color
# Get converter context from multiple sources (for backward compatibility):
# 1. Direct visitor context (set by legacy_adapter)
# 2. Strategy context (shared across all visitors, including child visitors)
context = getattr(self, '_converter_context', None)
if context is None and hasattr(self.strategy, '_converter_context'):
context = self.strategy._converter_context
return wrap_element_with_color(latex, node, context)
def can_handle(self, node: ET.Element) -> bool:
"""
Check if this visitor can handle the given node.
Args:
node: The MathML element to check
Returns:
True if node is an <mn> element, False otherwise
"""
from ..utils import local_name as _local
return _local(node.tag) == 'mn'
@property
def element_name(self) -> str:
"""Get the element name this visitor handles."""
return 'mn'
def __repr__(self) -> str:
"""String representation for debugging."""
return f"MnVisitor(strategy={self.strategy.name})"