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