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

164 lines
5.6 KiB
Python

"""
Conversion context data structure.
This module defines the ConversionContext dataclass that holds all state
and intermediate results throughout the conversion pipeline.
"""
from dataclasses import dataclass, field
from typing import Optional, List, Dict, Any
@dataclass
class ConversionContext:
"""
Shared state for the conversion pipeline.
This context object flows through all pipeline stages, accumulating
intermediate results and maintaining conversion state.
Attributes:
input_type: Type of input ('docx', 'omml', 'mathml', 'stdin')
subject_mode: Subject area hint ('math', 'chemistry', 'physics', 'statistics', 'auto')
wrap_identifiers: Whether to wrap identifiers in \\mathrm{} (default: True)
prefer_xlongequal: Use \\xlongequal for overset equals (default: True)
# Intermediate results
omml: OMML XML string (if applicable)
mathml: MathML XML string
latex: Generated LaTeX string
# Validation state
validation_errors: List of validation error messages
validation_warnings: List of validation warning messages
is_valid: Whether MathML passed validation
# Color optimization state
uniform_equation_color: Hex color if entire equation uses same color, else empty
suppress_leaf_colors: Flag to suppress individual element colors when uniform color detected
# Metadata
equation_count: Number of equations processed
current_equation_index: Index of current equation being processed
context_hints: Additional context information
# Performance metrics
stage_timings: Dict mapping stage name to execution time (seconds)
total_time: Total processing time (seconds)
"""
# Configuration
input_type: str
subject_mode: str = "math"
wrap_identifiers: bool = True
prefer_xlongequal: bool = True
# Intermediate results
omml: Optional[str] = None
mathml: Optional[str] = None
latex: Optional[str] = None
# Validation results
validation_errors: List[str] = field(default_factory=list)
validation_warnings: List[str] = field(default_factory=list)
is_valid: bool = True
# Color optimization state
uniform_equation_color: str = ""
suppress_leaf_colors: bool = False
# Script context tracking (for fraction sizing)
script_context_stack: List[str] = field(default_factory=list)
display_style_context: bool = False
# Set theory mode (for unclosed set notation)
set_theory_mode: bool = False
# Chemistry equation detection (cached)
is_chemistry_equation: Optional[bool] = None
# Metadata
equation_count: int = 0
current_equation_index: int = 0
context_hints: Dict[str, Any] = field(default_factory=dict)
# Performance metrics
stage_timings: Dict[str, float] = field(default_factory=dict)
total_time: float = 0.0
def add_error(self, error: str) -> None:
"""Add a validation error."""
self.validation_errors.append(error)
self.is_valid = False
def add_warning(self, warning: str) -> None:
"""Add a validation warning."""
self.validation_warnings.append(warning)
def record_stage_time(self, stage_name: str, time_seconds: float) -> None:
"""Record execution time for a stage."""
self.stage_timings[stage_name] = time_seconds
def has_errors(self) -> bool:
"""Check if any errors were recorded."""
return len(self.validation_errors) > 0
def has_warnings(self) -> bool:
"""Check if any warnings were recorded."""
return len(self.validation_warnings) > 0
def clear_validation_state(self) -> None:
"""Clear validation errors and warnings."""
self.validation_errors.clear()
self.validation_warnings.clear()
self.is_valid = True
def push_script_context(self, element_type: str) -> None:
"""
Push a script context (msup/msub/msubsup) onto the stack.
Args:
element_type: The element type entering script context
"""
self.script_context_stack.append(element_type)
def pop_script_context(self) -> Optional[str]:
"""
Pop a script context from the stack.
Returns:
The popped element type, or None if stack was empty
"""
return self.script_context_stack.pop() if self.script_context_stack else None
def in_script_context(self) -> bool:
"""Check if currently inside a script context (superscript/subscript)."""
return len(self.script_context_stack) > 0
def __str__(self) -> str:
"""String representation for debugging."""
status = "valid" if self.is_valid else "invalid"
return (
f"ConversionContext("
f"input_type={self.input_type}, "
f"subject_mode={self.subject_mode}, "
f"status={status}, "
f"equations={self.equation_count})"
)
def __repr__(self) -> str:
"""Detailed representation for debugging."""
return (
f"ConversionContext(\n"
f" input_type={self.input_type!r},\n"
f" subject_mode={self.subject_mode!r},\n"
f" wrap_identifiers={self.wrap_identifiers},\n"
f" prefer_xlongequal={self.prefer_xlongequal},\n"
f" is_valid={self.is_valid},\n"
f" equation_count={self.equation_count},\n"
f" errors={len(self.validation_errors)},\n"
f" warnings={len(self.validation_warnings)},\n"
f" total_time={self.total_time:.4f}s\n"
f")"
)