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,12 @@
1
1
  'use strict';
2
2
 
3
+ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
+
5
+ var domino = _interopDefault(require('domino'));
6
+
3
7
  const Brackets = {
4
- left: ['(', '[', '{', '|', '', '', '', '', ''],
5
- right: [')', ']', '}', '|', '', '', '', '', ''],
8
+ left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
9
+ right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
6
10
  isPair: function(l, r){
7
11
  const idx = this.left.indexOf(l);
8
12
  return r === this.right[idx];
@@ -24,17 +28,17 @@ const Brackets = {
24
28
  case '[':
25
29
  case '|': r = `\\left${it}`;
26
30
  break;
27
- case '': r = '\\left\\|';
31
+ case '\u2016': r = '\\left\\|';
28
32
  break;
29
33
  case '{': r = '\\left\\{';
30
34
  break;
31
- case '': r = '\\left\\langle ';
35
+ case '\u27E8': r = '\\left\\langle ';
32
36
  break;
33
- case '': r = '\\left\\lfloor ';
37
+ case '\u230A': r = '\\left\\lfloor ';
34
38
  break;
35
- case '': r = '\\left\\lceil ';
39
+ case '\u2308': r = '\\left\\lceil ';
36
40
  break;
37
- case '': r = '\\left\\ulcorner ';
41
+ case '\u231C': r = '\\left\\ulcorner ';
38
42
  break;
39
43
  }
40
44
  return (stretchy ? r : r.replace('\\left', ''));
@@ -48,17 +52,17 @@ const Brackets = {
48
52
  case ']':
49
53
  case '|': r = `\\right${it}`;
50
54
  break;
51
- case '': r = '\\right\\|';
55
+ case '\u2016': r = '\\right\\|';
52
56
  break;
53
57
  case '}': r = '\\right\\}';
54
58
  break;
55
- case '': r = ' \\right\\rangle';
59
+ case '\u27E9': r = ' \\right\\rangle';
56
60
  break;
57
- case '': r = ' \\right\\rfloor';
61
+ case '\u230B': r = ' \\right\\rfloor';
58
62
  break;
59
- case '': r = ' \\right\\rceil';
63
+ case '\u2309': r = ' \\right\\rceil';
60
64
  break;
61
- case '': r = ' \\right\\urcorner';
65
+ case '\u231D': r = ' \\right\\urcorner';
62
66
  break;
63
67
  }
64
68
  return (stretchy ? r : r.replace('\\right', ''));
@@ -94,11 +98,17 @@ function canParseHTMLNatively () {
94
98
  function createHTMLParser () {
95
99
  const Parser = function () {};
96
100
 
97
- if (typeof process !== 'undefined' && false) {
98
- if (shouldUseActiveX()) {
101
+ const hasDocument =
102
+ typeof document !== 'undefined' &&
103
+ document &&
104
+ document.implementation &&
105
+ typeof document.implementation.createHTMLDocument === 'function';
106
+
107
+ if (hasDocument) {
108
+ if (typeof process !== 'undefined' && false && shouldUseActiveX()) {
99
109
  Parser.prototype.parseFromString = function (string) {
100
110
  const doc = new window.ActiveXObject('htmlfile');
101
- doc.designMode = 'on'; // disable on-page scripts
111
+ doc.designMode = 'on';
102
112
  doc.open();
103
113
  doc.write(string);
104
114
  doc.close();
@@ -115,11 +125,9 @@ function createHTMLParser () {
115
125
  }
116
126
  } else {
117
127
  Parser.prototype.parseFromString = function (string) {
118
- const doc = document.implementation.createHTMLDocument('');
119
- doc.open();
120
- doc.write(string);
121
- doc.close();
122
- return doc
128
+ const domino$1 = domino;
129
+ const window = domino$1.createWindow(string || '');
130
+ return window.document
123
131
  };
124
132
  }
125
133
  return Parser
@@ -137,11 +145,54 @@ function shouldUseActiveX () {
137
145
 
138
146
  const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
139
147
 
148
+ function normalizeMathMLInput(input) {
149
+ let s = input == null ? '' : String(input);
150
+
151
+ if (s.includes('<math') && /\\[ntr"]/.test(s)) {
152
+ s = s
153
+ .replace(/\\r\\n/g, '\n')
154
+ .replace(/\\n/g, '\n')
155
+ .replace(/\\t/g, '\t')
156
+ .replace(/\\"/g, '"')
157
+ .replace(/\\'/g, "'");
158
+ }
159
+
160
+ s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
161
+
162
+ s = s.replace(
163
+ /xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
164
+ 'xmlns="$2"'
165
+ );
166
+
167
+ 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"');
168
+
169
+ if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
170
+ s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
171
+ }
172
+
173
+ return s;
174
+ }
175
+
140
176
  const NodeTool = {
141
177
  parseMath: function(html) {
178
+ const normalized = normalizeMathMLInput(html);
142
179
  const parser = new HTMLParser();
143
- const doc = parser.parseFromString(html, 'text/html');
144
- return doc.querySelector('math');
180
+ const doc = parser.parseFromString(normalized, 'text/html');
181
+ let math = doc && doc.querySelector ? doc.querySelector('math') : null;
182
+
183
+ if (!math) {
184
+ const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
185
+ if (match) {
186
+ const retryDoc = parser.parseFromString(match[0], 'text/html');
187
+ math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
188
+ }
189
+ }
190
+
191
+ if (!math) {
192
+ throw new Error('Invalid MathML: missing <math> root element');
193
+ }
194
+
195
+ return math;
145
196
  },
146
197
  getChildren: function(node) {
147
198
  return node.children;
@@ -277,24 +328,24 @@ const MathSymbol = {
277
328
  bigCommand: {
278
329
  decimals: [8721, 8719, 8720, 10753, 10754, 10752, 8899, 8898, 10756, 10758, 8897, 8896, 8747, 8750, 8748, 8749, 10764, 8747],
279
330
  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",
331
+ "\\sum ",
332
+ "\\prod ",
333
+ "\\coprod ",
334
+ "\\bigoplus ",
335
+ "\\bigotimes ",
336
+ "\\bigodot ",
337
+ "\\bigcup ",
338
+ "\\bigcap ",
339
+ "\\biguplus ",
340
+ "\\bigsqcup ",
341
+ "\\bigvee ",
342
+ "\\bigwedge ",
343
+ "\\int ",
344
+ "\\oint ",
345
+ "\\iint ",
346
+ "\\iiint ",
347
+ "\\iiiint ",
348
+ "\\idotsint ",
298
349
  ]
299
350
  },
300
351
 
@@ -612,9 +663,10 @@ function convert(mathmlHtml) {
612
663
  // Thêm xử lý cho các thẻ MathML khác
613
664
  result = result
614
665
  .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
666
+ .replace(/∫/g, " \\int "); // Tích phân
667
+
668
+ // Đả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)
669
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
618
670
 
619
671
  return result;
620
672
  }
@@ -760,20 +812,24 @@ function parseOperator(node) {
760
812
  "±": " \\pm ",
761
813
  "×": " \\times ",
762
814
  "÷": " \\div ",
763
- "∑": " \\sum ",
764
- "∏": " \\prod ",
765
- "∫": "\\int",
815
+ "∑": "\\sum ",
816
+ "∏": "\\prod ",
817
+ "∫": "\\int ",
766
818
  "−": "-",
767
819
  "≠": " \\neq ",
768
820
  ">": " > ",
769
821
  "=": " = ",
822
+ "(": "(",
823
+ ")": ")",
770
824
  ",": ", ", // Dấu phẩy trong tập hợp
771
825
  ";": ";",
772
826
  Ω: "\\Omega",
773
827
  "|": " \\mid ", // PATCH: set-builder mid
774
828
  π: " \\pi ", // PATCH: Greek letter
829
+ "...": "\\dots",
775
830
  };
776
- return operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
831
+ const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
832
+ return res;
777
833
  }
778
834
  // --- END PATCH ---
779
835
 
@@ -800,6 +856,20 @@ function parseElementMi(node) {
800
856
  // Math Number
801
857
  function parseElementMn(node) {
802
858
  let it = NodeTool.getNodeText(node).trim();
859
+ // Loại bỏ các ký tự điều khiển hoặc khoảng trắng lạ
860
+ it = it.replace(/[\u0000-\u001F\u007F-\u009F\u00A0]/g, "");
861
+
862
+ // Danh sách các hàm toán học mở rộng
863
+ const mathFunctions = ["cos", "sin", "tan", "cot", "arccos", "arcsin", "arctan", "arccot", "log", "ln", "lim", "sinh", "cosh", "tanh", "sec", "csc"];
864
+
865
+ if (mathFunctions.some(fn => it.toLowerCase().includes(fn))) {
866
+ // Tìm hàm khớp chính xác nhất
867
+ for (const fn of mathFunctions) {
868
+ if (it.toLowerCase() === fn) {
869
+ return "\\" + fn + " ";
870
+ }
871
+ }
872
+ }
803
873
  return escapeSpecialChars(it);
804
874
  }
805
875
 
@@ -833,8 +903,8 @@ function parseElementMtext(node) {
833
903
  // Nếu đã là lệnh LaTeX thì giữ nguyên
834
904
  if (content.startsWith("\\")) return content;
835
905
 
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)) {
906
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
907
+ if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
838
908
  return `\\text{${content}}`;
839
909
  }
840
910
 
@@ -889,6 +959,22 @@ function escapeSpecialChars(text) {
889
959
  return text;
890
960
  }
891
961
 
962
+ function wrapBaseForScript(base) {
963
+ const t = (base ?? "").trim();
964
+ if (!t) return "";
965
+ if (t.startsWith("{") && t.endsWith("}")) return t;
966
+ if (/^\\[a-zA-Z]+$/.test(t)) return t;
967
+ const needsWrap =
968
+ t.includes("_") ||
969
+ t.includes("^") ||
970
+ /\s/.test(t) ||
971
+ t.startsWith("\\left") ||
972
+ t.startsWith("\\right") ||
973
+ /\\[a-zA-Z]+/.test(t) ||
974
+ /[{}]/.test(t);
975
+ return needsWrap ? `{${t}}` : t;
976
+ }
977
+
892
978
  function parseContainer(node, children) {
893
979
  const render = getRender(node);
894
980
  if (render) {
@@ -907,22 +993,6 @@ function renderChildren(children) {
907
993
  // lefts là mảng object: { op: "(", index: 5 }
908
994
  let lefts = [];
909
995
 
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
996
  Array.prototype.forEach.call(children, (node, idx) => {
927
997
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
928
998
  if (
@@ -950,55 +1020,59 @@ function renderChildren(children) {
950
1020
  if (Brackets.contains(op)) {
951
1021
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
952
1022
  stretchy = ["", "true"].indexOf(stretchy) > -1;
953
-
954
- let escapedOp = op;
955
- if (op === "{" || op === "}") {
956
- escapedOp = `\\${op}`;
1023
+ const parentNode = node.parentNode;
1024
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1025
+ if (isInPower) {
1026
+ stretchy = false;
957
1027
  }
958
1028
 
959
1029
  if (Brackets.isRight(op)) {
960
1030
  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);
1031
+ if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
1032
+ const leftIdx = nearLeftObj.index;
1033
+ const leftRendered =
1034
+ typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
1035
+ let rightRendered = Brackets.parseRight(op, stretchy);
1036
+
1037
+ const leftRenderedTrimmed = leftRendered.trim();
1038
+ const rightRenderedTrimmed = String(rightRendered).trim();
1039
+
1040
+ if (
1041
+ leftRenderedTrimmed.startsWith("\\left") &&
1042
+ !rightRenderedTrimmed.startsWith("\\right")
1043
+ ) {
1044
+ parts[leftIdx] = leftRendered.replace("\\left", "");
1045
+ } else if (
1046
+ !leftRenderedTrimmed.startsWith("\\left") &&
1047
+ rightRenderedTrimmed.startsWith("\\right")
1048
+ ) {
1049
+ rightRendered = String(rightRendered).replace("\\right", "");
975
1050
  }
976
- // matched a left: remove corresponding left object
1051
+
1052
+ if (rightRendered) parts.push(rightRendered);
977
1053
  lefts.pop();
1054
+ } else if (Brackets.isLeft(op)) {
1055
+ // If it's a Right bracket but doesn't match the current Left,
1056
+ // AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
1057
+ const partToPush = Brackets.parseLeft(op, stretchy);
1058
+ if (partToPush) parts.push(partToPush);
1059
+ lefts.push({ op: op, index: parts.length - 1 });
978
1060
  } else {
979
- if (escapedOp) {
980
- // CHỈ PUSH NẾu KHÔNG RỖNG
981
- parts.push(escapedOp);
982
- }
1061
+ // Unmatched right bracket
1062
+ let rightRendered = Brackets.parseRight(op, stretchy);
1063
+ rightRendered = String(rightRendered).replace("\\right", "");
1064
+ if (rightRendered) parts.push(rightRendered);
983
1065
  }
984
1066
  } 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}`;
1067
+ // Must be Left bracket (or only Left)
1068
+ if (Brackets.isLeft(op)) {
1069
+ const partToPush = Brackets.parseLeft(op, stretchy);
1070
+ if (partToPush) parts.push(partToPush);
1071
+ lefts.push({ op: op, index: parts.length - 1 });
993
1072
  } else {
994
- partToPush = escapedOp;
1073
+ // Should not happen if Brackets.contains is correct
1074
+ if (op) parts.push(op);
995
1075
  }
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
1076
  }
1003
1077
  } else {
1004
1078
  const parsedOperator = parseOperator(node);
@@ -1007,45 +1081,6 @@ function renderChildren(children) {
1007
1081
  parts.push(parsedOperator);
1008
1082
  }
1009
1083
  }
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
1084
  } else {
1050
1085
  // Các node khác như <mtext>, #text, v.v.
1051
1086
  const parsed = parse(node);
@@ -1057,40 +1092,13 @@ function renderChildren(children) {
1057
1092
  });
1058
1093
 
1059
1094
  // 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
1095
+ // để tránh sinh LaTeX không hợp lệ (\left không có \right)
1061
1096
  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
1097
  lefts.forEach((leftObj) => {
1078
1098
  const idx = leftObj && leftObj.index;
1079
1099
  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
1100
+ // Use regex with whitespace support and global flag just in case
1101
+ parts[idx] = String(parts[idx]).replace(/\\left/g, "");
1094
1102
  });
1095
1103
  }
1096
1104
 
@@ -1098,6 +1106,44 @@ function renderChildren(children) {
1098
1106
  return parts;
1099
1107
  }
1100
1108
 
1109
+ function isLimitOperator(base) {
1110
+ const t = (base || "").trim();
1111
+ const ops = [
1112
+ "\\sum",
1113
+ "\\prod",
1114
+ "\\coprod",
1115
+ "\\int",
1116
+ "\\oint",
1117
+ "\\bigcap",
1118
+ "\\bigcup",
1119
+ "\\bigsqcup",
1120
+ "\\bigvee",
1121
+ "\\bigwedge",
1122
+ "\\bigodot",
1123
+ "\\bigotimes",
1124
+ "\\bigoplus",
1125
+ "\\biguplus",
1126
+ "\\lim",
1127
+ "\\max",
1128
+ "\\min",
1129
+ "\\sup",
1130
+ "\\inf",
1131
+ "\\det",
1132
+ "\\gcd",
1133
+ "\\Pr",
1134
+ "\\limsup",
1135
+ "\\liminf",
1136
+ ];
1137
+ return ops.some(
1138
+ (op) =>
1139
+ t === op ||
1140
+ t.startsWith(op + " ") ||
1141
+ t.startsWith(op + "\\") ||
1142
+ t.startsWith(op + "^") ||
1143
+ t.startsWith(op + "_")
1144
+ );
1145
+ }
1146
+
1101
1147
  function getRender(node) {
1102
1148
  let render = undefined;
1103
1149
  const nodeName = NodeTool.getNodeName(node);
@@ -1246,9 +1292,11 @@ function getRender(node) {
1246
1292
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1247
1293
  ) {
1248
1294
  const lastChild = sub.lastChild;
1249
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1295
+ return lastChild
1296
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1297
+ : base;
1250
1298
  }
1251
- return `${base}_{${parse(sub)}}`;
1299
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1252
1300
  };
1253
1301
  break;
1254
1302
 
@@ -1256,22 +1304,9 @@ function getRender(node) {
1256
1304
  render = function (node, children) {
1257
1305
  const childrenArray = Array.from(children);
1258
1306
  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
- }
1307
+ const base = parse(childrenArray[0]) || "";
1273
1308
  const sup = parse(childrenArray[1]) || "";
1274
- return `${base}^{${sup}}`;
1309
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1275
1310
  };
1276
1311
  break;
1277
1312
 
@@ -1295,7 +1330,7 @@ function getRender(node) {
1295
1330
  .join("");
1296
1331
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1297
1332
  }
1298
- return `${base}_{${sub}}^{${sup}}`;
1333
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1299
1334
  };
1300
1335
  break;
1301
1336
 
@@ -1305,9 +1340,15 @@ function getRender(node) {
1305
1340
  if (!childrenArray || childrenArray.length < 2) return "";
1306
1341
  const base = parse(childrenArray[0]) || "";
1307
1342
  const over = parse(childrenArray[1]) || "";
1308
- const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1343
+ const overNode = childrenArray[1];
1344
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1345
+ const overText = NodeTool.getNodeText(overNode)?.trim() || "";
1309
1346
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1310
1347
 
1348
+ // Handle arrows with extensible commands
1349
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1350
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1351
+
1311
1352
  // Handle biology notation (double overline)
1312
1353
  if (overText === "¯") {
1313
1354
  const parentNode = node.parentNode;
@@ -1323,6 +1364,27 @@ function getRender(node) {
1323
1364
 
1324
1365
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1325
1366
  if (overText === "^" && isAccent) return `\\hat{${base}}`;
1367
+ if (overText === "\u23DE") return `\\overbrace{${base}}`;
1368
+
1369
+ // Check for nested overbrace (layer-2)
1370
+ if (NodeTool.getNodeName(overNode) === "mover") {
1371
+ const innerChildren = Array.from(NodeTool.getChildren(overNode));
1372
+ if (innerChildren.length >= 2) {
1373
+ const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
1374
+ if (innerBaseText === "\u23DE") {
1375
+ const label = parse(innerChildren[1]);
1376
+ return `\\overbrace{${base}}\\limits^{${label}}`;
1377
+ }
1378
+ }
1379
+ }
1380
+
1381
+ if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
1382
+ return `${base}\\limits^{${over}}`;
1383
+ }
1384
+
1385
+ if (isLimitOperator(base)) {
1386
+ return `${base}\\limits^{${over}}`;
1387
+ }
1326
1388
  return `\\overset{${over}}{${base}}`;
1327
1389
  };
1328
1390
  break;
@@ -1333,10 +1395,21 @@ function getRender(node) {
1333
1395
  if (!childrenArray || childrenArray.length < 2) return "";
1334
1396
  const base = parse(childrenArray[0]) || "";
1335
1397
  const under = parse(childrenArray[1]) || "";
1398
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1399
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1336
1400
  const isUnderAccent =
1337
1401
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1338
1402
 
1403
+ // Handle arrows with extensible commands
1404
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1405
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1406
+
1339
1407
  if (base === "∫") return `\\int_{${under}}`;
1408
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1409
+
1410
+ if (isLimitOperator(base)) {
1411
+ return `${base}\\limits_{${under}}`;
1412
+ }
1340
1413
  return `\\underset{${under}}{${base}}`;
1341
1414
  };
1342
1415
  break;
@@ -1350,19 +1423,19 @@ function getRender(node) {
1350
1423
  const over = parse(childrenArray[2]);
1351
1424
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1352
1425
 
1353
- // Special handling for chemical reaction arrow
1354
- if (
1355
- baseText === "" &&
1356
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1357
- ) {
1358
- return `\\xrightarrow[${under}]{${over}}`;
1359
- }
1426
+ // Special handling for chemical reaction arrow and other arrows
1427
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
1428
+ if (baseText === "" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
1360
1429
 
1361
1430
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1362
1431
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1363
1432
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1364
1433
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1365
- return `${base}_{${under}}^{${over}}`;
1434
+
1435
+ if (isLimitOperator(base)) {
1436
+ return `${base}\\limits_{${under}}^{${over}}`;
1437
+ }
1438
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1366
1439
  };
1367
1440
  break;
1368
1441
 
@@ -1370,30 +1443,56 @@ function getRender(node) {
1370
1443
  render = function (node, children) {
1371
1444
  const childrenArray = Array.from(children);
1372
1445
  if (!childrenArray || childrenArray.length < 1) return "";
1373
- const base = parse(childrenArray[0]);
1374
- let prescripts = "";
1375
- let postscripts = "";
1446
+ const base = parse(childrenArray[0]) || "";
1447
+
1448
+ const postSub = [];
1449
+ const postSup = [];
1450
+ const preSub = [];
1451
+ const preSup = [];
1452
+
1376
1453
  let i = 1;
1454
+ let inPrescripts = false;
1377
1455
 
1378
1456
  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
- }
1457
+ const name = NodeTool.getNodeName(childrenArray[i]);
1458
+ if (name === "mprescripts") {
1459
+ inPrescripts = true;
1460
+ i += 1;
1461
+ continue;
1462
+ }
1463
+
1464
+ const subNode = childrenArray[i];
1465
+ const supNode = childrenArray[i + 1];
1466
+ if (!subNode || !supNode) break;
1467
+
1468
+ const sub = parse(subNode) || "";
1469
+ const sup = parse(supNode) || "";
1470
+
1471
+ if (inPrescripts) {
1472
+ if (sub) preSub.push(sub);
1473
+ if (sup) preSup.push(sup);
1387
1474
  } else {
1388
- if (i + 1 < childrenArray.length) {
1389
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1390
- childrenArray[i + 1]
1391
- )}}`;
1392
- i += 2;
1393
- } else break;
1475
+ if (sub) postSub.push(sub);
1476
+ if (sup) postSup.push(sup);
1394
1477
  }
1478
+
1479
+ i += 2;
1395
1480
  }
1396
- return `${base}${prescripts}${postscripts}`;
1481
+
1482
+ const preSubStr = preSub.join(" ");
1483
+ const preSupStr = preSup.join(" ");
1484
+ const postSubStr = postSub.join(" ");
1485
+ const postSupStr = postSup.join(" ");
1486
+
1487
+ let pre = "";
1488
+ if (preSubStr) pre += `_{${preSubStr}}`;
1489
+ if (preSupStr) pre += `^{${preSupStr}}`;
1490
+
1491
+ let post = "";
1492
+ if (postSubStr) post += `_{${postSubStr}}`;
1493
+ if (postSupStr) post += `^{${postSupStr}}`;
1494
+
1495
+ return `${pre}${wrapBaseForScript(base)}${post}`;
1397
1496
  };
1398
1497
  break;
1399
1498
 
@@ -1494,7 +1593,28 @@ function getRender(node) {
1494
1593
  const num = parse(childrenArray[0]);
1495
1594
  const den = parse(childrenArray[1]);
1496
1595
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1497
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
1596
+ const bevelled = NodeTool.getAttr(node, "bevelled", "false");
1597
+
1598
+ if (bevelled === "true") {
1599
+ return `{}^{${num}}/_{${den}}`;
1600
+ }
1601
+
1602
+ if (["0", "0px"].indexOf(linethickness) > -1) {
1603
+ const prevNode = NodeTool.getPrevNode(node);
1604
+ const nextNode = NodeTool.getNextNode(node);
1605
+ if (
1606
+ prevNode &&
1607
+ NodeTool.getNodeName(prevNode) === "mo" &&
1608
+ NodeTool.getNodeText(prevNode).trim() === "(" &&
1609
+ nextNode &&
1610
+ NodeTool.getNodeName(nextNode) === "mo" &&
1611
+ NodeTool.getNodeText(nextNode).trim() === ")"
1612
+ ) {
1613
+ return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
1614
+ }
1615
+ return `{}_{${den}}^{${num}}`;
1616
+ }
1617
+
1498
1618
  return `\\frac{${num}}{${den}}`;
1499
1619
  };
1500
1620
  break;
@@ -1504,7 +1624,10 @@ function getRender(node) {
1504
1624
  const childrenArray = Array.from(children);
1505
1625
  const open = NodeTool.getAttr(node, "open", "(");
1506
1626
  const close = NodeTool.getAttr(node, "close", ")");
1507
- const separators = NodeTool.getAttr(node, "separators", ",").split("");
1627
+ const separatorsStr = NodeTool.getAttr(node, "separators", ",");
1628
+ const separators = separatorsStr
1629
+ .split("")
1630
+ .filter((c) => c.trim().length === 1);
1508
1631
 
1509
1632
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1510
1633
  if (open === "|") {
@@ -1555,9 +1678,10 @@ function getRender(node) {
1555
1678
  parts.push(parse(child));
1556
1679
  if (
1557
1680
  index < childrenArray.length - 1 &&
1558
- separators[index % separators.length]
1681
+ separators.length > 0
1559
1682
  ) {
1560
- parts.push(separators[index % separators.length]);
1683
+ const sep = separators[index] ?? separators[separators.length - 1];
1684
+ if (sep) parts.push(sep);
1561
1685
  }
1562
1686
  });
1563
1687
  return `\\left[${parts.join("")}\\right)`;
@@ -1569,18 +1693,20 @@ function getRender(node) {
1569
1693
  parts.push(parse(child));
1570
1694
  if (
1571
1695
  index < childrenArray.length - 1 &&
1572
- separators[index % separators.length]
1696
+ separators.length > 0
1573
1697
  ) {
1574
- parts.push(separators[index % separators.length]);
1698
+ const sep = separators[index] ?? separators[separators.length - 1];
1699
+ if (sep) parts.push(sep);
1575
1700
  }
1576
1701
  });
1577
1702
  const content = parts.join("");
1578
1703
 
1579
1704
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1580
1705
  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}`;
1706
+ const left = open ? Brackets.parseLeft(open) : "";
1707
+ const right = close ? Brackets.parseRight(close) : "";
1708
+ if (!close && open) return `${left}${content}\\right.`;
1709
+ return `${left}${content}${right}`;
1584
1710
  };
1585
1711
  break;
1586
1712
 
@@ -1606,10 +1732,17 @@ function getRender(node) {
1606
1732
  case "mn":
1607
1733
  case "mo":
1608
1734
  case "ms":
1609
- case "mtext":
1610
1735
  render = getRender_joinSeparator("@content");
1611
1736
  break;
1612
1737
 
1738
+ case "mtext":
1739
+ render = function (node, children) {
1740
+ const childrenArray = Array.from(children);
1741
+ const content = renderChildren(childrenArray).join("");
1742
+ return `\\text{${content}}`;
1743
+ };
1744
+ break;
1745
+
1613
1746
  case "mphantom":
1614
1747
  render = function (node, children) {
1615
1748
  const childrenArray = Array.from(children);