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