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:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

View File

@@ -0,0 +1,283 @@
"""
Physics strategy for physical equations with units and constants.
This strategy handles formatting for physics equations, including physical
constants, SI units, vector notation, and standard physics symbols.
"""
from lxml import etree as ET
from typing import Set, Dict
from ..core.interfaces import Strategy
# Import from existing data modules
from ..data import UNICODE_TO_LATEX, MATH_FUNCTIONS, COMMON_UNITS, UNICODE_PASSTHROUGH
class PhysicsStrategy(Strategy):
"""
Physical equation notation strategy.
This strategy provides formatting for physics content:
- Physical constants → proper formatting (c, h, ħ, kB)
- SI units → \\mathrm{} (upright)
- Mathematical functions → \\function
- Variables remain italic (LaTeX default)
- Vector notation support
"""
# Mathematical functions that get special LaTeX commands
FUNCTIONS: Set[str] = MATH_FUNCTIONS
# Common physical constants (typically italic in physics)
PHYSICAL_CONSTANTS: Set[str] = {
'c', # speed of light
'h', # Planck's constant
'ℏ', # reduced Planck's constant (hbar)
'G', # gravitational constant
'e', # elementary charge
'k', # Boltzmann constant (also kB)
'kB', # Boltzmann constant
'NA', # Avogadro's number
'R', # gas constant
'ε0', # permittivity of free space
'μ0', # permeability of free space
'g', # gravitational acceleration
}
# SI units (upright formatting)
UNITS: Set[str] = COMMON_UNITS
def __init__(self):
"""Initialize the physics strategy."""
pass
def format_identifier(self, text: str, node: ET.Element) -> str:
"""
Format an identifier (<mi> element) for physics.
Args:
text: The identifier text content
node: The <mi> element node
Returns:
Formatted LaTeX string
Logic:
1. Mathematical functions → \\function (e.g., "sin" → "\\sin")
2. SI units → \\mathrm{unit} (e.g., "kg" → "\\mathrm{kg}")
3. Physical constants → unchanged (remain italic)
4. Single letters → unchanged (variables remain italic)
5. Multi-letter non-units → check context
Examples:
"sin" → "\\sin"
"kg" → "\\mathrm{kg}"
"m" → "m" (could be mass or meter, context-dependent)
"c" → "c" (speed of light, italic)
"force" → "force" (variable name, italic)
"""
# Check if it's a mathematical function
if text in self.FUNCTIONS:
return f"\\{text}"
# Check if it's an SI unit (upright)
if text in self.UNITS:
return f"\\mathrm{{{text}}}"
# UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility
if text in UNICODE_PASSTHROUGH:
return text
# Check for Unicode symbols
if text in UNICODE_TO_LATEX:
return UNICODE_TO_LATEX[text]
# Special physics symbols
if text == 'ℏ': # hbar
return '\\hbar'
elif text == 'ħ': # alternative hbar
return '\\hbar'
# Physical constants remain italic (default LaTeX)
if text in self.PHYSICAL_CONSTANTS:
return text
# Handle multi-character strings
if len(text) > 1:
# Check if it contains Unicode (passthrough or convertible)
has_unicode = any(char in UNICODE_TO_LATEX for char in text)
has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text)
if has_unicode or has_passthrough:
converted_parts = []
for char in text:
if char in UNICODE_PASSTHROUGH:
converted_parts.append(char)
elif char in UNICODE_TO_LATEX:
converted_parts.append(UNICODE_TO_LATEX[char])
elif char.isspace():
converted_parts.append(' ')
else:
converted_parts.append(char)
return ' '.join(converted_parts)
# Check if it looks like a unit (multiple known units combined)
# e.g., "kg·m/s²" → should be upright
# For now, let units be upright if explicitly in UNITS set
# Otherwise, remain italic (variable names)
# Default: return as-is (variables remain italic in LaTeX)
return text
def format_operator(self, text: str, node: ET.Element) -> str:
"""
Format an operator (<mo> element) for physics.
Args:
text: The operator text content
node: The <mo> element node
Returns:
Formatted LaTeX string
Logic:
1. Unicode symbols → LaTeX equivalents (via UNICODE_TO_LATEX)
2. Standard operators → unchanged (+, -, *, /, =)
3. Vector operators → proper notation (⃗, ×, ·)
Examples:
"+" → "+"
"×" → "\\times"
"·" → "\\cdot"
"∇" → "\\nabla"
"∂" → "\\partial"
"""
# Handle < and > specifically (LaTeX convention)
if text == "<":
return "\\lt"
if text == ">":
return "\\gt"
# Vector cross product
if text == '×':
return '\\times'
elif text == '·':
return '\\cdot'
# UNICODE_PASSTHROUGH: Keep certain symbols as Unicode for MathJax compatibility
if text in UNICODE_PASSTHROUGH:
return text
# Check for Unicode symbol mapping
if text in UNICODE_TO_LATEX:
return UNICODE_TO_LATEX[text]
# Handle multi-character operators with Unicode
if len(text) > 1:
has_unicode = any(char in UNICODE_TO_LATEX for char in text)
has_passthrough = any(char in UNICODE_PASSTHROUGH for char in text)
if has_unicode or has_passthrough:
converted_parts = []
for char in text:
if char in UNICODE_PASSTHROUGH:
converted_parts.append(char)
elif char in UNICODE_TO_LATEX:
converted_parts.append(UNICODE_TO_LATEX[char])
else:
converted_parts.append(char)
return ' '.join(converted_parts)
# Default: return as-is
return text
def format_number(self, text: str, node: ET.Element) -> str:
"""
Format a number (<mn> element) for physics.
Args:
text: The number text content
node: The <mn> element node
Returns:
Formatted LaTeX string
Logic:
Numbers are returned as-is in physics mode.
Examples:
"9.8" → "9.8" (gravitational acceleration)
"3.0×10⁸" → "3.0×10⁸" (speed of light)
"""
# Numbers are returned as-is
return text
def detect_context(self, equation_text: str) -> bool:
"""
Detect if an equation is physics content.
Args:
equation_text: The full equation text
Returns:
True if likely physics, False otherwise
Detection indicators:
- SI units (kg, m, s, N, J, etc.)
- Physical constants (c, h, G, etc.)
- Vector operators (∇, ∂, ×, ·)
- Common physics notation
Examples:
"F = ma" → True (if 'a' is acceleration)
"E = mc²" → True (energy-mass equivalence)
"∇×B = μ₀J" → True (Maxwell's equation)
"sin(x) + cos(x)" → False
"T = C" → False (just math variables, not Tesla/Coulomb)
"""
# CRITICAL: Check if we're in a mathematical context first
# If equation contains math functions like sin, cos, tan, log, etc.,
# it's likely a pure math equation, not physics
# This prevents false positives like "C = 2πr" being detected as physics
if any(func in equation_text for func in ['sin', 'cos', 'tan', 'cot', 'sec', 'csc',
'arcsin', 'arccos', 'arctan',
'sinh', 'cosh', 'tanh',
'log', 'ln', 'lg', 'exp',
'lim', 'max', 'min', 'sup', 'inf']):
return False
# CRITICAL: Filter out single-letter units that are too ambiguous
# 'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J' can all be variables!
# Only check for multi-character units or units in clear physics context
ambiguous_single_letters = {'C', 'T', 'P', 'A', 'K', 'S', 'H', 'N', 'V', 'F', 'W', 'J', 'L', 'M', 'R', 'G'}
# Check for UNAMBIGUOUS multi-character units
unambiguous_units = {u for u in self.UNITS if len(u) > 1 or u not in ambiguous_single_letters}
unit_count = sum(1 for unit in unambiguous_units if unit in equation_text)
if unit_count >= 1:
return True
# Check for physical constants (these are less ambiguous)
const_count = sum(1 for const in self.PHYSICAL_CONSTANTS if const in equation_text)
if const_count >= 1:
return True
# Check for vector operators
if any(op in equation_text for op in ['∇', '∂', '×', '·', '⃗']):
return True
# Check for common physics patterns (units with superscripts: m/s², kg·m, etc.)
import re
if re.search(r'(kg|m|s|N|J|W|Pa|V|A|K|mol|cd)[²³⁴]', equation_text):
return True
if re.search(r'(m|kg|s|N|J)/s', equation_text):
return True
return False
@property
def name(self) -> str:
"""Get the strategy name."""
return "physics"
def __repr__(self) -> str:
"""String representation for debugging."""
return f"PhysicsStrategy(units={len(self.UNITS)}, constants={len(self.PHYSICAL_CONSTANTS)})"