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

186 lines
5.7 KiB
Python

"""
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