ed-mathml2tex 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/mathml2latex.browser.cjs.js +369 -227
- package/lib/mathml2latex.browser.es.js +369 -227
- package/lib/mathml2latex.browser.umd.js +369 -227
- package/lib/mathml2latex.cjs.js +360 -227
- package/lib/mathml2latex.es.js +358 -227
- package/lib/mathml2latex.umd.js +362 -231
- package/package.json +1 -1
package/lib/mathml2latex.es.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import domino from 'domino';
|
|
2
|
+
|
|
1
3
|
const Brackets = {
|
|
2
|
-
left: ['(', '[', '{', '|', '
|
|
3
|
-
right: [')', ']', '}', '|', '
|
|
4
|
+
left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
|
|
5
|
+
right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
|
|
4
6
|
isPair: function(l, r){
|
|
5
7
|
const idx = this.left.indexOf(l);
|
|
6
8
|
return r === this.right[idx];
|
|
@@ -22,17 +24,17 @@ const Brackets = {
|
|
|
22
24
|
case '[':
|
|
23
25
|
case '|': r = `\\left${it}`;
|
|
24
26
|
break;
|
|
25
|
-
case '
|
|
27
|
+
case '\u2016': r = '\\left\\|';
|
|
26
28
|
break;
|
|
27
29
|
case '{': r = '\\left\\{';
|
|
28
30
|
break;
|
|
29
|
-
case '
|
|
31
|
+
case '\u27E8': r = '\\left\\langle ';
|
|
30
32
|
break;
|
|
31
|
-
case '
|
|
33
|
+
case '\u230A': r = '\\left\\lfloor ';
|
|
32
34
|
break;
|
|
33
|
-
case '
|
|
35
|
+
case '\u2308': r = '\\left\\lceil ';
|
|
34
36
|
break;
|
|
35
|
-
case '
|
|
37
|
+
case '\u231C': r = '\\left\\ulcorner ';
|
|
36
38
|
break;
|
|
37
39
|
}
|
|
38
40
|
return (stretchy ? r : r.replace('\\left', ''));
|
|
@@ -46,17 +48,17 @@ const Brackets = {
|
|
|
46
48
|
case ']':
|
|
47
49
|
case '|': r = `\\right${it}`;
|
|
48
50
|
break;
|
|
49
|
-
case '
|
|
51
|
+
case '\u2016': r = '\\right\\|';
|
|
50
52
|
break;
|
|
51
53
|
case '}': r = '\\right\\}';
|
|
52
54
|
break;
|
|
53
|
-
case '
|
|
55
|
+
case '\u27E9': r = ' \\right\\rangle';
|
|
54
56
|
break;
|
|
55
|
-
case '
|
|
57
|
+
case '\u230B': r = ' \\right\\rfloor';
|
|
56
58
|
break;
|
|
57
|
-
case '
|
|
59
|
+
case '\u2309': r = ' \\right\\rceil';
|
|
58
60
|
break;
|
|
59
|
-
case '
|
|
61
|
+
case '\u231D': r = ' \\right\\urcorner';
|
|
60
62
|
break;
|
|
61
63
|
}
|
|
62
64
|
return (stretchy ? r : r.replace('\\right', ''));
|
|
@@ -92,11 +94,17 @@ function canParseHTMLNatively () {
|
|
|
92
94
|
function createHTMLParser () {
|
|
93
95
|
const Parser = function () {};
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
const hasDocument =
|
|
98
|
+
typeof document !== 'undefined' &&
|
|
99
|
+
document &&
|
|
100
|
+
document.implementation &&
|
|
101
|
+
typeof document.implementation.createHTMLDocument === 'function';
|
|
102
|
+
|
|
103
|
+
if (hasDocument) {
|
|
104
|
+
if (typeof process !== 'undefined' && false && shouldUseActiveX()) {
|
|
97
105
|
Parser.prototype.parseFromString = function (string) {
|
|
98
106
|
const doc = new window.ActiveXObject('htmlfile');
|
|
99
|
-
doc.designMode = 'on';
|
|
107
|
+
doc.designMode = 'on';
|
|
100
108
|
doc.open();
|
|
101
109
|
doc.write(string);
|
|
102
110
|
doc.close();
|
|
@@ -113,11 +121,9 @@ function createHTMLParser () {
|
|
|
113
121
|
}
|
|
114
122
|
} else {
|
|
115
123
|
Parser.prototype.parseFromString = function (string) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
doc.close();
|
|
120
|
-
return doc
|
|
124
|
+
const domino$1 = domino;
|
|
125
|
+
const window = domino$1.createWindow(string || '');
|
|
126
|
+
return window.document
|
|
121
127
|
};
|
|
122
128
|
}
|
|
123
129
|
return Parser
|
|
@@ -135,11 +141,54 @@ function shouldUseActiveX () {
|
|
|
135
141
|
|
|
136
142
|
const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
|
|
137
143
|
|
|
144
|
+
function normalizeMathMLInput(input) {
|
|
145
|
+
let s = input == null ? '' : String(input);
|
|
146
|
+
|
|
147
|
+
if (s.includes('<math') && /\\[ntr"]/.test(s)) {
|
|
148
|
+
s = s
|
|
149
|
+
.replace(/\\r\\n/g, '\n')
|
|
150
|
+
.replace(/\\n/g, '\n')
|
|
151
|
+
.replace(/\\t/g, '\t')
|
|
152
|
+
.replace(/\\"/g, '"')
|
|
153
|
+
.replace(/\\'/g, "'");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
|
|
157
|
+
|
|
158
|
+
s = s.replace(
|
|
159
|
+
/xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
|
|
160
|
+
'xmlns="$2"'
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
s = s.replace(/xmlns\s*=\s*(["'])\s*`?\s*http:\/\/www\.w3\.org\/1998\/Math\/MathML\\`?\s*\1/gi, 'xmlns="http://www.w3.org/1998/Math/MathML"');
|
|
164
|
+
|
|
165
|
+
if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
|
|
166
|
+
s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return s;
|
|
170
|
+
}
|
|
171
|
+
|
|
138
172
|
const NodeTool = {
|
|
139
173
|
parseMath: function(html) {
|
|
174
|
+
const normalized = normalizeMathMLInput(html);
|
|
140
175
|
const parser = new HTMLParser();
|
|
141
|
-
const doc = parser.parseFromString(
|
|
142
|
-
|
|
176
|
+
const doc = parser.parseFromString(normalized, 'text/html');
|
|
177
|
+
let math = doc && doc.querySelector ? doc.querySelector('math') : null;
|
|
178
|
+
|
|
179
|
+
if (!math) {
|
|
180
|
+
const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
|
|
181
|
+
if (match) {
|
|
182
|
+
const retryDoc = parser.parseFromString(match[0], 'text/html');
|
|
183
|
+
math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!math) {
|
|
188
|
+
throw new Error('Invalid MathML: missing <math> root element');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return math;
|
|
143
192
|
},
|
|
144
193
|
getChildren: function(node) {
|
|
145
194
|
return node.children;
|
|
@@ -275,24 +324,24 @@ const MathSymbol = {
|
|
|
275
324
|
bigCommand: {
|
|
276
325
|
decimals: [8721, 8719, 8720, 10753, 10754, 10752, 8899, 8898, 10756, 10758, 8897, 8896, 8747, 8750, 8748, 8749, 10764, 8747],
|
|
277
326
|
scripts: [
|
|
278
|
-
"\\sum",
|
|
279
|
-
"\\prod",
|
|
280
|
-
"\\coprod",
|
|
281
|
-
"\\bigoplus",
|
|
282
|
-
"\\bigotimes",
|
|
283
|
-
"\\bigodot",
|
|
284
|
-
"\\bigcup",
|
|
285
|
-
"\\bigcap",
|
|
286
|
-
"\\biguplus",
|
|
287
|
-
"\\bigsqcup",
|
|
288
|
-
"\\bigvee",
|
|
289
|
-
"\\bigwedge",
|
|
290
|
-
"\\int",
|
|
291
|
-
"\\oint",
|
|
292
|
-
"\\iint",
|
|
293
|
-
"\\iiint",
|
|
294
|
-
"\\iiiint",
|
|
295
|
-
"\\idotsint",
|
|
327
|
+
"\\sum ",
|
|
328
|
+
"\\prod ",
|
|
329
|
+
"\\coprod ",
|
|
330
|
+
"\\bigoplus ",
|
|
331
|
+
"\\bigotimes ",
|
|
332
|
+
"\\bigodot ",
|
|
333
|
+
"\\bigcup ",
|
|
334
|
+
"\\bigcap ",
|
|
335
|
+
"\\biguplus ",
|
|
336
|
+
"\\bigsqcup ",
|
|
337
|
+
"\\bigvee ",
|
|
338
|
+
"\\bigwedge ",
|
|
339
|
+
"\\int ",
|
|
340
|
+
"\\oint ",
|
|
341
|
+
"\\iint ",
|
|
342
|
+
"\\iiint ",
|
|
343
|
+
"\\iiiint ",
|
|
344
|
+
"\\idotsint ",
|
|
296
345
|
]
|
|
297
346
|
},
|
|
298
347
|
|
|
@@ -610,9 +659,10 @@ function convert(mathmlHtml) {
|
|
|
610
659
|
// Thêm xử lý cho các thẻ MathML khác
|
|
611
660
|
result = result
|
|
612
661
|
.replace(/∞/g, "\\infty") // Vô cực
|
|
613
|
-
.replace(
|
|
614
|
-
|
|
615
|
-
|
|
662
|
+
.replace(/∫/g, " \\int "); // Tích phân
|
|
663
|
+
|
|
664
|
+
// Đảm bảo dấu cách sau các lệnh LaTeX quan trọng để tránh dính biến (vd: \intf -> \int f)
|
|
665
|
+
result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
|
|
616
666
|
|
|
617
667
|
return result;
|
|
618
668
|
}
|
|
@@ -758,20 +808,24 @@ function parseOperator(node) {
|
|
|
758
808
|
"±": " \\pm ",
|
|
759
809
|
"×": " \\times ",
|
|
760
810
|
"÷": " \\div ",
|
|
761
|
-
"∑": "
|
|
762
|
-
"∏": "
|
|
763
|
-
"∫": "\\int",
|
|
811
|
+
"∑": "\\sum ",
|
|
812
|
+
"∏": "\\prod ",
|
|
813
|
+
"∫": "\\int ",
|
|
764
814
|
"−": "-",
|
|
765
815
|
"≠": " \\neq ",
|
|
766
816
|
">": " > ",
|
|
767
817
|
"=": " = ",
|
|
818
|
+
"(": "(",
|
|
819
|
+
")": ")",
|
|
768
820
|
",": ", ", // Dấu phẩy trong tập hợp
|
|
769
821
|
";": ";",
|
|
770
822
|
Ω: "\\Omega",
|
|
771
823
|
"|": " \\mid ", // PATCH: set-builder mid
|
|
772
824
|
π: " \\pi ", // PATCH: Greek letter
|
|
825
|
+
"...": "\\dots",
|
|
773
826
|
};
|
|
774
|
-
|
|
827
|
+
const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
|
|
828
|
+
return res;
|
|
775
829
|
}
|
|
776
830
|
// --- END PATCH ---
|
|
777
831
|
|
|
@@ -798,6 +852,20 @@ function parseElementMi(node) {
|
|
|
798
852
|
// Math Number
|
|
799
853
|
function parseElementMn(node) {
|
|
800
854
|
let it = NodeTool.getNodeText(node).trim();
|
|
855
|
+
// Loại bỏ các ký tự điều khiển hoặc khoảng trắng lạ
|
|
856
|
+
it = it.replace(/[\u0000-\u001F\u007F-\u009F\u00A0]/g, "");
|
|
857
|
+
|
|
858
|
+
// Danh sách các hàm toán học mở rộng
|
|
859
|
+
const mathFunctions = ["cos", "sin", "tan", "cot", "arccos", "arcsin", "arctan", "arccot", "log", "ln", "lim", "sinh", "cosh", "tanh", "sec", "csc"];
|
|
860
|
+
|
|
861
|
+
if (mathFunctions.some(fn => it.toLowerCase().includes(fn))) {
|
|
862
|
+
// Tìm hàm khớp chính xác nhất
|
|
863
|
+
for (const fn of mathFunctions) {
|
|
864
|
+
if (it.toLowerCase() === fn) {
|
|
865
|
+
return "\\" + fn + " ";
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
801
869
|
return escapeSpecialChars(it);
|
|
802
870
|
}
|
|
803
871
|
|
|
@@ -831,8 +899,8 @@ function parseElementMtext(node) {
|
|
|
831
899
|
// Nếu đã là lệnh LaTeX thì giữ nguyên
|
|
832
900
|
if (content.startsWith("\\")) return content;
|
|
833
901
|
|
|
834
|
-
// Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH,
|
|
835
|
-
if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
|
|
902
|
+
// Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
|
|
903
|
+
if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
|
|
836
904
|
return `\\text{${content}}`;
|
|
837
905
|
}
|
|
838
906
|
|
|
@@ -887,6 +955,22 @@ function escapeSpecialChars(text) {
|
|
|
887
955
|
return text;
|
|
888
956
|
}
|
|
889
957
|
|
|
958
|
+
function wrapBaseForScript(base) {
|
|
959
|
+
const t = (base ?? "").trim();
|
|
960
|
+
if (!t) return "";
|
|
961
|
+
if (t.startsWith("{") && t.endsWith("}")) return t;
|
|
962
|
+
if (/^\\[a-zA-Z]+$/.test(t)) return t;
|
|
963
|
+
const needsWrap =
|
|
964
|
+
t.includes("_") ||
|
|
965
|
+
t.includes("^") ||
|
|
966
|
+
/\s/.test(t) ||
|
|
967
|
+
t.startsWith("\\left") ||
|
|
968
|
+
t.startsWith("\\right") ||
|
|
969
|
+
/\\[a-zA-Z]+/.test(t) ||
|
|
970
|
+
/[{}]/.test(t);
|
|
971
|
+
return needsWrap ? `{${t}}` : t;
|
|
972
|
+
}
|
|
973
|
+
|
|
890
974
|
function parseContainer(node, children) {
|
|
891
975
|
const render = getRender(node);
|
|
892
976
|
if (render) {
|
|
@@ -905,22 +989,6 @@ function renderChildren(children) {
|
|
|
905
989
|
// lefts là mảng object: { op: "(", index: 5 }
|
|
906
990
|
let lefts = [];
|
|
907
991
|
|
|
908
|
-
if (
|
|
909
|
-
children.length >= 3 &&
|
|
910
|
-
NodeTool.getNodeName(children[0]) === "mo" &&
|
|
911
|
-
NodeTool.getNodeText(children[0]).trim() === "{" &&
|
|
912
|
-
NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
|
|
913
|
-
NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
|
|
914
|
-
) {
|
|
915
|
-
// Render inner content
|
|
916
|
-
const innerContent = Array.prototype.slice
|
|
917
|
-
.call(children, 1, -1)
|
|
918
|
-
.map((child) => parse(child))
|
|
919
|
-
.join(""); // Chỉ trả về nếu nội dung không rỗng
|
|
920
|
-
const result = `\\left\\{${innerContent}\\right\\}`;
|
|
921
|
-
return result;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
992
|
Array.prototype.forEach.call(children, (node, idx) => {
|
|
925
993
|
// PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
|
|
926
994
|
if (
|
|
@@ -948,55 +1016,59 @@ function renderChildren(children) {
|
|
|
948
1016
|
if (Brackets.contains(op)) {
|
|
949
1017
|
let stretchy = NodeTool.getAttr(node, "stretchy", "true");
|
|
950
1018
|
stretchy = ["", "true"].indexOf(stretchy) > -1;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
|
|
1019
|
+
const parentNode = node.parentNode;
|
|
1020
|
+
const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
|
|
1021
|
+
if (isInPower) {
|
|
1022
|
+
stretchy = false;
|
|
955
1023
|
}
|
|
956
1024
|
|
|
957
1025
|
if (Brackets.isRight(op)) {
|
|
958
1026
|
const nearLeftObj = lefts[lefts.length - 1];
|
|
959
|
-
if (nearLeftObj) {
|
|
960
|
-
const
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
parts.
|
|
1027
|
+
if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
|
|
1028
|
+
const leftIdx = nearLeftObj.index;
|
|
1029
|
+
const leftRendered =
|
|
1030
|
+
typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
|
|
1031
|
+
let rightRendered = Brackets.parseRight(op, stretchy);
|
|
1032
|
+
|
|
1033
|
+
const leftRenderedTrimmed = leftRendered.trim();
|
|
1034
|
+
const rightRenderedTrimmed = String(rightRendered).trim();
|
|
1035
|
+
|
|
1036
|
+
if (
|
|
1037
|
+
leftRenderedTrimmed.startsWith("\\left") &&
|
|
1038
|
+
!rightRenderedTrimmed.startsWith("\\right")
|
|
1039
|
+
) {
|
|
1040
|
+
parts[leftIdx] = leftRendered.replace("\\left", "");
|
|
1041
|
+
} else if (
|
|
1042
|
+
!leftRenderedTrimmed.startsWith("\\left") &&
|
|
1043
|
+
rightRenderedTrimmed.startsWith("\\right")
|
|
1044
|
+
) {
|
|
1045
|
+
rightRendered = String(rightRendered).replace("\\right", "");
|
|
973
1046
|
}
|
|
974
|
-
|
|
1047
|
+
|
|
1048
|
+
if (rightRendered) parts.push(rightRendered);
|
|
975
1049
|
lefts.pop();
|
|
1050
|
+
} else if (Brackets.isLeft(op)) {
|
|
1051
|
+
// If it's a Right bracket but doesn't match the current Left,
|
|
1052
|
+
// AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
|
|
1053
|
+
const partToPush = Brackets.parseLeft(op, stretchy);
|
|
1054
|
+
if (partToPush) parts.push(partToPush);
|
|
1055
|
+
lefts.push({ op: op, index: parts.length - 1 });
|
|
976
1056
|
} else {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1057
|
+
// Unmatched right bracket
|
|
1058
|
+
let rightRendered = Brackets.parseRight(op, stretchy);
|
|
1059
|
+
rightRendered = String(rightRendered).replace("\\right", "");
|
|
1060
|
+
if (rightRendered) parts.push(rightRendered);
|
|
981
1061
|
}
|
|
982
1062
|
} else {
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
let partToPush = "";
|
|
989
|
-
if (stretchy && !isInPower) {
|
|
990
|
-
partToPush = `\\left${escapedOp}`;
|
|
1063
|
+
// Must be Left bracket (or only Left)
|
|
1064
|
+
if (Brackets.isLeft(op)) {
|
|
1065
|
+
const partToPush = Brackets.parseLeft(op, stretchy);
|
|
1066
|
+
if (partToPush) parts.push(partToPush);
|
|
1067
|
+
lefts.push({ op: op, index: parts.length - 1 });
|
|
991
1068
|
} else {
|
|
992
|
-
|
|
1069
|
+
// Should not happen if Brackets.contains is correct
|
|
1070
|
+
if (op) parts.push(op);
|
|
993
1071
|
}
|
|
994
|
-
if (partToPush) {
|
|
995
|
-
// CHỈ PUSH NẾU KHÔNG RỖNG
|
|
996
|
-
parts.push(partToPush);
|
|
997
|
-
}
|
|
998
|
-
// Lưu vị trí token left vừa push để có thể xử lý nếu không có right
|
|
999
|
-
lefts.push({ op: op, index: parts.length - 1 });
|
|
1000
1072
|
}
|
|
1001
1073
|
} else {
|
|
1002
1074
|
const parsedOperator = parseOperator(node);
|
|
@@ -1005,45 +1077,6 @@ function renderChildren(children) {
|
|
|
1005
1077
|
parts.push(parsedOperator);
|
|
1006
1078
|
}
|
|
1007
1079
|
}
|
|
1008
|
-
|
|
1009
|
-
// --- START PATCH V6 (Giữ nguyên logic V5) ---
|
|
1010
|
-
} else if (NodeTool.getNodeName(node) === "msub") {
|
|
1011
|
-
const subChildren = Array.from(NodeTool.getChildren(node));
|
|
1012
|
-
if (
|
|
1013
|
-
subChildren.length === 2 &&
|
|
1014
|
-
NodeTool.getNodeName(subChildren[0]) === "mo" &&
|
|
1015
|
-
NodeTool.getNodeText(subChildren[0]).trim() === ")"
|
|
1016
|
-
) {
|
|
1017
|
-
// ĐÚNG LÀ NGOẠI LỆ
|
|
1018
|
-
|
|
1019
|
-
const sub = parse(subChildren[1]);
|
|
1020
|
-
// Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
|
|
1021
|
-
const lastPart = parts.pop(); // lastPart = "\text{OH}"
|
|
1022
|
-
// Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
|
|
1023
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
1024
|
-
if (parts[i] && parts[i].trim() === "\\left(") {
|
|
1025
|
-
parts.splice(i, 1); // Xóa "\left("
|
|
1026
|
-
break;
|
|
1027
|
-
}
|
|
1028
|
-
_;
|
|
1029
|
-
} // Mảng 'parts' lúc này là: ["\text{Cu}"]
|
|
1030
|
-
|
|
1031
|
-
parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
|
|
1032
|
-
// Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
|
|
1033
|
-
|
|
1034
|
-
const nearLeftObj = lefts[lefts.length - 1];
|
|
1035
|
-
if (nearLeftObj) {
|
|
1036
|
-
lefts.pop();
|
|
1037
|
-
}
|
|
1038
|
-
} else {
|
|
1039
|
-
// <msub> bình thường
|
|
1040
|
-
const parsed = parse(node);
|
|
1041
|
-
if (parsed) {
|
|
1042
|
-
// CHỈ PUSH NẾU KHÔNG RỖNG
|
|
1043
|
-
parts.push(parsed);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
// --- END PATCH V6 ---
|
|
1047
1080
|
} else {
|
|
1048
1081
|
// Các node khác như <mtext>, #text, v.v.
|
|
1049
1082
|
const parsed = parse(node);
|
|
@@ -1055,40 +1088,13 @@ function renderChildren(children) {
|
|
|
1055
1088
|
});
|
|
1056
1089
|
|
|
1057
1090
|
// Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
|
|
1058
|
-
//
|
|
1091
|
+
// để tránh sinh LaTeX không hợp lệ (\left mà không có \right)
|
|
1059
1092
|
if (lefts && lefts.length > 0) {
|
|
1060
|
-
const rightForLeft = (left) => {
|
|
1061
|
-
switch (left) {
|
|
1062
|
-
case "(":
|
|
1063
|
-
return ")";
|
|
1064
|
-
case "[":
|
|
1065
|
-
return "]";
|
|
1066
|
-
case "{":
|
|
1067
|
-
return "}";
|
|
1068
|
-
case "|":
|
|
1069
|
-
return "|";
|
|
1070
|
-
default:
|
|
1071
|
-
return ".";
|
|
1072
|
-
}
|
|
1073
|
-
};
|
|
1074
|
-
|
|
1075
1093
|
lefts.forEach((leftObj) => {
|
|
1076
1094
|
const idx = leftObj && leftObj.index;
|
|
1077
1095
|
if (typeof idx !== "number" || !parts[idx]) return;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const right = rightForLeft(left);
|
|
1081
|
-
|
|
1082
|
-
// Kiểm tra phần tử sau vị trí left có chứa \right hoặc ký tự đóng tương ứng hay không
|
|
1083
|
-
const tail = parts.slice(idx + 1).map(String).join(" ");
|
|
1084
|
-
const hasMatchingRight =
|
|
1085
|
-
(/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
|
|
1086
|
-
|
|
1087
|
-
if (!hasMatchingRight) {
|
|
1088
|
-
// Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
|
|
1089
|
-
parts[idx] = String(parts[idx]).replace(/^\\left/, "");
|
|
1090
|
-
}
|
|
1091
|
-
// Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
|
|
1096
|
+
// Use regex with whitespace support and global flag just in case
|
|
1097
|
+
parts[idx] = String(parts[idx]).replace(/\\left/g, "");
|
|
1092
1098
|
});
|
|
1093
1099
|
}
|
|
1094
1100
|
|
|
@@ -1096,6 +1102,44 @@ function renderChildren(children) {
|
|
|
1096
1102
|
return parts;
|
|
1097
1103
|
}
|
|
1098
1104
|
|
|
1105
|
+
function isLimitOperator(base) {
|
|
1106
|
+
const t = (base || "").trim();
|
|
1107
|
+
const ops = [
|
|
1108
|
+
"\\sum",
|
|
1109
|
+
"\\prod",
|
|
1110
|
+
"\\coprod",
|
|
1111
|
+
"\\int",
|
|
1112
|
+
"\\oint",
|
|
1113
|
+
"\\bigcap",
|
|
1114
|
+
"\\bigcup",
|
|
1115
|
+
"\\bigsqcup",
|
|
1116
|
+
"\\bigvee",
|
|
1117
|
+
"\\bigwedge",
|
|
1118
|
+
"\\bigodot",
|
|
1119
|
+
"\\bigotimes",
|
|
1120
|
+
"\\bigoplus",
|
|
1121
|
+
"\\biguplus",
|
|
1122
|
+
"\\lim",
|
|
1123
|
+
"\\max",
|
|
1124
|
+
"\\min",
|
|
1125
|
+
"\\sup",
|
|
1126
|
+
"\\inf",
|
|
1127
|
+
"\\det",
|
|
1128
|
+
"\\gcd",
|
|
1129
|
+
"\\Pr",
|
|
1130
|
+
"\\limsup",
|
|
1131
|
+
"\\liminf",
|
|
1132
|
+
];
|
|
1133
|
+
return ops.some(
|
|
1134
|
+
(op) =>
|
|
1135
|
+
t === op ||
|
|
1136
|
+
t.startsWith(op + " ") ||
|
|
1137
|
+
t.startsWith(op + "\\") ||
|
|
1138
|
+
t.startsWith(op + "^") ||
|
|
1139
|
+
t.startsWith(op + "_")
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1099
1143
|
function getRender(node) {
|
|
1100
1144
|
let render = undefined;
|
|
1101
1145
|
const nodeName = NodeTool.getNodeName(node);
|
|
@@ -1244,9 +1288,11 @@ function getRender(node) {
|
|
|
1244
1288
|
NodeTool.getNodeText(sub.firstChild).trim() === "")
|
|
1245
1289
|
) {
|
|
1246
1290
|
const lastChild = sub.lastChild;
|
|
1247
|
-
return lastChild
|
|
1291
|
+
return lastChild
|
|
1292
|
+
? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
|
|
1293
|
+
: base;
|
|
1248
1294
|
}
|
|
1249
|
-
return `${base}_{${parse(sub)}}`;
|
|
1295
|
+
return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
|
|
1250
1296
|
};
|
|
1251
1297
|
break;
|
|
1252
1298
|
|
|
@@ -1254,22 +1300,9 @@ function getRender(node) {
|
|
|
1254
1300
|
render = function (node, children) {
|
|
1255
1301
|
const childrenArray = Array.from(children);
|
|
1256
1302
|
if (!childrenArray || childrenArray.length < 2) return "";
|
|
1257
|
-
|
|
1258
|
-
const baseNode = childrenArray[0];
|
|
1259
|
-
let base = parse(baseNode) || "";
|
|
1260
|
-
if (NodeTool.getNodeName(baseNode) === "mo") {
|
|
1261
|
-
const op = NodeTool.getNodeText(baseNode).trim();
|
|
1262
|
-
if (Brackets.isRight(op)) {
|
|
1263
|
-
// Escape brace characters
|
|
1264
|
-
if (op === "}" || op === "{") {
|
|
1265
|
-
base = `\\right\\${op}`;
|
|
1266
|
-
} else {
|
|
1267
|
-
base = `\\right${op}`;
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1303
|
+
const base = parse(childrenArray[0]) || "";
|
|
1271
1304
|
const sup = parse(childrenArray[1]) || "";
|
|
1272
|
-
return `${base}^{${sup}}`;
|
|
1305
|
+
return `${wrapBaseForScript(base)}^{${sup}}`;
|
|
1273
1306
|
};
|
|
1274
1307
|
break;
|
|
1275
1308
|
|
|
@@ -1293,7 +1326,7 @@ function getRender(node) {
|
|
|
1293
1326
|
.join("");
|
|
1294
1327
|
return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
|
|
1295
1328
|
}
|
|
1296
|
-
return `${base}_{${sub}}^{${sup}}`;
|
|
1329
|
+
return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
|
|
1297
1330
|
};
|
|
1298
1331
|
break;
|
|
1299
1332
|
|
|
@@ -1303,9 +1336,15 @@ function getRender(node) {
|
|
|
1303
1336
|
if (!childrenArray || childrenArray.length < 2) return "";
|
|
1304
1337
|
const base = parse(childrenArray[0]) || "";
|
|
1305
1338
|
const over = parse(childrenArray[1]) || "";
|
|
1306
|
-
const
|
|
1339
|
+
const overNode = childrenArray[1];
|
|
1340
|
+
const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
|
|
1341
|
+
const overText = NodeTool.getNodeText(overNode)?.trim() || "";
|
|
1307
1342
|
const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
|
|
1308
1343
|
|
|
1344
|
+
// Handle arrows with extensible commands
|
|
1345
|
+
if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
|
|
1346
|
+
if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
|
|
1347
|
+
|
|
1309
1348
|
// Handle biology notation (double overline)
|
|
1310
1349
|
if (overText === "¯") {
|
|
1311
1350
|
const parentNode = node.parentNode;
|
|
@@ -1321,6 +1360,27 @@ function getRender(node) {
|
|
|
1321
1360
|
|
|
1322
1361
|
if (overText === "→" && isAccent) return `\\vec{${base}}`;
|
|
1323
1362
|
if (overText === "^" && isAccent) return `\\hat{${base}}`;
|
|
1363
|
+
if (overText === "\u23DE") return `\\overbrace{${base}}`;
|
|
1364
|
+
|
|
1365
|
+
// Check for nested overbrace (layer-2)
|
|
1366
|
+
if (NodeTool.getNodeName(overNode) === "mover") {
|
|
1367
|
+
const innerChildren = Array.from(NodeTool.getChildren(overNode));
|
|
1368
|
+
if (innerChildren.length >= 2) {
|
|
1369
|
+
const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
|
|
1370
|
+
if (innerBaseText === "\u23DE") {
|
|
1371
|
+
const label = parse(innerChildren[1]);
|
|
1372
|
+
return `\\overbrace{${base}}\\limits^{${label}}`;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
|
|
1378
|
+
return `${base}\\limits^{${over}}`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (isLimitOperator(base)) {
|
|
1382
|
+
return `${base}\\limits^{${over}}`;
|
|
1383
|
+
}
|
|
1324
1384
|
return `\\overset{${over}}{${base}}`;
|
|
1325
1385
|
};
|
|
1326
1386
|
break;
|
|
@@ -1331,10 +1391,21 @@ function getRender(node) {
|
|
|
1331
1391
|
if (!childrenArray || childrenArray.length < 2) return "";
|
|
1332
1392
|
const base = parse(childrenArray[0]) || "";
|
|
1333
1393
|
const under = parse(childrenArray[1]) || "";
|
|
1394
|
+
const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
|
|
1395
|
+
const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
|
|
1334
1396
|
const isUnderAccent =
|
|
1335
1397
|
NodeTool.getAttr(node, "accentunder", "false") === "true";
|
|
1336
1398
|
|
|
1399
|
+
// Handle arrows with extensible commands
|
|
1400
|
+
if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
|
|
1401
|
+
if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
|
|
1402
|
+
|
|
1337
1403
|
if (base === "∫") return `\\int_{${under}}`;
|
|
1404
|
+
if (underText === "\u23DF") return `\\underbrace{${base}}`;
|
|
1405
|
+
|
|
1406
|
+
if (isLimitOperator(base)) {
|
|
1407
|
+
return `${base}\\limits_{${under}}`;
|
|
1408
|
+
}
|
|
1338
1409
|
return `\\underset{${under}}{${base}}`;
|
|
1339
1410
|
};
|
|
1340
1411
|
break;
|
|
@@ -1348,19 +1419,19 @@ function getRender(node) {
|
|
|
1348
1419
|
const over = parse(childrenArray[2]);
|
|
1349
1420
|
const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
|
|
1350
1421
|
|
|
1351
|
-
// Special handling for chemical reaction arrow
|
|
1352
|
-
if (
|
|
1353
|
-
|
|
1354
|
-
NodeTool.getNodeName(childrenArray[1]) === "msup"
|
|
1355
|
-
) {
|
|
1356
|
-
return `\\xrightarrow[${under}]{${over}}`;
|
|
1357
|
-
}
|
|
1422
|
+
// Special handling for chemical reaction arrow and other arrows
|
|
1423
|
+
if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
|
|
1424
|
+
if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
|
|
1358
1425
|
|
|
1359
1426
|
if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
|
|
1360
1427
|
if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
|
|
1361
1428
|
if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
|
|
1362
1429
|
if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
|
|
1363
|
-
|
|
1430
|
+
|
|
1431
|
+
if (isLimitOperator(base)) {
|
|
1432
|
+
return `${base}\\limits_{${under}}^{${over}}`;
|
|
1433
|
+
}
|
|
1434
|
+
return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
|
|
1364
1435
|
};
|
|
1365
1436
|
break;
|
|
1366
1437
|
|
|
@@ -1368,30 +1439,56 @@ function getRender(node) {
|
|
|
1368
1439
|
render = function (node, children) {
|
|
1369
1440
|
const childrenArray = Array.from(children);
|
|
1370
1441
|
if (!childrenArray || childrenArray.length < 1) return "";
|
|
1371
|
-
const base = parse(childrenArray[0]);
|
|
1372
|
-
|
|
1373
|
-
|
|
1442
|
+
const base = parse(childrenArray[0]) || "";
|
|
1443
|
+
|
|
1444
|
+
const postSub = [];
|
|
1445
|
+
const postSup = [];
|
|
1446
|
+
const preSub = [];
|
|
1447
|
+
const preSup = [];
|
|
1448
|
+
|
|
1374
1449
|
let i = 1;
|
|
1450
|
+
let inPrescripts = false;
|
|
1375
1451
|
|
|
1376
1452
|
while (i < childrenArray.length) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1453
|
+
const name = NodeTool.getNodeName(childrenArray[i]);
|
|
1454
|
+
if (name === "mprescripts") {
|
|
1455
|
+
inPrescripts = true;
|
|
1456
|
+
i += 1;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const subNode = childrenArray[i];
|
|
1461
|
+
const supNode = childrenArray[i + 1];
|
|
1462
|
+
if (!subNode || !supNode) break;
|
|
1463
|
+
|
|
1464
|
+
const sub = parse(subNode) || "";
|
|
1465
|
+
const sup = parse(supNode) || "";
|
|
1466
|
+
|
|
1467
|
+
if (inPrescripts) {
|
|
1468
|
+
if (sub) preSub.push(sub);
|
|
1469
|
+
if (sup) preSup.push(sup);
|
|
1385
1470
|
} else {
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1388
|
-
childrenArray[i + 1]
|
|
1389
|
-
)}}`;
|
|
1390
|
-
i += 2;
|
|
1391
|
-
} else break;
|
|
1471
|
+
if (sub) postSub.push(sub);
|
|
1472
|
+
if (sup) postSup.push(sup);
|
|
1392
1473
|
}
|
|
1474
|
+
|
|
1475
|
+
i += 2;
|
|
1393
1476
|
}
|
|
1394
|
-
|
|
1477
|
+
|
|
1478
|
+
const preSubStr = preSub.join(" ");
|
|
1479
|
+
const preSupStr = preSup.join(" ");
|
|
1480
|
+
const postSubStr = postSub.join(" ");
|
|
1481
|
+
const postSupStr = postSup.join(" ");
|
|
1482
|
+
|
|
1483
|
+
let pre = "";
|
|
1484
|
+
if (preSubStr) pre += `_{${preSubStr}}`;
|
|
1485
|
+
if (preSupStr) pre += `^{${preSupStr}}`;
|
|
1486
|
+
|
|
1487
|
+
let post = "";
|
|
1488
|
+
if (postSubStr) post += `_{${postSubStr}}`;
|
|
1489
|
+
if (postSupStr) post += `^{${postSupStr}}`;
|
|
1490
|
+
|
|
1491
|
+
return `${pre}${wrapBaseForScript(base)}${post}`;
|
|
1395
1492
|
};
|
|
1396
1493
|
break;
|
|
1397
1494
|
|
|
@@ -1492,7 +1589,28 @@ function getRender(node) {
|
|
|
1492
1589
|
const num = parse(childrenArray[0]);
|
|
1493
1590
|
const den = parse(childrenArray[1]);
|
|
1494
1591
|
const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
|
|
1495
|
-
|
|
1592
|
+
const bevelled = NodeTool.getAttr(node, "bevelled", "false");
|
|
1593
|
+
|
|
1594
|
+
if (bevelled === "true") {
|
|
1595
|
+
return `{}^{${num}}/_{${den}}`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (["0", "0px"].indexOf(linethickness) > -1) {
|
|
1599
|
+
const prevNode = NodeTool.getPrevNode(node);
|
|
1600
|
+
const nextNode = NodeTool.getNextNode(node);
|
|
1601
|
+
if (
|
|
1602
|
+
prevNode &&
|
|
1603
|
+
NodeTool.getNodeName(prevNode) === "mo" &&
|
|
1604
|
+
NodeTool.getNodeText(prevNode).trim() === "(" &&
|
|
1605
|
+
nextNode &&
|
|
1606
|
+
NodeTool.getNodeName(nextNode) === "mo" &&
|
|
1607
|
+
NodeTool.getNodeText(nextNode).trim() === ")"
|
|
1608
|
+
) {
|
|
1609
|
+
return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
|
|
1610
|
+
}
|
|
1611
|
+
return `{}_{${den}}^{${num}}`;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1496
1614
|
return `\\frac{${num}}{${den}}`;
|
|
1497
1615
|
};
|
|
1498
1616
|
break;
|
|
@@ -1502,7 +1620,10 @@ function getRender(node) {
|
|
|
1502
1620
|
const childrenArray = Array.from(children);
|
|
1503
1621
|
const open = NodeTool.getAttr(node, "open", "(");
|
|
1504
1622
|
const close = NodeTool.getAttr(node, "close", ")");
|
|
1505
|
-
const
|
|
1623
|
+
const separatorsStr = NodeTool.getAttr(node, "separators", ",");
|
|
1624
|
+
const separators = separatorsStr
|
|
1625
|
+
.split("")
|
|
1626
|
+
.filter((c) => c.trim().length === 1);
|
|
1506
1627
|
|
|
1507
1628
|
// Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
|
|
1508
1629
|
if (open === "|") {
|
|
@@ -1553,9 +1674,10 @@ function getRender(node) {
|
|
|
1553
1674
|
parts.push(parse(child));
|
|
1554
1675
|
if (
|
|
1555
1676
|
index < childrenArray.length - 1 &&
|
|
1556
|
-
separators
|
|
1677
|
+
separators.length > 0
|
|
1557
1678
|
) {
|
|
1558
|
-
|
|
1679
|
+
const sep = separators[index] ?? separators[separators.length - 1];
|
|
1680
|
+
if (sep) parts.push(sep);
|
|
1559
1681
|
}
|
|
1560
1682
|
});
|
|
1561
1683
|
return `\\left[${parts.join("")}\\right)`;
|
|
@@ -1567,18 +1689,20 @@ function getRender(node) {
|
|
|
1567
1689
|
parts.push(parse(child));
|
|
1568
1690
|
if (
|
|
1569
1691
|
index < childrenArray.length - 1 &&
|
|
1570
|
-
separators
|
|
1692
|
+
separators.length > 0
|
|
1571
1693
|
) {
|
|
1572
|
-
|
|
1694
|
+
const sep = separators[index] ?? separators[separators.length - 1];
|
|
1695
|
+
if (sep) parts.push(sep);
|
|
1573
1696
|
}
|
|
1574
1697
|
});
|
|
1575
1698
|
const content = parts.join("");
|
|
1576
1699
|
|
|
1577
1700
|
if (open === "{" && close === "}") return `\\{${content}\\}`;
|
|
1578
1701
|
if (open === "|" && close === "|") return `\\left|${content}\\right|`;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
return
|
|
1702
|
+
const left = open ? Brackets.parseLeft(open) : "";
|
|
1703
|
+
const right = close ? Brackets.parseRight(close) : "";
|
|
1704
|
+
if (!close && open) return `${left}${content}\\right.`;
|
|
1705
|
+
return `${left}${content}${right}`;
|
|
1582
1706
|
};
|
|
1583
1707
|
break;
|
|
1584
1708
|
|
|
@@ -1604,10 +1728,17 @@ function getRender(node) {
|
|
|
1604
1728
|
case "mn":
|
|
1605
1729
|
case "mo":
|
|
1606
1730
|
case "ms":
|
|
1607
|
-
case "mtext":
|
|
1608
1731
|
render = getRender_joinSeparator("@content");
|
|
1609
1732
|
break;
|
|
1610
1733
|
|
|
1734
|
+
case "mtext":
|
|
1735
|
+
render = function (node, children) {
|
|
1736
|
+
const childrenArray = Array.from(children);
|
|
1737
|
+
const content = renderChildren(childrenArray).join("");
|
|
1738
|
+
return `\\text{${content}}`;
|
|
1739
|
+
};
|
|
1740
|
+
break;
|
|
1741
|
+
|
|
1611
1742
|
case "mphantom":
|
|
1612
1743
|
render = function (node, children) {
|
|
1613
1744
|
const childrenArray = Array.from(children);
|