服务: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 默认密钥已清空
284 lines
10 KiB
Python
284 lines
10 KiB
Python
"""
|
||
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)})"
|