ed-mathml2tex 0.2.3 → 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;
@@ -615,7 +675,7 @@ function convert(mathmlHtml) {
615
675
  .replace(/∫/g, " \\int "); // Tích phân
616
676
 
617
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)
618
- result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
678
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
619
679
 
620
680
  return result;
621
681
  }
@@ -761,18 +821,21 @@ function parseOperator(node) {
761
821
  "±": " \\pm ",
762
822
  "×": " \\times ",
763
823
  "÷": " \\div ",
764
- "∑": " \\sum ",
765
- "∏": " \\prod ",
766
- "∫": " \\int ",
824
+ "∑": "\\sum ",
825
+ "∏": "\\prod ",
826
+ "∫": "\\int ",
767
827
  "−": "-",
768
828
  "≠": " \\neq ",
769
829
  ">": " > ",
770
830
  "=": " = ",
831
+ "(": "(",
832
+ ")": ")",
771
833
  ",": ", ", // Dấu phẩy trong tập hợp
772
834
  ";": ";",
773
835
  Ω: "\\Omega",
774
836
  "|": " \\mid ", // PATCH: set-builder mid
775
837
  π: " \\pi ", // PATCH: Greek letter
838
+ "...": "\\dots",
776
839
  };
777
840
  const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
778
841
  return res;
@@ -849,8 +912,8 @@ function parseElementMtext(node) {
849
912
  // Nếu đã là lệnh LaTeX thì giữ nguyên
850
913
  if (content.startsWith("\\")) return content;
851
914
 
852
- // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
853
- 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)) {
854
917
  return `\\text{${content}}`;
855
918
  }
856
919
 
@@ -905,6 +968,22 @@ function escapeSpecialChars(text) {
905
968
  return text;
906
969
  }
907
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
+
908
987
  function parseContainer(node, children) {
909
988
  const render = getRender(node);
910
989
  if (render) {
@@ -923,22 +1002,6 @@ function renderChildren(children) {
923
1002
  // lefts là mảng object: { op: "(", index: 5 }
924
1003
  let lefts = [];
925
1004
 
926
- if (
927
- children.length >= 3 &&
928
- NodeTool.getNodeName(children[0]) === "mo" &&
929
- NodeTool.getNodeText(children[0]).trim() === "{" &&
930
- NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
931
- NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
932
- ) {
933
- // Render inner content
934
- const innerContent = Array.prototype.slice
935
- .call(children, 1, -1)
936
- .map((child) => parse(child))
937
- .join(""); // Chỉ trả về nếu nội dung không rỗng
938
- const result = `\\left\\{${innerContent}\\right\\}`;
939
- return result;
940
- }
941
-
942
1005
  Array.prototype.forEach.call(children, (node, idx) => {
943
1006
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
944
1007
  if (
@@ -966,55 +1029,59 @@ function renderChildren(children) {
966
1029
  if (Brackets.contains(op)) {
967
1030
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
968
1031
  stretchy = ["", "true"].indexOf(stretchy) > -1;
969
-
970
- let escapedOp = op;
971
- if (op === "{" || op === "}") {
972
- escapedOp = `\\${op}`;
1032
+ const parentNode = node.parentNode;
1033
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1034
+ if (isInPower) {
1035
+ stretchy = false;
973
1036
  }
974
1037
 
975
1038
  if (Brackets.isRight(op)) {
976
1039
  const nearLeftObj = lefts[lefts.length - 1];
977
- if (nearLeftObj) {
978
- const parentNode = node.parentNode;
979
- const isInPower =
980
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
981
-
982
- let partToPush = "";
983
- if (stretchy && !isInPower) {
984
- partToPush = `\\right${escapedOp}`;
985
- } else {
986
- partToPush = escapedOp;
987
- }
988
- if (partToPush) {
989
- // CHỈ PUSH NẾU KHÔNG RỖNG
990
- 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", "");
991
1059
  }
992
- // matched a left: remove corresponding left object
1060
+
1061
+ if (rightRendered) parts.push(rightRendered);
993
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 });
994
1069
  } else {
995
- if (escapedOp) {
996
- // CHỈ PUSH NẾu KHÔNG RỖNG
997
- parts.push(escapedOp);
998
- }
1070
+ // Unmatched right bracket
1071
+ let rightRendered = Brackets.parseRight(op, stretchy);
1072
+ rightRendered = String(rightRendered).replace("\\right", "");
1073
+ if (rightRendered) parts.push(rightRendered);
999
1074
  }
1000
1075
  } else {
1001
- // ngoặc trái
1002
- const parentNode = node.parentNode;
1003
- const isInPower =
1004
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
1005
-
1006
- let partToPush = "";
1007
- if (stretchy && !isInPower) {
1008
- 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 });
1009
1081
  } else {
1010
- partToPush = escapedOp;
1011
- }
1012
- if (partToPush) {
1013
- // CHỈ PUSH NẾU KHÔNG RỖNG
1014
- parts.push(partToPush);
1082
+ // Should not happen if Brackets.contains is correct
1083
+ if (op) parts.push(op);
1015
1084
  }
1016
- // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1017
- lefts.push({ op: op, index: parts.length - 1 });
1018
1085
  }
1019
1086
  } else {
1020
1087
  const parsedOperator = parseOperator(node);
@@ -1023,45 +1090,6 @@ function renderChildren(children) {
1023
1090
  parts.push(parsedOperator);
1024
1091
  }
1025
1092
  }
1026
-
1027
- // --- START PATCH V6 (Giữ nguyên logic V5) ---
1028
- } else if (NodeTool.getNodeName(node) === "msub") {
1029
- const subChildren = Array.from(NodeTool.getChildren(node));
1030
- if (
1031
- subChildren.length === 2 &&
1032
- NodeTool.getNodeName(subChildren[0]) === "mo" &&
1033
- NodeTool.getNodeText(subChildren[0]).trim() === ")"
1034
- ) {
1035
- // ĐÚNG LÀ NGOẠI LỆ
1036
-
1037
- const sub = parse(subChildren[1]);
1038
- // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1039
- const lastPart = parts.pop(); // lastPart = "\text{OH}"
1040
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1041
- for (let i = parts.length - 1; i >= 0; i--) {
1042
- if (parts[i] && parts[i].trim() === "\\left(") {
1043
- parts.splice(i, 1); // Xóa "\left("
1044
- break;
1045
- }
1046
- _;
1047
- } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1048
-
1049
- parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1050
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1051
-
1052
- const nearLeftObj = lefts[lefts.length - 1];
1053
- if (nearLeftObj) {
1054
- lefts.pop();
1055
- }
1056
- } else {
1057
- // <msub> bình thường
1058
- const parsed = parse(node);
1059
- if (parsed) {
1060
- // CHỈ PUSH NẾU KHÔNG RỖNG
1061
- parts.push(parsed);
1062
- }
1063
- }
1064
- // --- END PATCH V6 ---
1065
1093
  } else {
1066
1094
  // Các node khác như <mtext>, #text, v.v.
1067
1095
  const parsed = parse(node);
@@ -1073,40 +1101,13 @@ function renderChildren(children) {
1073
1101
  });
1074
1102
 
1075
1103
  // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1076
- // 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)
1077
1105
  if (lefts && lefts.length > 0) {
1078
- const rightForLeft = (left) => {
1079
- switch (left) {
1080
- case "(":
1081
- return ")";
1082
- case "[":
1083
- return "]";
1084
- case "{":
1085
- return "}";
1086
- case "|":
1087
- return "|";
1088
- default:
1089
- return ".";
1090
- }
1091
- };
1092
-
1093
1106
  lefts.forEach((leftObj) => {
1094
1107
  const idx = leftObj && leftObj.index;
1095
1108
  if (typeof idx !== "number" || !parts[idx]) return;
1096
-
1097
- const left = leftObj.op;
1098
- const right = rightForLeft(left);
1099
-
1100
- // 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
1101
- const tail = parts.slice(idx + 1).map(String).join(" ");
1102
- const hasMatchingRight =
1103
- (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1104
-
1105
- if (!hasMatchingRight) {
1106
- // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1107
- parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1108
- }
1109
- // 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, "");
1110
1111
  });
1111
1112
  }
1112
1113
 
@@ -1114,6 +1115,44 @@ function renderChildren(children) {
1114
1115
  return parts;
1115
1116
  }
1116
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
+
1117
1156
  function getRender(node) {
1118
1157
  let render = undefined;
1119
1158
  const nodeName = NodeTool.getNodeName(node);
@@ -1262,9 +1301,11 @@ function getRender(node) {
1262
1301
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1263
1302
  ) {
1264
1303
  const lastChild = sub.lastChild;
1265
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1304
+ return lastChild
1305
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1306
+ : base;
1266
1307
  }
1267
- return `${base}_{${parse(sub)}}`;
1308
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1268
1309
  };
1269
1310
  break;
1270
1311
 
@@ -1272,22 +1313,9 @@ function getRender(node) {
1272
1313
  render = function (node, children) {
1273
1314
  const childrenArray = Array.from(children);
1274
1315
  if (!childrenArray || childrenArray.length < 2) return "";
1275
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
1276
- const baseNode = childrenArray[0];
1277
- let base = parse(baseNode) || "";
1278
- if (NodeTool.getNodeName(baseNode) === "mo") {
1279
- const op = NodeTool.getNodeText(baseNode).trim();
1280
- if (Brackets.isRight(op)) {
1281
- // Escape brace characters
1282
- if (op === "}" || op === "{") {
1283
- base = `\\right\\${op}`;
1284
- } else {
1285
- base = `\\right${op}`;
1286
- }
1287
- }
1288
- }
1316
+ const base = parse(childrenArray[0]) || "";
1289
1317
  const sup = parse(childrenArray[1]) || "";
1290
- return `${base}^{${sup}}`;
1318
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1291
1319
  };
1292
1320
  break;
1293
1321
 
@@ -1311,7 +1339,7 @@ function getRender(node) {
1311
1339
  .join("");
1312
1340
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1313
1341
  }
1314
- return `${base}_{${sub}}^{${sup}}`;
1342
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1315
1343
  };
1316
1344
  break;
1317
1345
 
@@ -1321,9 +1349,15 @@ function getRender(node) {
1321
1349
  if (!childrenArray || childrenArray.length < 2) return "";
1322
1350
  const base = parse(childrenArray[0]) || "";
1323
1351
  const over = parse(childrenArray[1]) || "";
1324
- 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() || "";
1325
1355
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1326
1356
 
1357
+ // Handle arrows with extensible commands
1358
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1359
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1360
+
1327
1361
  // Handle biology notation (double overline)
1328
1362
  if (overText === "¯") {
1329
1363
  const parentNode = node.parentNode;
@@ -1339,6 +1373,27 @@ function getRender(node) {
1339
1373
 
1340
1374
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1341
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
+ }
1342
1397
  return `\\overset{${over}}{${base}}`;
1343
1398
  };
1344
1399
  break;
@@ -1349,10 +1404,21 @@ function getRender(node) {
1349
1404
  if (!childrenArray || childrenArray.length < 2) return "";
1350
1405
  const base = parse(childrenArray[0]) || "";
1351
1406
  const under = parse(childrenArray[1]) || "";
1407
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1408
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1352
1409
  const isUnderAccent =
1353
1410
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1354
1411
 
1412
+ // Handle arrows with extensible commands
1413
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1414
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1415
+
1355
1416
  if (base === "∫") return `\\int_{${under}}`;
1417
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1418
+
1419
+ if (isLimitOperator(base)) {
1420
+ return `${base}\\limits_{${under}}`;
1421
+ }
1356
1422
  return `\\underset{${under}}{${base}}`;
1357
1423
  };
1358
1424
  break;
@@ -1366,19 +1432,19 @@ function getRender(node) {
1366
1432
  const over = parse(childrenArray[2]);
1367
1433
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1368
1434
 
1369
- // Special handling for chemical reaction arrow
1370
- if (
1371
- baseText === "" &&
1372
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1373
- ) {
1374
- return `\\xrightarrow[${under}]{${over}}`;
1375
- }
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}}`;
1376
1438
 
1377
1439
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1378
1440
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1379
1441
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1380
1442
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1381
- return `${base}_{${under}}^{${over}}`;
1443
+
1444
+ if (isLimitOperator(base)) {
1445
+ return `${base}\\limits_{${under}}^{${over}}`;
1446
+ }
1447
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1382
1448
  };
1383
1449
  break;
1384
1450
 
@@ -1386,30 +1452,56 @@ function getRender(node) {
1386
1452
  render = function (node, children) {
1387
1453
  const childrenArray = Array.from(children);
1388
1454
  if (!childrenArray || childrenArray.length < 1) return "";
1389
- const base = parse(childrenArray[0]);
1390
- let prescripts = "";
1391
- let postscripts = "";
1455
+ const base = parse(childrenArray[0]) || "";
1456
+
1457
+ const postSub = [];
1458
+ const postSup = [];
1459
+ const preSub = [];
1460
+ const preSup = [];
1461
+
1392
1462
  let i = 1;
1463
+ let inPrescripts = false;
1393
1464
 
1394
1465
  while (i < childrenArray.length) {
1395
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1396
- i++;
1397
- if (i + 1 < childrenArray.length) {
1398
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1399
- childrenArray[i + 1]
1400
- )}}`;
1401
- i += 2;
1402
- }
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);
1403
1483
  } else {
1404
- if (i + 1 < childrenArray.length) {
1405
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1406
- childrenArray[i + 1]
1407
- )}}`;
1408
- i += 2;
1409
- } else break;
1484
+ if (sub) postSub.push(sub);
1485
+ if (sup) postSup.push(sup);
1410
1486
  }
1487
+
1488
+ i += 2;
1411
1489
  }
1412
- 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}`;
1413
1505
  };
1414
1506
  break;
1415
1507
 
@@ -1510,7 +1602,28 @@ function getRender(node) {
1510
1602
  const num = parse(childrenArray[0]);
1511
1603
  const den = parse(childrenArray[1]);
1512
1604
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1513
- 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
+
1514
1627
  return `\\frac{${num}}{${den}}`;
1515
1628
  };
1516
1629
  break;
@@ -1520,7 +1633,10 @@ function getRender(node) {
1520
1633
  const childrenArray = Array.from(children);
1521
1634
  const open = NodeTool.getAttr(node, "open", "(");
1522
1635
  const close = NodeTool.getAttr(node, "close", ")");
1523
- 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);
1524
1640
 
1525
1641
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1526
1642
  if (open === "|") {
@@ -1571,9 +1687,10 @@ function getRender(node) {
1571
1687
  parts.push(parse(child));
1572
1688
  if (
1573
1689
  index < childrenArray.length - 1 &&
1574
- separators[index % separators.length]
1690
+ separators.length > 0
1575
1691
  ) {
1576
- parts.push(separators[index % separators.length]);
1692
+ const sep = separators[index] ?? separators[separators.length - 1];
1693
+ if (sep) parts.push(sep);
1577
1694
  }
1578
1695
  });
1579
1696
  return `\\left[${parts.join("")}\\right)`;
@@ -1585,18 +1702,20 @@ function getRender(node) {
1585
1702
  parts.push(parse(child));
1586
1703
  if (
1587
1704
  index < childrenArray.length - 1 &&
1588
- separators[index % separators.length]
1705
+ separators.length > 0
1589
1706
  ) {
1590
- parts.push(separators[index % separators.length]);
1707
+ const sep = separators[index] ?? separators[separators.length - 1];
1708
+ if (sep) parts.push(sep);
1591
1709
  }
1592
1710
  });
1593
1711
  const content = parts.join("");
1594
1712
 
1595
1713
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1596
1714
  if (open === "|" && close === "|") return `\\left|${content}\\right|`;
1597
- if (!close) return `\\left${open}${content}\\right.`;
1598
- if (!open) return `\\left.${content}\\right${close}`;
1599
- 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}`;
1600
1719
  };
1601
1720
  break;
1602
1721
 
@@ -1622,10 +1741,17 @@ function getRender(node) {
1622
1741
  case "mn":
1623
1742
  case "mo":
1624
1743
  case "ms":
1625
- case "mtext":
1626
1744
  render = getRender_joinSeparator("@content");
1627
1745
  break;
1628
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
+
1629
1755
  case "mphantom":
1630
1756
  render = function (node, children) {
1631
1757
  const childrenArray = Array.from(children);