ed-mathml2tex 0.2.3 → 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;
@@ -613,7 +662,7 @@ function convert(mathmlHtml) {
613
662
  .replace(/∫/g, " \\int "); // Tích phân
614
663
 
615
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)
616
- result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
665
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
617
666
 
618
667
  return result;
619
668
  }
@@ -759,18 +808,21 @@ function parseOperator(node) {
759
808
  "±": " \\pm ",
760
809
  "×": " \\times ",
761
810
  "÷": " \\div ",
762
- "∑": " \\sum ",
763
- "∏": " \\prod ",
764
- "∫": " \\int ",
811
+ "∑": "\\sum ",
812
+ "∏": "\\prod ",
813
+ "∫": "\\int ",
765
814
  "−": "-",
766
815
  "≠": " \\neq ",
767
816
  ">": " > ",
768
817
  "=": " = ",
818
+ "(": "(",
819
+ ")": ")",
769
820
  ",": ", ", // Dấu phẩy trong tập hợp
770
821
  ";": ";",
771
822
  Ω: "\\Omega",
772
823
  "|": " \\mid ", // PATCH: set-builder mid
773
824
  π: " \\pi ", // PATCH: Greek letter
825
+ "...": "\\dots",
774
826
  };
775
827
  const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
776
828
  return res;
@@ -847,8 +899,8 @@ function parseElementMtext(node) {
847
899
  // Nếu đã là lệnh LaTeX thì giữ nguyên
848
900
  if (content.startsWith("\\")) return content;
849
901
 
850
- // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
851
- 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)) {
852
904
  return `\\text{${content}}`;
853
905
  }
854
906
 
@@ -903,6 +955,22 @@ function escapeSpecialChars(text) {
903
955
  return text;
904
956
  }
905
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
+
906
974
  function parseContainer(node, children) {
907
975
  const render = getRender(node);
908
976
  if (render) {
@@ -921,22 +989,6 @@ function renderChildren(children) {
921
989
  // lefts là mảng object: { op: "(", index: 5 }
922
990
  let lefts = [];
923
991
 
924
- if (
925
- children.length >= 3 &&
926
- NodeTool.getNodeName(children[0]) === "mo" &&
927
- NodeTool.getNodeText(children[0]).trim() === "{" &&
928
- NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
929
- NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
930
- ) {
931
- // Render inner content
932
- const innerContent = Array.prototype.slice
933
- .call(children, 1, -1)
934
- .map((child) => parse(child))
935
- .join(""); // Chỉ trả về nếu nội dung không rỗng
936
- const result = `\\left\\{${innerContent}\\right\\}`;
937
- return result;
938
- }
939
-
940
992
  Array.prototype.forEach.call(children, (node, idx) => {
941
993
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
942
994
  if (
@@ -964,55 +1016,59 @@ function renderChildren(children) {
964
1016
  if (Brackets.contains(op)) {
965
1017
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
966
1018
  stretchy = ["", "true"].indexOf(stretchy) > -1;
967
-
968
- let escapedOp = op;
969
- if (op === "{" || op === "}") {
970
- escapedOp = `\\${op}`;
1019
+ const parentNode = node.parentNode;
1020
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1021
+ if (isInPower) {
1022
+ stretchy = false;
971
1023
  }
972
1024
 
973
1025
  if (Brackets.isRight(op)) {
974
1026
  const nearLeftObj = lefts[lefts.length - 1];
975
- if (nearLeftObj) {
976
- const parentNode = node.parentNode;
977
- const isInPower =
978
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
979
-
980
- let partToPush = "";
981
- if (stretchy && !isInPower) {
982
- partToPush = `\\right${escapedOp}`;
983
- } else {
984
- partToPush = escapedOp;
985
- }
986
- if (partToPush) {
987
- // CHỈ PUSH NẾU KHÔNG RỖNG
988
- 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", "");
989
1046
  }
990
- // matched a left: remove corresponding left object
1047
+
1048
+ if (rightRendered) parts.push(rightRendered);
991
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 });
992
1056
  } else {
993
- if (escapedOp) {
994
- // CHỈ PUSH NẾu KHÔNG RỖNG
995
- parts.push(escapedOp);
996
- }
1057
+ // Unmatched right bracket
1058
+ let rightRendered = Brackets.parseRight(op, stretchy);
1059
+ rightRendered = String(rightRendered).replace("\\right", "");
1060
+ if (rightRendered) parts.push(rightRendered);
997
1061
  }
998
1062
  } else {
999
- // ngoặc trái
1000
- const parentNode = node.parentNode;
1001
- const isInPower =
1002
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
1003
-
1004
- let partToPush = "";
1005
- if (stretchy && !isInPower) {
1006
- 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 });
1007
1068
  } else {
1008
- partToPush = escapedOp;
1069
+ // Should not happen if Brackets.contains is correct
1070
+ if (op) parts.push(op);
1009
1071
  }
1010
- if (partToPush) {
1011
- // CHỈ PUSH NẾU KHÔNG RỖNG
1012
- parts.push(partToPush);
1013
- }
1014
- // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1015
- lefts.push({ op: op, index: parts.length - 1 });
1016
1072
  }
1017
1073
  } else {
1018
1074
  const parsedOperator = parseOperator(node);
@@ -1021,45 +1077,6 @@ function renderChildren(children) {
1021
1077
  parts.push(parsedOperator);
1022
1078
  }
1023
1079
  }
1024
-
1025
- // --- START PATCH V6 (Giữ nguyên logic V5) ---
1026
- } else if (NodeTool.getNodeName(node) === "msub") {
1027
- const subChildren = Array.from(NodeTool.getChildren(node));
1028
- if (
1029
- subChildren.length === 2 &&
1030
- NodeTool.getNodeName(subChildren[0]) === "mo" &&
1031
- NodeTool.getNodeText(subChildren[0]).trim() === ")"
1032
- ) {
1033
- // ĐÚNG LÀ NGOẠI LỆ
1034
-
1035
- const sub = parse(subChildren[1]);
1036
- // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1037
- const lastPart = parts.pop(); // lastPart = "\text{OH}"
1038
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1039
- for (let i = parts.length - 1; i >= 0; i--) {
1040
- if (parts[i] && parts[i].trim() === "\\left(") {
1041
- parts.splice(i, 1); // Xóa "\left("
1042
- break;
1043
- }
1044
- _;
1045
- } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1046
-
1047
- parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1048
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1049
-
1050
- const nearLeftObj = lefts[lefts.length - 1];
1051
- if (nearLeftObj) {
1052
- lefts.pop();
1053
- }
1054
- } else {
1055
- // <msub> bình thường
1056
- const parsed = parse(node);
1057
- if (parsed) {
1058
- // CHỈ PUSH NẾU KHÔNG RỖNG
1059
- parts.push(parsed);
1060
- }
1061
- }
1062
- // --- END PATCH V6 ---
1063
1080
  } else {
1064
1081
  // Các node khác như <mtext>, #text, v.v.
1065
1082
  const parsed = parse(node);
@@ -1071,40 +1088,13 @@ function renderChildren(children) {
1071
1088
  });
1072
1089
 
1073
1090
  // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1074
- // 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)
1075
1092
  if (lefts && lefts.length > 0) {
1076
- const rightForLeft = (left) => {
1077
- switch (left) {
1078
- case "(":
1079
- return ")";
1080
- case "[":
1081
- return "]";
1082
- case "{":
1083
- return "}";
1084
- case "|":
1085
- return "|";
1086
- default:
1087
- return ".";
1088
- }
1089
- };
1090
-
1091
1093
  lefts.forEach((leftObj) => {
1092
1094
  const idx = leftObj && leftObj.index;
1093
1095
  if (typeof idx !== "number" || !parts[idx]) return;
1094
-
1095
- const left = leftObj.op;
1096
- const right = rightForLeft(left);
1097
-
1098
- // 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
1099
- const tail = parts.slice(idx + 1).map(String).join(" ");
1100
- const hasMatchingRight =
1101
- (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1102
-
1103
- if (!hasMatchingRight) {
1104
- // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1105
- parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1106
- }
1107
- // 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, "");
1108
1098
  });
1109
1099
  }
1110
1100
 
@@ -1112,6 +1102,44 @@ function renderChildren(children) {
1112
1102
  return parts;
1113
1103
  }
1114
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
+
1115
1143
  function getRender(node) {
1116
1144
  let render = undefined;
1117
1145
  const nodeName = NodeTool.getNodeName(node);
@@ -1260,9 +1288,11 @@ function getRender(node) {
1260
1288
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1261
1289
  ) {
1262
1290
  const lastChild = sub.lastChild;
1263
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1291
+ return lastChild
1292
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1293
+ : base;
1264
1294
  }
1265
- return `${base}_{${parse(sub)}}`;
1295
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1266
1296
  };
1267
1297
  break;
1268
1298
 
@@ -1270,22 +1300,9 @@ function getRender(node) {
1270
1300
  render = function (node, children) {
1271
1301
  const childrenArray = Array.from(children);
1272
1302
  if (!childrenArray || childrenArray.length < 2) return "";
1273
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
1274
- const baseNode = childrenArray[0];
1275
- let base = parse(baseNode) || "";
1276
- if (NodeTool.getNodeName(baseNode) === "mo") {
1277
- const op = NodeTool.getNodeText(baseNode).trim();
1278
- if (Brackets.isRight(op)) {
1279
- // Escape brace characters
1280
- if (op === "}" || op === "{") {
1281
- base = `\\right\\${op}`;
1282
- } else {
1283
- base = `\\right${op}`;
1284
- }
1285
- }
1286
- }
1303
+ const base = parse(childrenArray[0]) || "";
1287
1304
  const sup = parse(childrenArray[1]) || "";
1288
- return `${base}^{${sup}}`;
1305
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1289
1306
  };
1290
1307
  break;
1291
1308
 
@@ -1309,7 +1326,7 @@ function getRender(node) {
1309
1326
  .join("");
1310
1327
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1311
1328
  }
1312
- return `${base}_{${sub}}^{${sup}}`;
1329
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1313
1330
  };
1314
1331
  break;
1315
1332
 
@@ -1319,9 +1336,15 @@ function getRender(node) {
1319
1336
  if (!childrenArray || childrenArray.length < 2) return "";
1320
1337
  const base = parse(childrenArray[0]) || "";
1321
1338
  const over = parse(childrenArray[1]) || "";
1322
- 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() || "";
1323
1342
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1324
1343
 
1344
+ // Handle arrows with extensible commands
1345
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1346
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1347
+
1325
1348
  // Handle biology notation (double overline)
1326
1349
  if (overText === "¯") {
1327
1350
  const parentNode = node.parentNode;
@@ -1337,6 +1360,27 @@ function getRender(node) {
1337
1360
 
1338
1361
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1339
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
+ }
1340
1384
  return `\\overset{${over}}{${base}}`;
1341
1385
  };
1342
1386
  break;
@@ -1347,10 +1391,21 @@ function getRender(node) {
1347
1391
  if (!childrenArray || childrenArray.length < 2) return "";
1348
1392
  const base = parse(childrenArray[0]) || "";
1349
1393
  const under = parse(childrenArray[1]) || "";
1394
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1395
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1350
1396
  const isUnderAccent =
1351
1397
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1352
1398
 
1399
+ // Handle arrows with extensible commands
1400
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1401
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1402
+
1353
1403
  if (base === "∫") return `\\int_{${under}}`;
1404
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1405
+
1406
+ if (isLimitOperator(base)) {
1407
+ return `${base}\\limits_{${under}}`;
1408
+ }
1354
1409
  return `\\underset{${under}}{${base}}`;
1355
1410
  };
1356
1411
  break;
@@ -1364,19 +1419,19 @@ function getRender(node) {
1364
1419
  const over = parse(childrenArray[2]);
1365
1420
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1366
1421
 
1367
- // Special handling for chemical reaction arrow
1368
- if (
1369
- baseText === "" &&
1370
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1371
- ) {
1372
- return `\\xrightarrow[${under}]{${over}}`;
1373
- }
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}}`;
1374
1425
 
1375
1426
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1376
1427
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1377
1428
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1378
1429
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1379
- return `${base}_{${under}}^{${over}}`;
1430
+
1431
+ if (isLimitOperator(base)) {
1432
+ return `${base}\\limits_{${under}}^{${over}}`;
1433
+ }
1434
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1380
1435
  };
1381
1436
  break;
1382
1437
 
@@ -1384,30 +1439,56 @@ function getRender(node) {
1384
1439
  render = function (node, children) {
1385
1440
  const childrenArray = Array.from(children);
1386
1441
  if (!childrenArray || childrenArray.length < 1) return "";
1387
- const base = parse(childrenArray[0]);
1388
- let prescripts = "";
1389
- let postscripts = "";
1442
+ const base = parse(childrenArray[0]) || "";
1443
+
1444
+ const postSub = [];
1445
+ const postSup = [];
1446
+ const preSub = [];
1447
+ const preSup = [];
1448
+
1390
1449
  let i = 1;
1450
+ let inPrescripts = false;
1391
1451
 
1392
1452
  while (i < childrenArray.length) {
1393
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1394
- i++;
1395
- if (i + 1 < childrenArray.length) {
1396
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1397
- childrenArray[i + 1]
1398
- )}}`;
1399
- i += 2;
1400
- }
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);
1401
1470
  } else {
1402
- if (i + 1 < childrenArray.length) {
1403
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1404
- childrenArray[i + 1]
1405
- )}}`;
1406
- i += 2;
1407
- } else break;
1471
+ if (sub) postSub.push(sub);
1472
+ if (sup) postSup.push(sup);
1408
1473
  }
1474
+
1475
+ i += 2;
1409
1476
  }
1410
- 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}`;
1411
1492
  };
1412
1493
  break;
1413
1494
 
@@ -1508,7 +1589,28 @@ function getRender(node) {
1508
1589
  const num = parse(childrenArray[0]);
1509
1590
  const den = parse(childrenArray[1]);
1510
1591
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1511
- 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
+
1512
1614
  return `\\frac{${num}}{${den}}`;
1513
1615
  };
1514
1616
  break;
@@ -1518,7 +1620,10 @@ function getRender(node) {
1518
1620
  const childrenArray = Array.from(children);
1519
1621
  const open = NodeTool.getAttr(node, "open", "(");
1520
1622
  const close = NodeTool.getAttr(node, "close", ")");
1521
- 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);
1522
1627
 
1523
1628
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1524
1629
  if (open === "|") {
@@ -1569,9 +1674,10 @@ function getRender(node) {
1569
1674
  parts.push(parse(child));
1570
1675
  if (
1571
1676
  index < childrenArray.length - 1 &&
1572
- separators[index % separators.length]
1677
+ separators.length > 0
1573
1678
  ) {
1574
- parts.push(separators[index % separators.length]);
1679
+ const sep = separators[index] ?? separators[separators.length - 1];
1680
+ if (sep) parts.push(sep);
1575
1681
  }
1576
1682
  });
1577
1683
  return `\\left[${parts.join("")}\\right)`;
@@ -1583,18 +1689,20 @@ function getRender(node) {
1583
1689
  parts.push(parse(child));
1584
1690
  if (
1585
1691
  index < childrenArray.length - 1 &&
1586
- separators[index % separators.length]
1692
+ separators.length > 0
1587
1693
  ) {
1588
- parts.push(separators[index % separators.length]);
1694
+ const sep = separators[index] ?? separators[separators.length - 1];
1695
+ if (sep) parts.push(sep);
1589
1696
  }
1590
1697
  });
1591
1698
  const content = parts.join("");
1592
1699
 
1593
1700
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1594
1701
  if (open === "|" && close === "|") return `\\left|${content}\\right|`;
1595
- if (!close) return `\\left${open}${content}\\right.`;
1596
- if (!open) return `\\left.${content}\\right${close}`;
1597
- 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}`;
1598
1706
  };
1599
1707
  break;
1600
1708
 
@@ -1620,10 +1728,17 @@ function getRender(node) {
1620
1728
  case "mn":
1621
1729
  case "mo":
1622
1730
  case "ms":
1623
- case "mtext":
1624
1731
  render = getRender_joinSeparator("@content");
1625
1732
  break;
1626
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
+
1627
1742
  case "mphantom":
1628
1743
  render = function (node, children) {
1629
1744
  const childrenArray = Array.from(children);