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

149 lines
5.1 KiB
Python

"""
Visitor registry/factory for MathML element visitors.
This module provides a centralized registry for mapping MathML element
names to their corresponding visitor classes.
"""
from typing import Dict, Type, Optional, List
from ..core.interfaces import Visitor, Strategy, VisitorRegistry as IVisitorRegistry
from ..core.context import ConversionContext
class VisitorRegistry(IVisitorRegistry):
"""
Registry for MathML element visitors.
This class maintains a mapping of element names (e.g., 'mi', 'mo', 'mn')
to their corresponding visitor classes. It provides visitor instances
on demand with the appropriate strategy and context injected.
Example:
>>> registry = VisitorRegistry()
>>> registry.register('mi', MiVisitor)
>>> registry.register('mo', MoVisitor)
>>> strategy = MathStrategy()
>>> context = ConversionContext(input_type='mathml')
>>> mi_visitor = registry.get_visitor('mi', strategy, context)
"""
def __init__(self, context: Optional[ConversionContext] = None):
"""
Initialize the registry with empty mappings.
Args:
context: Optional shared conversion context for all visitors
"""
self._visitors: Dict[str, Type[Visitor]] = {}
self._default_visitor: Type[Visitor] | None = None
self.context = context
def register(self, element_name: str, visitor_class: type) -> None:
"""
Register a visitor class for a specific element type.
Args:
element_name: MathML element name (e.g., 'mi', 'mo', 'mn')
visitor_class: The visitor class to handle this element
Raises:
TypeError: If visitor_class is not a subclass of Visitor
"""
if not issubclass(visitor_class, Visitor):
raise TypeError(
f"visitor_class must be a subclass of Visitor, "
f"got {type(visitor_class)}"
)
self._visitors[element_name] = visitor_class
def get_visitor(self, element_name: str, strategy: Strategy, context: Optional[ConversionContext] = None) -> Visitor:
"""
Get a visitor instance for the given element type.
Args:
element_name: MathML element name (e.g., 'mi', 'mo')
strategy: The conversion strategy to inject into the visitor
context: Optional conversion context (uses registry's context if not provided)
Returns:
Visitor instance configured with the strategy and context
Raises:
ValueError: If no visitor registered for element_name and no default set
"""
visitor_class = self._visitors.get(element_name)
if visitor_class is None:
if self._default_visitor is not None:
visitor_class = self._default_visitor
else:
raise ValueError(
f"No visitor registered for element '{element_name}' "
f"and no default visitor configured"
)
# Use provided context or fall back to registry's context
ctx = context if context is not None else self.context
# Try to instantiate with context, fall back to strategy-only if needed
try:
return visitor_class(strategy, ctx)
except TypeError:
# Visitor doesn't accept context parameter yet
return visitor_class(strategy)
def has_visitor(self, element_name: str) -> bool:
"""
Check if a visitor is registered for the element type.
Args:
element_name: MathML element name
Returns:
True if visitor is registered or default exists, False otherwise
"""
return element_name in self._visitors or self._default_visitor is not None
def set_default_visitor(self, visitor_class: Type[Visitor]) -> None:
"""
Set a default visitor for unregistered element types.
Args:
visitor_class: The visitor class to use as default
Raises:
TypeError: If visitor_class is not a subclass of Visitor
"""
if not issubclass(visitor_class, Visitor):
raise TypeError(
f"visitor_class must be a subclass of Visitor, "
f"got {type(visitor_class)}"
)
self._default_visitor = visitor_class
def get_registered_elements(self) -> List[str]:
"""
Get list of all registered element names.
Returns:
List of element names with registered visitors
"""
return list(self._visitors.keys())
def clear(self) -> None:
"""Clear all registered visitors."""
self._visitors.clear()
self._default_visitor = None
def __repr__(self) -> str:
"""String representation for debugging."""
elements = ', '.join(self._visitors.keys()) if self._visitors else 'none'
default = f", default={self._default_visitor.__name__}" if self._default_visitor else ""
return f"VisitorRegistry(registered=[{elements}]{default})"
def __len__(self) -> int:
"""Get number of registered visitors."""
return len(self._visitors)