feat(symbol): 正文标准 Unicode 数学符号 → LaTeX(与公式同一条渲染路径)
背景:公式(OMML/MathType/EQ)已转 LaTeX,但正文里的裸 Unicode 符号 (⊆ ∈ ∪ ≤ × → ∅ ∞ ∑ √ …)docx4j 只原样输出,靠系统字体渲染,字形/字号 与公式不一致;且 EqToLatexUtil 未覆盖的符号会以裸字符留在公式 LaTeX 内部。 实现 UnicodeSymbolLatex: - 53 条映射,取自 jby omml_mathml2latex/data/symbols.py,保证与公式内转换一致 (⊆→\subseteq、∈→\in、≤→\leqslant、×→\times、→→\rightarrow、∅→\emptyset …) - 文本节点内:符号 → \( … \)(MathJax 渲染) - 已在 \(…\)/\[…\] 内的片段:只换命令不加包裹(修 LaTeX 内部裸字符) - 跳过 <script>/<style>/<textarea> 与所有标签属性 - 开关 wordparse.convert-unicode-symbols(默认 true) 效果:46 份教辅重新生成后,正文裸 Unicode 符号 0 处,公式片段 5394 处
This commit is contained in:
@@ -15,6 +15,8 @@ public class WordParseConfig {
|
|||||||
private String pythonCommand = "python3";
|
private String pythonCommand = "python3";
|
||||||
/** 私有区符号图的基址(默认我们自己的 RustFS,不依赖 jby CDN) */
|
/** 私有区符号图的基址(默认我们自己的 RustFS,不依赖 jby CDN) */
|
||||||
private String symbolBaseUrl = "https://fs.code-lab.cn/temp/wordparse-symbols/";
|
private String symbolBaseUrl = "https://fs.code-lab.cn/temp/wordparse-symbols/";
|
||||||
|
/** 是否把正文里的标准 Unicode 数学符号转成 LaTeX(默认开) */
|
||||||
|
private Boolean convertUnicodeSymbols = Boolean.TRUE;
|
||||||
|
|
||||||
public String getTmpPath() { return tmpPath; }
|
public String getTmpPath() { return tmpPath; }
|
||||||
public Double getImageScalingFactor() { return imageScalingFactor; }
|
public Double getImageScalingFactor() { return imageScalingFactor; }
|
||||||
@@ -23,6 +25,8 @@ public class WordParseConfig {
|
|||||||
public String getPythonCommand() { return pythonCommand; }
|
public String getPythonCommand() { return pythonCommand; }
|
||||||
public String getSymbolBaseUrl() { return symbolBaseUrl; }
|
public String getSymbolBaseUrl() { return symbolBaseUrl; }
|
||||||
public void setSymbolBaseUrl(String symbolBaseUrl) { this.symbolBaseUrl = symbolBaseUrl; }
|
public void setSymbolBaseUrl(String symbolBaseUrl) { this.symbolBaseUrl = symbolBaseUrl; }
|
||||||
|
public Boolean getConvertUnicodeSymbols() { return convertUnicodeSymbols; }
|
||||||
|
public void setConvertUnicodeSymbols(Boolean v) { this.convertUnicodeSymbols = v; }
|
||||||
|
|
||||||
public void setTmpPath(String v) { this.tmpPath = v; }
|
public void setTmpPath(String v) { this.tmpPath = v; }
|
||||||
public void setImageScalingFactor(Double v) { this.imageScalingFactor = v; }
|
public void setImageScalingFactor(Double v) { this.imageScalingFactor = v; }
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package com.wordparse.core;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正文里的标准 Unicode 数学符号 → LaTeX(供前端 MathJax 渲染)。
|
||||||
|
*
|
||||||
|
* 为什么需要:公式(OMML/MathType/EQ)已转成 LaTeX,但正文里的裸 Unicode 符号
|
||||||
|
* (⊆ ∈ ∪ ≤ × …)docx4j 只会原样输出,靠系统字体渲染,字形/字号与公式不一致。
|
||||||
|
* 这里把正文符号也包成 \(...\),与公式走同一条渲染路径。
|
||||||
|
*
|
||||||
|
* 映射表取自 jby 的 omml_mathml2latex/data/symbols.py,保证与公式内转换一致。
|
||||||
|
* 跳过:<script>/<style> 内容、标签属性、以及已处于 \(...\) / \[...\] 内的文本。
|
||||||
|
*/
|
||||||
|
public final class UnicodeSymbolLatex {
|
||||||
|
|
||||||
|
private static final Map<String, String> MAP;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Map<String, String> m = new LinkedHashMap<>();
|
||||||
|
m.put("±", "\\pm");
|
||||||
|
m.put("×", "\\times");
|
||||||
|
m.put("÷", "\\div");
|
||||||
|
m.put("←", "\\leftarrow");
|
||||||
|
m.put("↑", "\\uparrow");
|
||||||
|
m.put("→", "\\rightarrow");
|
||||||
|
m.put("↓", "\\downarrow");
|
||||||
|
m.put("⇌", "\\rightleftharpoons");
|
||||||
|
m.put("⇒", "\\Longrightarrow");
|
||||||
|
m.put("⇔", "\\Longleftrightarrow");
|
||||||
|
m.put("∅", "\\emptyset");
|
||||||
|
m.put("∈", "\\in");
|
||||||
|
m.put("∉", "\\notin");
|
||||||
|
m.put("∏", "\\prod");
|
||||||
|
m.put("∑", "\\sum");
|
||||||
|
m.put("√", "\\sqrt{}");
|
||||||
|
m.put("∝", "\\propto");
|
||||||
|
m.put("∞", "\\infty");
|
||||||
|
m.put("∠", "\\angle");
|
||||||
|
m.put("∥", "\\parallel");
|
||||||
|
m.put("∩", "\\cap");
|
||||||
|
m.put("∪", "\\cup");
|
||||||
|
m.put("∫", "\\int");
|
||||||
|
m.put("∴", "\\therefore");
|
||||||
|
m.put("∵", "\\because");
|
||||||
|
m.put("∽", "\\backsim");
|
||||||
|
m.put("≈", "\\approx");
|
||||||
|
m.put("≌", "\\cong");
|
||||||
|
m.put("≠", "\\neq");
|
||||||
|
m.put("≡", "\\equiv");
|
||||||
|
m.put("≤", "\\leqslant");
|
||||||
|
m.put("≥", "\\geqslant");
|
||||||
|
m.put("⊂", "\\subset");
|
||||||
|
m.put("⊃", "\\supset");
|
||||||
|
m.put("⊆", "\\subseteq");
|
||||||
|
m.put("⊇", "\\supseteq");
|
||||||
|
m.put("⊥", "\\perp");
|
||||||
|
m.put("⊙", "\\odot");
|
||||||
|
m.put("△", "\\triangle");
|
||||||
|
m.put("∼", "\\sim");
|
||||||
|
m.put("≅", "\\cong");
|
||||||
|
m.put("⇐", "\\Longleftarrow");
|
||||||
|
m.put("⟹", "\\Longrightarrow");
|
||||||
|
m.put("⟸", "\\Longleftarrow");
|
||||||
|
m.put("⟺", "\\Longleftrightarrow");
|
||||||
|
m.put("∀", "\\forall");
|
||||||
|
m.put("∃", "\\exists");
|
||||||
|
m.put("∂", "\\partial");
|
||||||
|
m.put("∇", "\\nabla");
|
||||||
|
m.put("ℝ", "\\mathbb{R}");
|
||||||
|
m.put("ℕ", "\\mathbb{N}");
|
||||||
|
m.put("ℤ", "\\mathbb{Z}");
|
||||||
|
m.put("ℚ", "\\mathbb{Q}");
|
||||||
|
MAP = Map.copyOf(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Pattern MATH_SPAN =
|
||||||
|
Pattern.compile("\\\\\\(.*?\\\\\\)|\\\\\\[.*?\\\\\\]", Pattern.DOTALL);
|
||||||
|
private static final Pattern TOKEN = Pattern.compile("<[^>]*>|[^<]+");
|
||||||
|
private static final Set<String> SKIP_TAGS = Set.of("script", "style", "textarea");
|
||||||
|
|
||||||
|
private UnicodeSymbolLatex() {}
|
||||||
|
|
||||||
|
public static boolean containsSymbol(String text) {
|
||||||
|
for (String key : MAP.keySet()) {
|
||||||
|
if (text.contains(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 转换整段 HTML:只处理文本节点,跳过属性/脚本/已数学化的片段 */
|
||||||
|
public static String convert(String html) {
|
||||||
|
Matcher tokens = TOKEN.matcher(html);
|
||||||
|
StringBuilder out = new StringBuilder(html.length() + 256);
|
||||||
|
String skipUntil = null;
|
||||||
|
while (tokens.find()) {
|
||||||
|
String t = tokens.group();
|
||||||
|
if (t.startsWith("<")) {
|
||||||
|
out.append(t);
|
||||||
|
String lower = t.toLowerCase();
|
||||||
|
if (skipUntil == null) {
|
||||||
|
for (String tag : SKIP_TAGS) {
|
||||||
|
if (lower.startsWith("<" + tag) && !lower.endsWith("/>")) {
|
||||||
|
skipUntil = "</" + tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (lower.startsWith(skipUntil)) {
|
||||||
|
skipUntil = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.append(skipUntil != null ? t : convertText(t));
|
||||||
|
}
|
||||||
|
return out.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单个文本片段:保护已有 \(...\) 区间,其余部分替换符号 */
|
||||||
|
private static String convertText(String text) {
|
||||||
|
if (!containsSymbol(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder(text.length() + 64);
|
||||||
|
int last = 0;
|
||||||
|
Matcher m = MATH_SPAN.matcher(text);
|
||||||
|
while (m.find()) {
|
||||||
|
sb.append(replaceSymbols(text.substring(last, m.start())));
|
||||||
|
// 数学区内部同样规范化:裸 ∈ / ⊆ 等 → LaTeX 命令(EqToLatexUtil 未覆盖的字符)
|
||||||
|
sb.append(replaceSymbolsBare(m.group()));
|
||||||
|
last = m.end();
|
||||||
|
}
|
||||||
|
sb.append(replaceSymbols(text.substring(last)));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String replaceSymbols(String s) {
|
||||||
|
String r = s;
|
||||||
|
for (Map.Entry<String, String> e : MAP.entrySet()) {
|
||||||
|
if (r.contains(e.getKey())) {
|
||||||
|
r = r.replace(e.getKey(), "\\(" + e.getValue() + "\\)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数学区内部:只换命令,不加 \( \) 包裹 */
|
||||||
|
private static String replaceSymbolsBare(String s) {
|
||||||
|
String r = s;
|
||||||
|
for (Map.Entry<String, String> e : MAP.entrySet()) {
|
||||||
|
if (r.contains(e.getKey())) {
|
||||||
|
r = r.replace(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1243,6 +1243,10 @@ public class WordParseUtil {
|
|||||||
}
|
}
|
||||||
// ⇌(U+E225 U+E226 成对出现)直接转 LaTeX
|
// ⇌(U+E225 U+E226 成对出现)直接转 LaTeX
|
||||||
result = result.replace("\ue225\ue226", "\\(\\rightleftharpoons\\)");
|
result = result.replace("\ue225\ue226", "\\(\\rightleftharpoons\\)");
|
||||||
|
// 正文里的标准 Unicode 数学符号(⊆ ∈ ∪ ≤ × …)→ LaTeX,与公式走同一条渲染路径
|
||||||
|
if (Boolean.TRUE.equals(config.getConvertUnicodeSymbols())) {
|
||||||
|
result = UnicodeSymbolLatex.convert(result);
|
||||||
|
}
|
||||||
writeHtml(result);
|
writeHtml(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ wordparse:
|
|||||||
transform-scripts-path: /opt/transform
|
transform-scripts-path: /opt/transform
|
||||||
python-command: python3
|
python-command: python3
|
||||||
symbol-base-url: https://fs.code-lab.cn/temp/wordparse-symbols/
|
symbol-base-url: https://fs.code-lab.cn/temp/wordparse-symbols/
|
||||||
|
convert-unicode-symbols: true
|
||||||
oss:
|
oss:
|
||||||
endpoint: https://fs.code-lab.cn
|
endpoint: https://fs.code-lab.cn
|
||||||
access-key: ${WORDPARSE_OSS_AK:}
|
access-key: ${WORDPARSE_OSS_AK:}
|
||||||
|
|||||||
Reference in New Issue
Block a user