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:
185
scripts/omml_mathml2latex/utils/text_processing.py
Normal file
185
scripts/omml_mathml2latex/utils/text_processing.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Text processing utilities for file I/O and LaTeX formatting.
|
||||
|
||||
This module provides functions for reading/writing text files and converting
|
||||
various space characters to appropriate LaTeX spacing commands.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def read_text(path: str, encoding: str = "utf-8") -> str:
|
||||
"""Read text from a file.
|
||||
|
||||
Args:
|
||||
path: Path to the file to read
|
||||
encoding: Text encoding (default: utf-8)
|
||||
|
||||
Returns:
|
||||
File contents as string
|
||||
|
||||
Example:
|
||||
>>> content = read_text('input.tex')
|
||||
"""
|
||||
with open(path, "r", encoding=encoding) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_text(path: str, content: str, encoding: str = "utf-8") -> None:
|
||||
"""Write text to a file, creating parent directories if needed.
|
||||
|
||||
Args:
|
||||
path: Path to the file to write
|
||||
content: Text content to write
|
||||
encoding: Text encoding (default: utf-8)
|
||||
|
||||
Example:
|
||||
>>> write_text('output.tex', '\\\\sin x')
|
||||
"""
|
||||
dirpath = os.path.dirname(path)
|
||||
if dirpath:
|
||||
os.makedirs(dirpath, exist_ok=True)
|
||||
with open(path, "w", encoding=encoding) as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def convert_spaces_to_latex(text: str) -> str:
|
||||
"""Convert space characters in text to appropriate LaTeX spacing commands.
|
||||
|
||||
Handles:
|
||||
- U+00A0 (NBSP, \\xa0) → ~ (tilde, non-breaking space in LaTeX)
|
||||
- U+0020 (regular space) → \\\\ (backslash-space) for each space
|
||||
|
||||
Args:
|
||||
text: Input text possibly containing various space characters
|
||||
|
||||
Returns:
|
||||
Text with spaces converted to LaTeX commands
|
||||
|
||||
Example:
|
||||
>>> convert_spaces_to_latex('sin x')
|
||||
'sin\\\\ x'
|
||||
>>> convert_spaces_to_latex('sin\\xa0x') # NBSP
|
||||
'sin~x'
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# Quick check: if no spaces present, return as-is
|
||||
if '\xa0' not in text and ' ' not in text:
|
||||
return text
|
||||
|
||||
result = []
|
||||
i = 0
|
||||
|
||||
while i < len(text):
|
||||
char = text[i]
|
||||
|
||||
if char == '\xa0': # U+00A0: Non-breaking space
|
||||
# Always use tilde for NBSP in LaTeX
|
||||
result.append('~')
|
||||
i += 1
|
||||
|
||||
elif char == ' ': # U+0020: Regular space
|
||||
# Count consecutive spaces
|
||||
space_count = 1
|
||||
while i + space_count < len(text) and text[i + space_count] == ' ':
|
||||
space_count += 1
|
||||
|
||||
# Convert each space to backslash-space (backslash followed by space)
|
||||
# For N spaces, produce "\\ " repeated N times: "\\ \\ \\ "
|
||||
# Each '\\ ' is already a complete unit, so use empty join (not space join)
|
||||
result.append('\\ ' * space_count)
|
||||
|
||||
i += space_count # Skip all processed spaces
|
||||
|
||||
else:
|
||||
# Regular character
|
||||
result.append(char)
|
||||
i += 1
|
||||
|
||||
# Join results but preserve trailing spaces that are part of LaTeX commands
|
||||
result_str = ''.join(result)
|
||||
# Don't strip if the string ends with LaTeX space command '\\ '
|
||||
# The trailing space in '\\ ' is semantically significant
|
||||
if result_str.endswith('\\ '):
|
||||
return result_str
|
||||
else:
|
||||
return result_str.rstrip() # Remove trailing space from last element
|
||||
|
||||
|
||||
def escape_latex_special_chars(text: str, preserve_nbsp: bool = True) -> str:
|
||||
"""Escape special characters that have meaning in LaTeX.
|
||||
|
||||
LaTeX special characters that need escaping:
|
||||
- $ (math mode delimiter)
|
||||
- ^ (superscript)
|
||||
- _ (subscript)
|
||||
- # (parameter token)
|
||||
- @ (letter category code)
|
||||
- ! (negative thin space)
|
||||
- % (comment)
|
||||
- & (alignment tab)
|
||||
- * (discretionary multiplication)
|
||||
- ~ (tilde) → \\sim in math mode
|
||||
|
||||
Args:
|
||||
text: Input text possibly containing special characters
|
||||
preserve_nbsp: If True, preserve NBSP (\\xa0) for later conversion to ~
|
||||
If False, convert NBSP to ~ before escaping
|
||||
|
||||
Returns:
|
||||
Text with special characters escaped
|
||||
|
||||
Example:
|
||||
>>> escape_latex_special_chars('$100 ^ 2')
|
||||
'\\$100 \\^ 2'
|
||||
>>> escape_latex_special_chars('H_2O')
|
||||
'H\\_2O'
|
||||
>>> escape_latex_special_chars('a ~ b')
|
||||
'a \\sim b'
|
||||
"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# CRITICAL: Preserve marker strings used for brace resolution
|
||||
# These markers distinguish source braces from generated LaTeX braces
|
||||
if text in ('__LBRACE__', '__RBRACE__', '__DELIM__'):
|
||||
return text
|
||||
|
||||
# Use a temporary placeholder for NBSP to preserve it during escaping
|
||||
# This allows NBSP to be converted to ~ later for LaTeX spacing
|
||||
NBSP_PLACEHOLDER = '\x00NBSP\x00' # Using null bytes as unlikely to appear in text
|
||||
|
||||
if preserve_nbsp and '\xa0' in text:
|
||||
# Temporarily replace NBSP with placeholder
|
||||
text = text.replace('\xa0', NBSP_PLACEHOLDER)
|
||||
|
||||
# Mapping of special characters to their escaped versions
|
||||
# NOTE: In math mode, * is a valid operator and doesn't need escaping
|
||||
# NOTE: { and } are NOT escaped here - they're handled via __LBRACE__/__RBRACE__ markers
|
||||
# to distinguish source braces from generated LaTeX braces (e.g., \mathrm{...})
|
||||
ESCAPE_MAP = {
|
||||
'$': r'\$',
|
||||
'%': r'\%',
|
||||
'#': r'\#',
|
||||
'&': r'\&',
|
||||
'_': r'\_',
|
||||
'^': r'\^',
|
||||
# NOTE: '~' is NOT escaped here - it's handled via UNICODE_TO_LATEX mapping to \sim
|
||||
'@': r'\@',
|
||||
'!': r'\!',
|
||||
# NOTE: '*' is NOT escaped - it's a valid multiplication operator in math mode
|
||||
'{': r'\{',
|
||||
'}': r'\}',
|
||||
}
|
||||
|
||||
result = text
|
||||
for char, escaped in ESCAPE_MAP.items():
|
||||
result = result.replace(char, escaped)
|
||||
|
||||
# Restore NBSP placeholder back to NBSP for later processing
|
||||
if preserve_nbsp and NBSP_PLACEHOLDER in result:
|
||||
result = result.replace(NBSP_PLACEHOLDER, '\xa0')
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user