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