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

137 lines
4.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Pattern and symbol replacement helper functions.
This module provides utilities for applying symbol mappings and regex patterns
to text, used throughout the conversion pipeline.
"""
import re
import logging
logger = logging.getLogger(__name__)
def apply_symbol_replacements(text: str, mapping: dict) -> str:
"""Apply simple symbol replacements from a mapping dictionary.
Args:
text: Input text
mapping: Dictionary of {symbol: replacement}
Returns:
Text with all symbols replaced
Example:
>>> mapping = {'α': '\\\\alpha', 'β': '\\\\beta'}
>>> apply_symbol_replacements('α + β', mapping)
'\\\\alpha + \\\\beta'
"""
for symbol, replacement in mapping.items():
text = text.replace(symbol, replacement)
return text
def apply_pattern_replacements(text: str, patterns: list, verbose: bool = False) -> str:
"""Apply regex pattern replacements from a list of (pattern, replacement, description) tuples.
Args:
text: Input text
patterns: List of (pattern, replacement, description) tuples
verbose: If True, log each replacement
Returns:
Text with all patterns applied
Example:
>>> patterns = [(r' {3,}', ' ', 'Reduce multiple spaces')]
>>> apply_pattern_replacements('a b', patterns)
'a b'
"""
for pattern, replacement, description in patterns:
if verbose:
count = len(re.findall(pattern, text))
if count > 0:
logger.debug(f"Applying: {description} ({count} matches)")
text = re.sub(pattern, replacement, text)
return text
def apply_unicode_symbol_mapping(
text: str,
unicode_to_latex: dict,
additional_replacements: dict = None,
subscript_context: bool = False
) -> str:
"""Apply Unicode to LaTeX symbol mapping.
Args:
text: Input text containing Unicode symbols
unicode_to_latex: Main Unicode to LaTeX mapping dictionary
additional_replacements: Optional additional symbol replacements
subscript_context: If True, use subscript-specific mappings (with spaces)
Returns:
Text with Unicode symbols converted to LaTeX
Example:
>>> mapping = {'α': '\\\\alpha', '→': '\\\\to'}
>>> apply_unicode_symbol_mapping('α → β', mapping, {})
'\\\\alpha \\\\to β'
"""
# First apply standard UNICODE_TO_LATEX mapping
for symbol, latex in unicode_to_latex.items():
text = text.replace(symbol, latex)
# In subscript context, apply additional replacements with proper spacing
if subscript_context and additional_replacements:
for symbol, latex in additional_replacements.items():
text = text.replace(symbol, latex)
return text
def is_default_color(color: str) -> bool:
"""Check if a color value represents the default (black) color.
Args:
color: Color value in various formats (hex, name, rgb)
Returns:
True if the color is black/default, False otherwise
Examples:
>>> is_default_color('#000000')
True
>>> is_default_color('#000')
True
>>> is_default_color('black')
True
>>> is_default_color('rgb(0, 0, 0)')
True
>>> is_default_color('#FF0000')
False
>>> is_default_color('red')
False
"""
if not color:
return False
# Normalize the color string
color_lower = color.lower().strip()
# Check for common black color representations
black_colors = {
'#000000', # Full hex
'#000', # Short hex
'black', # Named color
'rgb(0,0,0)', # RGB without spaces
'rgb(0, 0, 0)', # RGB with spaces
'rgba(0,0,0,1)', # RGBA without spaces
'rgba(0, 0, 0, 1)', # RGBA with spaces
}
# Remove all whitespace for comparison
color_normalized = ''.join(color_lower.split())
return color_normalized in {c.replace(' ', '') for c in black_colors}