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,6 @@
1
1
  const Brackets = {
2
- left: ['(', '[', '{', '|', '', '', '', '', ''],
3
- right: [')', ']', '}', '|', '', '', '', '', ''],
2
+ left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
3
+ right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
4
4
  isPair: function(l, r){
5
5
  const idx = this.left.indexOf(l);
6
6
  return r === this.right[idx];
@@ -22,17 +22,17 @@ const Brackets = {
22
22
  case '[':
23
23
  case '|': r = `\\left${it}`;
24
24
  break;
25
- case '': r = '\\left\\|';
25
+ case '\u2016': r = '\\left\\|';
26
26
  break;
27
27
  case '{': r = '\\left\\{';
28
28
  break;
29
- case '': r = '\\left\\langle ';
29
+ case '\u27E8': r = '\\left\\langle ';
30
30
  break;
31
- case '': r = '\\left\\lfloor ';
31
+ case '\u230A': r = '\\left\\lfloor ';
32
32
  break;
33
- case '': r = '\\left\\lceil ';
33
+ case '\u2308': r = '\\left\\lceil ';
34
34
  break;
35
- case '': r = '\\left\\ulcorner ';
35
+ case '\u231C': r = '\\left\\ulcorner ';
36
36
  break;
37
37
  }
38
38
  return (stretchy ? r : r.replace('\\left', ''));
@@ -46,23 +46,36 @@ const Brackets = {
46
46
  case ']':
47
47
  case '|': r = `\\right${it}`;
48
48
  break;
49
- case '': r = '\\right\\|';
49
+ case '\u2016': r = '\\right\\|';
50
50
  break;
51
51
  case '}': r = '\\right\\}';
52
52
  break;
53
- case '': r = ' \\right\\rangle';
53
+ case '\u27E9': r = ' \\right\\rangle';
54
54
  break;
55
- case '': r = ' \\right\\rfloor';
55
+ case '\u230B': r = ' \\right\\rfloor';
56
56
  break;
57
- case '': r = ' \\right\\rceil';
57
+ case '\u2309': r = ' \\right\\rceil';
58
58
  break;
59
- case '': r = ' \\right\\urcorner';
59
+ case '\u231D': r = ' \\right\\urcorner';
60
60
  break;
61
61
  }
62
62
  return (stretchy ? r : r.replace('\\right', ''));
63
63
  }
64
64
  };
65
65
 
66
+ var _nodeResolve_empty = {};
67
+
68
+ var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
69
+ __proto__: null,
70
+ 'default': _nodeResolve_empty
71
+ });
72
+
73
+ function getCjsExportFromNamespace (n) {
74
+ return n && n['default'] || n;
75
+ }
76
+
77
+ var require$$0 = getCjsExportFromNamespace(_nodeResolve_empty$1);
78
+
66
79
  /*
67
80
  * Set up window for Node.js
68
81
  */
@@ -92,11 +105,17 @@ function canParseHTMLNatively () {
92
105
  function createHTMLParser () {
93
106
  const Parser = function () {};
94
107
 
95
- if (typeof process !== 'undefined' && true) {
96
- if (shouldUseActiveX()) {
108
+ const hasDocument =
109
+ typeof document !== 'undefined' &&
110
+ document &&
111
+ document.implementation &&
112
+ typeof document.implementation.createHTMLDocument === 'function';
113
+
114
+ if (hasDocument) {
115
+ if (typeof process !== 'undefined' && true && shouldUseActiveX()) {
97
116
  Parser.prototype.parseFromString = function (string) {
98
117
  const doc = new window.ActiveXObject('htmlfile');
99
- doc.designMode = 'on'; // disable on-page scripts
118
+ doc.designMode = 'on';
100
119
  doc.open();
101
120
  doc.write(string);
102
121
  doc.close();
@@ -113,11 +132,9 @@ function createHTMLParser () {
113
132
  }
114
133
  } else {
115
134
  Parser.prototype.parseFromString = function (string) {
116
- const doc = document.implementation.createHTMLDocument('');
117
- doc.open();
118
- doc.write(string);
119
- doc.close();
120
- return doc
135
+ const domino = require$$0;
136
+ const window = domino.createWindow(string || '');
137
+ return window.document
121
138
  };
122
139
  }
123
140
  return Parser
@@ -135,11 +152,54 @@ function shouldUseActiveX () {
135
152
 
136
153
  const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
137
154
 
155
+ function normalizeMathMLInput(input) {
156
+ let s = input == null ? '' : String(input);
157
+
158
+ if (s.includes('<math') && /\\[ntr"]/.test(s)) {
159
+ s = s
160
+ .replace(/\\r\\n/g, '\n')
161
+ .replace(/\\n/g, '\n')
162
+ .replace(/\\t/g, '\t')
163
+ .replace(/\\"/g, '"')
164
+ .replace(/\\'/g, "'");
165
+ }
166
+
167
+ s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
168
+
169
+ s = s.replace(
170
+ /xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
171
+ 'xmlns="$2"'
172
+ );
173
+
174
+ 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"');
175
+
176
+ if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
177
+ s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
178
+ }
179
+
180
+ return s;
181
+ }
182
+
138
183
  const NodeTool = {
139
184
  parseMath: function(html) {
185
+ const normalized = normalizeMathMLInput(html);
140
186
  const parser = new HTMLParser();
141
- const doc = parser.parseFromString(html, 'text/html');
142
- return doc.querySelector('math');
187
+ const doc = parser.parseFromString(normalized, 'text/html');
188
+ let math = doc && doc.querySelector ? doc.querySelector('math') : null;
189
+
190
+ if (!math) {
191
+ const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
192
+ if (match) {
193
+ const retryDoc = parser.parseFromString(match[0], 'text/html');
194
+ math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
195
+ }
196
+ }
197
+
198
+ if (!math) {
199
+ throw new Error('Invalid MathML: missing <math> root element');
200
+ }
201
+
202
+ return math;
143
203
  },
144
204
  getChildren: function(node) {
145
205
  return node.children;
@@ -275,24 +335,24 @@ const MathSymbol = {
275
335
  bigCommand: {
276
336
  decimals: [8721, 8719, 8720, 10753, 10754, 10752, 8899, 8898, 10756, 10758, 8897, 8896, 8747, 8750, 8748, 8749, 10764, 8747],
277
337
  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",
338
+ "\\sum ",
339
+ "\\prod ",
340
+ "\\coprod ",
341
+ "\\bigoplus ",
342
+ "\\bigotimes ",
343
+ "\\bigodot ",
344
+ "\\bigcup ",
345
+ "\\bigcap ",
346
+ "\\biguplus ",
347
+ "\\bigsqcup ",
348
+ "\\bigvee ",
349
+ "\\bigwedge ",
350
+ "\\int ",
351
+ "\\oint ",
352
+ "\\iint ",
353
+ "\\iiint ",
354
+ "\\iiiint ",
355
+ "\\idotsint ",
296
356
  ]
297
357
  },
298
358
 
@@ -610,9 +670,10 @@ function convert(mathmlHtml) {
610
670
  // Thêm xử lý cho các thẻ MathML khác
611
671
  result = result
612
672
  .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
673
+ .replace(/∫/g, " \\int "); // Tích phân
674
+
675
+ // Đả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)
676
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
616
677
 
617
678
  return result;
618
679
  }
@@ -758,20 +819,24 @@ function parseOperator(node) {
758
819
  "±": " \\pm ",
759
820
  "×": " \\times ",
760
821
  "÷": " \\div ",
761
- "∑": " \\sum ",
762
- "∏": " \\prod ",
763
- "∫": "\\int",
822
+ "∑": "\\sum ",
823
+ "∏": "\\prod ",
824
+ "∫": "\\int ",
764
825
  "−": "-",
765
826
  "≠": " \\neq ",
766
827
  ">": " > ",
767
828
  "=": " = ",
829
+ "(": "(",
830
+ ")": ")",
768
831
  ",": ", ", // Dấu phẩy trong tập hợp
769
832
  ";": ";",
770
833
  Ω: "\\Omega",
771
834
  "|": " \\mid ", // PATCH: set-builder mid
772
835
  π: " \\pi ", // PATCH: Greek letter
836
+ "...": "\\dots",
773
837
  };
774
- return operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
838
+ const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
839
+ return res;
775
840
  }
776
841
  // --- END PATCH ---
777
842
 
@@ -798,6 +863,20 @@ function parseElementMi(node) {
798
863
  // Math Number
799
864
  function parseElementMn(node) {
800
865
  let it = NodeTool.getNodeText(node).trim();
866
+ // Loại bỏ các ký tự điều khiển hoặc khoảng trắng lạ
867
+ it = it.replace(/[\u0000-\u001F\u007F-\u009F\u00A0]/g, "");
868
+
869
+ // Danh sách các hàm toán học mở rộng
870
+ const mathFunctions = ["cos", "sin", "tan", "cot", "arccos", "arcsin", "arctan", "arccot", "log", "ln", "lim", "sinh", "cosh", "tanh", "sec", "csc"];
871
+
872
+ if (mathFunctions.some(fn => it.toLowerCase().includes(fn))) {
873
+ // Tìm hàm khớp chính xác nhất
874
+ for (const fn of mathFunctions) {
875
+ if (it.toLowerCase() === fn) {
876
+ return "\\" + fn + " ";
877
+ }
878
+ }
879
+ }
801
880
  return escapeSpecialChars(it);
802
881
  }
803
882
 
@@ -831,8 +910,8 @@ function parseElementMtext(node) {
831
910
  // Nếu đã là lệnh LaTeX thì giữ nguyên
832
911
  if (content.startsWith("\\")) return content;
833
912
 
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)) {
913
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
914
+ if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
836
915
  return `\\text{${content}}`;
837
916
  }
838
917
 
@@ -887,6 +966,22 @@ function escapeSpecialChars(text) {
887
966
  return text;
888
967
  }
889
968
 
969
+ function wrapBaseForScript(base) {
970
+ const t = (base ?? "").trim();
971
+ if (!t) return "";
972
+ if (t.startsWith("{") && t.endsWith("}")) return t;
973
+ if (/^\\[a-zA-Z]+$/.test(t)) return t;
974
+ const needsWrap =
975
+ t.includes("_") ||
976
+ t.includes("^") ||
977
+ /\s/.test(t) ||
978
+ t.startsWith("\\left") ||
979
+ t.startsWith("\\right") ||
980
+ /\\[a-zA-Z]+/.test(t) ||
981
+ /[{}]/.test(t);
982
+ return needsWrap ? `{${t}}` : t;
983
+ }
984
+
890
985
  function parseContainer(node, children) {
891
986
  const render = getRender(node);
892
987
  if (render) {
@@ -905,22 +1000,6 @@ function renderChildren(children) {
905
1000
  // lefts là mảng object: { op: "(", index: 5 }
906
1001
  let lefts = [];
907
1002
 
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
1003
  Array.prototype.forEach.call(children, (node, idx) => {
925
1004
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
926
1005
  if (
@@ -948,55 +1027,59 @@ function renderChildren(children) {
948
1027
  if (Brackets.contains(op)) {
949
1028
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
950
1029
  stretchy = ["", "true"].indexOf(stretchy) > -1;
951
-
952
- let escapedOp = op;
953
- if (op === "{" || op === "}") {
954
- escapedOp = `\\${op}`;
1030
+ const parentNode = node.parentNode;
1031
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1032
+ if (isInPower) {
1033
+ stretchy = false;
955
1034
  }
956
1035
 
957
1036
  if (Brackets.isRight(op)) {
958
1037
  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);
1038
+ if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
1039
+ const leftIdx = nearLeftObj.index;
1040
+ const leftRendered =
1041
+ typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
1042
+ let rightRendered = Brackets.parseRight(op, stretchy);
1043
+
1044
+ const leftRenderedTrimmed = leftRendered.trim();
1045
+ const rightRenderedTrimmed = String(rightRendered).trim();
1046
+
1047
+ if (
1048
+ leftRenderedTrimmed.startsWith("\\left") &&
1049
+ !rightRenderedTrimmed.startsWith("\\right")
1050
+ ) {
1051
+ parts[leftIdx] = leftRendered.replace("\\left", "");
1052
+ } else if (
1053
+ !leftRenderedTrimmed.startsWith("\\left") &&
1054
+ rightRenderedTrimmed.startsWith("\\right")
1055
+ ) {
1056
+ rightRendered = String(rightRendered).replace("\\right", "");
973
1057
  }
974
- // matched a left: remove corresponding left object
1058
+
1059
+ if (rightRendered) parts.push(rightRendered);
975
1060
  lefts.pop();
1061
+ } else if (Brackets.isLeft(op)) {
1062
+ // If it's a Right bracket but doesn't match the current Left,
1063
+ // AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
1064
+ const partToPush = Brackets.parseLeft(op, stretchy);
1065
+ if (partToPush) parts.push(partToPush);
1066
+ lefts.push({ op: op, index: parts.length - 1 });
976
1067
  } else {
977
- if (escapedOp) {
978
- // CHỈ PUSH NẾu KHÔNG RỖNG
979
- parts.push(escapedOp);
980
- }
1068
+ // Unmatched right bracket
1069
+ let rightRendered = Brackets.parseRight(op, stretchy);
1070
+ rightRendered = String(rightRendered).replace("\\right", "");
1071
+ if (rightRendered) parts.push(rightRendered);
981
1072
  }
982
1073
  } 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}`;
1074
+ // Must be Left bracket (or only Left)
1075
+ if (Brackets.isLeft(op)) {
1076
+ const partToPush = Brackets.parseLeft(op, stretchy);
1077
+ if (partToPush) parts.push(partToPush);
1078
+ lefts.push({ op: op, index: parts.length - 1 });
991
1079
  } else {
992
- partToPush = escapedOp;
1080
+ // Should not happen if Brackets.contains is correct
1081
+ if (op) parts.push(op);
993
1082
  }
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
1083
  }
1001
1084
  } else {
1002
1085
  const parsedOperator = parseOperator(node);
@@ -1005,45 +1088,6 @@ function renderChildren(children) {
1005
1088
  parts.push(parsedOperator);
1006
1089
  }
1007
1090
  }
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
1091
  } else {
1048
1092
  // Các node khác như <mtext>, #text, v.v.
1049
1093
  const parsed = parse(node);
@@ -1055,40 +1099,13 @@ function renderChildren(children) {
1055
1099
  });
1056
1100
 
1057
1101
  // 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
1102
+ // để tránh sinh LaTeX không hợp lệ (\left không có \right)
1059
1103
  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
1104
  lefts.forEach((leftObj) => {
1076
1105
  const idx = leftObj && leftObj.index;
1077
1106
  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
1107
+ // Use regex with whitespace support and global flag just in case
1108
+ parts[idx] = String(parts[idx]).replace(/\\left/g, "");
1092
1109
  });
1093
1110
  }
1094
1111
 
@@ -1096,6 +1113,44 @@ function renderChildren(children) {
1096
1113
  return parts;
1097
1114
  }
1098
1115
 
1116
+ function isLimitOperator(base) {
1117
+ const t = (base || "").trim();
1118
+ const ops = [
1119
+ "\\sum",
1120
+ "\\prod",
1121
+ "\\coprod",
1122
+ "\\int",
1123
+ "\\oint",
1124
+ "\\bigcap",
1125
+ "\\bigcup",
1126
+ "\\bigsqcup",
1127
+ "\\bigvee",
1128
+ "\\bigwedge",
1129
+ "\\bigodot",
1130
+ "\\bigotimes",
1131
+ "\\bigoplus",
1132
+ "\\biguplus",
1133
+ "\\lim",
1134
+ "\\max",
1135
+ "\\min",
1136
+ "\\sup",
1137
+ "\\inf",
1138
+ "\\det",
1139
+ "\\gcd",
1140
+ "\\Pr",
1141
+ "\\limsup",
1142
+ "\\liminf",
1143
+ ];
1144
+ return ops.some(
1145
+ (op) =>
1146
+ t === op ||
1147
+ t.startsWith(op + " ") ||
1148
+ t.startsWith(op + "\\") ||
1149
+ t.startsWith(op + "^") ||
1150
+ t.startsWith(op + "_")
1151
+ );
1152
+ }
1153
+
1099
1154
  function getRender(node) {
1100
1155
  let render = undefined;
1101
1156
  const nodeName = NodeTool.getNodeName(node);
@@ -1244,9 +1299,11 @@ function getRender(node) {
1244
1299
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1245
1300
  ) {
1246
1301
  const lastChild = sub.lastChild;
1247
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1302
+ return lastChild
1303
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1304
+ : base;
1248
1305
  }
1249
- return `${base}_{${parse(sub)}}`;
1306
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1250
1307
  };
1251
1308
  break;
1252
1309
 
@@ -1254,22 +1311,9 @@ function getRender(node) {
1254
1311
  render = function (node, children) {
1255
1312
  const childrenArray = Array.from(children);
1256
1313
  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
- }
1314
+ const base = parse(childrenArray[0]) || "";
1271
1315
  const sup = parse(childrenArray[1]) || "";
1272
- return `${base}^{${sup}}`;
1316
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1273
1317
  };
1274
1318
  break;
1275
1319
 
@@ -1293,7 +1337,7 @@ function getRender(node) {
1293
1337
  .join("");
1294
1338
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1295
1339
  }
1296
- return `${base}_{${sub}}^{${sup}}`;
1340
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1297
1341
  };
1298
1342
  break;
1299
1343
 
@@ -1303,9 +1347,15 @@ function getRender(node) {
1303
1347
  if (!childrenArray || childrenArray.length < 2) return "";
1304
1348
  const base = parse(childrenArray[0]) || "";
1305
1349
  const over = parse(childrenArray[1]) || "";
1306
- const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1350
+ const overNode = childrenArray[1];
1351
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1352
+ const overText = NodeTool.getNodeText(overNode)?.trim() || "";
1307
1353
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1308
1354
 
1355
+ // Handle arrows with extensible commands
1356
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1357
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1358
+
1309
1359
  // Handle biology notation (double overline)
1310
1360
  if (overText === "¯") {
1311
1361
  const parentNode = node.parentNode;
@@ -1321,6 +1371,27 @@ function getRender(node) {
1321
1371
 
1322
1372
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1323
1373
  if (overText === "^" && isAccent) return `\\hat{${base}}`;
1374
+ if (overText === "\u23DE") return `\\overbrace{${base}}`;
1375
+
1376
+ // Check for nested overbrace (layer-2)
1377
+ if (NodeTool.getNodeName(overNode) === "mover") {
1378
+ const innerChildren = Array.from(NodeTool.getChildren(overNode));
1379
+ if (innerChildren.length >= 2) {
1380
+ const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
1381
+ if (innerBaseText === "\u23DE") {
1382
+ const label = parse(innerChildren[1]);
1383
+ return `\\overbrace{${base}}\\limits^{${label}}`;
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
1389
+ return `${base}\\limits^{${over}}`;
1390
+ }
1391
+
1392
+ if (isLimitOperator(base)) {
1393
+ return `${base}\\limits^{${over}}`;
1394
+ }
1324
1395
  return `\\overset{${over}}{${base}}`;
1325
1396
  };
1326
1397
  break;
@@ -1331,10 +1402,21 @@ function getRender(node) {
1331
1402
  if (!childrenArray || childrenArray.length < 2) return "";
1332
1403
  const base = parse(childrenArray[0]) || "";
1333
1404
  const under = parse(childrenArray[1]) || "";
1405
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1406
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1334
1407
  const isUnderAccent =
1335
1408
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1336
1409
 
1410
+ // Handle arrows with extensible commands
1411
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1412
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1413
+
1337
1414
  if (base === "∫") return `\\int_{${under}}`;
1415
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1416
+
1417
+ if (isLimitOperator(base)) {
1418
+ return `${base}\\limits_{${under}}`;
1419
+ }
1338
1420
  return `\\underset{${under}}{${base}}`;
1339
1421
  };
1340
1422
  break;
@@ -1348,19 +1430,19 @@ function getRender(node) {
1348
1430
  const over = parse(childrenArray[2]);
1349
1431
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1350
1432
 
1351
- // Special handling for chemical reaction arrow
1352
- if (
1353
- baseText === "" &&
1354
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1355
- ) {
1356
- return `\\xrightarrow[${under}]{${over}}`;
1357
- }
1433
+ // Special handling for chemical reaction arrow and other arrows
1434
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
1435
+ if (baseText === "" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
1358
1436
 
1359
1437
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1360
1438
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1361
1439
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1362
1440
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1363
- return `${base}_{${under}}^{${over}}`;
1441
+
1442
+ if (isLimitOperator(base)) {
1443
+ return `${base}\\limits_{${under}}^{${over}}`;
1444
+ }
1445
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1364
1446
  };
1365
1447
  break;
1366
1448
 
@@ -1368,30 +1450,56 @@ function getRender(node) {
1368
1450
  render = function (node, children) {
1369
1451
  const childrenArray = Array.from(children);
1370
1452
  if (!childrenArray || childrenArray.length < 1) return "";
1371
- const base = parse(childrenArray[0]);
1372
- let prescripts = "";
1373
- let postscripts = "";
1453
+ const base = parse(childrenArray[0]) || "";
1454
+
1455
+ const postSub = [];
1456
+ const postSup = [];
1457
+ const preSub = [];
1458
+ const preSup = [];
1459
+
1374
1460
  let i = 1;
1461
+ let inPrescripts = false;
1375
1462
 
1376
1463
  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
- }
1464
+ const name = NodeTool.getNodeName(childrenArray[i]);
1465
+ if (name === "mprescripts") {
1466
+ inPrescripts = true;
1467
+ i += 1;
1468
+ continue;
1469
+ }
1470
+
1471
+ const subNode = childrenArray[i];
1472
+ const supNode = childrenArray[i + 1];
1473
+ if (!subNode || !supNode) break;
1474
+
1475
+ const sub = parse(subNode) || "";
1476
+ const sup = parse(supNode) || "";
1477
+
1478
+ if (inPrescripts) {
1479
+ if (sub) preSub.push(sub);
1480
+ if (sup) preSup.push(sup);
1385
1481
  } else {
1386
- if (i + 1 < childrenArray.length) {
1387
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1388
- childrenArray[i + 1]
1389
- )}}`;
1390
- i += 2;
1391
- } else break;
1482
+ if (sub) postSub.push(sub);
1483
+ if (sup) postSup.push(sup);
1392
1484
  }
1485
+
1486
+ i += 2;
1393
1487
  }
1394
- return `${base}${prescripts}${postscripts}`;
1488
+
1489
+ const preSubStr = preSub.join(" ");
1490
+ const preSupStr = preSup.join(" ");
1491
+ const postSubStr = postSub.join(" ");
1492
+ const postSupStr = postSup.join(" ");
1493
+
1494
+ let pre = "";
1495
+ if (preSubStr) pre += `_{${preSubStr}}`;
1496
+ if (preSupStr) pre += `^{${preSupStr}}`;
1497
+
1498
+ let post = "";
1499
+ if (postSubStr) post += `_{${postSubStr}}`;
1500
+ if (postSupStr) post += `^{${postSupStr}}`;
1501
+
1502
+ return `${pre}${wrapBaseForScript(base)}${post}`;
1395
1503
  };
1396
1504
  break;
1397
1505
 
@@ -1492,7 +1600,28 @@ function getRender(node) {
1492
1600
  const num = parse(childrenArray[0]);
1493
1601
  const den = parse(childrenArray[1]);
1494
1602
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1495
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
1603
+ const bevelled = NodeTool.getAttr(node, "bevelled", "false");
1604
+
1605
+ if (bevelled === "true") {
1606
+ return `{}^{${num}}/_{${den}}`;
1607
+ }
1608
+
1609
+ if (["0", "0px"].indexOf(linethickness) > -1) {
1610
+ const prevNode = NodeTool.getPrevNode(node);
1611
+ const nextNode = NodeTool.getNextNode(node);
1612
+ if (
1613
+ prevNode &&
1614
+ NodeTool.getNodeName(prevNode) === "mo" &&
1615
+ NodeTool.getNodeText(prevNode).trim() === "(" &&
1616
+ nextNode &&
1617
+ NodeTool.getNodeName(nextNode) === "mo" &&
1618
+ NodeTool.getNodeText(nextNode).trim() === ")"
1619
+ ) {
1620
+ return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
1621
+ }
1622
+ return `{}_{${den}}^{${num}}`;
1623
+ }
1624
+
1496
1625
  return `\\frac{${num}}{${den}}`;
1497
1626
  };
1498
1627
  break;
@@ -1502,7 +1631,10 @@ function getRender(node) {
1502
1631
  const childrenArray = Array.from(children);
1503
1632
  const open = NodeTool.getAttr(node, "open", "(");
1504
1633
  const close = NodeTool.getAttr(node, "close", ")");
1505
- const separators = NodeTool.getAttr(node, "separators", ",").split("");
1634
+ const separatorsStr = NodeTool.getAttr(node, "separators", ",");
1635
+ const separators = separatorsStr
1636
+ .split("")
1637
+ .filter((c) => c.trim().length === 1);
1506
1638
 
1507
1639
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1508
1640
  if (open === "|") {
@@ -1553,9 +1685,10 @@ function getRender(node) {
1553
1685
  parts.push(parse(child));
1554
1686
  if (
1555
1687
  index < childrenArray.length - 1 &&
1556
- separators[index % separators.length]
1688
+ separators.length > 0
1557
1689
  ) {
1558
- parts.push(separators[index % separators.length]);
1690
+ const sep = separators[index] ?? separators[separators.length - 1];
1691
+ if (sep) parts.push(sep);
1559
1692
  }
1560
1693
  });
1561
1694
  return `\\left[${parts.join("")}\\right)`;
@@ -1567,18 +1700,20 @@ function getRender(node) {
1567
1700
  parts.push(parse(child));
1568
1701
  if (
1569
1702
  index < childrenArray.length - 1 &&
1570
- separators[index % separators.length]
1703
+ separators.length > 0
1571
1704
  ) {
1572
- parts.push(separators[index % separators.length]);
1705
+ const sep = separators[index] ?? separators[separators.length - 1];
1706
+ if (sep) parts.push(sep);
1573
1707
  }
1574
1708
  });
1575
1709
  const content = parts.join("");
1576
1710
 
1577
1711
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1578
1712
  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}`;
1713
+ const left = open ? Brackets.parseLeft(open) : "";
1714
+ const right = close ? Brackets.parseRight(close) : "";
1715
+ if (!close && open) return `${left}${content}\\right.`;
1716
+ return `${left}${content}${right}`;
1582
1717
  };
1583
1718
  break;
1584
1719
 
@@ -1604,10 +1739,17 @@ function getRender(node) {
1604
1739
  case "mn":
1605
1740
  case "mo":
1606
1741
  case "ms":
1607
- case "mtext":
1608
1742
  render = getRender_joinSeparator("@content");
1609
1743
  break;
1610
1744
 
1745
+ case "mtext":
1746
+ render = function (node, children) {
1747
+ const childrenArray = Array.from(children);
1748
+ const content = renderChildren(childrenArray).join("");
1749
+ return `\\text{${content}}`;
1750
+ };
1751
+ break;
1752
+
1611
1753
  case "mphantom":
1612
1754
  render = function (node, children) {
1613
1755
  const childrenArray = Array.from(children);