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,12 +1,14 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
- typeof define === 'function' && define.amd ? define(factory) :
4
- (global = global || self, global.MathML2LaTeX = factory());
5
- }(this, (function () { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('domino')) :
3
+ typeof define === 'function' && define.amd ? define(['domino'], factory) :
4
+ (global = global || self, global.MathML2LaTeX = factory(global.domino));
5
+ }(this, (function (domino) { 'use strict';
6
+
7
+ domino = domino && Object.prototype.hasOwnProperty.call(domino, 'default') ? domino['default'] : domino;
6
8
 
7
9
  const Brackets = {
8
- left: ['(', '[', '{', '|', '', '', '', '', ''],
9
- right: [')', ']', '}', '|', '', '', '', '', ''],
10
+ left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
11
+ right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
10
12
  isPair: function(l, r){
11
13
  const idx = this.left.indexOf(l);
12
14
  return r === this.right[idx];
@@ -28,17 +30,17 @@
28
30
  case '[':
29
31
  case '|': r = `\\left${it}`;
30
32
  break;
31
- case '': r = '\\left\\|';
33
+ case '\u2016': r = '\\left\\|';
32
34
  break;
33
35
  case '{': r = '\\left\\{';
34
36
  break;
35
- case '': r = '\\left\\langle ';
37
+ case '\u27E8': r = '\\left\\langle ';
36
38
  break;
37
- case '': r = '\\left\\lfloor ';
39
+ case '\u230A': r = '\\left\\lfloor ';
38
40
  break;
39
- case '': r = '\\left\\lceil ';
41
+ case '\u2308': r = '\\left\\lceil ';
40
42
  break;
41
- case '': r = '\\left\\ulcorner ';
43
+ case '\u231C': r = '\\left\\ulcorner ';
42
44
  break;
43
45
  }
44
46
  return (stretchy ? r : r.replace('\\left', ''));
@@ -52,17 +54,17 @@
52
54
  case ']':
53
55
  case '|': r = `\\right${it}`;
54
56
  break;
55
- case '': r = '\\right\\|';
57
+ case '\u2016': r = '\\right\\|';
56
58
  break;
57
59
  case '}': r = '\\right\\}';
58
60
  break;
59
- case '': r = ' \\right\\rangle';
61
+ case '\u27E9': r = ' \\right\\rangle';
60
62
  break;
61
- case '': r = ' \\right\\rfloor';
63
+ case '\u230B': r = ' \\right\\rfloor';
62
64
  break;
63
- case '': r = ' \\right\\rceil';
65
+ case '\u2309': r = ' \\right\\rceil';
64
66
  break;
65
- case '': r = ' \\right\\urcorner';
67
+ case '\u231D': r = ' \\right\\urcorner';
66
68
  break;
67
69
  }
68
70
  return (stretchy ? r : r.replace('\\right', ''));
@@ -98,11 +100,17 @@
98
100
  function createHTMLParser () {
99
101
  const Parser = function () {};
100
102
 
101
- if (typeof process !== 'undefined' && false) {
102
- if (shouldUseActiveX()) {
103
+ const hasDocument =
104
+ typeof document !== 'undefined' &&
105
+ document &&
106
+ document.implementation &&
107
+ typeof document.implementation.createHTMLDocument === 'function';
108
+
109
+ if (hasDocument) {
110
+ if (typeof process !== 'undefined' && false && shouldUseActiveX()) {
103
111
  Parser.prototype.parseFromString = function (string) {
104
112
  const doc = new window.ActiveXObject('htmlfile');
105
- doc.designMode = 'on'; // disable on-page scripts
113
+ doc.designMode = 'on';
106
114
  doc.open();
107
115
  doc.write(string);
108
116
  doc.close();
@@ -119,11 +127,9 @@
119
127
  }
120
128
  } else {
121
129
  Parser.prototype.parseFromString = function (string) {
122
- const doc = document.implementation.createHTMLDocument('');
123
- doc.open();
124
- doc.write(string);
125
- doc.close();
126
- return doc
130
+ const domino$1 = domino;
131
+ const window = domino$1.createWindow(string || '');
132
+ return window.document
127
133
  };
128
134
  }
129
135
  return Parser
@@ -141,11 +147,54 @@
141
147
 
142
148
  const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
143
149
 
150
+ function normalizeMathMLInput(input) {
151
+ let s = input == null ? '' : String(input);
152
+
153
+ if (s.includes('<math') && /\\[ntr"]/.test(s)) {
154
+ s = s
155
+ .replace(/\\r\\n/g, '\n')
156
+ .replace(/\\n/g, '\n')
157
+ .replace(/\\t/g, '\t')
158
+ .replace(/\\"/g, '"')
159
+ .replace(/\\'/g, "'");
160
+ }
161
+
162
+ s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
163
+
164
+ s = s.replace(
165
+ /xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
166
+ 'xmlns="$2"'
167
+ );
168
+
169
+ 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"');
170
+
171
+ if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
172
+ s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
173
+ }
174
+
175
+ return s;
176
+ }
177
+
144
178
  const NodeTool = {
145
179
  parseMath: function(html) {
180
+ const normalized = normalizeMathMLInput(html);
146
181
  const parser = new HTMLParser();
147
- const doc = parser.parseFromString(html, 'text/html');
148
- return doc.querySelector('math');
182
+ const doc = parser.parseFromString(normalized, 'text/html');
183
+ let math = doc && doc.querySelector ? doc.querySelector('math') : null;
184
+
185
+ if (!math) {
186
+ const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
187
+ if (match) {
188
+ const retryDoc = parser.parseFromString(match[0], 'text/html');
189
+ math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
190
+ }
191
+ }
192
+
193
+ if (!math) {
194
+ throw new Error('Invalid MathML: missing <math> root element');
195
+ }
196
+
197
+ return math;
149
198
  },
150
199
  getChildren: function(node) {
151
200
  return node.children;
@@ -619,7 +668,7 @@
619
668
  .replace(/∫/g, " \\int "); // Tích phân
620
669
 
621
670
  // Đả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)
622
- result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
671
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
623
672
 
624
673
  return result;
625
674
  }
@@ -765,18 +814,21 @@
765
814
  "±": " \\pm ",
766
815
  "×": " \\times ",
767
816
  "÷": " \\div ",
768
- "∑": " \\sum ",
769
- "∏": " \\prod ",
770
- "∫": " \\int ",
817
+ "∑": "\\sum ",
818
+ "∏": "\\prod ",
819
+ "∫": "\\int ",
771
820
  "−": "-",
772
821
  "≠": " \\neq ",
773
822
  ">": " > ",
774
823
  "=": " = ",
824
+ "(": "(",
825
+ ")": ")",
775
826
  ",": ", ", // Dấu phẩy trong tập hợp
776
827
  ";": ";",
777
828
  Ω: "\\Omega",
778
829
  "|": " \\mid ", // PATCH: set-builder mid
779
830
  π: " \\pi ", // PATCH: Greek letter
831
+ "...": "\\dots",
780
832
  };
781
833
  const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
782
834
  return res;
@@ -853,8 +905,8 @@
853
905
  // Nếu đã là lệnh LaTeX thì giữ nguyên
854
906
  if (content.startsWith("\\")) return content;
855
907
 
856
- // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
857
- if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
908
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
909
+ if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
858
910
  return `\\text{${content}}`;
859
911
  }
860
912
 
@@ -909,6 +961,22 @@
909
961
  return text;
910
962
  }
911
963
 
964
+ function wrapBaseForScript(base) {
965
+ const t = (base ?? "").trim();
966
+ if (!t) return "";
967
+ if (t.startsWith("{") && t.endsWith("}")) return t;
968
+ if (/^\\[a-zA-Z]+$/.test(t)) return t;
969
+ const needsWrap =
970
+ t.includes("_") ||
971
+ t.includes("^") ||
972
+ /\s/.test(t) ||
973
+ t.startsWith("\\left") ||
974
+ t.startsWith("\\right") ||
975
+ /\\[a-zA-Z]+/.test(t) ||
976
+ /[{}]/.test(t);
977
+ return needsWrap ? `{${t}}` : t;
978
+ }
979
+
912
980
  function parseContainer(node, children) {
913
981
  const render = getRender(node);
914
982
  if (render) {
@@ -927,22 +995,6 @@
927
995
  // lefts là mảng object: { op: "(", index: 5 }
928
996
  let lefts = [];
929
997
 
930
- if (
931
- children.length >= 3 &&
932
- NodeTool.getNodeName(children[0]) === "mo" &&
933
- NodeTool.getNodeText(children[0]).trim() === "{" &&
934
- NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
935
- NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
936
- ) {
937
- // Render inner content
938
- const innerContent = Array.prototype.slice
939
- .call(children, 1, -1)
940
- .map((child) => parse(child))
941
- .join(""); // Chỉ trả về nếu nội dung không rỗng
942
- const result = `\\left\\{${innerContent}\\right\\}`;
943
- return result;
944
- }
945
-
946
998
  Array.prototype.forEach.call(children, (node, idx) => {
947
999
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
948
1000
  if (
@@ -970,55 +1022,59 @@
970
1022
  if (Brackets.contains(op)) {
971
1023
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
972
1024
  stretchy = ["", "true"].indexOf(stretchy) > -1;
973
-
974
- let escapedOp = op;
975
- if (op === "{" || op === "}") {
976
- escapedOp = `\\${op}`;
1025
+ const parentNode = node.parentNode;
1026
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1027
+ if (isInPower) {
1028
+ stretchy = false;
977
1029
  }
978
1030
 
979
1031
  if (Brackets.isRight(op)) {
980
1032
  const nearLeftObj = lefts[lefts.length - 1];
981
- if (nearLeftObj) {
982
- const parentNode = node.parentNode;
983
- const isInPower =
984
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
985
-
986
- let partToPush = "";
987
- if (stretchy && !isInPower) {
988
- partToPush = `\\right${escapedOp}`;
989
- } else {
990
- partToPush = escapedOp;
991
- }
992
- if (partToPush) {
993
- // CHỈ PUSH NẾU KHÔNG RỖNG
994
- parts.push(partToPush);
1033
+ if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
1034
+ const leftIdx = nearLeftObj.index;
1035
+ const leftRendered =
1036
+ typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
1037
+ let rightRendered = Brackets.parseRight(op, stretchy);
1038
+
1039
+ const leftRenderedTrimmed = leftRendered.trim();
1040
+ const rightRenderedTrimmed = String(rightRendered).trim();
1041
+
1042
+ if (
1043
+ leftRenderedTrimmed.startsWith("\\left") &&
1044
+ !rightRenderedTrimmed.startsWith("\\right")
1045
+ ) {
1046
+ parts[leftIdx] = leftRendered.replace("\\left", "");
1047
+ } else if (
1048
+ !leftRenderedTrimmed.startsWith("\\left") &&
1049
+ rightRenderedTrimmed.startsWith("\\right")
1050
+ ) {
1051
+ rightRendered = String(rightRendered).replace("\\right", "");
995
1052
  }
996
- // matched a left: remove corresponding left object
1053
+
1054
+ if (rightRendered) parts.push(rightRendered);
997
1055
  lefts.pop();
1056
+ } else if (Brackets.isLeft(op)) {
1057
+ // If it's a Right bracket but doesn't match the current Left,
1058
+ // AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
1059
+ const partToPush = Brackets.parseLeft(op, stretchy);
1060
+ if (partToPush) parts.push(partToPush);
1061
+ lefts.push({ op: op, index: parts.length - 1 });
998
1062
  } else {
999
- if (escapedOp) {
1000
- // CHỈ PUSH NẾu KHÔNG RỖNG
1001
- parts.push(escapedOp);
1002
- }
1063
+ // Unmatched right bracket
1064
+ let rightRendered = Brackets.parseRight(op, stretchy);
1065
+ rightRendered = String(rightRendered).replace("\\right", "");
1066
+ if (rightRendered) parts.push(rightRendered);
1003
1067
  }
1004
1068
  } else {
1005
- // ngoặc trái
1006
- const parentNode = node.parentNode;
1007
- const isInPower =
1008
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
1009
-
1010
- let partToPush = "";
1011
- if (stretchy && !isInPower) {
1012
- partToPush = `\\left${escapedOp}`;
1069
+ // Must be Left bracket (or only Left)
1070
+ if (Brackets.isLeft(op)) {
1071
+ const partToPush = Brackets.parseLeft(op, stretchy);
1072
+ if (partToPush) parts.push(partToPush);
1073
+ lefts.push({ op: op, index: parts.length - 1 });
1013
1074
  } else {
1014
- partToPush = escapedOp;
1075
+ // Should not happen if Brackets.contains is correct
1076
+ if (op) parts.push(op);
1015
1077
  }
1016
- if (partToPush) {
1017
- // CHỈ PUSH NẾU KHÔNG RỖNG
1018
- parts.push(partToPush);
1019
- }
1020
- // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1021
- lefts.push({ op: op, index: parts.length - 1 });
1022
1078
  }
1023
1079
  } else {
1024
1080
  const parsedOperator = parseOperator(node);
@@ -1027,45 +1083,6 @@
1027
1083
  parts.push(parsedOperator);
1028
1084
  }
1029
1085
  }
1030
-
1031
- // --- START PATCH V6 (Giữ nguyên logic V5) ---
1032
- } else if (NodeTool.getNodeName(node) === "msub") {
1033
- const subChildren = Array.from(NodeTool.getChildren(node));
1034
- if (
1035
- subChildren.length === 2 &&
1036
- NodeTool.getNodeName(subChildren[0]) === "mo" &&
1037
- NodeTool.getNodeText(subChildren[0]).trim() === ")"
1038
- ) {
1039
- // ĐÚNG LÀ NGOẠI LỆ
1040
-
1041
- const sub = parse(subChildren[1]);
1042
- // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1043
- const lastPart = parts.pop(); // lastPart = "\text{OH}"
1044
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1045
- for (let i = parts.length - 1; i >= 0; i--) {
1046
- if (parts[i] && parts[i].trim() === "\\left(") {
1047
- parts.splice(i, 1); // Xóa "\left("
1048
- break;
1049
- }
1050
- _;
1051
- } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1052
-
1053
- parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1054
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1055
-
1056
- const nearLeftObj = lefts[lefts.length - 1];
1057
- if (nearLeftObj) {
1058
- lefts.pop();
1059
- }
1060
- } else {
1061
- // <msub> bình thường
1062
- const parsed = parse(node);
1063
- if (parsed) {
1064
- // CHỈ PUSH NẾU KHÔNG RỖNG
1065
- parts.push(parsed);
1066
- }
1067
- }
1068
- // --- END PATCH V6 ---
1069
1086
  } else {
1070
1087
  // Các node khác như <mtext>, #text, v.v.
1071
1088
  const parsed = parse(node);
@@ -1077,40 +1094,13 @@
1077
1094
  });
1078
1095
 
1079
1096
  // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1080
- // khi không đóng tương ứng phía sau nếu có \right (hoặc dấu đóng) thì giữ \left
1097
+ // để tránh sinh LaTeX không hợp lệ (\left không có \right)
1081
1098
  if (lefts && lefts.length > 0) {
1082
- const rightForLeft = (left) => {
1083
- switch (left) {
1084
- case "(":
1085
- return ")";
1086
- case "[":
1087
- return "]";
1088
- case "{":
1089
- return "}";
1090
- case "|":
1091
- return "|";
1092
- default:
1093
- return ".";
1094
- }
1095
- };
1096
-
1097
1099
  lefts.forEach((leftObj) => {
1098
1100
  const idx = leftObj && leftObj.index;
1099
1101
  if (typeof idx !== "number" || !parts[idx]) return;
1100
-
1101
- const left = leftObj.op;
1102
- const right = rightForLeft(left);
1103
-
1104
- // 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
1105
- const tail = parts.slice(idx + 1).map(String).join(" ");
1106
- const hasMatchingRight =
1107
- (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1108
-
1109
- if (!hasMatchingRight) {
1110
- // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1111
- parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1112
- }
1113
- // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1102
+ // Use regex with whitespace support and global flag just in case
1103
+ parts[idx] = String(parts[idx]).replace(/\\left/g, "");
1114
1104
  });
1115
1105
  }
1116
1106
 
@@ -1118,6 +1108,44 @@
1118
1108
  return parts;
1119
1109
  }
1120
1110
 
1111
+ function isLimitOperator(base) {
1112
+ const t = (base || "").trim();
1113
+ const ops = [
1114
+ "\\sum",
1115
+ "\\prod",
1116
+ "\\coprod",
1117
+ "\\int",
1118
+ "\\oint",
1119
+ "\\bigcap",
1120
+ "\\bigcup",
1121
+ "\\bigsqcup",
1122
+ "\\bigvee",
1123
+ "\\bigwedge",
1124
+ "\\bigodot",
1125
+ "\\bigotimes",
1126
+ "\\bigoplus",
1127
+ "\\biguplus",
1128
+ "\\lim",
1129
+ "\\max",
1130
+ "\\min",
1131
+ "\\sup",
1132
+ "\\inf",
1133
+ "\\det",
1134
+ "\\gcd",
1135
+ "\\Pr",
1136
+ "\\limsup",
1137
+ "\\liminf",
1138
+ ];
1139
+ return ops.some(
1140
+ (op) =>
1141
+ t === op ||
1142
+ t.startsWith(op + " ") ||
1143
+ t.startsWith(op + "\\") ||
1144
+ t.startsWith(op + "^") ||
1145
+ t.startsWith(op + "_")
1146
+ );
1147
+ }
1148
+
1121
1149
  function getRender(node) {
1122
1150
  let render = undefined;
1123
1151
  const nodeName = NodeTool.getNodeName(node);
@@ -1266,9 +1294,11 @@
1266
1294
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1267
1295
  ) {
1268
1296
  const lastChild = sub.lastChild;
1269
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1297
+ return lastChild
1298
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1299
+ : base;
1270
1300
  }
1271
- return `${base}_{${parse(sub)}}`;
1301
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1272
1302
  };
1273
1303
  break;
1274
1304
 
@@ -1276,22 +1306,9 @@
1276
1306
  render = function (node, children) {
1277
1307
  const childrenArray = Array.from(children);
1278
1308
  if (!childrenArray || childrenArray.length < 2) return "";
1279
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
1280
- const baseNode = childrenArray[0];
1281
- let base = parse(baseNode) || "";
1282
- if (NodeTool.getNodeName(baseNode) === "mo") {
1283
- const op = NodeTool.getNodeText(baseNode).trim();
1284
- if (Brackets.isRight(op)) {
1285
- // Escape brace characters
1286
- if (op === "}" || op === "{") {
1287
- base = `\\right\\${op}`;
1288
- } else {
1289
- base = `\\right${op}`;
1290
- }
1291
- }
1292
- }
1309
+ const base = parse(childrenArray[0]) || "";
1293
1310
  const sup = parse(childrenArray[1]) || "";
1294
- return `${base}^{${sup}}`;
1311
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1295
1312
  };
1296
1313
  break;
1297
1314
 
@@ -1315,7 +1332,7 @@
1315
1332
  .join("");
1316
1333
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1317
1334
  }
1318
- return `${base}_{${sub}}^{${sup}}`;
1335
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1319
1336
  };
1320
1337
  break;
1321
1338
 
@@ -1325,9 +1342,15 @@
1325
1342
  if (!childrenArray || childrenArray.length < 2) return "";
1326
1343
  const base = parse(childrenArray[0]) || "";
1327
1344
  const over = parse(childrenArray[1]) || "";
1328
- const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1345
+ const overNode = childrenArray[1];
1346
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1347
+ const overText = NodeTool.getNodeText(overNode)?.trim() || "";
1329
1348
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1330
1349
 
1350
+ // Handle arrows with extensible commands
1351
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1352
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1353
+
1331
1354
  // Handle biology notation (double overline)
1332
1355
  if (overText === "¯") {
1333
1356
  const parentNode = node.parentNode;
@@ -1343,6 +1366,27 @@
1343
1366
 
1344
1367
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1345
1368
  if (overText === "^" && isAccent) return `\\hat{${base}}`;
1369
+ if (overText === "\u23DE") return `\\overbrace{${base}}`;
1370
+
1371
+ // Check for nested overbrace (layer-2)
1372
+ if (NodeTool.getNodeName(overNode) === "mover") {
1373
+ const innerChildren = Array.from(NodeTool.getChildren(overNode));
1374
+ if (innerChildren.length >= 2) {
1375
+ const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
1376
+ if (innerBaseText === "\u23DE") {
1377
+ const label = parse(innerChildren[1]);
1378
+ return `\\overbrace{${base}}\\limits^{${label}}`;
1379
+ }
1380
+ }
1381
+ }
1382
+
1383
+ if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
1384
+ return `${base}\\limits^{${over}}`;
1385
+ }
1386
+
1387
+ if (isLimitOperator(base)) {
1388
+ return `${base}\\limits^{${over}}`;
1389
+ }
1346
1390
  return `\\overset{${over}}{${base}}`;
1347
1391
  };
1348
1392
  break;
@@ -1353,10 +1397,21 @@
1353
1397
  if (!childrenArray || childrenArray.length < 2) return "";
1354
1398
  const base = parse(childrenArray[0]) || "";
1355
1399
  const under = parse(childrenArray[1]) || "";
1400
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1401
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1356
1402
  const isUnderAccent =
1357
1403
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1358
1404
 
1405
+ // Handle arrows with extensible commands
1406
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1407
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1408
+
1359
1409
  if (base === "∫") return `\\int_{${under}}`;
1410
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1411
+
1412
+ if (isLimitOperator(base)) {
1413
+ return `${base}\\limits_{${under}}`;
1414
+ }
1360
1415
  return `\\underset{${under}}{${base}}`;
1361
1416
  };
1362
1417
  break;
@@ -1370,19 +1425,19 @@
1370
1425
  const over = parse(childrenArray[2]);
1371
1426
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1372
1427
 
1373
- // Special handling for chemical reaction arrow
1374
- if (
1375
- baseText === "" &&
1376
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1377
- ) {
1378
- return `\\xrightarrow[${under}]{${over}}`;
1379
- }
1428
+ // Special handling for chemical reaction arrow and other arrows
1429
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
1430
+ if (baseText === "" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
1380
1431
 
1381
1432
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1382
1433
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1383
1434
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1384
1435
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1385
- return `${base}_{${under}}^{${over}}`;
1436
+
1437
+ if (isLimitOperator(base)) {
1438
+ return `${base}\\limits_{${under}}^{${over}}`;
1439
+ }
1440
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1386
1441
  };
1387
1442
  break;
1388
1443
 
@@ -1390,30 +1445,56 @@
1390
1445
  render = function (node, children) {
1391
1446
  const childrenArray = Array.from(children);
1392
1447
  if (!childrenArray || childrenArray.length < 1) return "";
1393
- const base = parse(childrenArray[0]);
1394
- let prescripts = "";
1395
- let postscripts = "";
1448
+ const base = parse(childrenArray[0]) || "";
1449
+
1450
+ const postSub = [];
1451
+ const postSup = [];
1452
+ const preSub = [];
1453
+ const preSup = [];
1454
+
1396
1455
  let i = 1;
1456
+ let inPrescripts = false;
1397
1457
 
1398
1458
  while (i < childrenArray.length) {
1399
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1400
- i++;
1401
- if (i + 1 < childrenArray.length) {
1402
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1403
- childrenArray[i + 1]
1404
- )}}`;
1405
- i += 2;
1406
- }
1459
+ const name = NodeTool.getNodeName(childrenArray[i]);
1460
+ if (name === "mprescripts") {
1461
+ inPrescripts = true;
1462
+ i += 1;
1463
+ continue;
1464
+ }
1465
+
1466
+ const subNode = childrenArray[i];
1467
+ const supNode = childrenArray[i + 1];
1468
+ if (!subNode || !supNode) break;
1469
+
1470
+ const sub = parse(subNode) || "";
1471
+ const sup = parse(supNode) || "";
1472
+
1473
+ if (inPrescripts) {
1474
+ if (sub) preSub.push(sub);
1475
+ if (sup) preSup.push(sup);
1407
1476
  } else {
1408
- if (i + 1 < childrenArray.length) {
1409
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1410
- childrenArray[i + 1]
1411
- )}}`;
1412
- i += 2;
1413
- } else break;
1477
+ if (sub) postSub.push(sub);
1478
+ if (sup) postSup.push(sup);
1414
1479
  }
1480
+
1481
+ i += 2;
1415
1482
  }
1416
- return `${base}${prescripts}${postscripts}`;
1483
+
1484
+ const preSubStr = preSub.join(" ");
1485
+ const preSupStr = preSup.join(" ");
1486
+ const postSubStr = postSub.join(" ");
1487
+ const postSupStr = postSup.join(" ");
1488
+
1489
+ let pre = "";
1490
+ if (preSubStr) pre += `_{${preSubStr}}`;
1491
+ if (preSupStr) pre += `^{${preSupStr}}`;
1492
+
1493
+ let post = "";
1494
+ if (postSubStr) post += `_{${postSubStr}}`;
1495
+ if (postSupStr) post += `^{${postSupStr}}`;
1496
+
1497
+ return `${pre}${wrapBaseForScript(base)}${post}`;
1417
1498
  };
1418
1499
  break;
1419
1500
 
@@ -1514,7 +1595,28 @@
1514
1595
  const num = parse(childrenArray[0]);
1515
1596
  const den = parse(childrenArray[1]);
1516
1597
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1517
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
1598
+ const bevelled = NodeTool.getAttr(node, "bevelled", "false");
1599
+
1600
+ if (bevelled === "true") {
1601
+ return `{}^{${num}}/_{${den}}`;
1602
+ }
1603
+
1604
+ if (["0", "0px"].indexOf(linethickness) > -1) {
1605
+ const prevNode = NodeTool.getPrevNode(node);
1606
+ const nextNode = NodeTool.getNextNode(node);
1607
+ if (
1608
+ prevNode &&
1609
+ NodeTool.getNodeName(prevNode) === "mo" &&
1610
+ NodeTool.getNodeText(prevNode).trim() === "(" &&
1611
+ nextNode &&
1612
+ NodeTool.getNodeName(nextNode) === "mo" &&
1613
+ NodeTool.getNodeText(nextNode).trim() === ")"
1614
+ ) {
1615
+ return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
1616
+ }
1617
+ return `{}_{${den}}^{${num}}`;
1618
+ }
1619
+
1518
1620
  return `\\frac{${num}}{${den}}`;
1519
1621
  };
1520
1622
  break;
@@ -1524,7 +1626,10 @@
1524
1626
  const childrenArray = Array.from(children);
1525
1627
  const open = NodeTool.getAttr(node, "open", "(");
1526
1628
  const close = NodeTool.getAttr(node, "close", ")");
1527
- const separators = NodeTool.getAttr(node, "separators", ",").split("");
1629
+ const separatorsStr = NodeTool.getAttr(node, "separators", ",");
1630
+ const separators = separatorsStr
1631
+ .split("")
1632
+ .filter((c) => c.trim().length === 1);
1528
1633
 
1529
1634
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1530
1635
  if (open === "|") {
@@ -1575,9 +1680,10 @@
1575
1680
  parts.push(parse(child));
1576
1681
  if (
1577
1682
  index < childrenArray.length - 1 &&
1578
- separators[index % separators.length]
1683
+ separators.length > 0
1579
1684
  ) {
1580
- parts.push(separators[index % separators.length]);
1685
+ const sep = separators[index] ?? separators[separators.length - 1];
1686
+ if (sep) parts.push(sep);
1581
1687
  }
1582
1688
  });
1583
1689
  return `\\left[${parts.join("")}\\right)`;
@@ -1589,18 +1695,20 @@
1589
1695
  parts.push(parse(child));
1590
1696
  if (
1591
1697
  index < childrenArray.length - 1 &&
1592
- separators[index % separators.length]
1698
+ separators.length > 0
1593
1699
  ) {
1594
- parts.push(separators[index % separators.length]);
1700
+ const sep = separators[index] ?? separators[separators.length - 1];
1701
+ if (sep) parts.push(sep);
1595
1702
  }
1596
1703
  });
1597
1704
  const content = parts.join("");
1598
1705
 
1599
1706
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1600
1707
  if (open === "|" && close === "|") return `\\left|${content}\\right|`;
1601
- if (!close) return `\\left${open}${content}\\right.`;
1602
- if (!open) return `\\left.${content}\\right${close}`;
1603
- return `\\left${open}${content}\\right${close}`;
1708
+ const left = open ? Brackets.parseLeft(open) : "";
1709
+ const right = close ? Brackets.parseRight(close) : "";
1710
+ if (!close && open) return `${left}${content}\\right.`;
1711
+ return `${left}${content}${right}`;
1604
1712
  };
1605
1713
  break;
1606
1714
 
@@ -1626,10 +1734,17 @@
1626
1734
  case "mn":
1627
1735
  case "mo":
1628
1736
  case "ms":
1629
- case "mtext":
1630
1737
  render = getRender_joinSeparator("@content");
1631
1738
  break;
1632
1739
 
1740
+ case "mtext":
1741
+ render = function (node, children) {
1742
+ const childrenArray = Array.from(children);
1743
+ const content = renderChildren(childrenArray).join("");
1744
+ return `\\text{${content}}`;
1745
+ };
1746
+ break;
1747
+
1633
1748
  case "mphantom":
1634
1749
  render = function (node, children) {
1635
1750
  const childrenArray = Array.from(children);