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,6 +1,6 @@
1
1
  const Brackets = {
2
- left: ['(', '[', '{', '|', '', '', '', '', ''],
3
- right: [')', ']', '}', '|', '', '', '', '', ''],
2
+ left: ['(', '[', '{', '|', '\u2016', '\u27E8', '\u230A', '\u2308', '\u231C'],
3
+ right: [')', ']', '}', '|', '\u2016', '\u27E9', '\u230B', '\u2309', '\u231D'],
4
4
  isPair: function(l, r){
5
5
  const idx = this.left.indexOf(l);
6
6
  return r === this.right[idx];
@@ -22,17 +22,17 @@ const Brackets = {
22
22
  case '[':
23
23
  case '|': r = `\\left${it}`;
24
24
  break;
25
- case '': r = '\\left\\|';
25
+ case '\u2016': r = '\\left\\|';
26
26
  break;
27
27
  case '{': r = '\\left\\{';
28
28
  break;
29
- case '': r = '\\left\\langle ';
29
+ case '\u27E8': r = '\\left\\langle ';
30
30
  break;
31
- case '': r = '\\left\\lfloor ';
31
+ case '\u230A': r = '\\left\\lfloor ';
32
32
  break;
33
- case '': r = '\\left\\lceil ';
33
+ case '\u2308': r = '\\left\\lceil ';
34
34
  break;
35
- case '': r = '\\left\\ulcorner ';
35
+ case '\u231C': r = '\\left\\ulcorner ';
36
36
  break;
37
37
  }
38
38
  return (stretchy ? r : r.replace('\\left', ''));
@@ -46,23 +46,36 @@ const Brackets = {
46
46
  case ']':
47
47
  case '|': r = `\\right${it}`;
48
48
  break;
49
- case '': r = '\\right\\|';
49
+ case '\u2016': r = '\\right\\|';
50
50
  break;
51
51
  case '}': r = '\\right\\}';
52
52
  break;
53
- case '': r = ' \\right\\rangle';
53
+ case '\u27E9': r = ' \\right\\rangle';
54
54
  break;
55
- case '': r = ' \\right\\rfloor';
55
+ case '\u230B': r = ' \\right\\rfloor';
56
56
  break;
57
- case '': r = ' \\right\\rceil';
57
+ case '\u2309': r = ' \\right\\rceil';
58
58
  break;
59
- case '': r = ' \\right\\urcorner';
59
+ case '\u231D': r = ' \\right\\urcorner';
60
60
  break;
61
61
  }
62
62
  return (stretchy ? r : r.replace('\\right', ''));
63
63
  }
64
64
  };
65
65
 
66
+ var _nodeResolve_empty = {};
67
+
68
+ var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
69
+ __proto__: null,
70
+ 'default': _nodeResolve_empty
71
+ });
72
+
73
+ function getCjsExportFromNamespace (n) {
74
+ return n && n['default'] || n;
75
+ }
76
+
77
+ var require$$0 = getCjsExportFromNamespace(_nodeResolve_empty$1);
78
+
66
79
  /*
67
80
  * Set up window for Node.js
68
81
  */
@@ -92,11 +105,17 @@ function canParseHTMLNatively () {
92
105
  function createHTMLParser () {
93
106
  const Parser = function () {};
94
107
 
95
- if (typeof process !== 'undefined' && true) {
96
- if (shouldUseActiveX()) {
108
+ const hasDocument =
109
+ typeof document !== 'undefined' &&
110
+ document &&
111
+ document.implementation &&
112
+ typeof document.implementation.createHTMLDocument === 'function';
113
+
114
+ if (hasDocument) {
115
+ if (typeof process !== 'undefined' && true && shouldUseActiveX()) {
97
116
  Parser.prototype.parseFromString = function (string) {
98
117
  const doc = new window.ActiveXObject('htmlfile');
99
- doc.designMode = 'on'; // disable on-page scripts
118
+ doc.designMode = 'on';
100
119
  doc.open();
101
120
  doc.write(string);
102
121
  doc.close();
@@ -113,11 +132,9 @@ function createHTMLParser () {
113
132
  }
114
133
  } else {
115
134
  Parser.prototype.parseFromString = function (string) {
116
- const doc = document.implementation.createHTMLDocument('');
117
- doc.open();
118
- doc.write(string);
119
- doc.close();
120
- return doc
135
+ const domino = require$$0;
136
+ const window = domino.createWindow(string || '');
137
+ return window.document
121
138
  };
122
139
  }
123
140
  return Parser
@@ -135,11 +152,54 @@ function shouldUseActiveX () {
135
152
 
136
153
  const HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser();
137
154
 
155
+ function normalizeMathMLInput(input) {
156
+ let s = input == null ? '' : String(input);
157
+
158
+ if (s.includes('<math') && /\\[ntr"]/.test(s)) {
159
+ s = s
160
+ .replace(/\\r\\n/g, '\n')
161
+ .replace(/\\n/g, '\n')
162
+ .replace(/\\t/g, '\t')
163
+ .replace(/\\"/g, '"')
164
+ .replace(/\\'/g, "'");
165
+ }
166
+
167
+ s = s.replace(/^\s*<\?xml[\s\S]*?\?>\s*/i, '');
168
+
169
+ s = s.replace(
170
+ /xmlns\s*=\s*(["'])\s*`?\s*(https?:\/\/www\.w3\.org\/1998\/Math\/MathML)\s*`?\s*\1/gi,
171
+ 'xmlns="$2"'
172
+ );
173
+
174
+ s = s.replace(/xmlns\s*=\s*(["'])\s*`?\s*http:\/\/www\.w3\.org\/1998\/Math\/MathML\\`?\s*\1/gi, 'xmlns="http://www.w3.org/1998/Math/MathML"');
175
+
176
+ if (!/<math\b/i.test(s) && /<(msub|mrow|mi|mn|mo|mfrac|msup|msubsup|munder|mover|munderover|mtable)\b/i.test(s)) {
177
+ s = `<math xmlns="http://www.w3.org/1998/Math/MathML">${s}</math>`;
178
+ }
179
+
180
+ return s;
181
+ }
182
+
138
183
  const NodeTool = {
139
184
  parseMath: function(html) {
185
+ const normalized = normalizeMathMLInput(html);
140
186
  const parser = new HTMLParser();
141
- const doc = parser.parseFromString(html, 'text/html');
142
- return doc.querySelector('math');
187
+ const doc = parser.parseFromString(normalized, 'text/html');
188
+ let math = doc && doc.querySelector ? doc.querySelector('math') : null;
189
+
190
+ if (!math) {
191
+ const match = normalized.match(/<math\b[\s\S]*?<\/math>/i);
192
+ if (match) {
193
+ const retryDoc = parser.parseFromString(match[0], 'text/html');
194
+ math = retryDoc && retryDoc.querySelector ? retryDoc.querySelector('math') : null;
195
+ }
196
+ }
197
+
198
+ if (!math) {
199
+ throw new Error('Invalid MathML: missing <math> root element');
200
+ }
201
+
202
+ return math;
143
203
  },
144
204
  getChildren: function(node) {
145
205
  return node.children;
@@ -613,7 +673,7 @@ function convert(mathmlHtml) {
613
673
  .replace(/∫/g, " \\int "); // Tích phân
614
674
 
615
675
  // Đảm bảo dấu cách sau các lệnh LaTeX quan trọng để tránh dính biến (vd: \intf -> \int f)
616
- result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
676
+ result = result.replace(/\\(int|sum|prod|cos|sin|tan|cot|lim(?!its)|log|ln)([a-zA-Z0-9])/g, "\\$1 $2");
617
677
 
618
678
  return result;
619
679
  }
@@ -759,18 +819,21 @@ function parseOperator(node) {
759
819
  "±": " \\pm ",
760
820
  "×": " \\times ",
761
821
  "÷": " \\div ",
762
- "∑": " \\sum ",
763
- "∏": " \\prod ",
764
- "∫": " \\int ",
822
+ "∑": "\\sum ",
823
+ "∏": "\\prod ",
824
+ "∫": "\\int ",
765
825
  "−": "-",
766
826
  "≠": " \\neq ",
767
827
  ">": " > ",
768
828
  "=": " = ",
829
+ "(": "(",
830
+ ")": ")",
769
831
  ",": ", ", // Dấu phẩy trong tập hợp
770
832
  ";": ";",
771
833
  Ω: "\\Omega",
772
834
  "|": " \\mid ", // PATCH: set-builder mid
773
835
  π: " \\pi ", // PATCH: Greek letter
836
+ "...": "\\dots",
774
837
  };
775
838
  const res = operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
776
839
  return res;
@@ -847,8 +910,8 @@ function parseElementMtext(node) {
847
910
  // Nếu đã là lệnh LaTeX thì giữ nguyên
848
911
  if (content.startsWith("\\")) return content;
849
912
 
850
- // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
851
- if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
913
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ..., k times)
914
+ if (/^[A-Za-z][A-Za-z0-9\s]*$/.test(content)) {
852
915
  return `\\text{${content}}`;
853
916
  }
854
917
 
@@ -903,6 +966,22 @@ function escapeSpecialChars(text) {
903
966
  return text;
904
967
  }
905
968
 
969
+ function wrapBaseForScript(base) {
970
+ const t = (base ?? "").trim();
971
+ if (!t) return "";
972
+ if (t.startsWith("{") && t.endsWith("}")) return t;
973
+ if (/^\\[a-zA-Z]+$/.test(t)) return t;
974
+ const needsWrap =
975
+ t.includes("_") ||
976
+ t.includes("^") ||
977
+ /\s/.test(t) ||
978
+ t.startsWith("\\left") ||
979
+ t.startsWith("\\right") ||
980
+ /\\[a-zA-Z]+/.test(t) ||
981
+ /[{}]/.test(t);
982
+ return needsWrap ? `{${t}}` : t;
983
+ }
984
+
906
985
  function parseContainer(node, children) {
907
986
  const render = getRender(node);
908
987
  if (render) {
@@ -921,22 +1000,6 @@ function renderChildren(children) {
921
1000
  // lefts là mảng object: { op: "(", index: 5 }
922
1001
  let lefts = [];
923
1002
 
924
- if (
925
- children.length >= 3 &&
926
- NodeTool.getNodeName(children[0]) === "mo" &&
927
- NodeTool.getNodeText(children[0]).trim() === "{" &&
928
- NodeTool.getNodeName(children[children.length - 1]) === "mo" &&
929
- NodeTool.getNodeText(children[children.length - 1]).trim() === "}"
930
- ) {
931
- // Render inner content
932
- const innerContent = Array.prototype.slice
933
- .call(children, 1, -1)
934
- .map((child) => parse(child))
935
- .join(""); // Chỉ trả về nếu nội dung không rỗng
936
- const result = `\\left\\{${innerContent}\\right\\}`;
937
- return result;
938
- }
939
-
940
1003
  Array.prototype.forEach.call(children, (node, idx) => {
941
1004
  // PATCH: Thin space between variables/numbers in mfrac numerator (k 2 π)
942
1005
  if (
@@ -964,55 +1027,59 @@ function renderChildren(children) {
964
1027
  if (Brackets.contains(op)) {
965
1028
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
966
1029
  stretchy = ["", "true"].indexOf(stretchy) > -1;
967
-
968
- let escapedOp = op;
969
- if (op === "{" || op === "}") {
970
- escapedOp = `\\${op}`;
1030
+ const parentNode = node.parentNode;
1031
+ const isInPower = parentNode && NodeTool.getNodeName(parentNode) === "msup";
1032
+ if (isInPower) {
1033
+ stretchy = false;
971
1034
  }
972
1035
 
973
1036
  if (Brackets.isRight(op)) {
974
1037
  const nearLeftObj = lefts[lefts.length - 1];
975
- if (nearLeftObj) {
976
- const parentNode = node.parentNode;
977
- const isInPower =
978
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
979
-
980
- let partToPush = "";
981
- if (stretchy && !isInPower) {
982
- partToPush = `\\right${escapedOp}`;
983
- } else {
984
- partToPush = escapedOp;
985
- }
986
- if (partToPush) {
987
- // CHỈ PUSH NẾU KHÔNG RỖNG
988
- parts.push(partToPush);
1038
+ if (nearLeftObj && Brackets.isPair(nearLeftObj.op, op)) {
1039
+ const leftIdx = nearLeftObj.index;
1040
+ const leftRendered =
1041
+ typeof leftIdx === "number" ? String(parts[leftIdx] ?? "") : "";
1042
+ let rightRendered = Brackets.parseRight(op, stretchy);
1043
+
1044
+ const leftRenderedTrimmed = leftRendered.trim();
1045
+ const rightRenderedTrimmed = String(rightRendered).trim();
1046
+
1047
+ if (
1048
+ leftRenderedTrimmed.startsWith("\\left") &&
1049
+ !rightRenderedTrimmed.startsWith("\\right")
1050
+ ) {
1051
+ parts[leftIdx] = leftRendered.replace("\\left", "");
1052
+ } else if (
1053
+ !leftRenderedTrimmed.startsWith("\\left") &&
1054
+ rightRenderedTrimmed.startsWith("\\right")
1055
+ ) {
1056
+ rightRendered = String(rightRendered).replace("\\right", "");
989
1057
  }
990
- // matched a left: remove corresponding left object
1058
+
1059
+ if (rightRendered) parts.push(rightRendered);
991
1060
  lefts.pop();
1061
+ } else if (Brackets.isLeft(op)) {
1062
+ // If it's a Right bracket but doesn't match the current Left,
1063
+ // AND it is also a Left bracket (e.g. '|'), treat it as a new Left.
1064
+ const partToPush = Brackets.parseLeft(op, stretchy);
1065
+ if (partToPush) parts.push(partToPush);
1066
+ lefts.push({ op: op, index: parts.length - 1 });
992
1067
  } else {
993
- if (escapedOp) {
994
- // CHỈ PUSH NẾu KHÔNG RỖNG
995
- parts.push(escapedOp);
996
- }
1068
+ // Unmatched right bracket
1069
+ let rightRendered = Brackets.parseRight(op, stretchy);
1070
+ rightRendered = String(rightRendered).replace("\\right", "");
1071
+ if (rightRendered) parts.push(rightRendered);
997
1072
  }
998
1073
  } else {
999
- // ngoặc trái
1000
- const parentNode = node.parentNode;
1001
- const isInPower =
1002
- parentNode && NodeTool.getNodeName(parentNode) === "msup";
1003
-
1004
- let partToPush = "";
1005
- if (stretchy && !isInPower) {
1006
- partToPush = `\\left${escapedOp}`;
1074
+ // Must be Left bracket (or only Left)
1075
+ if (Brackets.isLeft(op)) {
1076
+ const partToPush = Brackets.parseLeft(op, stretchy);
1077
+ if (partToPush) parts.push(partToPush);
1078
+ lefts.push({ op: op, index: parts.length - 1 });
1007
1079
  } else {
1008
- partToPush = escapedOp;
1009
- }
1010
- if (partToPush) {
1011
- // CHỈ PUSH NẾU KHÔNG RỖNG
1012
- parts.push(partToPush);
1080
+ // Should not happen if Brackets.contains is correct
1081
+ if (op) parts.push(op);
1013
1082
  }
1014
- // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1015
- lefts.push({ op: op, index: parts.length - 1 });
1016
1083
  }
1017
1084
  } else {
1018
1085
  const parsedOperator = parseOperator(node);
@@ -1021,45 +1088,6 @@ function renderChildren(children) {
1021
1088
  parts.push(parsedOperator);
1022
1089
  }
1023
1090
  }
1024
-
1025
- // --- START PATCH V6 (Giữ nguyên logic V5) ---
1026
- } else if (NodeTool.getNodeName(node) === "msub") {
1027
- const subChildren = Array.from(NodeTool.getChildren(node));
1028
- if (
1029
- subChildren.length === 2 &&
1030
- NodeTool.getNodeName(subChildren[0]) === "mo" &&
1031
- NodeTool.getNodeText(subChildren[0]).trim() === ")"
1032
- ) {
1033
- // ĐÚNG LÀ NGOẠI LỆ
1034
-
1035
- const sub = parse(subChildren[1]);
1036
- // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1037
- const lastPart = parts.pop(); // lastPart = "\text{OH}"
1038
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1039
- for (let i = parts.length - 1; i >= 0; i--) {
1040
- if (parts[i] && parts[i].trim() === "\\left(") {
1041
- parts.splice(i, 1); // Xóa "\left("
1042
- break;
1043
- }
1044
- _;
1045
- } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1046
-
1047
- parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1048
- // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1049
-
1050
- const nearLeftObj = lefts[lefts.length - 1];
1051
- if (nearLeftObj) {
1052
- lefts.pop();
1053
- }
1054
- } else {
1055
- // <msub> bình thường
1056
- const parsed = parse(node);
1057
- if (parsed) {
1058
- // CHỈ PUSH NẾU KHÔNG RỖNG
1059
- parts.push(parsed);
1060
- }
1061
- }
1062
- // --- END PATCH V6 ---
1063
1091
  } else {
1064
1092
  // Các node khác như <mtext>, #text, v.v.
1065
1093
  const parsed = parse(node);
@@ -1071,40 +1099,13 @@ function renderChildren(children) {
1071
1099
  });
1072
1100
 
1073
1101
  // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1074
- // khi không đóng tương ứng phía sau nếu có \right (hoặc dấu đóng) thì giữ \left
1102
+ // để tránh sinh LaTeX không hợp lệ (\left không có \right)
1075
1103
  if (lefts && lefts.length > 0) {
1076
- const rightForLeft = (left) => {
1077
- switch (left) {
1078
- case "(":
1079
- return ")";
1080
- case "[":
1081
- return "]";
1082
- case "{":
1083
- return "}";
1084
- case "|":
1085
- return "|";
1086
- default:
1087
- return ".";
1088
- }
1089
- };
1090
-
1091
1104
  lefts.forEach((leftObj) => {
1092
1105
  const idx = leftObj && leftObj.index;
1093
1106
  if (typeof idx !== "number" || !parts[idx]) return;
1094
-
1095
- const left = leftObj.op;
1096
- const right = rightForLeft(left);
1097
-
1098
- // 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
1099
- const tail = parts.slice(idx + 1).map(String).join(" ");
1100
- const hasMatchingRight =
1101
- (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1102
-
1103
- if (!hasMatchingRight) {
1104
- // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1105
- parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1106
- }
1107
- // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1107
+ // Use regex with whitespace support and global flag just in case
1108
+ parts[idx] = String(parts[idx]).replace(/\\left/g, "");
1108
1109
  });
1109
1110
  }
1110
1111
 
@@ -1112,6 +1113,44 @@ function renderChildren(children) {
1112
1113
  return parts;
1113
1114
  }
1114
1115
 
1116
+ function isLimitOperator(base) {
1117
+ const t = (base || "").trim();
1118
+ const ops = [
1119
+ "\\sum",
1120
+ "\\prod",
1121
+ "\\coprod",
1122
+ "\\int",
1123
+ "\\oint",
1124
+ "\\bigcap",
1125
+ "\\bigcup",
1126
+ "\\bigsqcup",
1127
+ "\\bigvee",
1128
+ "\\bigwedge",
1129
+ "\\bigodot",
1130
+ "\\bigotimes",
1131
+ "\\bigoplus",
1132
+ "\\biguplus",
1133
+ "\\lim",
1134
+ "\\max",
1135
+ "\\min",
1136
+ "\\sup",
1137
+ "\\inf",
1138
+ "\\det",
1139
+ "\\gcd",
1140
+ "\\Pr",
1141
+ "\\limsup",
1142
+ "\\liminf",
1143
+ ];
1144
+ return ops.some(
1145
+ (op) =>
1146
+ t === op ||
1147
+ t.startsWith(op + " ") ||
1148
+ t.startsWith(op + "\\") ||
1149
+ t.startsWith(op + "^") ||
1150
+ t.startsWith(op + "_")
1151
+ );
1152
+ }
1153
+
1115
1154
  function getRender(node) {
1116
1155
  let render = undefined;
1117
1156
  const nodeName = NodeTool.getNodeName(node);
@@ -1260,9 +1299,11 @@ function getRender(node) {
1260
1299
  NodeTool.getNodeText(sub.firstChild).trim() === "")
1261
1300
  ) {
1262
1301
  const lastChild = sub.lastChild;
1263
- return lastChild ? `${base}_${parse(lastChild)}` : base;
1302
+ return lastChild
1303
+ ? `${wrapBaseForScript(base)}_{${parse(lastChild)}}`
1304
+ : base;
1264
1305
  }
1265
- return `${base}_{${parse(sub)}}`;
1306
+ return `${wrapBaseForScript(base)}_{${parse(sub)}}`;
1266
1307
  };
1267
1308
  break;
1268
1309
 
@@ -1270,22 +1311,9 @@ function getRender(node) {
1270
1311
  render = function (node, children) {
1271
1312
  const childrenArray = Array.from(children);
1272
1313
  if (!childrenArray || childrenArray.length < 2) return "";
1273
- // Nếu base một <mo> và là ngoặc phải, chuyển thành \right<op>
1274
- const baseNode = childrenArray[0];
1275
- let base = parse(baseNode) || "";
1276
- if (NodeTool.getNodeName(baseNode) === "mo") {
1277
- const op = NodeTool.getNodeText(baseNode).trim();
1278
- if (Brackets.isRight(op)) {
1279
- // Escape brace characters
1280
- if (op === "}" || op === "{") {
1281
- base = `\\right\\${op}`;
1282
- } else {
1283
- base = `\\right${op}`;
1284
- }
1285
- }
1286
- }
1314
+ const base = parse(childrenArray[0]) || "";
1287
1315
  const sup = parse(childrenArray[1]) || "";
1288
- return `${base}^{${sup}}`;
1316
+ return `${wrapBaseForScript(base)}^{${sup}}`;
1289
1317
  };
1290
1318
  break;
1291
1319
 
@@ -1309,7 +1337,7 @@ function getRender(node) {
1309
1337
  .join("");
1310
1338
  return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1311
1339
  }
1312
- return `${base}_{${sub}}^{${sup}}`;
1340
+ return `${wrapBaseForScript(base)}_{${sub}}^{${sup}}`;
1313
1341
  };
1314
1342
  break;
1315
1343
 
@@ -1319,9 +1347,15 @@ function getRender(node) {
1319
1347
  if (!childrenArray || childrenArray.length < 2) return "";
1320
1348
  const base = parse(childrenArray[0]) || "";
1321
1349
  const over = parse(childrenArray[1]) || "";
1322
- const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1350
+ const overNode = childrenArray[1];
1351
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1352
+ const overText = NodeTool.getNodeText(overNode)?.trim() || "";
1323
1353
  const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1324
1354
 
1355
+ // Handle arrows with extensible commands
1356
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow{${over}}`;
1357
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow{${over}}`;
1358
+
1325
1359
  // Handle biology notation (double overline)
1326
1360
  if (overText === "¯") {
1327
1361
  const parentNode = node.parentNode;
@@ -1337,6 +1371,27 @@ function getRender(node) {
1337
1371
 
1338
1372
  if (overText === "→" && isAccent) return `\\vec{${base}}`;
1339
1373
  if (overText === "^" && isAccent) return `\\hat{${base}}`;
1374
+ if (overText === "\u23DE") return `\\overbrace{${base}}`;
1375
+
1376
+ // Check for nested overbrace (layer-2)
1377
+ if (NodeTool.getNodeName(overNode) === "mover") {
1378
+ const innerChildren = Array.from(NodeTool.getChildren(overNode));
1379
+ if (innerChildren.length >= 2) {
1380
+ const innerBaseText = NodeTool.getNodeText(innerChildren[0]).trim();
1381
+ if (innerBaseText === "\u23DE") {
1382
+ const label = parse(innerChildren[1]);
1383
+ return `\\overbrace{${base}}\\limits^{${label}}`;
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ if (base.startsWith("\\overbrace") || base.startsWith("\\underbrace")) {
1389
+ return `${base}\\limits^{${over}}`;
1390
+ }
1391
+
1392
+ if (isLimitOperator(base)) {
1393
+ return `${base}\\limits^{${over}}`;
1394
+ }
1340
1395
  return `\\overset{${over}}{${base}}`;
1341
1396
  };
1342
1397
  break;
@@ -1347,10 +1402,21 @@ function getRender(node) {
1347
1402
  if (!childrenArray || childrenArray.length < 2) return "";
1348
1403
  const base = parse(childrenArray[0]) || "";
1349
1404
  const under = parse(childrenArray[1]) || "";
1405
+ const baseText = NodeTool.getNodeText(childrenArray[0])?.trim() || "";
1406
+ const underText = NodeTool.getNodeText(childrenArray[1])?.trim() || "";
1350
1407
  const isUnderAccent =
1351
1408
  NodeTool.getAttr(node, "accentunder", "false") === "true";
1352
1409
 
1410
+ // Handle arrows with extensible commands
1411
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{}`;
1412
+ if (baseText === "←" || baseText === "⟵") return `\\xleftarrow[${under}]{}`;
1413
+
1353
1414
  if (base === "∫") return `\\int_{${under}}`;
1415
+ if (underText === "\u23DF") return `\\underbrace{${base}}`;
1416
+
1417
+ if (isLimitOperator(base)) {
1418
+ return `${base}\\limits_{${under}}`;
1419
+ }
1354
1420
  return `\\underset{${under}}{${base}}`;
1355
1421
  };
1356
1422
  break;
@@ -1364,19 +1430,19 @@ function getRender(node) {
1364
1430
  const over = parse(childrenArray[2]);
1365
1431
  const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1366
1432
 
1367
- // Special handling for chemical reaction arrow
1368
- if (
1369
- baseText === "" &&
1370
- NodeTool.getNodeName(childrenArray[1]) === "msup"
1371
- ) {
1372
- return `\\xrightarrow[${under}]{${over}}`;
1373
- }
1433
+ // Special handling for chemical reaction arrow and other arrows
1434
+ if (baseText === "→" || baseText === "⟶") return `\\xrightarrow[${under}]{${over}}`;
1435
+ if (baseText === "" || baseText === "⟵") return `\\xleftarrow[${under}]{${over}}`;
1374
1436
 
1375
1437
  if (baseText === "∫") return `\\int_{${under}}^{${over}}`;
1376
1438
  if (baseText === "∑") return `\\sum_{${under}}^{${over}}`;
1377
1439
  if (baseText === "∏") return `\\prod_{${under}}^{${over}}`;
1378
1440
  if (baseText === "|") return `\\big|_{${under}}^{${over}}`;
1379
- return `${base}_{${under}}^{${over}}`;
1441
+
1442
+ if (isLimitOperator(base)) {
1443
+ return `${base}\\limits_{${under}}^{${over}}`;
1444
+ }
1445
+ return `\\underset{${under}}{\\overset{${over}}{${base}}}`;
1380
1446
  };
1381
1447
  break;
1382
1448
 
@@ -1384,30 +1450,56 @@ function getRender(node) {
1384
1450
  render = function (node, children) {
1385
1451
  const childrenArray = Array.from(children);
1386
1452
  if (!childrenArray || childrenArray.length < 1) return "";
1387
- const base = parse(childrenArray[0]);
1388
- let prescripts = "";
1389
- let postscripts = "";
1453
+ const base = parse(childrenArray[0]) || "";
1454
+
1455
+ const postSub = [];
1456
+ const postSup = [];
1457
+ const preSub = [];
1458
+ const preSup = [];
1459
+
1390
1460
  let i = 1;
1461
+ let inPrescripts = false;
1391
1462
 
1392
1463
  while (i < childrenArray.length) {
1393
- if (NodeTool.getNodeName(childrenArray[i]) === "mprescripts") {
1394
- i++;
1395
- if (i + 1 < childrenArray.length) {
1396
- prescripts = `_{${parse(childrenArray[i])}}^{${parse(
1397
- childrenArray[i + 1]
1398
- )}}`;
1399
- i += 2;
1400
- }
1464
+ const name = NodeTool.getNodeName(childrenArray[i]);
1465
+ if (name === "mprescripts") {
1466
+ inPrescripts = true;
1467
+ i += 1;
1468
+ continue;
1469
+ }
1470
+
1471
+ const subNode = childrenArray[i];
1472
+ const supNode = childrenArray[i + 1];
1473
+ if (!subNode || !supNode) break;
1474
+
1475
+ const sub = parse(subNode) || "";
1476
+ const sup = parse(supNode) || "";
1477
+
1478
+ if (inPrescripts) {
1479
+ if (sub) preSub.push(sub);
1480
+ if (sup) preSup.push(sup);
1401
1481
  } else {
1402
- if (i + 1 < childrenArray.length) {
1403
- postscripts += `_{${parse(childrenArray[i])}}^{${parse(
1404
- childrenArray[i + 1]
1405
- )}}`;
1406
- i += 2;
1407
- } else break;
1482
+ if (sub) postSub.push(sub);
1483
+ if (sup) postSup.push(sup);
1408
1484
  }
1485
+
1486
+ i += 2;
1409
1487
  }
1410
- return `${base}${prescripts}${postscripts}`;
1488
+
1489
+ const preSubStr = preSub.join(" ");
1490
+ const preSupStr = preSup.join(" ");
1491
+ const postSubStr = postSub.join(" ");
1492
+ const postSupStr = postSup.join(" ");
1493
+
1494
+ let pre = "";
1495
+ if (preSubStr) pre += `_{${preSubStr}}`;
1496
+ if (preSupStr) pre += `^{${preSupStr}}`;
1497
+
1498
+ let post = "";
1499
+ if (postSubStr) post += `_{${postSubStr}}`;
1500
+ if (postSupStr) post += `^{${postSupStr}}`;
1501
+
1502
+ return `${pre}${wrapBaseForScript(base)}${post}`;
1411
1503
  };
1412
1504
  break;
1413
1505
 
@@ -1508,7 +1600,28 @@ function getRender(node) {
1508
1600
  const num = parse(childrenArray[0]);
1509
1601
  const den = parse(childrenArray[1]);
1510
1602
  const linethickness = NodeTool.getAttr(node, "linethickness", "medium");
1511
- if (linethickness === "0") return `\\binom{${num}}{${den}}`;
1603
+ const bevelled = NodeTool.getAttr(node, "bevelled", "false");
1604
+
1605
+ if (bevelled === "true") {
1606
+ return `{}^{${num}}/_{${den}}`;
1607
+ }
1608
+
1609
+ if (["0", "0px"].indexOf(linethickness) > -1) {
1610
+ const prevNode = NodeTool.getPrevNode(node);
1611
+ const nextNode = NodeTool.getNextNode(node);
1612
+ if (
1613
+ prevNode &&
1614
+ NodeTool.getNodeName(prevNode) === "mo" &&
1615
+ NodeTool.getNodeText(prevNode).trim() === "(" &&
1616
+ nextNode &&
1617
+ NodeTool.getNodeName(nextNode) === "mo" &&
1618
+ NodeTool.getNodeText(nextNode).trim() === ")"
1619
+ ) {
1620
+ return `\\DELETE_BRACKET_L\\binom{${num}}{${den}}\\DELETE_BRACKET_R`;
1621
+ }
1622
+ return `{}_{${den}}^{${num}}`;
1623
+ }
1624
+
1512
1625
  return `\\frac{${num}}{${den}}`;
1513
1626
  };
1514
1627
  break;
@@ -1518,7 +1631,10 @@ function getRender(node) {
1518
1631
  const childrenArray = Array.from(children);
1519
1632
  const open = NodeTool.getAttr(node, "open", "(");
1520
1633
  const close = NodeTool.getAttr(node, "close", ")");
1521
- const separators = NodeTool.getAttr(node, "separators", ",").split("");
1634
+ const separatorsStr = NodeTool.getAttr(node, "separators", ",");
1635
+ const separators = separatorsStr
1636
+ .split("")
1637
+ .filter((c) => c.trim().length === 1);
1522
1638
 
1523
1639
  // Xử lý đặc biệt cho trường hợp dấu ngoặc đơn |
1524
1640
  if (open === "|") {
@@ -1569,9 +1685,10 @@ function getRender(node) {
1569
1685
  parts.push(parse(child));
1570
1686
  if (
1571
1687
  index < childrenArray.length - 1 &&
1572
- separators[index % separators.length]
1688
+ separators.length > 0
1573
1689
  ) {
1574
- parts.push(separators[index % separators.length]);
1690
+ const sep = separators[index] ?? separators[separators.length - 1];
1691
+ if (sep) parts.push(sep);
1575
1692
  }
1576
1693
  });
1577
1694
  return `\\left[${parts.join("")}\\right)`;
@@ -1583,18 +1700,20 @@ function getRender(node) {
1583
1700
  parts.push(parse(child));
1584
1701
  if (
1585
1702
  index < childrenArray.length - 1 &&
1586
- separators[index % separators.length]
1703
+ separators.length > 0
1587
1704
  ) {
1588
- parts.push(separators[index % separators.length]);
1705
+ const sep = separators[index] ?? separators[separators.length - 1];
1706
+ if (sep) parts.push(sep);
1589
1707
  }
1590
1708
  });
1591
1709
  const content = parts.join("");
1592
1710
 
1593
1711
  if (open === "{" && close === "}") return `\\{${content}\\}`;
1594
1712
  if (open === "|" && close === "|") return `\\left|${content}\\right|`;
1595
- if (!close) return `\\left${open}${content}\\right.`;
1596
- if (!open) return `\\left.${content}\\right${close}`;
1597
- return `\\left${open}${content}\\right${close}`;
1713
+ const left = open ? Brackets.parseLeft(open) : "";
1714
+ const right = close ? Brackets.parseRight(close) : "";
1715
+ if (!close && open) return `${left}${content}\\right.`;
1716
+ return `${left}${content}${right}`;
1598
1717
  };
1599
1718
  break;
1600
1719
 
@@ -1620,10 +1739,17 @@ function getRender(node) {
1620
1739
  case "mn":
1621
1740
  case "mo":
1622
1741
  case "ms":
1623
- case "mtext":
1624
1742
  render = getRender_joinSeparator("@content");
1625
1743
  break;
1626
1744
 
1745
+ case "mtext":
1746
+ render = function (node, children) {
1747
+ const childrenArray = Array.from(children);
1748
+ const content = renderChildren(childrenArray).join("");
1749
+ return `\\text{${content}}`;
1750
+ };
1751
+ break;
1752
+
1627
1753
  case "mphantom":
1628
1754
  render = function (node, children) {
1629
1755
  const childrenArray = Array.from(children);