Files
DoLuEdu-WordFileParse/java-src/utils/EqToLatexUtil.java
Quentin 3271fee5cc 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 默认密钥已清空
2026-09-29 09:43:13 +08:00

1035 lines
46 KiB
Java

/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* org.apache.commons.lang3.StringUtils
* org.slf4j.Logger
* org.slf4j.LoggerFactory
*/
package com.jby.officecore.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EqToLatexUtil {
private static final Logger log = LoggerFactory.getLogger(EqToLatexUtil.class);
private static final Pattern LONG_EQUALS_PATTERN = Pattern.compile("^=+$");
private static final int LONG_EQUALS_MIN_LENGTH = 3;
private static final Map<String, CommandHandler> COMMAND_HANDLERS = new HashMap<String, CommandHandler>();
public static String sanitizeInput(String input) {
if (input == null) {
return "";
}
HashMap<String, String> replacements = new HashMap<String, String>();
replacements.put("\uff08", "(");
replacements.put("\uff09", ")");
replacements.put("\uff5b", "{");
replacements.put("\uff5d", "}");
replacements.put("\uff0b", "+");
replacements.put("\uff0d", "-");
replacements.put("\u2212", "-");
replacements.put("\u00d7", " \\times ");
replacements.put("\u00f7", " \\div ");
replacements.put("\uff1d", "=");
replacements.put("\u2260", "\\ne ");
replacements.put("\u2248", " \\approx ");
replacements.put("\u2264", "\\le ");
replacements.put("\u2265", "\\ge ");
replacements.put("\uff1c", "<");
replacements.put("\uff1e", "> ");
replacements.put("\u00b7", " \\cdot ");
replacements.put("\u2026", "\\dots ");
replacements.put("\uff5e", "~");
replacements.put("\uff1b", ";");
replacements.put("\uff1a", ":");
replacements.put("\uf028", "(");
replacements.put("\uf029", ")");
replacements.put("~", "\\sim");
replacements.put("\u221d", "\\propto");
String sanitized = input;
for (Map.Entry entry : replacements.entrySet()) {
sanitized = sanitized.replace((CharSequence)entry.getKey(), (CharSequence)entry.getValue());
}
sanitized = sanitized.replaceAll("(?i)eq", "EQ ");
return sanitized.trim();
}
private static String getCharFromOpt(String opt, int startIdx, String defaultChar) {
if (opt.length() <= startIdx) {
return defaultChar;
}
String charPart = opt.substring(startIdx).trim();
if (charPart.isEmpty()) {
return defaultChar;
}
if (charPart.startsWith("\\")) {
return charPart.substring(1);
}
return charPart;
}
private static String escapeChar(String ch) {
String specialChars = "\\{}$&#%";
if ("\\{}$&#%".contains(ch)) {
return "\\" + ch;
}
if ("~".equals(ch)) {
return "\\ ";
}
return ch;
}
public static String convert(String eqField) {
if (eqField == null) {
throw new IllegalArgumentException("\u8f93\u5165\u5fc5\u987b\u662f\u5b57\u7b26\u4e32\u7c7b\u578b");
}
if (eqField.trim().isEmpty()) {
return "";
}
try {
String sanitizedField = EqToLatexUtil.sanitizeInput(EqToLatexUtil.stripFormattingMarkers(eqField));
String content = sanitizedField.replaceAll("^\\s*\\{?\\s*EQ\\s*|\\s*EQ\\s*|\\s*}\\s*$", "").trim();
String result = EqToLatexUtil.parseInstructions(content, new HashMap<String, Object>());
result = EqToLatexUtil.replaceJbyuWithMeasurements(result);
result = result.replaceAll("JBYU2BEGIN", "\\\\underline{").replaceAll("JBYU2END", "}");
result = result.replaceAll("\u2192", "\\\\to ");
log.info("\u539fEQ\u516c\u5f0f\uff1a{} \u8f6c\u6362\u540e\u7684Latex\u4ee3\u7801\uff1a{}", (Object)eqField, (Object)result);
return result;
}
catch (Exception error) {
System.err.println("\u8f6c\u6362\u8fc7\u7a0b\u4e2d\u51fa\u73b0\u9519\u8bef: " + error.getMessage());
error.printStackTrace();
return "\\text{\u8f6c\u6362\u9519\u8bef: " + error.getMessage() + "}";
}
}
private static String stripFormattingMarkers(String input) {
return input.replaceAll("\\\\(?:\\s*\\\\?JBYU\\s*2(?:BEGIN|END))+\\s*(?!(?:[eE][qQ])\\b)(?=[A-Za-z])", Matcher.quoteReplacement("\\")).replaceAll("\\\\?JBYU\\s*2BEGIN", "").replaceAll("\\\\?JBYU\\s*2END", "");
}
private static String parseInstructions(String str, Map<String, Object> context) {
ArrayList<String> output = new ArrayList<String>();
int index = 0;
while (index < str.length()) {
int textEnd;
char ch = str.charAt(index);
if (ch == '\\') {
String lastOutput;
ParseResult result = EqToLatexUtil.parseCommand(str, index, context);
String trimmedOutput = result.output.trim();
if ((trimmedOutput.startsWith("^") || trimmedOutput.startsWith("_")) && !output.isEmpty()) {
lastOutput = (String)output.get(output.size() - 1);
if (EqToLatexUtil.isConsecutiveScript(lastOutput, trimmedOutput)) {
output.set(output.size() - 1, EqToLatexUtil.mergeConsecutiveScripts(lastOutput, trimmedOutput));
} else {
output.set(output.size() - 1, lastOutput + result.output);
}
} else if ((trimmedOutput.startsWith("\\^") || trimmedOutput.startsWith("\\_")) && !output.isEmpty()) {
lastOutput = (String)output.get(output.size() - 1);
if (EqToLatexUtil.isConsecutiveScript(lastOutput, trimmedOutput)) {
output.set(output.size() - 1, EqToLatexUtil.mergeConsecutiveScripts(lastOutput, trimmedOutput));
} else {
output.set(output.size() - 1, lastOutput + result.output);
}
} else if (EqToLatexUtil.isSpecialScriptMarker(trimmedOutput) && !output.isEmpty() && EqToLatexUtil.isSpecialScriptMarker((String)output.get(output.size() - 1))) {
String merged = EqToLatexUtil.mergeSpecialScriptMarkers((String)output.get(output.size() - 1), trimmedOutput);
output.set(output.size() - 1, merged);
} else {
output.add(result.output);
}
index = result.newIndex;
continue;
}
if (ch == '{' || ch == '}') {
++index;
continue;
}
for (textEnd = index; textEnd < str.length() && "\\{}".indexOf(str.charAt(textEnd)) == -1; ++textEnd) {
}
String text = str.substring(index, textEnd);
output.add(text.replaceAll("[&%$#_]", "\\\\$0"));
index = textEnd;
}
String result = String.join((CharSequence)" ", output).replaceAll("\\s+", " ").trim();
return EqToLatexUtil.postProcessConsecutiveScripts(result);
}
private static ParseResult parseCommand(String str, int startIndex, Map<String, Object> context) {
int argParenStartIndex;
String cmdName;
int currentIndex = startIndex;
ArrayList<String> commandChain = new ArrayList<String>();
ArrayList<Integer> commandEndIndices = new ArrayList<Integer>();
while (currentIndex < str.length()) {
int cmdNameEnd;
int tempIndex;
for (tempIndex = currentIndex; tempIndex < str.length() && Character.isWhitespace(str.charAt(tempIndex)); ++tempIndex) {
}
if (tempIndex >= str.length() || str.charAt(tempIndex) != '\\') {
currentIndex = tempIndex;
break;
}
currentIndex = tempIndex;
for (cmdNameEnd = currentIndex + 1; cmdNameEnd < str.length() && Character.isLetter(str.charAt(cmdNameEnd)); ++cmdNameEnd) {
}
String cmdName2 = str.substring(currentIndex + 1, cmdNameEnd);
String token = "\\" + cmdName2;
int argStartIndex = cmdNameEnd;
List<String> numericArgCmds = Arrays.asList("co", "vs", "hs", "fo", "ba", "up", "do", "ai", "di");
if (numericArgCmds.contains(cmdName2.toLowerCase())) {
int numEnd;
for (numEnd = argStartIndex; numEnd < str.length() && Character.isDigit(str.charAt(numEnd)); ++numEnd) {
}
if (numEnd > argStartIndex) {
token = token + str.substring(argStartIndex, numEnd);
argStartIndex = numEnd;
}
} else if (Arrays.asList("lc", "rc", "bc", "fc", "vc").contains(cmdName2.toLowerCase()) && argStartIndex < str.length()) {
if (str.charAt(argStartIndex) == '\\' && argStartIndex + 1 < str.length()) {
token = token + str.substring(argStartIndex, argStartIndex + 2);
argStartIndex += 2;
} else {
token = token + str.charAt(argStartIndex);
++argStartIndex;
}
}
commandChain.add(token);
commandEndIndices.add(argStartIndex);
currentIndex = argStartIndex;
}
if (commandChain.isEmpty()) {
return new ParseResult(EqToLatexUtil.escapeChar(String.valueOf(str.charAt(startIndex))), startIndex + 1);
}
CommandHandler mainCommandHandler = null;
String mainCommandName = "";
String firstToken = (String)commandChain.get(0);
Matcher matcher = Pattern.compile("^\\\\([a-zA-Z]+)").matcher(firstToken);
if (matcher.find() && COMMAND_HANDLERS.containsKey(cmdName = matcher.group(1).toLowerCase())) {
mainCommandHandler = COMMAND_HANDLERS.get(cmdName);
mainCommandName = cmdName;
}
if (mainCommandHandler == null) {
return new ParseResult((String)commandChain.get(0), (Integer)commandEndIndices.get(0));
}
ArrayList<String> args = new ArrayList<String>();
for (argParenStartIndex = currentIndex; argParenStartIndex < str.length() && Character.isWhitespace(str.charAt(argParenStartIndex)); ++argParenStartIndex) {
}
if (argParenStartIndex < str.length() && (str.charAt(argParenStartIndex) == '(' || str.charAt(argParenStartIndex) == '[')) {
ArgumentResult argsResult = EqToLatexUtil.extractArguments(str, argParenStartIndex);
if (argsResult.success) {
List<String> rawArgs = EqToLatexUtil.splitArguments(argsResult.content);
HashMap<String, Object> childContext = new HashMap<String, Object>();
childContext.put("parentCommand", mainCommandName);
for (String arg : rawArgs) {
args.add(EqToLatexUtil.parseInstructions(arg, childContext));
}
currentIndex = argsResult.endIndex;
} else {
throw new RuntimeException("Mismatched parentheses starting at index " + argParenStartIndex);
}
}
String output = mainCommandHandler.handle(commandChain, args, mainCommandName, context);
return new ParseResult(output, currentIndex);
}
private static ArgumentResult extractArguments(String str, int startIndex) {
char openParen = str.charAt(startIndex);
char closeParen = openParen == '(' ? (char)')' : ']';
int depth = 1;
StringBuilder content = new StringBuilder();
int index = startIndex + 1;
while (index < str.length() && depth > 0) {
char ch = str.charAt(index);
if (ch == '\\' && index + 1 < str.length()) {
content.append(ch).append(str.charAt(index + 1));
index += 2;
continue;
}
if (ch == openParen) {
if (++depth > 1) {
content.append(ch);
}
++index;
continue;
}
if (ch == closeParen) {
if (--depth > 0) {
content.append(ch);
}
++index;
continue;
}
if (depth > 0) {
content.append(ch);
}
++index;
}
return new ArgumentResult(depth == 0, content.toString(), index);
}
private static List<String> splitArguments(String str) {
ArrayList<String> args = new ArrayList<String>();
StringBuilder current = new StringBuilder();
int depth = 0;
int i = 0;
while (i < str.length()) {
char ch = str.charAt(i);
if (ch == '\\' && i + 1 < str.length()) {
current.append(ch).append(str.charAt(i + 1));
i += 2;
continue;
}
if (ch == '(') {
++depth;
} else if (ch == ')') {
--depth;
}
if (ch == ',' && depth == 0) {
args.add(current.toString().trim());
current = new StringBuilder();
} else {
current.append(ch);
}
++i;
}
if (current.length() > 0) {
args.add(current.toString().trim());
}
return args;
}
private static String handleFraction(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
if (args.size() != 2) {
return "\\text{\u9519\u8bef\u7684\u5206\u6570\u53c2\u6570}";
}
return String.format("\\dfrac{%s}{%s}", args.get(0), args.get(1));
}
private static String handleRoot(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
if (args.isEmpty()) {
return "";
}
if (args.size() == 1) {
return String.format("\\sqrt{%s}", args.get(0));
}
return String.format("\\sqrt[%s]{%s}", args.get(0), args.get(1));
}
private static String handleIntegral(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
String symbol = "\\int";
if (options.stream().anyMatch(o -> o.toLowerCase().startsWith("\\su"))) {
symbol = "\\sum";
} else if (options.stream().anyMatch(o -> o.toLowerCase().startsWith("\\pr"))) {
symbol = "\\prod";
} else {
String fc = options.stream().filter(o -> o.toLowerCase().startsWith("\\fc")).findFirst().orElse(null);
String vc = options.stream().filter(o -> o.toLowerCase().startsWith("\\vc")).findFirst().orElse(null);
if (fc != null) {
symbol = EqToLatexUtil.escapeChar(EqToLatexUtil.getCharFromOpt(fc, 3, ""));
} else if (vc != null) {
symbol = EqToLatexUtil.escapeChar(EqToLatexUtil.getCharFromOpt(vc, 3, ""));
}
}
if (args.isEmpty()) {
return symbol;
}
String lower = args.get(0);
String upper = args.size() > 1 ? args.get(1) : "";
String integrand = args.size() > 2 ? args.subList(2, args.size()).stream().collect(Collectors.joining(" ")) : "";
String limits = !lower.isEmpty() || !upper.isEmpty() ? String.format("^{%s}_{%s}", upper, lower) : "";
return String.format("%s%s %s", symbol, limits, integrand);
}
private static String handleScript(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
ArrayList<String> argsCopy = new ArrayList<String>(args);
if (argsCopy.size() > 2) {
String arrayContent = String.join((CharSequence)"\\\\", argsCopy);
return String.format("\\begin{array}{l}%s\\end{array}", arrayContent);
}
String up = "";
String down = "";
boolean doOptExists = options.stream().anyMatch(o -> o.toLowerCase().startsWith("\\do"));
if (argsCopy.size() == 2) {
up = (String)argsCopy.remove(0);
down = (String)argsCopy.remove(0);
} else if (argsCopy.size() == 1) {
if (doOptExists) {
down = (String)argsCopy.remove(0);
} else {
up = (String)argsCopy.remove(0);
}
}
if ("o".equals(context.get("parentCommand"))) {
String upTrimmed = up.trim();
if ("\u2192".equals(upTrimmed) || "\\rightarrow".equals(upTrimmed)) {
return "OVERSET_ARROW";
}
if ("^".equals(upTrimmed) || "IS_HAT".equals(upTrimmed)) {
return "OVERSET_HAT";
}
if (!up.isEmpty() && !down.isEmpty()) {
return "OVER_UNDER_SCRIPT:" + up + "," + down;
}
if (!up.isEmpty()) {
return "OVER_SCRIPT:" + up;
}
if (!down.isEmpty()) {
return "UNDER_SCRIPT:" + down;
}
}
StringBuilder output = new StringBuilder();
if (!up.isEmpty()) {
output.append(String.format("^{%s}", up));
}
if (!down.isEmpty()) {
output.append(String.format("_{%s}", down));
}
return output.toString();
}
private static String handleBrackets(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
String content = String.join((CharSequence)",", args);
HashMap<String, String> delimiterPairMap = new HashMap<String, String>();
delimiterPairMap.put("(", ")");
delimiterPairMap.put("[", "]");
delimiterPairMap.put("{", "}");
delimiterPairMap.put("<", ">");
String left = null;
String right = null;
String bcOpt = options.stream().filter(o -> o.startsWith("\\bc")).findFirst().orElse(null);
String lcOpt = options.stream().filter(o -> o.startsWith("\\lc")).findFirst().orElse(null);
String rcOpt = options.stream().filter(o -> o.startsWith("\\rc")).findFirst().orElse(null);
if (bcOpt != null) {
String charStr = EqToLatexUtil.getCharFromOpt(bcOpt, 3, "|");
left = EqToLatexUtil.getDelimiter(charStr);
right = EqToLatexUtil.getDelimiter(delimiterPairMap.getOrDefault(charStr, charStr));
} else {
if (lcOpt != null) {
left = EqToLatexUtil.getDelimiter(EqToLatexUtil.getCharFromOpt(lcOpt, 3, "("));
}
if (rcOpt != null) {
right = EqToLatexUtil.getDelimiter(EqToLatexUtil.getCharFromOpt(rcOpt, 3, ")"));
}
}
if (bcOpt == null && lcOpt == null && rcOpt == null) {
left = "(";
right = ")";
}
return String.format("\\left%s %s \\right%s", StringUtils.isNotBlank((CharSequence)left) ? left : ".", content, StringUtils.isNotBlank((CharSequence)right) ? right : ".");
}
private static String getDelimiter(String ch) {
return "{".equals(ch) || "}".equals(ch) ? "\\" + ch : ch;
}
private static String handleArray(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
if (args.isEmpty()) {
return "";
}
if (args.size() == 1) {
return args.get(0);
}
int cols = 1;
String colAlign = "c";
for (String opt : options) {
Matcher colMatch = Pattern.compile("^\\\\co(\\d+)$").matcher(opt);
if (colMatch.matches()) {
cols = Integer.parseInt(colMatch.group(1));
}
if ("\\al".equals(opt)) {
colAlign = "l";
continue;
}
if ("\\ac".equals(opt)) {
colAlign = "c";
continue;
}
if (!"\\ar".equals(opt)) continue;
colAlign = "r";
}
StringBuilder alignSpec = new StringBuilder();
for (int i = 0; i < cols; ++i) {
alignSpec.append(colAlign);
}
ArrayList<String> matrixRows = new ArrayList<String>();
int i = 0;
while ((double)i < Math.ceil((double)args.size() / (double)cols)) {
ArrayList<String> row = new ArrayList<String>(args.subList(i * cols, Math.min((i + 1) * cols, args.size())));
while (row.size() < cols) {
row.add("");
}
matrixRows.add(String.join((CharSequence)" & ", row));
++i;
}
return String.format("\\begin{array}{%s}%s\\end{array}", alignSpec, String.join((CharSequence)"\\\\", matrixRows));
}
private static String handleBox(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
if (options.stream().anyMatch(o -> o.equalsIgnoreCase("\\to"))) {
return args.isEmpty() ? "\\overline{}" : String.format("\\overline{%s}", args.get(0));
}
String content = String.join((CharSequence)",", args);
return args.isEmpty() ? "\\boxed{}" : String.format("\\boxed{%s}", content);
}
private static String handleList(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
return String.join((CharSequence)",\\ ", args);
}
private static String handleOverstrike(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
String limitResult;
String firstArg;
if (args.isEmpty()) {
return "";
}
if (args.size() >= 1 && ("lim".equals(firstArg = args.get(0).trim().toLowerCase()) || "limit".equals(firstArg)) && (limitResult = EqToLatexUtil.handleLimitExpression(args)) != null) {
return limitResult;
}
String chemResult = EqToLatexUtil.handleChemicalSymbols(args, context);
if (chemResult != null) {
return chemResult;
}
boolean allArgsAreScriptMarkers = true;
for (String arg : args) {
if (EqToLatexUtil.isScriptMarker(arg)) continue;
allArgsAreScriptMarkers = false;
break;
}
if (allArgsAreScriptMarkers) {
boolean hasAlignCenter = options.stream().anyMatch(o -> "\\ac".equalsIgnoreCase((String)o));
if (hasAlignCenter) {
ArrayList<String> scriptContents = new ArrayList<String>();
for (String script2 : args) {
if (!script2.startsWith("OVER_SCRIPT:")) continue;
scriptContents.add(script2.substring("OVER_SCRIPT:".length()).trim());
}
if (!scriptContents.isEmpty()) {
String result = (String)scriptContents.get(scriptContents.size() - 1);
for (int i = scriptContents.size() - 2; i >= 0; --i) {
result = String.format("\\rlap{%s}{%s}", scriptContents.get(i), result);
}
return String.format("^{%s}", result);
}
}
StringBuilder up = new StringBuilder();
StringBuilder down = new StringBuilder();
args.forEach(script -> {
if (script.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = script.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
up.append(up.length() > 0 ? " " : "").append(parts[0]);
down.append(down.length() > 0 ? " " : "").append(parts[1]);
} else if (script.startsWith("OVER_SCRIPT:")) {
up.append(up.length() > 0 ? " " : "").append(script.substring("OVER_SCRIPT:".length()));
} else if (script.startsWith("UNDER_SCRIPT:")) {
down.append(down.length() > 0 ? " " : "").append(script.substring("UNDER_SCRIPT:".length()));
} else if ("OVERSET_ARROW".equals(script)) {
up.append(up.length() > 0 ? " " : "").append("\\rightarrow");
} else if ("OVERSET_HAT".equals(script)) {
up.append(up.length() > 0 ? " " : "").append("^");
} else {
MathJaxScripts extracted = EqToLatexUtil.extractMathJaxScripts(script);
if (!extracted.up.isEmpty()) {
up.append(up.length() > 0 ? " " : "").append(extracted.up);
}
if (!extracted.down.isEmpty()) {
down.append(down.length() > 0 ? " " : "").append(extracted.down);
}
}
});
String newUp = up.toString().replaceAll("\\s?OVER_SCRIPT:|\\s?UNDER_SCRIPT:", "");
String newDown = down.toString().replaceAll("\\s?OVER_SCRIPT:|\\s?UNDER_SCRIPT:", "");
StringBuilder output = new StringBuilder();
if (up.length() > 0) {
output.append(String.format("^{%s}", newUp));
}
if (down.length() > 0) {
output.append(String.format("_{%s}", newDown));
}
return output.toString();
}
int baseIndex = -1;
for (int i = 0; i < args.size(); ++i) {
if (EqToLatexUtil.isScriptMarker(args.get(i))) continue;
baseIndex = i;
break;
}
if (baseIndex == -1) {
String result = args.get(0);
for (int i = 1; i < args.size(); ++i) {
result = String.format("\\rlap{%s}{%s}", args.get(i), result);
}
return result;
}
String base = args.get(baseIndex);
base = EqToLatexUtil.resolveNestedScripts(base);
ArrayList<String> otherArgs = new ArrayList<String>();
for (int i = 0; i < args.size(); ++i) {
if (i == baseIndex) continue;
otherArgs.add(args.get(i));
}
boolean otherArgsAreScriptMarkers = true;
for (String arg : otherArgs) {
if (EqToLatexUtil.isScriptMarker(arg)) continue;
otherArgsAreScriptMarkers = false;
break;
}
if (!otherArgs.isEmpty() && otherArgsAreScriptMarkers) {
StringBuilder over = new StringBuilder();
StringBuilder under = new StringBuilder();
otherArgs.forEach(script -> {
if ("OVERSET_ARROW".equals(script)) {
over.append("\\rightarrow");
} else if ("OVERSET_HAT".equals(script)) {
over.append("^");
} else if (script.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = script.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
over.append(over.length() > 0 ? " " : "").append(parts[0]);
under.append(under.length() > 0 ? " " : "").append(parts[1]);
} else if (script.startsWith("OVER_SCRIPT:")) {
over.append(over.length() > 0 ? " " : "").append(script.substring("OVER_SCRIPT:".length()));
} else if (script.startsWith("UNDER_SCRIPT:")) {
under.append(under.length() > 0 ? " " : "").append(script.substring("UNDER_SCRIPT:".length()));
}
});
if ("^".equals(over.toString())) {
return String.format("\\hat{%s}", base);
}
String cleanOver = over.toString().replace(" ", "").replace("\\to", "\\rightarrow").replace("\u2192", "\\rightarrow");
if ("\\rightarrow".equals(cleanOver) || cleanOver.contains("\\rightarrow") && (cleanOver.matches(".*[\u2014\u2013\u2212\u2015\u2012\u2010\\-].*") || cleanOver.contains("OVERSET_ARROW"))) {
return String.format("\\overrightarrow{%s}", base);
}
if ("\\rightarrow".equals(over.toString())) {
return String.format("\\overrightarrow{%s}", base);
}
String result = base;
if (over.length() > 0) {
result = String.format("\\overset{%s}{%s}", over.toString(), result);
}
if (under.length() > 0) {
result = String.format("\\underset{%s}{%s}", under.toString(), result);
}
return result;
}
String result = base;
for (int i = otherArgs.size() - 1; i >= 0; --i) {
result = String.format("\\rlap{%s}{%s}", otherArgs.get(i), result);
}
return result;
}
private static String handleChemicalSymbols(List<String> args, Map<String, Object> context) {
if (args.isEmpty()) {
return null;
}
String firstArg = args.get(0).trim();
if (LONG_EQUALS_PATTERN.matcher(firstArg).matches() && firstArg.length() >= 3) {
return EqToLatexUtil.handleLongEqual(args);
}
if ((firstArg.contains("\u2192") || firstArg.contains("\\rightarrow")) && !firstArg.contains("\u2190") && !firstArg.contains("\\leftarrow")) {
return EqToLatexUtil.handleRightArrow(args);
}
if (firstArg.matches("^\u2190[\u2015\uff0d\u2014\\-]+$") || firstArg.contains("\u2190") || firstArg.contains("\\leftarrow")) {
return EqToLatexUtil.handleLeftArrow(args);
}
return null;
}
private static String handleLongEqual(List<String> args) {
String upper = "";
String lower = "";
for (int i = 1; i < args.size(); ++i) {
String content;
String arg = args.get(i);
if (arg.startsWith("OVER_SCRIPT:")) {
upper = (upper + " " + arg.substring("OVER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("UNDER_SCRIPT:")) {
lower = (lower + " " + arg.substring("UNDER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = arg.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
upper = (upper + " " + parts[0]).trim();
lower = (lower + " " + (parts.length > 1 ? parts[1] : "")).trim();
continue;
}
if (arg.startsWith("^{") && arg.endsWith("}")) {
content = arg.substring(2, arg.length() - 1);
upper = (upper + " " + content).trim();
continue;
}
if (!arg.startsWith("_{") || !arg.endsWith("}")) continue;
content = arg.substring(2, arg.length() - 1);
lower = (lower + " " + content).trim();
}
if (!upper.isEmpty() && !lower.isEmpty()) {
return String.format("\\xlongequal[%s]{%s}", lower, upper);
}
if (!upper.isEmpty()) {
return String.format("\\xlongequal{%s}", upper);
}
return "\\xlongequal{}";
}
private static String handleRightArrow(List<String> args) {
String upper = "";
String lower = "";
for (int i = 1; i < args.size(); ++i) {
String content;
String arg = args.get(i);
if (arg.startsWith("OVER_SCRIPT:")) {
upper = (upper + " " + arg.substring("OVER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("UNDER_SCRIPT:")) {
lower = (lower + " " + arg.substring("UNDER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = arg.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
upper = (upper + " " + parts[0]).trim();
lower = (lower + " " + (parts.length > 1 ? parts[1] : "")).trim();
continue;
}
if (arg.startsWith("^{") && arg.endsWith("}")) {
content = arg.substring(2, arg.length() - 1);
upper = (upper + " " + content).trim();
continue;
}
if (!arg.startsWith("_{") || !arg.endsWith("}")) continue;
content = arg.substring(2, arg.length() - 1);
lower = (lower + " " + content).trim();
}
if (!upper.isEmpty() && !lower.isEmpty()) {
return String.format("\\xrightarrow[%s]{%s}", lower, upper);
}
if (!upper.isEmpty()) {
return String.format("\\xrightarrow{%s}", upper);
}
if (!lower.isEmpty()) {
return String.format("\\xrightarrow[%s]{}", lower);
}
return "\\xrightarrow{}";
}
private static String handleLeftArrow(List<String> args) {
String upper = "";
String lower = "";
for (int i = 1; i < args.size(); ++i) {
String content;
String arg = args.get(i);
if (arg.startsWith("OVER_SCRIPT:")) {
upper = (upper + " " + arg.substring("OVER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("UNDER_SCRIPT:")) {
lower = (lower + " " + arg.substring("UNDER_SCRIPT:".length())).trim();
continue;
}
if (arg.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = arg.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
upper = (upper + " " + parts[0]).trim();
lower = (lower + " " + (parts.length > 1 ? parts[1] : "")).trim();
continue;
}
if (arg.startsWith("^{") && arg.endsWith("}")) {
content = arg.substring(2, arg.length() - 1);
upper = (upper + " " + content).trim();
continue;
}
if (!arg.startsWith("_{") || !arg.endsWith("}")) continue;
content = arg.substring(2, arg.length() - 1);
lower = (lower + " " + content).trim();
}
if (!upper.isEmpty() && !lower.isEmpty()) {
return String.format("\\xleftarrow[%s]{%s}", lower, upper);
}
if (!upper.isEmpty()) {
return String.format("\\xleftarrow{%s}", upper);
}
if (!lower.isEmpty()) {
return String.format("\\xleftarrow[%s]{}", lower);
}
return "\\xleftarrow{}";
}
private static String handleDisplacement(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
if (args.isEmpty()) {
return "";
}
return String.join((CharSequence)" ", args);
}
private static String handleUp(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
return String.format("^{%s}", String.join((CharSequence)",", args));
}
private static String handleDo(List<String> options, List<String> args, String mainCommandName, Map<String, Object> context) {
return String.format("_{%s}", String.join((CharSequence)",", args));
}
public static String replaceJbyuWithMeasurements(String input) {
String pattern = "(JBYU1JBYU)+";
Matcher matcher = Pattern.compile(pattern).matcher(input);
StringBuffer result = new StringBuffer();
while (matcher.find()) {
String match = matcher.group();
int count = match.length() / 4;
int groups = (count + 4) / 5;
StringBuilder replacement = new StringBuilder();
for (int i = 0; i < groups; ++i) {
replacement.append("\\\\underline{\\\\qquad}");
}
matcher.appendReplacement(result, replacement.toString());
}
matcher.appendTail(result);
return result.toString();
}
private static String handleLimitExpression(List<String> args) {
Matcher m;
if (args.size() < 2) {
return null;
}
String limitArg = args.get(1).trim();
Pattern nestedDoPattern = Pattern.compile("\\\\s\\\\do4\\(\\\\s\\\\do4\\(([^)]+)\\)\\)");
Matcher match = nestedDoPattern.matcher(limitArg);
if (match.find()) {
return String.format("\\lim_{%s}", match.group(1));
}
Pattern singleDoPattern = Pattern.compile("\\\\s\\\\do4\\(([^)]+)\\)");
Matcher singleMatch = singleDoPattern.matcher(limitArg);
if (singleMatch.find()) {
return String.format("\\lim_{%s}", singleMatch.group(1));
}
if (limitArg.startsWith("_{") && limitArg.endsWith("}")) {
return String.format("\\lim_{%s}", limitArg.substring(2, limitArg.length() - 1));
}
if (limitArg.matches("^_\\{_\\{([^}]+)}}$") && (m = Pattern.compile("^_\\{_\\{([^}]+)}}$").matcher(limitArg)).find()) {
return String.format("\\lim_{%s}", m.group(1));
}
Pattern anySubPattern = Pattern.compile("_\\{([^}]+)}");
Matcher anySubMatch = anySubPattern.matcher(limitArg);
if (anySubMatch.find()) {
Matcher inner;
String limitContent = anySubMatch.group(1);
if (limitContent.contains("_{") && limitContent.contains("}") && (inner = Pattern.compile("_\\{([^}]+)}").matcher(limitContent)).find()) {
limitContent = inner.group(1);
}
return String.format("\\lim_{%s}", limitContent);
}
if (!limitArg.isEmpty() && !limitArg.contains("\\")) {
return String.format("\\lim_{%s}", limitArg);
}
return null;
}
private static boolean isConsecutiveScript(String first, String second) {
if (first == null || second == null) {
return false;
}
String firstTrimmed = first.trim();
String secondTrimmed = second.trim();
boolean firstIsSuper = firstTrimmed.matches(".*\\\\?\\^\\{\\s*[^}]*\\s*}$");
boolean secondIsSuper = secondTrimmed.matches("^\\s*\\\\?\\^\\{.*");
boolean firstIsSub = firstTrimmed.matches(".*\\\\?_\\{\\s*[^}]*\\s*}$");
boolean secondIsSub = secondTrimmed.matches("^\\s*\\\\?_\\{.*");
return firstIsSuper && secondIsSuper || firstIsSub && secondIsSub;
}
private static String mergeConsecutiveScripts(String first, String second) {
if (EqToLatexUtil.isConsecutiveScript(first, second)) {
Matcher firstMatch = Pattern.compile("(\\\\?)(\\^|_)\\{([^}]*)}$").matcher(first.trim());
if (!firstMatch.find()) {
return first + second;
}
String firstBackslash = firstMatch.group(1);
String scriptType = firstMatch.group(2);
String firstContent = firstMatch.group(3);
Matcher secondMatch = Pattern.compile("^\\s*(\\\\?)(\\^|_)\\{([^}]*)}").matcher(second.trim());
if (!secondMatch.find() || !secondMatch.group(2).equals(scriptType)) {
return first + second;
}
String secondContent = secondMatch.group(3);
String mergedContent = firstContent + secondContent;
String beforeScript = first.replaceAll("(\\\\?)(\\^|_)\\{[^}]*}$", "");
return beforeScript + firstBackslash + scriptType + "{" + mergedContent + "}";
}
return first + second;
}
private static String postProcessConsecutiveScripts(String text) {
String previousText;
String result = text;
do {
previousText = result;
result = result.replaceAll("(\\\\?)\\^\\{([^}]*?)}\\s+(\\\\?)\\^\\{([^}]*?)}", "$1^{$2$4}");
} while (!(result = result.replaceAll("(\\\\?)_\\{([^}]*?)}\\s+(\\\\?)_\\{([^}]*?)}", "$1_{$2$4}")).equals(previousText));
return result;
}
private static boolean isScriptMarker(String str) {
if (str == null) {
return false;
}
return str.equals("OVERSET_ARROW") || str.equals("OVERSET_HAT") || str.startsWith("OVER_UNDER_SCRIPT:") || str.startsWith("OVER_SCRIPT:") || str.startsWith("UNDER_SCRIPT:") || str.matches("^\\^\\{.*}$") || str.matches("^_\\{.*}$") || str.matches("^(\\^\\{[^}]*})+$") || str.matches("^(_\\{[^}]*})+$") || str.matches("^(\\^\\{[^}]*}|_\\{[^}]*})+$");
}
private static MathJaxScripts extractMathJaxScripts(String mathjaxScript) {
MathJaxScripts result = new MathJaxScripts();
Matcher upMatcher = Pattern.compile("\\^\\{([^}]*)}").matcher(mathjaxScript);
while (upMatcher.find()) {
result.up = result.up + upMatcher.group(1);
}
Matcher downMatcher = Pattern.compile("_\\{([^}]*)}").matcher(mathjaxScript);
while (downMatcher.find()) {
result.down = result.down + downMatcher.group(1);
}
return result;
}
private static String resolveNestedScripts(String text) {
if (text == null) {
return null;
}
String result = text;
result = result.replaceAll("OVERSET_ARROW", "\\\\rightarrow");
result = result.replaceAll("OVERSET_HAT", "^");
Pattern p = Pattern.compile("OVER_UNDER_SCRIPT:([^ ]+)");
Matcher m = p.matcher(result);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String content = m.group(1);
String[] parts = content.split(",", 2);
String up = parts[0].trim();
String down = (parts.length > 1 ? parts[1] : "").trim();
m.appendReplacement(sb, "^{" + up + "}_{" + down + "}");
}
m.appendTail(sb);
result = sb.toString();
result = result.replaceAll("OVER_SCRIPT:([^ ]+)", "^{$1}");
result = result.replaceAll("UNDER_SCRIPT:([^ ]+)", "_{$1}");
return result;
}
private static boolean isSpecialScriptMarker(String str) {
return EqToLatexUtil.isScriptMarker(str);
}
private static String mergeSpecialScriptMarkers(String first, String second) {
String upFirst = "";
String downFirst = "";
if (first.startsWith("OVER_SCRIPT:")) {
upFirst = first.substring("OVER_SCRIPT:".length());
} else if (first.startsWith("UNDER_SCRIPT:")) {
downFirst = first.substring("UNDER_SCRIPT:".length());
} else if (first.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = first.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
upFirst = parts[0];
downFirst = parts.length > 1 ? parts[1] : "";
}
String upSecond = "";
String downSecond = "";
if (second.startsWith("OVER_SCRIPT:")) {
upSecond = second.substring("OVER_SCRIPT:".length());
} else if (second.startsWith("UNDER_SCRIPT:")) {
downSecond = second.substring("UNDER_SCRIPT:".length());
} else if (second.startsWith("OVER_UNDER_SCRIPT:")) {
String[] parts = second.substring("OVER_UNDER_SCRIPT:".length()).split(",", 2);
upSecond = parts[0];
downSecond = parts.length > 1 ? parts[1] : "";
}
String upMerged = (upFirst + upSecond).trim();
String downMerged = (downFirst + downSecond).trim();
if (!upMerged.isEmpty() && !downMerged.isEmpty()) {
return "OVER_UNDER_SCRIPT:" + upMerged + "," + downMerged;
}
if (!upMerged.isEmpty()) {
return "OVER_SCRIPT:" + upMerged;
}
if (!downMerged.isEmpty()) {
return "UNDER_SCRIPT:" + downMerged;
}
return first;
}
public static void main(String[] args) {
EqToLatexUtil.expect("P1-1", "eq \\o\\al(\\s\\up4(\u00b7),\\s\\do4(\u00b7))", "^{\\cdot}_{\\cdot}");
EqToLatexUtil.expect("P1-2", "eq \\o(Cl,\\s\\up6(\\s\\up4(\u00b7\u00b7)),\\s\\do4(\\s\\do4(\u00b7\u00b7)))", "\\underset{_{\\cdot \\cdot}}{\\overset{^{\\cdot \\cdot}}{Cl}}");
EqToLatexUtil.expect("P1-3", "eq \\o\\al(\\s\\up4(\u00b7),\\s\\do4(\u00b7))", "^{\\cdot}_{\\cdot}");
EqToLatexUtil.expect("P3-1", "\\JBYU2BEGINeq \\\\JBYU2END\\JBYU2BEGINo\\al(\\JBYU2END\\JBYU2BEGIN\\s\\up4(\u00b7)\\JBYU2END,\\JBYU2BEGIN\\s\\do4(\u00b7)\\JBYU2END)\\JBYU2END", "^{\\cdot}_{\\cdot}");
EqToLatexUtil.expect("P3-2", "\\JBYU2BEGINeq \\\\JBYU2END\\JBYU2BEGINo(Cl,\\s\\up6(\\JBYU2BEGIN\\s\\up4(\u00b7\u00b7)\\JBYU2END),\\s\\do4(\\JBYU2BEGIN\\s\\do4(\u00b7\u00b7)\\JBYU2END))\\JBYU2END", "\\underset{_{\\cdot \\cdot}}{\\overset{^{\\cdot \\cdot}}{Cl}}");
EqToLatexUtil.expect("P3-3", "\\JBYU2BEGINeq \\\\JBYU2END\\JBYU2BEGINo\\al(\\JBYU2END\\JBYU2BEGIN\\s\\up4(\u00b7)\\JBYU2END,\\JBYU2BEGIN\\s\\do4(\u00b7)\\JBYU2END)\\JBYU2END", "^{\\cdot}_{\\cdot}");
System.out.println("OK");
}
private static void expect(String label, String input, String expected) {
String actual = EqToLatexUtil.convert(input);
if (!expected.equals(actual)) {
throw new AssertionError((Object)(label + " expected " + expected + " but got " + actual));
}
}
static {
COMMAND_HANDLERS.put("f", EqToLatexUtil::handleFraction);
COMMAND_HANDLERS.put("r", EqToLatexUtil::handleRoot);
COMMAND_HANDLERS.put("i", EqToLatexUtil::handleIntegral);
COMMAND_HANDLERS.put("s", EqToLatexUtil::handleScript);
COMMAND_HANDLERS.put("b", EqToLatexUtil::handleBrackets);
COMMAND_HANDLERS.put("a", EqToLatexUtil::handleArray);
COMMAND_HANDLERS.put("x", EqToLatexUtil::handleBox);
COMMAND_HANDLERS.put("l", EqToLatexUtil::handleList);
COMMAND_HANDLERS.put("o", EqToLatexUtil::handleOverstrike);
COMMAND_HANDLERS.put("d", EqToLatexUtil::handleDisplacement);
COMMAND_HANDLERS.put("up", EqToLatexUtil::handleUp);
COMMAND_HANDLERS.put("do", EqToLatexUtil::handleDo);
}
private static class MathJaxScripts {
String up = "";
String down = "";
private MathJaxScripts() {
}
}
private static class ArgumentResult {
boolean success;
String content;
int endIndex;
ArgumentResult(boolean success, String content, int endIndex) {
this.success = success;
this.content = content;
this.endIndex = endIndex;
}
}
private static class ParseResult {
String output;
int newIndex;
ParseResult(String output, int newIndex) {
this.output = output;
this.newIndex = newIndex;
}
}
@FunctionalInterface
static interface CommandHandler {
public String handle(List<String> var1, List<String> var2, String var3, Map<String, Object> var4);
}
}