服务: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 默认密钥已清空
299 lines
7.7 KiB
Python
299 lines
7.7 KiB
Python
"""
|
|
Base interfaces for the refactored architecture.
|
|
|
|
This module defines the abstract base classes (interfaces) that establish
|
|
the contracts for all components in the refactored converter architecture.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from typing import Any, Optional, List
|
|
from lxml import etree as ET
|
|
|
|
|
|
class ConversionContext:
|
|
"""
|
|
Shared context object that flows through the conversion pipeline.
|
|
|
|
This context holds all state and intermediate results during the conversion
|
|
process, allowing stages to communicate and share data.
|
|
"""
|
|
pass # Will be implemented as dataclass in context.py
|
|
|
|
|
|
class Stage(ABC):
|
|
"""
|
|
Abstract base class for pipeline stages.
|
|
|
|
Each stage represents a distinct step in the conversion pipeline
|
|
(e.g., input parsing, validation, transformation, conversion, output).
|
|
"""
|
|
|
|
@abstractmethod
|
|
def process(self, context: ConversionContext) -> ConversionContext:
|
|
"""
|
|
Process the conversion context and return updated context.
|
|
|
|
Args:
|
|
context: The current conversion context with all state
|
|
|
|
Returns:
|
|
Updated conversion context with stage results
|
|
|
|
Raises:
|
|
ValueError: If stage preconditions are not met
|
|
RuntimeError: If stage processing fails
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def validate_input(self, context: ConversionContext) -> bool:
|
|
"""
|
|
Validate that stage preconditions are met.
|
|
|
|
Args:
|
|
context: The current conversion context
|
|
|
|
Returns:
|
|
True if preconditions are met, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Get the human-readable name of this stage."""
|
|
pass
|
|
|
|
|
|
class Visitor(ABC):
|
|
"""
|
|
Abstract base class for MathML element visitors.
|
|
|
|
Each visitor handles conversion of a specific MathML element type
|
|
(e.g., <mi>, <mo>, <mn>, <mfrac>) to LaTeX format.
|
|
"""
|
|
|
|
def __init__(self, strategy: 'Strategy'):
|
|
"""
|
|
Initialize visitor with a conversion strategy.
|
|
|
|
Args:
|
|
strategy: The subject-specific formatting strategy to use
|
|
"""
|
|
self.strategy = strategy
|
|
self.context: Optional[ConversionContext] = None
|
|
|
|
@abstractmethod
|
|
def visit(self, node: ET.Element) -> str:
|
|
"""
|
|
Convert MathML element to LaTeX string.
|
|
|
|
Args:
|
|
node: The MathML element to convert
|
|
|
|
Returns:
|
|
LaTeX string representation of the element
|
|
|
|
Raises:
|
|
ValueError: If node is not the expected element type
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
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 this visitor can convert the node, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def element_name(self) -> str:
|
|
"""Get the MathML element name this visitor handles (e.g., 'mi', 'mo')."""
|
|
pass
|
|
|
|
|
|
class Strategy(ABC):
|
|
"""
|
|
Abstract base class for subject-specific formatting strategies.
|
|
|
|
Strategies encapsulate the subject-specific logic for formatting
|
|
mathematical notation (e.g., math, chemistry, physics, statistics).
|
|
"""
|
|
|
|
@abstractmethod
|
|
def format_identifier(self, text: str, node: ET.Element) -> str:
|
|
"""
|
|
Format an identifier (<mi> element) according to subject conventions.
|
|
|
|
Args:
|
|
text: The identifier text content
|
|
node: The <mi> element node (for accessing attributes)
|
|
|
|
Returns:
|
|
Formatted LaTeX string for the identifier
|
|
|
|
Examples:
|
|
Math: "sin" → "\\sin" (function recognition)
|
|
Chemistry: "Fe" → "\\mathrm{Fe}" (element formatting)
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def format_operator(self, text: str, node: ET.Element) -> str:
|
|
"""
|
|
Format an operator (<mo> element) according to subject conventions.
|
|
|
|
Args:
|
|
text: The operator text content
|
|
node: The <mo> element node
|
|
|
|
Returns:
|
|
Formatted LaTeX string for the operator
|
|
|
|
Examples:
|
|
Math: "+" → "+"
|
|
Chemistry: "→" → "\\rightarrow"
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def format_number(self, text: str, node: ET.Element) -> str:
|
|
"""
|
|
Format a number (<mn> element) according to subject conventions.
|
|
|
|
Args:
|
|
text: The number text content
|
|
node: The <mn> element node
|
|
|
|
Returns:
|
|
Formatted LaTeX string for the number
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def detect_context(self, equation_text: str) -> bool:
|
|
"""
|
|
Detect if an equation belongs to this subject area.
|
|
|
|
Args:
|
|
equation_text: The full equation text to analyze
|
|
|
|
Returns:
|
|
True if equation likely belongs to this subject, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Get the strategy name (e.g., 'math', 'chemistry', 'physics')."""
|
|
pass
|
|
|
|
|
|
class VisitorRegistry(ABC):
|
|
"""
|
|
Abstract base class for visitor registry/factory.
|
|
|
|
The registry maintains a mapping of MathML element names to their
|
|
corresponding visitor classes and provides visitor instances on demand.
|
|
"""
|
|
|
|
@abstractmethod
|
|
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')
|
|
visitor_class: The visitor class to handle this element
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def get_visitor(self, element_name: str, strategy: Strategy) -> Visitor:
|
|
"""
|
|
Get a visitor instance for the given element type.
|
|
|
|
Args:
|
|
element_name: MathML element name
|
|
strategy: The conversion strategy to use
|
|
|
|
Returns:
|
|
Visitor instance configured with the strategy
|
|
|
|
Raises:
|
|
ValueError: If no visitor registered for element_name
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
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, False otherwise
|
|
"""
|
|
pass
|
|
|
|
|
|
class Parser(ABC):
|
|
"""
|
|
Abstract base class for input parsers.
|
|
|
|
Parsers handle different input formats (DOCX, OMML, MathML) and convert
|
|
them to a unified format (MathML strings) for the conversion pipeline.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def parse(self, source: str) -> List[str]:
|
|
"""
|
|
Parse input and return list of MathML strings.
|
|
|
|
Args:
|
|
source: Input source (file path, XML string, etc.)
|
|
|
|
Returns:
|
|
List of MathML equation strings
|
|
|
|
Raises:
|
|
ValueError: If input format is invalid
|
|
FileNotFoundError: If source file doesn't exist (for file-based parsers)
|
|
RuntimeError: If parsing fails
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
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 this parser can handle the source, False otherwise
|
|
"""
|
|
pass
|
|
|
|
@property
|
|
@abstractmethod
|
|
def parser_type(self) -> str:
|
|
"""
|
|
Get the parser type identifier.
|
|
|
|
Returns:
|
|
Parser type string (e.g., "docx", "omml", "mathml")
|
|
"""
|
|
pass
|