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