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:
298
scripts/omml_mathml2latex/core/interfaces.py
Normal file
298
scripts/omml_mathml2latex/core/interfaces.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user