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.
@@ -5,8 +5,8 @@
5
5
  }(this, (function () { 'use strict';
6
6
 
7
7
  const Brackets = {
8
- left: ['(', '[', '{', '|', '', '', '', '', ''],
9
- right: [')', ']', '}', '|', '', '', '', '', ''],
8
+ left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
9
+ right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
10
10
  isPair: function(l, r){
11
11
  const idx = this.left.indexOf(l);
12
12
  return r === this.right[idx];
@@ -28,17 +28,17 @@
28
28
  case '[':
29
29
  case '|': r = `\\left${it}`;
30
30
  break;
31
- case '': r = '\\left\\|';
31
+ case '\u2016': r = '\\left\\|';
32
32
  break;
33
33
  case '{': r = '\\left\\{';
34
34
  break;
35
- case '': r = '\\left\\langle ';
35
+ case '\u27E8': r = '\\left\\langle ';
36
36
  break;
37
- case '': r = '\\left\\lfloor ';
37
+ case '\u230A': r = '\\left\\lfloor ';
38
38
  break;
39
- case '': r = '\\left\\lceil ';
39
+ case '\u2308': r = '\\left\\lceil ';
40
40
  break;
41
- case '': r = '\\left\\ulcorner ';
41
+ case '\u231C': r = '\\left\\ulcorner ';
42
42
  break;
43
43
  }
44
44
  return (stretchy ? r : r.replace('\\left', ''));
@@ -52,23 +52,36 @@
52
52
  case ']':
53
53
  case '|': r = `\\right${it}`;
54
54
  break;
55
- case '': r = '\\right\\|';
55
+ case '\u2016': r = '\\right\\|';
56
56
  break;
57
57
  case '}': r = '\\right\\}';
58
58
  break;
59
- case '': r = ' \\right\\rangle';
59
+ case '\u27E9': r = ' \\right\\rangle';
60
60
  break;
61
- case '': r = ' \\right\\rfloor';
61
+ case '\u230B': r = ' \\right\\rfloor';
62
62
  break;
63
- case '': r = ' \\right\\rceil';
63
+ case '\u2309': r = ' \\right\\rceil';
64
64
  break;
65
- case '': r = ' \\right\\urcorner';
65
+ case '\u231D': r = ' \\right\\urcorner';
66
66
  break;
67
67
  }
68
68
  return (stretchy ? r : r.replace('\\right', ''));
69
69
  }
70
70
  };
71
71
 
72
+ var _nodeResolve_empty = {};
73
+
74
+ var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
75
+ __proto__: null,
76
+ 'default': _nodeResolve_empty
77
+ });
78
+
79
+ function getCjsExportFromNamespace (n) {
80
+ return n && n['default'] || n;
81
+ }
82
+
83
+ var require$$0 = getCjsExportFromNamespace(_nodeResolve_empty$1);
84
+
72
85
  /*
73
86
  * Set up window for Node.js
74
87
  */
@@ -98,11 +111,17 @@
98
111
  function createHTMLParser () {
99
112
  const Parser = function () {};
100
113
 
101
- if (typeof process !== 'undefined' && true) {
102
- if (shouldUseActiveX()) {
114
+ const hasDocument =
115
+ typeof document !== 'undefined' &&
116
+ document &&
117
+ document.implementation &&
118
+ typeof document.implementation.createHTMLDocument === 'function';
119
+
120
+ if (hasDocument) {
121
+ if (typeof process !== 'undefined' && true && shouldUseActiveX()) {
103
122
  Parser.prototype.parseFromString = function (string) {
104
123
  const doc = new window.ActiveXObject('htmlfile');
105
- doc.designMode = 'on'; // disable on-page scripts
124
+ doc.designMode = 'on';
106
125
  doc.open();
107
126
  doc.write(string);
108
127
  doc.close();
@@ -119,11 +138,9 @@
119
138
  }
120
139
  } else {
121
140
  Parser.prototype.parseFromString = function (string) {
122
- const doc = document.implementation.createHTMLDocument('');
123
- doc.open();
124
- doc.write(string);
125
- doc.close();
126
- return doc
141
+ const domino = require$$0;
142
+ const window = domino.createWindow(string || '');
143
+ return window.document
127
144
  };
128
145
  }
129
146
  return Parser
@@ -141,11 +158,54 @@
141
158
 
142
159
  const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
143
160
 
161
+ function normalizeMathMLInput(input) {
162
+ let s = input == null ? '' : String(input);
163
+
164
+ if (s.includes('<math') && /\\[ntr"]/.test(s)) {
165
+ s = s
166
+ .replace(/\\r\\n/g, '\n')
167
+ .replace(/\\n/g, '\n')
168
+ .replace(/\\t/g, '\t')
169
+ .replace(/\\"/g, '"')
170
+ .replace(/\\'/g, "'");
171
+ }
172
+
173
+ s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
174
+
175
+ s = s.replace(
176
+ /xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
177
+ 'xmlns="$2"'
178
+ );
179
+
180
+ 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"');
181
+
182
+ if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
183
+ s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
184
+ }
185
+
186
+ return s;
187
+ }
188
+
144
189
  const NodeTool = {
145
190
  parseMath: function(html) {
191
+ const normalized = normalizeMathMLInput(html);
146
192
  const parser = new HTMLParser();
147
- const doc = parser.parseFromString(html, 'text/html');
148
- return doc.querySelector('math');
193
+ const doc = parser.parseFromString(normalized, 'text/html');
194
+ let math = doc && doc.querySelector ? doc.querySelector('math') : null;
195
+
196
+ if (!math) {
197
+ const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
198
+ if (match) {
199
+ const retryDoc = parser.parseFromString(match[0], 'text/html');
200
+ math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
201
+ }
202
+ }
203
+
204
+ if (!math) {
205
+ throw new Error('Invalid MathML: missing <math> root element');
206
+ }
207
+
208
+ return math;
149
209
  },
150
210
  getChildren: function(node) {
151
211
  return node.children;
@@ -281,24 +341,24 @@
281
341
  bigCommand: {
282
342
  decimals: [8721, 8719, 8720, 10753, 10754, 10752, 8899, 8898, 10756, 10758, 8897, 8896, 8747, 8750, 8748, 8749, 10764, 8747],
283
343
  scripts: [
284
- "\\sum",
285
- "\\prod",
286
- "\\coprod",
287
- "\\bigoplus",
288
- "\\bigotimes",
289
- "\\bigodot",
290
- "\\bigcup",
291
- "\\bigcap",
292
- "\\biguplus",
293
- "\\bigsqcup",
294
- "\\bigvee",
295
- "\\bigwedge",
296
- "\\int",
297
- "\\oint",
298
- "\\iint",
299
- "\\iiint",
300
- "\\iiiint",
301
- "\\idotsint",
344
+ "\\sum ",
345
+ "\\prod ",
346
+ "\\coprod ",
347
+ "\\bigoplus ",
348
+ "\\bigotimes ",
349
+ "\\bigodot ",
350
+ "\\bigcup ",
351
+ "\\bigcap ",
352
+ "\\biguplus ",
353
+ "\\bigsqcup ",
354
+ "\\bigvee ",
355
+ "\\bigwedge ",
356
+ "\\int ",
357
+ "\\oint ",
358
+ "\\iint ",
359
+ "\\iiint ",
360
+ "\\iiiint ",
361
+ "\\idotsint ",
302
362
  ]
303
363
  },
304
364
 
@@ -616,9 +676,10 @@
616
676
  // Thêm xử lý cho các thẻ MathML khác
617
677
  result = result
618
678
  .replace(/∞/g, "\\infty") // Vô cực
619
- .replace(/∑/g, "\\sum") // Tổng
620
- .replace(/∏/g, "\\prod") // Tích
621
- .replace(/∫/g, "\\int"); // Tích phân
679
+ .replace(/∫/g, " \\int "); // Tích phân
680
+
681
+ // Đả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)
682
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
622
683
 
623
684
  return result;
624
685
  }
@@ -764,20 +825,24 @@
764
825
  "±": " \\pm ",
765
826
  "×": " \\times ",
766
827
  "÷": " \\div ",
767
- "∑": " \\sum ",
768
- "∏": " \\prod ",
769
- "∫": "\\int",
828
+ "∑": "\\sum ",
829
+ "∏": "\\prod ",
830
+ "∫": "\\int ",
770
831
  "−": "-",
771
832
  "≠": " \\neq ",
772
833
  ">": " > ",
773
834
  "=": " = ",
835
+ "(": "(",
836
+ ")": ")",
774
837
  ",": ", ", // Dấu phẩy trong tập hợp
775
838
  ";": ";",
776
839
  Ω: "\\Omega",
777
840
  "|": " \\mid ", // PATCH: set-builder mid
778
841
  π: " \\pi ", // PATCH: Greek letter
842
+ "...": "\\dots",
779
843
  };
780
- return operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
844
+ const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
845
+ return res;
781
846
  }
782
847
  // --- END PATCH ---
783
848
 
@@ -804,6 +869,20 @@
804
869
  // Math Number
805
870
  function parseElementMn(node) {
806
871
  let it = NodeTool.getNodeText(node).trim();
872
+ // Loại bỏ các ký tự điều khiển hoặc khoảng trắng lạ
873
+ it = it.replace(/[\u0000-\u001F\u007F-\u009F\u00A0]/g, "");
874
+
875
+ // Danh sách các hàm toán học mở rộng
876
+ const mathFunctions = ["cos", "sin", "tan", "cot", "arccos", "arcsin", "arctan", "arccot", "log", "ln", "lim", "sinh", "cosh", "tanh", "sec", "csc"];
877
+
878
+ if (mathFunctions.some(fn => it.toLowerCase().includes(fn))) {
879
+ // Tìm hàm khớp chính xác nhất
880
+ for (const fn of mathFunctions) {
881
+ if (it.toLowerCase() === fn) {
882
+ return "\\" + fn + " ";
883
+ }
884
+ }
885
+ }
807
886
  return escapeSpecialChars(it);
808
887
  }
809
888
 
@@ -837,8 +916,8 @@
837
916
  // Nếu đã là lệnh LaTeX thì giữ nguyên
838
917
  if (content.startsWith("\\")) return content;
839
918
 
840
- // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
841
- if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
919
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
920
+ if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
842
921
  return `\\text{${content}}`;
843
922
  }
844
923
 
@@ -893,6 +972,22 @@
893
972
  return text;
894
973
  }
895
974
 
975
+ function wrapBaseForScript(base) {
976
+ const t = (base ?? "").trim();
977
+ if (!t) return "";
978
+ if (t.startsWith("{") && t.endsWith("}")) return t;
979
+ if (/^\\[a-zA-Z]+$/.test(t)) return t;
980
+ const needsWrap =
981
+ t.includes("_") ||
982
+ t.includes("^") ||
983
+ /\s/.test(t) ||
984
+ t.startsWith("\\left") ||
985
+ t.startsWith("\\right") ||
986
+ /\\[a-zA-Z]+/.test(t) ||
987
+ /[{}]/.test(t);
988
+ return needsWrap ? `{${t}}` : t;
989
+ }
990
+
896
991
  function parseContainer(node, children) {
897
992
  const render = getRender(node);
898
993
  if (render) {
@@ -911,22 +1006,6 @@
911
1006
  // lefts là mảng object: { op: "(", index: 5 }
912
1007
  let lefts = [];
913
1008
 
914
- if (
915
- children.length >= 3 &&
916
- NodeTool.getNodeName(children[0]) === "mo" &&
917
- NodeTool.getNodeText(children[0]).trim() === "{" &&
918
- NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
919
- NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
920
- ) {
921
- // Render inner content
922
- const innerContent = Array.prototype.slice
923
- .call(children, 1, -1)
924
- .map((child) => parse(child))
925
- .join(""); // Chỉ trả về nếu nội dung không rỗng
926
- const result = `\\left\\{${innerContent}\\right\\}`;
927
- return result;
928
- }
929
-
930
1009
  Array.prototype.forEach.call(children, (node, idx) => {
931
1010
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
932
1011
  if (
@@ -954,55 +1033,59 @@
954
1033
  if (Brackets.contains(op)) {
955
1034
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
956
1035
  stretchy = ["", "true"].indexOf(stretchy) > -1;
957
-
958
- let escapedOp = op;
959
- if (op === "{" || op === "}") {
960
- escapedOp = `\\${op}`;
1036
+ const parentNode = node.parentNode;
1037
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1038
+ if (isInPower) {
1039
+ stretchy = false;
961
1040
  }
962
1041
 
963
1042
  if (Brackets.isRight(op)) {
964
1043
  const nearLeftObj = lefts[lefts.length - 1];
965
- if (nearLeftObj) {
966
- const parentNode = node.parentNode;
967
- const isInPower =
968
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
969
-
970
- let partToPush = "";
971
- if (stretchy && !isInPower) {
972
- partToPush = `\\right${escapedOp}`;
973
- } else {
974
- partToPush = escapedOp;
975
- }
976
- if (partToPush) {
977
- // CHỈ PUSH NẾU KHÔNG RỖNG
978
- parts.push(partToPush);
1044
+ if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
1045
+ const leftIdx = nearLeftObj.index;
1046
+ const leftRendered =
1047
+ typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
1048
+ let rightRendered = Brackets.parseRight(op, stretchy);
1049
+
1050
+ const leftRenderedTrimmed = leftRendered.trim();
1051
+ const rightRenderedTrimmed = String(rightRendered).trim();
1052
+
1053
+ if (
1054
+ leftRenderedTrimmed.startsWith("\\left") &&
1055
+ !rightRenderedTrimmed.startsWith("\\right")
1056
+ ) {
1057
+ parts[leftIdx] = leftRendered.replace("\\left", "");
1058
+ } else if (
1059
+ !leftRenderedTrimmed.startsWith("\\left") &&
1060
+ rightRenderedTrimmed.startsWith("\\right")
1061
+ ) {
1062
+ rightRendered = String(rightRendered).replace("\\right", "");
979
1063
  }
980
- // matched a left: remove corresponding left object
1064
+
1065
+ if (rightRendered) parts.push(rightRendered);
981
1066
  lefts.pop();
1067
+ } else if (Brackets.isLeft(op)) {
1068
+ // If it's a Right bracket but doesn't match the current Left,
1069
+ // AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
1070
+ const partToPush = Brackets.parseLeft(op, stretchy);
1071
+ if (partToPush) parts.push(partToPush);
1072
+ lefts.push({ op: op, index: parts.length - 1 });
982
1073
  } else {
983
- if (escapedOp) {
984
- // CHỈ PUSH NẾu KHÔNG RỖNG
985
- parts.push(escapedOp);
986
- }
1074
+ // Unmatched right bracket
1075
+ let rightRendered = Brackets.parseRight(op, stretchy);
1076
+ rightRendered = String(rightRendered).replace("\\right", "");
1077
+ if (rightRendered) parts.push(rightRendered);
987
1078
  }
988
1079
  } else {
989
- // ngoặc trái
990
- const parentNode = node.parentNode;
991
- const isInPower =
992
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
993
-
994
- let partToPush = "";
995
- if (stretchy && !isInPower) {
996
- partToPush = `\\left${escapedOp}`;
1080
+ // Must be Left bracket (or only Left)
1081
+ if (Brackets.isLeft(op)) {
1082
+ const partToPush = Brackets.parseLeft(op, stretchy);
1083
+ if (partToPush) parts.push(partToPush);
1084
+ lefts.push({ op: op, index: parts.length - 1 });
997
1085
  } else {
998
- partToPush = escapedOp;
1086
+ // Should not happen if Brackets.contains is correct
1087
+ if (op) parts.push(op);
999
1088
  }
1000
- if (partToPush) {
1001
- // CHỈ PUSH NẾU KHÔNG RỖNG
1002
- parts.push(partToPush);
1003
- }
1004
- // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1005
- lefts.push({ op: op, index: parts.length - 1 });
1006
1089
  }
1007
1090
  } else {
1008
1091
  const parsedOperator = parseOperator(node);
@@ -1011,45 +1094,6 @@
1011
1094
  parts.push(parsedOperator);
1012
1095
  }
1013
1096
  }
1014
-
1015
- // --- START PATCH V6 (Giữ nguyên logic V5) ---
1016
- } else if (NodeTool.getNodeName(node) === "msub") {
1017
- const subChildren = Array.from(NodeTool.getChildren(node));
1018
- if (
1019
- subChildren.length === 2 &&
1020
- NodeTool.getNodeName(subChildren[0]) === "mo" &&
1021
- NodeTool.getNodeText(subChildren[0]).trim() === ")"
1022
- ) {
1023
- // ĐÚNG LÀ NGOẠI LỆ
1024
-
1025
- const sub = parse(subChildren[1]);
1026
- // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1027
- const lastPart = parts.pop(); // lastPart = "\text{OH}"
1028
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1029
- for (let i = parts.length - 1; i >= 0; i--) {
1030
- if (parts[i] && parts[i].trim() === "\\left(") {
1031
- parts.splice(i, 1); // Xóa "\left("
1032
- break;
1033
- }
1034
- _;
1035
- } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1036
-
1037
- parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1038
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1039
-
1040
- const nearLeftObj = lefts[lefts.length - 1];
1041
- if (nearLeftObj) {
1042
- lefts.pop();
1043
- }
1044
- } else {
1045
- // <msub> bình thường
1046
- const parsed = parse(node);
1047
- if (parsed) {
1048
- // CHỈ PUSH NẾU KHÔNG RỖNG
1049
- parts.push(parsed);
1050
- }
1051
- }
1052
- // --- END PATCH V6 ---
1053
1097
  } else {
1054
1098
  // Các node khác như <mtext>, #text, v.v.
1055
1099
  const parsed = parse(node);
@@ -1061,40 +1105,13 @@
1061
1105
  });
1062
1106
 
1063
1107
  // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1064
- // khi không đóng tương ứng phía sau nếu có \right (hoặc dấu đóng) thì giữ \left
1108
+ // để tránh sinh LaTeX không hợp lệ (\left không có \right)
1065
1109
  if (lefts && lefts.length > 0) {
1066
- const rightForLeft = (left) => {
1067
- switch (left) {
1068
- case "(":
1069
- return ")";
1070
- case "[":
1071
- return "]";
1072
- case "{":
1073
- return "}";
1074
- case "|":
1075
- return "|";
1076
- default:
1077
- return ".";
1078
- }
1079
- };
1080
-
1081
1110
  lefts.forEach((leftObj) => {
1082
1111
  const idx = leftObj && leftObj.index;
1083
1112
  if (typeof idx !== "number" || !parts[idx]) return;
1084
-
1085
- const left = leftObj.op;
1086
- const right = rightForLeft(left);
1087
-
1088
- // 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
1089
- const tail = parts.slice(idx + 1).map(String).join(" ");
1090
- const hasMatchingRight =
1091
- (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1092
-
1093
- if (!hasMatchingRight) {
1094
- // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1095
- parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1096
- }
1097
- // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1113
+ // Use regex with whitespace support and global flag just in case
1114
+ parts[idx] = String(parts[idx]).replace(/\\left/g, "");
1098
1115
  });
1099
1116
  }
1100
1117
 
@@ -1102,6 +1119,44 @@
1102
1119
  return parts;
1103
1120
  }
1104
1121
 
1122
+ function isLimitOperator(base) {
1123
+ const t = (base || "").trim();
1124
+ const ops = [
1125
+ "\\sum",
1126
+ "\\prod",
1127
+ "\\coprod",
1128
+ "\\int",
1129
+ "\\oint",
1130
+ "\\bigcap",
1131
+ "\\bigcup",
1132
+ "\\bigsqcup",
1133
+ "\\bigvee",
1134
+ "\\bigwedge",
1135
+ "\\bigodot",
1136
+ "\\bigotimes",
1137
+ "\\bigoplus",
1138
+ "\\biguplus",
1139
+ "\\lim",
1140
+ "\\max",
1141
+ "\\min",
1142
+ "\\sup",
1143
+ "\\inf",
1144
+ "\\det",
1145
+ "\\gcd",
1146
+ "\\Pr",
1147
+ "\\limsup",
1148
+ "\\liminf",
1149
+ ];
1150
+ return ops.some(
1151
+ (op) =>
1152
+ t === op ||
1153
+ t.startsWith(op + " ") ||
1154
+ t.startsWith(op + "\\") ||
1155
+ t.startsWith(op + "^") ||
1156
+ t.startsWith(op + "_")
1157
+ );
1158
+ }
1159
+
1105
1160
  function getRender(node) {
1106
1161
  let render = undefined;
1107
1162
  const nodeName = NodeTool.getNodeName(node);
@@ -1250,9 +1305,11 @@
1250
1305
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1251
1306
  ) {
1252
1307
  const lastChild = sub.lastChild;
1253
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1308
+ return lastChild
1309
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1310
+ : base;
1254
1311
  }
1255
- return `${base}_{${parse(sub)}}`;
1312
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1256
1313
  };
1257
1314
  break;
1258
1315
 
@@ -1260,22 +1317,9 @@
1260
1317
  render = function (node, children) {
1261
1318
  const childrenArray = Array.from(children);
1262
1319
  if (!childrenArray || childrenArray.length < 2) return "";
1263
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
1264
- const baseNode = childrenArray[0];
1265
- let base = parse(baseNode) || "";
1266
- if (NodeTool.getNodeName(baseNode) === "mo") {
1267
- const op = NodeTool.getNodeText(baseNode).trim();
1268
- if (Brackets.isRight(op)) {
1269
- // Escape brace characters
1270
- if (op === "}" || op === "{") {
1271
- base = `\\right\\${op}`;
1272
- } else {
1273
- base = `\\right${op}`;
1274
- }
1275
- }
1276
- }
1320
+ const base = parse(childrenArray[0]) || "";
1277
1321
  const sup = parse(childrenArray[1]) || "";
1278
- return `${base}^{${sup}}`;
1322
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1279
1323
  };
1280
1324
  break;
1281
1325
 
@@ -1299,7 +1343,7 @@
1299
1343
  .join("");
1300
1344
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1301
1345
  }
1302
- return `${base}_{${sub}}^{${sup}}`;
1346
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1303
1347
  };
1304
1348
  break;
1305
1349
 
@@ -1309,9 +1353,15 @@
1309
1353
  if (!childrenArray || childrenArray.length < 2) return "";
1310
1354
  const base = parse(childrenArray[0]) || "";
1311
1355
  const over = parse(childrenArray[1]) || "";
1312
- const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1356
+ const overNode = childrenArray[1];
1357
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1358
+ const overText = NodeTool.getNodeText(overNode)?.trim() || "";
1313
1359
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1314
1360
 
1361
+ // Handle arrows with extensible commands
1362
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1363
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1364
+
1315
1365
  // Handle biology notation (double overline)
1316
1366
  if (overText === "¯") {
1317
1367
  const parentNode = node.parentNode;
@@ -1327,6 +1377,27 @@
1327
1377
 
1328
1378
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1329
1379
  if (overText === "^" && isAccent) return `\\hat{${base}}`;
1380
+ if (overText === "\u23DE") return `\\overbrace{${base}}`;
1381
+
1382
+ // Check for nested overbrace (layer-2)
1383
+ if (NodeTool.getNodeName(overNode) === "mover") {
1384
+ const innerChildren = Array.from(NodeTool.getChildren(overNode));
1385
+ if (innerChildren.length >= 2) {
1386
+ const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
1387
+ if (innerBaseText === "\u23DE") {
1388
+ const label = parse(innerChildren[1]);
1389
+ return `\\overbrace{${base}}\\limits^{${label}}`;
1390
+ }
1391
+ }
1392
+ }
1393
+
1394
+ if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
1395
+ return `${base}\\limits^{${over}}`;
1396
+ }
1397
+
1398
+ if (isLimitOperator(base)) {
1399
+ return `${base}\\limits^{${over}}`;
1400
+ }
1330
1401
  return `\\overset{${over}}{${base}}`;
1331
1402
  };
1332
1403
  break;
@@ -1337,10 +1408,21 @@
1337
1408
  if (!childrenArray || childrenArray.length < 2) return "";
1338
1409
  const base = parse(childrenArray[0]) || "";
1339
1410
  const under = parse(childrenArray[1]) || "";
1411
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1412
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1340
1413
  const isUnderAccent =
1341
1414
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1342
1415
 
1416
+ // Handle arrows with extensible commands
1417
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1418
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1419
+
1343
1420
  if (base === "∫") return `\\int_{${under}}`;
1421
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1422
+
1423
+ if (isLimitOperator(base)) {
1424
+ return `${base}\\limits_{${under}}`;
1425
+ }
1344
1426
  return `\\underset{${under}}{${base}}`;
1345
1427
  };
1346
1428
  break;
@@ -1354,19 +1436,19 @@
1354
1436
  const over = parse(childrenArray[2]);
1355
1437
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1356
1438
 
1357
- // Special handling for chemical reaction arrow
1358
- if (
1359
- baseText === "" &&
1360
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1361
- ) {
1362
- return `\\xrightarrow[${under}]{${over}}`;
1363
- }
1439
+ // Special handling for chemical reaction arrow and other arrows
1440
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
1441
+ if (baseText === "" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
1364
1442
 
1365
1443
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1366
1444
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1367
1445
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1368
1446
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1369
- return `${base}_{${under}}^{${over}}`;
1447
+
1448
+ if (isLimitOperator(base)) {
1449
+ return `${base}\\limits_{${under}}^{${over}}`;
1450
+ }
1451
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1370
1452
  };
1371
1453
  break;
1372
1454
 
@@ -1374,30 +1456,56 @@
1374
1456
  render = function (node, children) {
1375
1457
  const childrenArray = Array.from(children);
1376
1458
  if (!childrenArray || childrenArray.length < 1) return "";
1377
- const base = parse(childrenArray[0]);
1378
- let prescripts = "";
1379
- let postscripts = "";
1459
+ const base = parse(childrenArray[0]) || "";
1460
+
1461
+ const postSub = [];
1462
+ const postSup = [];
1463
+ const preSub = [];
1464
+ const preSup = [];
1465
+
1380
1466
  let i = 1;
1467
+ let inPrescripts = false;
1381
1468
 
1382
1469
  while (i < childrenArray.length) {
1383
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1384
- i++;
1385
- if (i + 1 < childrenArray.length) {
1386
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1387
- childrenArray[i + 1]
1388
- )}}`;
1389
- i += 2;
1390
- }
1470
+ const name = NodeTool.getNodeName(childrenArray[i]);
1471
+ if (name === "mprescripts") {
1472
+ inPrescripts = true;
1473
+ i += 1;
1474
+ continue;
1475
+ }
1476
+
1477
+ const subNode = childrenArray[i];
1478
+ const supNode = childrenArray[i + 1];
1479
+ if (!subNode || !supNode) break;
1480
+
1481
+ const sub = parse(subNode) || "";
1482
+ const sup = parse(supNode) || "";
1483
+
1484
+ if (inPrescripts) {
1485
+ if (sub) preSub.push(sub);
1486
+ if (sup) preSup.push(sup);
1391
1487
  } else {
1392
- if (i + 1 < childrenArray.length) {
1393
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1394
- childrenArray[i + 1]
1395
- )}}`;
1396
- i += 2;
1397
- } else break;
1488
+ if (sub) postSub.push(sub);
1489
+ if (sup) postSup.push(sup);
1398
1490
  }
1491
+
1492
+ i += 2;
1399
1493
  }
1400
- return `${base}${prescripts}${postscripts}`;
1494
+
1495
+ const preSubStr = preSub.join(" ");
1496
+ const preSupStr = preSup.join(" ");
1497
+ const postSubStr = postSub.join(" ");
1498
+ const postSupStr = postSup.join(" ");
1499
+
1500
+ let pre = "";
1501
+ if (preSubStr) pre += `_{${preSubStr}}`;
1502
+ if (preSupStr) pre += `^{${preSupStr}}`;
1503
+
1504
+ let post = "";
1505
+ if (postSubStr) post += `_{${postSubStr}}`;
1506
+ if (postSupStr) post += `^{${postSupStr}}`;
1507
+
1508
+ return `${pre}${wrapBaseForScript(base)}${post}`;
1401
1509
  };
1402
1510
  break;
1403
1511
 
@@ -1498,7 +1606,28 @@
1498
1606
  const num = parse(childrenArray[0]);
1499
1607
  const den = parse(childrenArray[1]);
1500
1608
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1501
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
1609
+ const bevelled = NodeTool.getAttr(node, "bevelled", "false");
1610
+
1611
+ if (bevelled === "true") {
1612
+ return `{}^{${num}}/_{${den}}`;
1613
+ }
1614
+
1615
+ if (["0", "0px"].indexOf(linethickness) > -1) {
1616
+ const prevNode = NodeTool.getPrevNode(node);
1617
+ const nextNode = NodeTool.getNextNode(node);
1618
+ if (
1619
+ prevNode &&
1620
+ NodeTool.getNodeName(prevNode) === "mo" &&
1621
+ NodeTool.getNodeText(prevNode).trim() === "(" &&
1622
+ nextNode &&
1623
+ NodeTool.getNodeName(nextNode) === "mo" &&
1624
+ NodeTool.getNodeText(nextNode).trim() === ")"
1625
+ ) {
1626
+ return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
1627
+ }
1628
+ return `{}_{${den}}^{${num}}`;
1629
+ }
1630
+
1502
1631
  return `\\frac{${num}}{${den}}`;
1503
1632
  };
1504
1633
  break;
@@ -1508,7 +1637,10 @@
1508
1637
  const childrenArray = Array.from(children);
1509
1638
  const open = NodeTool.getAttr(node, "open", "(");
1510
1639
  const close = NodeTool.getAttr(node, "close", ")");
1511
- const separators = NodeTool.getAttr(node, "separators", ",").split("");
1640
+ const separatorsStr = NodeTool.getAttr(node, "separators", ",");
1641
+ const separators = separatorsStr
1642
+ .split("")
1643
+ .filter((c) => c.trim().length === 1);
1512
1644
 
1513
1645
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1514
1646
  if (open === "|") {
@@ -1559,9 +1691,10 @@
1559
1691
  parts.push(parse(child));
1560
1692
  if (
1561
1693
  index < childrenArray.length - 1 &&
1562
- separators[index % separators.length]
1694
+ separators.length > 0
1563
1695
  ) {
1564
- parts.push(separators[index % separators.length]);
1696
+ const sep = separators[index] ?? separators[separators.length - 1];
1697
+ if (sep) parts.push(sep);
1565
1698
  }
1566
1699
  });
1567
1700
  return `\\left[${parts.join("")}\\right)`;
@@ -1573,18 +1706,20 @@
1573
1706
  parts.push(parse(child));
1574
1707
  if (
1575
1708
  index < childrenArray.length - 1 &&
1576
- separators[index % separators.length]
1709
+ separators.length > 0
1577
1710
  ) {
1578
- parts.push(separators[index % separators.length]);
1711
+ const sep = separators[index] ?? separators[separators.length - 1];
1712
+ if (sep) parts.push(sep);
1579
1713
  }
1580
1714
  });
1581
1715
  const content = parts.join("");
1582
1716
 
1583
1717
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1584
1718
  if (open === "|" && close === "|") return `\\left|${content}\\right|`;
1585
- if (!close) return `\\left${open}${content}\\right.`;
1586
- if (!open) return `\\left.${content}\\right${close}`;
1587
- return `\\left${open}${content}\\right${close}`;
1719
+ const left = open ? Brackets.parseLeft(open) : "";
1720
+ const right = close ? Brackets.parseRight(close) : "";
1721
+ if (!close && open) return `${left}${content}\\right.`;
1722
+ return `${left}${content}${right}`;
1588
1723
  };
1589
1724
  break;
1590
1725
 
@@ -1610,10 +1745,17 @@
1610
1745
  case "mn":
1611
1746
  case "mo":
1612
1747
  case "ms":
1613
- case "mtext":
1614
1748
  render = getRender_joinSeparator("@content");
1615
1749
  break;
1616
1750
 
1751
+ case "mtext":
1752
+ render = function (node, children) {
1753
+ const childrenArray = Array.from(children);
1754
+ const content = renderChildren(childrenArray).join("");
1755
+ return `\\text{${content}}`;
1756
+ };
1757
+ break;
1758
+
1617
1759
  case "mphantom":
1618
1760
  render = function (node, children) {
1619
1761
  const childrenArray = Array.from(children);