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.
@@ -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 '': r = '\\left\\|';
27
+ case '\u2016': r = '\\left\\|';
26
28
  break;
27
29
  case '{': r = '\\left\\{';
28
30
  break;
29
- case '': r = '\\left\\langle ';
31
+ case '\u27E8': r = '\\left\\langle ';
30
32
  break;
31
- case '': r = '\\left\\lfloor ';
33
+ case '\u230A': r = '\\left\\lfloor ';
32
34
  break;
33
- case '': r = '\\left\\lceil ';
35
+ case '\u2308': r = '\\left\\lceil ';
34
36
  break;
35
- case '': r = '\\left\\ulcorner ';
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 '': r = '\\right\\|';
51
+ case '\u2016': r = '\\right\\|';
50
52
  break;
51
53
  case '}': r = '\\right\\}';
52
54
  break;
53
- case '': r = ' \\right\\rangle';
55
+ case '\u27E9': r = ' \\right\\rangle';
54
56
  break;
55
- case '': r = ' \\right\\rfloor';
57
+ case '\u230B': r = ' \\right\\rfloor';
56
58
  break;
57
- case '': r = ' \\right\\rceil';
59
+ case '\u2309': r = ' \\right\\rceil';
58
60
  break;
59
- case '': r = ' \\right\\urcorner';
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
- if (typeof process !== 'undefined' && false) {
96
- if (shouldUseActiveX()) {
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'; // disable on-page scripts
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 doc = document.implementation.createHTMLDocument('');
117
- doc.open();
118
- doc.write(string);
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(html, 'text/html');
142
- return doc.querySelector('math');
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(/∑/g, "\\sum") // Tổng
614
- .replace(/∏/g, "\\prod") // Tích
615
- .replace(/∫/g, "\\int"); // Tích phân
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
- "∑": " \\sum ",
762
- "∏": " \\prod ",
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
- return operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
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
- let escapedOp = op;
953
- if (op === "{" || op === "}") {
954
- escapedOp = `\\${op}`;
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 parentNode = node.parentNode;
961
- const isInPower =
962
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
963
-
964
- let partToPush = "";
965
- if (stretchy && !isInPower) {
966
- partToPush = `\\right${escapedOp}`;
967
- } else {
968
- partToPush = escapedOp;
969
- }
970
- if (partToPush) {
971
- // CHỈ PUSH NẾU KHÔNG RỖNG
972
- parts.push(partToPush);
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
- // matched a left: remove corresponding left object
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
- if (escapedOp) {
978
- // CHỈ PUSH NẾu KHÔNG RỖNG
979
- parts.push(escapedOp);
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
- // ngoặc trái
984
- const parentNode = node.parentNode;
985
- const isInPower =
986
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
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
- partToPush = escapedOp;
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
- // khi không đóng tương ứng phía sau nếu có \right (hoặc dấu đóng) thì giữ \left
1091
+ // để tránh sinh LaTeX không hợp lệ (\left 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
- const left = leftObj.op;
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 ? `${base}_${parse(lastChild)}` : base;
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
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
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 overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
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
- baseText === "" &&
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
- return `${base}_{${under}}^{${over}}`;
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
- let prescripts = "";
1373
- let postscripts = "";
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
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1378
- i++;
1379
- if (i + 1 < childrenArray.length) {
1380
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1381
- childrenArray[i + 1]
1382
- )}}`;
1383
- i += 2;
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 (i + 1 < childrenArray.length) {
1387
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
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
- return `${base}${prescripts}${postscripts}`;
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
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
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 separators = NodeTool.getAttr(node, "separators", ",").split("");
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[index % separators.length]
1677
+ separators.length > 0
1557
1678
  ) {
1558
- parts.push(separators[index % separators.length]);
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[index % separators.length]
1692
+ separators.length > 0
1571
1693
  ) {
1572
- parts.push(separators[index % separators.length]);
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
- if (!close) return `\\left${open}${content}\\right.`;
1580
- if (!open) return `\\left.${content}\\right${close}`;
1581
- return `\\left${open}${content}\\right${close}`;
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);