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,79 @@
/*
* Decompiled with CFR 0.152.
*/
package com.jby.officecore.enums;
public enum AttrEnum {
STYLE("style"),
COLOR("color"),
BACKGROUND_COLOR("background-color"),
BACKGROUND("background"),
FONT_WEIGHT("font-weight"),
FONT_SIZE("font-size"),
MIN_HEIGHT("min-height"),
LINE_HEIGHT("line-height"),
TEXT_ALIGN("text-align"),
PADDING("padding"),
PADDING_TOP("padding-top"),
PADDING_RIGHT("padding-right"),
PADDING_BOTTOM("padding-bottom"),
PADDING_LEFT("padding-left"),
TEXT_INDENT("text-indent"),
MARGIN("margin"),
MARGIN_TOP("margin-top"),
MARGIN_RIGHT("margin-right"),
MARGIN_BOTTOM("margin-bottom"),
MARGIN_LEFT("margin-left"),
VERTICAL_ALIGN("vertical-align"),
FONT_FAMILY("font-family"),
FONT_STYLE("font-style"),
FACE("face"),
SIZE("size"),
HREF("href"),
SRC("src"),
WIDTH("width"),
HEIGHT("height"),
CLASS("class"),
BORDER_STYLE("border-style"),
COLSPAN("colspan"),
ROWSPAN("rowspan"),
TEXT_DECORATION("text-decoration"),
BACKGROUND_IMAGE("background-image"),
TINYMCE_HORIZONTAL_LINE("tinymce-horizontal-line"),
TINYMCE_LINE_NUMBER("tinymce-line-number"),
TEXT_EMPHASIS_POSITION("text-emphasis-position"),
DATA_TYPE("data-type"),
SEGMENT("segment"),
BUSINESS("business"),
LIST_STYLE_TYPE("list-style-type"),
ALT("alt"),
NAME("name"),
DATA_HREF("data-href"),
DATA_File_SUFFIX("data-file-suffix"),
TINYMCE_LINK("tinymce-link"),
DATA_RESOURCE_URL("data-resource-url"),
DATA_COURSE_NETWORK_LESSON_RESOURCE_NAME("data-course-network-lesson-resource-name"),
TINYMCE_HOLLOWOUT("tinymce-hollowOut"),
TINYMCE_HIDDENONLY("tinymce-hiddenOnly"),
TINYMCE_HIDDENANDANSWER("tinymce-hiddenAndAnswer"),
BORDER("border"),
BORDER_LEFT("border-left"),
BORDER_BOTTOM("border-bottom"),
BORDER_RIGHT("border-right"),
BORDER_TOP("border-top"),
BORDER_LEFT_COLOR("border-left-color"),
BORDER_BOTTOM_COLOR("border-bottom-color"),
BORDER_RIGHT_COLOR("border-right-color"),
BORDER_TOP_COLOR("border-top-color");
private final String attr;
public String getAttr() {
return this.attr;
}
private AttrEnum(String attr) {
this.attr = attr;
}
}

140
java-src/enums/TagEnum.java Normal file
View File

@@ -0,0 +1,140 @@
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* com.jby.common.util.DistinctUtils
* org.apache.commons.collections4.CollectionUtils
* org.apache.commons.lang3.StringUtils
* org.jsoup.nodes.Element
*/
package com.jby.officecore.enums;
import com.jby.common.util.DistinctUtils;
import com.jby.officecore.domain.bo.TagStyleBO;
import com.jby.officecore.enums.AttrEnum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.nodes.Element;
public enum TagEnum {
H1("h1", true, null),
H2("h2", true, null),
H3("h3", true, null),
H4("h4", true, null),
P("p", false, null),
DIV("div", false, null),
FONT("font", false, Stream.of(AttrEnum.FACE, AttrEnum.SIZE, AttrEnum.COLOR).collect(Collectors.toList())),
STRIKE("strike", true, null),
BR("br", false, null),
PBR("pbr", false, null),
HR("hr", false, null),
U("u", true, null),
I("i", true, null),
SUB("sub", true, null),
SUP("sup", true, null),
WAVE("wave", true, null),
DOT("dot", true, null),
B("b", true, null),
A("a", false, Stream.of(AttrEnum.HREF).collect(Collectors.toList())),
TABLE("table", false, null),
UL("ul", false, null),
OL("ol", false, null),
LI("li", false, null),
IMG("img", false, Stream.of(AttrEnum.WIDTH, AttrEnum.HEIGHT).collect(Collectors.toList())),
IMQ("imq", false, Stream.of(AttrEnum.WIDTH, AttrEnum.HEIGHT).collect(Collectors.toList())),
VIDEO("video", false, Stream.of(AttrEnum.WIDTH, AttrEnum.HEIGHT).collect(Collectors.toList())),
SOURCE("source", false, null),
MATH("math", false, null),
TAB("tab", false, null),
TEXT("#text", false, null),
SPAN("span", false, Stream.of(AttrEnum.CLASS).collect(Collectors.toList())),
EM("em", true, null),
STRONG("strong", true, null);
private final String name;
private final Boolean dealTagStyle;
private final List<AttrEnum> attrList;
public static final Set<String> H_P_SET;
public static final Set<String> H_SET;
public static final Set<String> UL_OL_SET;
private static final Map<String, TagEnum> MAP;
public static TagEnum getTagEnum(String tagName) {
return MAP.get(tagName);
}
public static List<TagStyleBO> getStyleList(Element element) {
ArrayList<TagStyleBO> styleList = new ArrayList<TagStyleBO>();
TagEnum tagEnum = TagEnum.getTagEnum(element.tagName());
if (Objects.nonNull((Object)tagEnum) && Boolean.TRUE.equals(tagEnum.dealTagStyle)) {
styleList.add(new TagStyleBO().setTagName(element.tagName()).setAttr(tagEnum.getName()).setValue(tagEnum.getName()));
}
if (Objects.nonNull((Object)tagEnum) && CollectionUtils.isNotEmpty(tagEnum.getAttrList())) {
for (AttrEnum attrEnum : tagEnum.getAttrList()) {
if (!StringUtils.isNotBlank((CharSequence)element.attr(attrEnum.getAttr())) || "null".equals(element.attr(attrEnum.getAttr()))) continue;
styleList.add(new TagStyleBO().setTagName(element.tagName()).setAttr(attrEnum.getAttr()).setValue(element.attr(attrEnum.getAttr())));
}
}
List<String> styles = StringUtils.isBlank((CharSequence)element.attr(AttrEnum.STYLE.getAttr())) ? Collections.emptyList() : Arrays.asList(element.attr(AttrEnum.STYLE.getAttr()).split(";"));
styles.forEach(style -> {
if (StringUtils.isBlank((CharSequence)style)) {
return;
}
String[] split = style.split(":");
if (split.length < 2) {
return;
}
String attr = split[0].trim();
String value = split[1].trim();
if (StringUtils.isBlank((CharSequence)value) || "null".equals(value) || StringUtils.isBlank((CharSequence)attr) || "null".equals(attr)) {
return;
}
styleList.add(new TagStyleBO().setTagName(element.tagName()).setAttr(attr).setValue(value));
});
return styleList.stream().filter(DistinctUtils.distinctByKey(TagStyleBO::getAttr)).filter(e -> !e.getValue().contains("auto")).collect(Collectors.toList());
}
public static Map<String, String> getStyleMap(Element element) {
List<TagStyleBO> styleList = TagEnum.getStyleList(element);
return styleList.stream().collect(Collectors.toMap(TagStyleBO::getAttr, TagStyleBO::getValue, (l, r) -> l));
}
public String getName() {
return this.name;
}
public Boolean getDealTagStyle() {
return this.dealTagStyle;
}
public List<AttrEnum> getAttrList() {
return this.attrList;
}
private TagEnum(String name, Boolean dealTagStyle, List<AttrEnum> attrList) {
this.name = name;
this.dealTagStyle = dealTagStyle;
this.attrList = attrList;
}
static {
H_P_SET = Stream.of(H1.getName(), H2.getName(), H3.getName(), H4.getName(), P.getName()).collect(Collectors.toSet());
H_SET = Stream.of(H1.getName(), H2.getName(), H3.getName(), H4.getName()).collect(Collectors.toSet());
UL_OL_SET = Stream.of(UL.getName(), OL.getName()).collect(Collectors.toSet());
MAP = new HashMap<String, TagEnum>();
for (TagEnum tagEnum : TagEnum.values()) {
MAP.put(tagEnum.getName(), tagEnum);
}
}
}