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,12 @@
"""
Builder classes for fluent construction of LaTeX and MathML expressions.
This package contains builder classes that provide fluent interfaces for
constructing complex expressions.
"""
from .latex_builder import LaTeXBuilder
__all__ = [
'LaTeXBuilder',
]

View File

@@ -0,0 +1,320 @@
"""
LaTeX Builder for fluent construction of LaTeX expressions.
This builder provides a fluent interface for constructing complex LaTeX
expressions with proper brace balancing and formatting.
"""
from typing import List, Optional
class LaTeXBuilder:
"""
Fluent builder for constructing LaTeX expressions.
This builder provides method chaining for building complex LaTeX
expressions with automatic brace balancing and spacing optimization.
Examples:
Simple fraction:
>>> builder = LaTeXBuilder()
>>> result = builder.add_fraction("a", "b").build()
>>> result
'\\frac{a}{b}'
Superscript:
>>> builder = LaTeXBuilder()
>>> result = builder.add_superscript("x", "2").build()
>>> result
'x^{2}'
Complex expression:
>>> builder = LaTeXBuilder()
>>> result = (builder
... .add_command("sin", "x")
... .add_text(" + ")
... .add_fraction("1", "2")
... .build())
>>> result
'\\sin{x} + \\frac{1}{2}'
Nested fractions:
>>> builder = LaTeXBuilder()
>>> inner = LaTeXBuilder().add_fraction("a", "b").build()
>>> result = builder.add_fraction(inner, "c").build()
>>> result
'\\frac{\\frac{a}{b}}{c}'
"""
def __init__(self):
"""Initialize the LaTeX builder."""
self._parts: List[str] = []
def add_text(self, text: str) -> 'LaTeXBuilder':
"""
Add raw text to the LaTeX expression.
Args:
text: The text to add (no processing)
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_text("x + y").build()
'x + y'
"""
if text:
self._parts.append(text)
return self
def add_command(self, cmd: str, arg: str) -> 'LaTeXBuilder':
"""
Add a LaTeX command with one argument: \\cmd{arg}
Args:
cmd: The command name (without backslash)
arg: The argument content
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_command("sin", "x").build()
'\\sin{x}'
>>> builder = LaTeXBuilder()
>>> builder.add_command("text", "hello").build()
'\\text{hello}'
"""
self._parts.append(f"\\{cmd}{{{arg}}}")
return self
def add_fraction(self, numerator: str, denominator: str) -> 'LaTeXBuilder':
"""
Add a fraction: \\frac{numerator}{denominator}
Args:
numerator: The numerator expression
denominator: The denominator expression
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_fraction("a", "b").build()
'\\frac{a}{b}'
>>> builder = LaTeXBuilder()
>>> builder.add_fraction("x + 1", "y - 2").build()
'\\frac{x + 1}{y - 2}'
"""
self._parts.append(f"\\frac{{{numerator}}}{{{denominator}}}")
return self
def add_superscript(self, base: str, superscript: str) -> 'LaTeXBuilder':
"""
Add a superscript: base^{superscript}
Args:
base: The base expression
superscript: The superscript expression
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_superscript("x", "2").build()
'x^{2}'
>>> builder = LaTeXBuilder()
>>> builder.add_superscript("e", "i\\pi").build()
'e^{i\\pi}'
"""
self._parts.append(f"{base}^{{{superscript}}}")
return self
def add_subscript(self, base: str, subscript: str) -> 'LaTeXBuilder':
"""
Add a subscript: base_{subscript}
Args:
base: The base expression
subscript: The subscript expression
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_subscript("x", "1").build()
'x_{1}'
>>> builder = LaTeXBuilder()
>>> builder.add_subscript("a", "i+j").build()
'a_{i+j}'
"""
self._parts.append(f"{base}_{{{subscript}}}")
return self
def add_subscript_superscript(self, base: str, subscript: str, superscript: str) -> 'LaTeXBuilder':
"""
Add both subscript and superscript: base_{subscript}^{superscript}
Args:
base: The base expression
subscript: The subscript expression
superscript: The superscript expression
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_subscript_superscript("x", "1", "2").build()
'x_{1}^{2}'
>>> builder = LaTeXBuilder()
>>> builder.add_subscript_superscript("R", "i", "j").build()
'R_{i}^{j}'
"""
self._parts.append(f"{base}_{{{subscript}}}^{{{superscript}}}")
return self
def add_sqrt(self, content: str, root: Optional[str] = None) -> 'LaTeXBuilder':
"""
Add a square root or nth root.
Args:
content: The content under the root
root: Optional root index (None for square root)
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_sqrt("x").build()
'\\sqrt{x}'
>>> builder = LaTeXBuilder()
>>> builder.add_sqrt("x", "3").build()
'\\sqrt[3]{x}'
"""
if root:
self._parts.append(f"\\sqrt[{root}]{{{content}}}")
else:
self._parts.append(f"\\sqrt{{{content}}}")
return self
def add_environment(self, env: str, content: str) -> 'LaTeXBuilder':
"""
Add a LaTeX environment: \\begin{env}content\\end{env}
Args:
env: The environment name
content: The environment content
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_environment("cases", "x & x > 0 \\\\\\\\ 0 & x \\\\leq 0").build()
'\\begin{cases}x & x > 0 \\\\\\\\ 0 & x \\\\leq 0\\end{cases}'
"""
self._parts.append(f"\\begin{{{env}}}{content}\\end{{{env}}}")
return self
def add_delimiter_pair(self, left: str, content: str, right: str) -> 'LaTeXBuilder':
"""
Add delimiter pair: \\left{left}content\\right{right}
Args:
left: The left delimiter
content: The content between delimiters
right: The right delimiter
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_delimiter_pair("(", "x + y", ")").build()
'\\left(x + y\\right)'
>>> builder = LaTeXBuilder()
>>> builder.add_delimiter_pair("\\\\{", "a, b, c", "\\\\}").build()
'\\left\\\\{a, b, c\\right\\\\}'
"""
self._parts.append(f"\\left{left}{content}\\right{right}")
return self
def add_matrix(self, rows: List[List[str]], environment: str = "matrix") -> 'LaTeXBuilder':
"""
Add a matrix environment.
Args:
rows: List of rows, each row is a list of cell contents
environment: The matrix environment (matrix, pmatrix, bmatrix, etc.)
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> rows = [["a", "b"], ["c", "d"]]
>>> builder.add_matrix(rows, "pmatrix").build()
'\\begin{pmatrix}a & b \\\\\\\\ c & d\\end{pmatrix}'
"""
matrix_content = " \\\\ ".join(
" & ".join(cell for cell in row)
for row in rows
)
self._parts.append(f"\\begin{{{environment}}}{matrix_content}\\end{{{environment}}}")
return self
def clear(self) -> 'LaTeXBuilder':
"""
Clear all parts (reset the builder).
Returns:
Self for method chaining
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_text("x").clear().add_text("y").build()
'y'
"""
self._parts.clear()
return self
def build(self) -> str:
"""
Build the final LaTeX string.
Returns:
The constructed LaTeX expression
Examples:
>>> builder = LaTeXBuilder()
>>> builder.add_fraction("a", "b").build()
'\\frac{a}{b}'
>>> builder = LaTeXBuilder()
>>> builder.add_text("x").add_text(" + ").add_text("y").build()
'x + y'
"""
return "".join(self._parts)
def __str__(self) -> str:
"""String representation (same as build())."""
return self.build()
def __repr__(self) -> str:
"""Debug representation."""
return f"LaTeXBuilder(parts={len(self._parts)})"