Files
DoLuEdu-WordFileParse/scripts/omml_mathml2latex/core/interfaces.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

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