ed-mathml2tex 0.1.7 → 0.1.9

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.
@@ -813,11 +813,11 @@ function parseElementMs(node) {
813
813
  // Math Text
814
814
  function parseElementMtext(node) {
815
815
  let content = NodeTool.getNodeText(node)
816
- // Handle operators and spacing only
817
816
  .replace(/\s*=\s*/g, " = ")
818
817
  .replace(/\s*\.\s*/g, " \\cdot ")
819
818
  .trim();
820
819
 
820
+ // Map đặc biệt
821
821
  const specialMTextMap = {
822
822
  ℝ: "\\mathbb{R}",
823
823
  ℤ: "\\mathbb{Z}",
@@ -830,14 +830,21 @@ function parseElementMtext(node) {
830
830
  };
831
831
  if (specialMTextMap[content]) return specialMTextMap[content];
832
832
 
833
- // Handle units with proper \mathrm formatting
833
+ // Nếu đã lệnh LaTeX thì giữ nguyên
834
+ if (content.startsWith("\\")) return content;
835
+
836
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
837
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
838
+ return `\\text{${content}}`;
839
+ }
840
+
841
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
834
842
  if (content.includes("(") && content.includes(")")) {
835
843
  const parts = content.split(/(\([^)]+\))/);
836
844
  content = parts
837
845
  .map((part) => {
838
846
  if (part.startsWith("(") && part.endsWith(")")) {
839
- // Keep original characters in units
840
- return `\\mathrm{${part}}`;
847
+ return `\\text{${part}}`;
841
848
  }
842
849
  return part;
843
850
  })
@@ -889,9 +896,8 @@ function parseContainer(node, children) {
889
896
 
890
897
  function renderChildren(children) {
891
898
  const parts = [];
892
- let lefts = [];
899
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
893
900
 
894
- // PATCH: Special case for set-builder style: leading { ... trailing }
895
901
  if (
896
902
  children.length >= 3 &&
897
903
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -903,8 +909,9 @@ function renderChildren(children) {
903
909
  const innerContent = Array.prototype.slice
904
910
  .call(children, 1, -1)
905
911
  .map((child) => parse(child))
906
- .join("");
907
- return `\\left\\{${innerContent}\\right\\}`;
912
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
913
+ const result = `\\left\\{${innerContent}\\right\\}`;
914
+ return result;
908
915
  }
909
916
 
910
917
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -922,18 +929,19 @@ function renderChildren(children) {
922
929
  ) {
923
930
  // Only vars, nums, ops (possibly Greek); join with thin space
924
931
  const str = mrowKids.map((k) => parse(k)).join("\\,");
925
- parts.push(str);
932
+ if (str) {
933
+ // CHỈ PUSH NẾU KHÔNG RỖNG
934
+ parts.push(str);
935
+ }
926
936
  return;
927
937
  }
928
- }
929
- // END PATCH
938
+ } // END PATCH
930
939
  if (NodeTool.getNodeName(node) === "mo") {
931
940
  const op = NodeTool.getNodeText(node).trim();
932
941
  if (Brackets.contains(op)) {
933
942
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
934
943
  stretchy = ["", "true"].indexOf(stretchy) > -1;
935
944
 
936
- // Luôn escape dấu ngoặc nhọn cho LaTeX
937
945
  let escapedOp = op;
938
946
  if (op === "{" || op === "}") {
939
947
  escapedOp = `\\${op}`;
@@ -941,19 +949,27 @@ function renderChildren(children) {
941
949
 
942
950
  if (Brackets.isRight(op)) {
943
951
  const nearLeft = lefts[lefts.length - 1];
944
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
952
+ if (nearLeft) {
945
953
  const parentNode = node.parentNode;
946
954
  const isInPower =
947
955
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
948
956
 
957
+ let partToPush = "";
949
958
  if (stretchy && !isInPower) {
950
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
959
+ partToPush = `\\right${escapedOp}`;
951
960
  } else {
952
- parts.push(escapedOp); // Dùng escapedOp
961
+ partToPush = escapedOp;
962
+ }
963
+ if (partToPush) {
964
+ // CHỈ PUSH NẾU KHÔNG RỖNG
965
+ parts.push(partToPush);
953
966
  }
954
967
  lefts.pop();
955
968
  } else {
956
- parts.push(escapedOp); // Ngoặc phải không khớp
969
+ if (escapedOp) {
970
+ // CHỈ PUSH NẾU KHÔNG RỖNG
971
+ parts.push(escapedOp);
972
+ }
957
973
  }
958
974
  } else {
959
975
  // Là ngoặc trái
@@ -961,18 +977,71 @@ function renderChildren(children) {
961
977
  const isInPower =
962
978
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
963
979
 
980
+ let partToPush = "";
964
981
  if (stretchy && !isInPower) {
965
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
982
+ partToPush = `\\left${escapedOp}`;
966
983
  } else {
967
- parts.push(escapedOp); // Dùng escapedOp
984
+ partToPush = escapedOp;
985
+ }
986
+ if (partToPush) {
987
+ // CHỈ PUSH NẾU KHÔNG RỖNG
988
+ parts.push(partToPush);
989
+ }
990
+ lefts.push(op);
991
+ }
992
+ } else {
993
+ const parsedOperator = parseOperator(node);
994
+ if (parsedOperator) {
995
+ // CHỈ PUSH NẾU KHÔNG RỖNG
996
+ parts.push(parsedOperator);
997
+ }
998
+ }
999
+
1000
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
1001
+ } else if (NodeTool.getNodeName(node) === "msub") {
1002
+ const subChildren = Array.from(NodeTool.getChildren(node));
1003
+ if (
1004
+ subChildren.length === 2 &&
1005
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1006
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1007
+ ) {
1008
+ // ĐÚNG LÀ NGOẠI LỆ
1009
+
1010
+ const sub = parse(subChildren[1]);
1011
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1012
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1013
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1014
+ for (let i = parts.length - 1; i >= 0; i--) {
1015
+ if (parts[i] && parts[i].trim() === "\\left(") {
1016
+ parts.splice(i, 1); // Xóa "\left("
1017
+ break;
968
1018
  }
969
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1019
+ _;
1020
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1021
+
1022
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1023
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1024
+
1025
+ const nearLeft = lefts[lefts.length - 1];
1026
+ if (nearLeft) {
1027
+ lefts.pop();
970
1028
  }
971
1029
  } else {
972
- parts.push(parseOperator(node));
1030
+ // <msub> bình thường
1031
+ const parsed = parse(node);
1032
+ if (parsed) {
1033
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1034
+ parts.push(parsed);
1035
+ }
973
1036
  }
1037
+ // --- END PATCH V6 ---
974
1038
  } else {
975
- parts.push(parse(node));
1039
+ // Các node khác như <mtext>, #text, v.v.
1040
+ const parsed = parse(node);
1041
+ if (parsed) {
1042
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1043
+ parts.push(parsed);
1044
+ }
976
1045
  }
977
1046
  });
978
1047
  lefts = undefined;
@@ -811,11 +811,11 @@ function parseElementMs(node) {
811
811
  // Math Text
812
812
  function parseElementMtext(node) {
813
813
  let content = NodeTool.getNodeText(node)
814
- // Handle operators and spacing only
815
814
  .replace(/\s*=\s*/g, " = ")
816
815
  .replace(/\s*\.\s*/g, " \\cdot ")
817
816
  .trim();
818
817
 
818
+ // Map đặc biệt
819
819
  const specialMTextMap = {
820
820
  ℝ: "\\mathbb{R}",
821
821
  ℤ: "\\mathbb{Z}",
@@ -828,14 +828,21 @@ function parseElementMtext(node) {
828
828
  };
829
829
  if (specialMTextMap[content]) return specialMTextMap[content];
830
830
 
831
- // Handle units with proper \mathrm formatting
831
+ // Nếu đã lệnh LaTeX thì giữ nguyên
832
+ if (content.startsWith("\\")) return content;
833
+
834
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
835
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
836
+ return `\\text{${content}}`;
837
+ }
838
+
839
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
832
840
  if (content.includes("(") && content.includes(")")) {
833
841
  const parts = content.split(/(\([^)]+\))/);
834
842
  content = parts
835
843
  .map((part) => {
836
844
  if (part.startsWith("(") && part.endsWith(")")) {
837
- // Keep original characters in units
838
- return `\\mathrm{${part}}`;
845
+ return `\\text{${part}}`;
839
846
  }
840
847
  return part;
841
848
  })
@@ -887,9 +894,8 @@ function parseContainer(node, children) {
887
894
 
888
895
  function renderChildren(children) {
889
896
  const parts = [];
890
- let lefts = [];
897
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
891
898
 
892
- // PATCH: Special case for set-builder style: leading { ... trailing }
893
899
  if (
894
900
  children.length >= 3 &&
895
901
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -901,8 +907,9 @@ function renderChildren(children) {
901
907
  const innerContent = Array.prototype.slice
902
908
  .call(children, 1, -1)
903
909
  .map((child) => parse(child))
904
- .join("");
905
- return `\\left\\{${innerContent}\\right\\}`;
910
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
911
+ const result = `\\left\\{${innerContent}\\right\\}`;
912
+ return result;
906
913
  }
907
914
 
908
915
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -920,18 +927,19 @@ function renderChildren(children) {
920
927
  ) {
921
928
  // Only vars, nums, ops (possibly Greek); join with thin space
922
929
  const str = mrowKids.map((k) => parse(k)).join("\\,");
923
- parts.push(str);
930
+ if (str) {
931
+ // CHỈ PUSH NẾU KHÔNG RỖNG
932
+ parts.push(str);
933
+ }
924
934
  return;
925
935
  }
926
- }
927
- // END PATCH
936
+ } // END PATCH
928
937
  if (NodeTool.getNodeName(node) === "mo") {
929
938
  const op = NodeTool.getNodeText(node).trim();
930
939
  if (Brackets.contains(op)) {
931
940
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
932
941
  stretchy = ["", "true"].indexOf(stretchy) > -1;
933
942
 
934
- // Luôn escape dấu ngoặc nhọn cho LaTeX
935
943
  let escapedOp = op;
936
944
  if (op === "{" || op === "}") {
937
945
  escapedOp = `\\${op}`;
@@ -939,19 +947,27 @@ function renderChildren(children) {
939
947
 
940
948
  if (Brackets.isRight(op)) {
941
949
  const nearLeft = lefts[lefts.length - 1];
942
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
950
+ if (nearLeft) {
943
951
  const parentNode = node.parentNode;
944
952
  const isInPower =
945
953
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
946
954
 
955
+ let partToPush = "";
947
956
  if (stretchy && !isInPower) {
948
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
957
+ partToPush = `\\right${escapedOp}`;
949
958
  } else {
950
- parts.push(escapedOp); // Dùng escapedOp
959
+ partToPush = escapedOp;
960
+ }
961
+ if (partToPush) {
962
+ // CHỈ PUSH NẾU KHÔNG RỖNG
963
+ parts.push(partToPush);
951
964
  }
952
965
  lefts.pop();
953
966
  } else {
954
- parts.push(escapedOp); // Ngoặc phải không khớp
967
+ if (escapedOp) {
968
+ // CHỈ PUSH NẾU KHÔNG RỖNG
969
+ parts.push(escapedOp);
970
+ }
955
971
  }
956
972
  } else {
957
973
  // Là ngoặc trái
@@ -959,18 +975,71 @@ function renderChildren(children) {
959
975
  const isInPower =
960
976
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
961
977
 
978
+ let partToPush = "";
962
979
  if (stretchy && !isInPower) {
963
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
980
+ partToPush = `\\left${escapedOp}`;
964
981
  } else {
965
- parts.push(escapedOp); // Dùng escapedOp
982
+ partToPush = escapedOp;
983
+ }
984
+ if (partToPush) {
985
+ // CHỈ PUSH NẾU KHÔNG RỖNG
986
+ parts.push(partToPush);
987
+ }
988
+ lefts.push(op);
989
+ }
990
+ } else {
991
+ const parsedOperator = parseOperator(node);
992
+ if (parsedOperator) {
993
+ // CHỈ PUSH NẾU KHÔNG RỖNG
994
+ parts.push(parsedOperator);
995
+ }
996
+ }
997
+
998
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
999
+ } else if (NodeTool.getNodeName(node) === "msub") {
1000
+ const subChildren = Array.from(NodeTool.getChildren(node));
1001
+ if (
1002
+ subChildren.length === 2 &&
1003
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1004
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1005
+ ) {
1006
+ // ĐÚNG LÀ NGOẠI LỆ
1007
+
1008
+ const sub = parse(subChildren[1]);
1009
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1010
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1011
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1012
+ for (let i = parts.length - 1; i >= 0; i--) {
1013
+ if (parts[i] && parts[i].trim() === "\\left(") {
1014
+ parts.splice(i, 1); // Xóa "\left("
1015
+ break;
966
1016
  }
967
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1017
+ _;
1018
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1019
+
1020
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1021
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1022
+
1023
+ const nearLeft = lefts[lefts.length - 1];
1024
+ if (nearLeft) {
1025
+ lefts.pop();
968
1026
  }
969
1027
  } else {
970
- parts.push(parseOperator(node));
1028
+ // <msub> bình thường
1029
+ const parsed = parse(node);
1030
+ if (parsed) {
1031
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1032
+ parts.push(parsed);
1033
+ }
971
1034
  }
1035
+ // --- END PATCH V6 ---
972
1036
  } else {
973
- parts.push(parse(node));
1037
+ // Các node khác như <mtext>, #text, v.v.
1038
+ const parsed = parse(node);
1039
+ if (parsed) {
1040
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1041
+ parts.push(parsed);
1042
+ }
974
1043
  }
975
1044
  });
976
1045
  lefts = undefined;
@@ -817,11 +817,11 @@
817
817
  // Math Text
818
818
  function parseElementMtext(node) {
819
819
  let content = NodeTool.getNodeText(node)
820
- // Handle operators and spacing only
821
820
  .replace(/\s*=\s*/g, " = ")
822
821
  .replace(/\s*\.\s*/g, " \\cdot ")
823
822
  .trim();
824
823
 
824
+ // Map đặc biệt
825
825
  const specialMTextMap = {
826
826
  ℝ: "\\mathbb{R}",
827
827
  ℤ: "\\mathbb{Z}",
@@ -834,14 +834,21 @@
834
834
  };
835
835
  if (specialMTextMap[content]) return specialMTextMap[content];
836
836
 
837
- // Handle units with proper \mathrm formatting
837
+ // Nếu đã lệnh LaTeX thì giữ nguyên
838
+ if (content.startsWith("\\")) return content;
839
+
840
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
841
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
842
+ return `\\text{${content}}`;
843
+ }
844
+
845
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
838
846
  if (content.includes("(") && content.includes(")")) {
839
847
  const parts = content.split(/(\([^)]+\))/);
840
848
  content = parts
841
849
  .map((part) => {
842
850
  if (part.startsWith("(") && part.endsWith(")")) {
843
- // Keep original characters in units
844
- return `\\mathrm{${part}}`;
851
+ return `\\text{${part}}`;
845
852
  }
846
853
  return part;
847
854
  })
@@ -893,9 +900,8 @@
893
900
 
894
901
  function renderChildren(children) {
895
902
  const parts = [];
896
- let lefts = [];
903
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
897
904
 
898
- // PATCH: Special case for set-builder style: leading { ... trailing }
899
905
  if (
900
906
  children.length >= 3 &&
901
907
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -907,8 +913,9 @@
907
913
  const innerContent = Array.prototype.slice
908
914
  .call(children, 1, -1)
909
915
  .map((child) => parse(child))
910
- .join("");
911
- return `\\left\\{${innerContent}\\right\\}`;
916
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
917
+ const result = `\\left\\{${innerContent}\\right\\}`;
918
+ return result;
912
919
  }
913
920
 
914
921
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -926,18 +933,19 @@
926
933
  ) {
927
934
  // Only vars, nums, ops (possibly Greek); join with thin space
928
935
  const str = mrowKids.map((k) => parse(k)).join("\\,");
929
- parts.push(str);
936
+ if (str) {
937
+ // CHỈ PUSH NẾU KHÔNG RỖNG
938
+ parts.push(str);
939
+ }
930
940
  return;
931
941
  }
932
- }
933
- // END PATCH
942
+ } // END PATCH
934
943
  if (NodeTool.getNodeName(node) === "mo") {
935
944
  const op = NodeTool.getNodeText(node).trim();
936
945
  if (Brackets.contains(op)) {
937
946
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
938
947
  stretchy = ["", "true"].indexOf(stretchy) > -1;
939
948
 
940
- // Luôn escape dấu ngoặc nhọn cho LaTeX
941
949
  let escapedOp = op;
942
950
  if (op === "{" || op === "}") {
943
951
  escapedOp = `\\${op}`;
@@ -945,19 +953,27 @@
945
953
 
946
954
  if (Brackets.isRight(op)) {
947
955
  const nearLeft = lefts[lefts.length - 1];
948
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
956
+ if (nearLeft) {
949
957
  const parentNode = node.parentNode;
950
958
  const isInPower =
951
959
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
952
960
 
961
+ let partToPush = "";
953
962
  if (stretchy && !isInPower) {
954
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
963
+ partToPush = `\\right${escapedOp}`;
955
964
  } else {
956
- parts.push(escapedOp); // Dùng escapedOp
965
+ partToPush = escapedOp;
966
+ }
967
+ if (partToPush) {
968
+ // CHỈ PUSH NẾU KHÔNG RỖNG
969
+ parts.push(partToPush);
957
970
  }
958
971
  lefts.pop();
959
972
  } else {
960
- parts.push(escapedOp); // Ngoặc phải không khớp
973
+ if (escapedOp) {
974
+ // CHỈ PUSH NẾU KHÔNG RỖNG
975
+ parts.push(escapedOp);
976
+ }
961
977
  }
962
978
  } else {
963
979
  // Là ngoặc trái
@@ -965,18 +981,71 @@
965
981
  const isInPower =
966
982
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
967
983
 
984
+ let partToPush = "";
968
985
  if (stretchy && !isInPower) {
969
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
986
+ partToPush = `\\left${escapedOp}`;
970
987
  } else {
971
- parts.push(escapedOp); // Dùng escapedOp
988
+ partToPush = escapedOp;
989
+ }
990
+ if (partToPush) {
991
+ // CHỈ PUSH NẾU KHÔNG RỖNG
992
+ parts.push(partToPush);
993
+ }
994
+ lefts.push(op);
995
+ }
996
+ } else {
997
+ const parsedOperator = parseOperator(node);
998
+ if (parsedOperator) {
999
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1000
+ parts.push(parsedOperator);
1001
+ }
1002
+ }
1003
+
1004
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
1005
+ } else if (NodeTool.getNodeName(node) === "msub") {
1006
+ const subChildren = Array.from(NodeTool.getChildren(node));
1007
+ if (
1008
+ subChildren.length === 2 &&
1009
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1010
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1011
+ ) {
1012
+ // ĐÚNG LÀ NGOẠI LỆ
1013
+
1014
+ const sub = parse(subChildren[1]);
1015
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1016
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1017
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1018
+ for (let i = parts.length - 1; i >= 0; i--) {
1019
+ if (parts[i] && parts[i].trim() === "\\left(") {
1020
+ parts.splice(i, 1); // Xóa "\left("
1021
+ break;
972
1022
  }
973
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1023
+ _;
1024
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1025
+
1026
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1027
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1028
+
1029
+ const nearLeft = lefts[lefts.length - 1];
1030
+ if (nearLeft) {
1031
+ lefts.pop();
974
1032
  }
975
1033
  } else {
976
- parts.push(parseOperator(node));
1034
+ // <msub> bình thường
1035
+ const parsed = parse(node);
1036
+ if (parsed) {
1037
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1038
+ parts.push(parsed);
1039
+ }
977
1040
  }
1041
+ // --- END PATCH V6 ---
978
1042
  } else {
979
- parts.push(parse(node));
1043
+ // Các node khác như <mtext>, #text, v.v.
1044
+ const parsed = parse(node);
1045
+ if (parsed) {
1046
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1047
+ parts.push(parsed);
1048
+ }
980
1049
  }
981
1050
  });
982
1051
  lefts = undefined;
@@ -813,11 +813,11 @@ function parseElementMs(node) {
813
813
  // Math Text
814
814
  function parseElementMtext(node) {
815
815
  let content = NodeTool.getNodeText(node)
816
- // Handle operators and spacing only
817
816
  .replace(/\s*=\s*/g, " = ")
818
817
  .replace(/\s*\.\s*/g, " \\cdot ")
819
818
  .trim();
820
819
 
820
+ // Map đặc biệt
821
821
  const specialMTextMap = {
822
822
  ℝ: "\\mathbb{R}",
823
823
  ℤ: "\\mathbb{Z}",
@@ -830,14 +830,21 @@ function parseElementMtext(node) {
830
830
  };
831
831
  if (specialMTextMap[content]) return specialMTextMap[content];
832
832
 
833
- // Handle units with proper \mathrm formatting
833
+ // Nếu đã lệnh LaTeX thì giữ nguyên
834
+ if (content.startsWith("\\")) return content;
835
+
836
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
837
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
838
+ return `\\text{${content}}`;
839
+ }
840
+
841
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
834
842
  if (content.includes("(") && content.includes(")")) {
835
843
  const parts = content.split(/(\([^)]+\))/);
836
844
  content = parts
837
845
  .map((part) => {
838
846
  if (part.startsWith("(") && part.endsWith(")")) {
839
- // Keep original characters in units
840
- return `\\mathrm{${part}}`;
847
+ return `\\text{${part}}`;
841
848
  }
842
849
  return part;
843
850
  })
@@ -889,9 +896,8 @@ function parseContainer(node, children) {
889
896
 
890
897
  function renderChildren(children) {
891
898
  const parts = [];
892
- let lefts = [];
899
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
893
900
 
894
- // PATCH: Special case for set-builder style: leading { ... trailing }
895
901
  if (
896
902
  children.length >= 3 &&
897
903
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -903,8 +909,9 @@ function renderChildren(children) {
903
909
  const innerContent = Array.prototype.slice
904
910
  .call(children, 1, -1)
905
911
  .map((child) => parse(child))
906
- .join("");
907
- return `\\left\\{${innerContent}\\right\\}`;
912
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
913
+ const result = `\\left\\{${innerContent}\\right\\}`;
914
+ return result;
908
915
  }
909
916
 
910
917
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -922,18 +929,19 @@ function renderChildren(children) {
922
929
  ) {
923
930
  // Only vars, nums, ops (possibly Greek); join with thin space
924
931
  const str = mrowKids.map((k) => parse(k)).join("\\,");
925
- parts.push(str);
932
+ if (str) {
933
+ // CHỈ PUSH NẾU KHÔNG RỖNG
934
+ parts.push(str);
935
+ }
926
936
  return;
927
937
  }
928
- }
929
- // END PATCH
938
+ } // END PATCH
930
939
  if (NodeTool.getNodeName(node) === "mo") {
931
940
  const op = NodeTool.getNodeText(node).trim();
932
941
  if (Brackets.contains(op)) {
933
942
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
934
943
  stretchy = ["", "true"].indexOf(stretchy) > -1;
935
944
 
936
- // Luôn escape dấu ngoặc nhọn cho LaTeX
937
945
  let escapedOp = op;
938
946
  if (op === "{" || op === "}") {
939
947
  escapedOp = `\\${op}`;
@@ -941,19 +949,27 @@ function renderChildren(children) {
941
949
 
942
950
  if (Brackets.isRight(op)) {
943
951
  const nearLeft = lefts[lefts.length - 1];
944
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
952
+ if (nearLeft) {
945
953
  const parentNode = node.parentNode;
946
954
  const isInPower =
947
955
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
948
956
 
957
+ let partToPush = "";
949
958
  if (stretchy && !isInPower) {
950
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
959
+ partToPush = `\\right${escapedOp}`;
951
960
  } else {
952
- parts.push(escapedOp); // Dùng escapedOp
961
+ partToPush = escapedOp;
962
+ }
963
+ if (partToPush) {
964
+ // CHỈ PUSH NẾU KHÔNG RỖNG
965
+ parts.push(partToPush);
953
966
  }
954
967
  lefts.pop();
955
968
  } else {
956
- parts.push(escapedOp); // Ngoặc phải không khớp
969
+ if (escapedOp) {
970
+ // CHỈ PUSH NẾU KHÔNG RỖNG
971
+ parts.push(escapedOp);
972
+ }
957
973
  }
958
974
  } else {
959
975
  // Là ngoặc trái
@@ -961,18 +977,71 @@ function renderChildren(children) {
961
977
  const isInPower =
962
978
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
963
979
 
980
+ let partToPush = "";
964
981
  if (stretchy && !isInPower) {
965
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
982
+ partToPush = `\\left${escapedOp}`;
966
983
  } else {
967
- parts.push(escapedOp); // Dùng escapedOp
984
+ partToPush = escapedOp;
985
+ }
986
+ if (partToPush) {
987
+ // CHỈ PUSH NẾU KHÔNG RỖNG
988
+ parts.push(partToPush);
989
+ }
990
+ lefts.push(op);
991
+ }
992
+ } else {
993
+ const parsedOperator = parseOperator(node);
994
+ if (parsedOperator) {
995
+ // CHỈ PUSH NẾU KHÔNG RỖNG
996
+ parts.push(parsedOperator);
997
+ }
998
+ }
999
+
1000
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
1001
+ } else if (NodeTool.getNodeName(node) === "msub") {
1002
+ const subChildren = Array.from(NodeTool.getChildren(node));
1003
+ if (
1004
+ subChildren.length === 2 &&
1005
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1006
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1007
+ ) {
1008
+ // ĐÚNG LÀ NGOẠI LỆ
1009
+
1010
+ const sub = parse(subChildren[1]);
1011
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1012
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1013
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1014
+ for (let i = parts.length - 1; i >= 0; i--) {
1015
+ if (parts[i] && parts[i].trim() === "\\left(") {
1016
+ parts.splice(i, 1); // Xóa "\left("
1017
+ break;
968
1018
  }
969
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1019
+ _;
1020
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1021
+
1022
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1023
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1024
+
1025
+ const nearLeft = lefts[lefts.length - 1];
1026
+ if (nearLeft) {
1027
+ lefts.pop();
970
1028
  }
971
1029
  } else {
972
- parts.push(parseOperator(node));
1030
+ // <msub> bình thường
1031
+ const parsed = parse(node);
1032
+ if (parsed) {
1033
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1034
+ parts.push(parsed);
1035
+ }
973
1036
  }
1037
+ // --- END PATCH V6 ---
974
1038
  } else {
975
- parts.push(parse(node));
1039
+ // Các node khác như <mtext>, #text, v.v.
1040
+ const parsed = parse(node);
1041
+ if (parsed) {
1042
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1043
+ parts.push(parsed);
1044
+ }
976
1045
  }
977
1046
  });
978
1047
  lefts = undefined;
@@ -811,11 +811,11 @@ function parseElementMs(node) {
811
811
  // Math Text
812
812
  function parseElementMtext(node) {
813
813
  let content = NodeTool.getNodeText(node)
814
- // Handle operators and spacing only
815
814
  .replace(/\s*=\s*/g, " = ")
816
815
  .replace(/\s*\.\s*/g, " \\cdot ")
817
816
  .trim();
818
817
 
818
+ // Map đặc biệt
819
819
  const specialMTextMap = {
820
820
  ℝ: "\\mathbb{R}",
821
821
  ℤ: "\\mathbb{Z}",
@@ -828,14 +828,21 @@ function parseElementMtext(node) {
828
828
  };
829
829
  if (specialMTextMap[content]) return specialMTextMap[content];
830
830
 
831
- // Handle units with proper \mathrm formatting
831
+ // Nếu đã lệnh LaTeX thì giữ nguyên
832
+ if (content.startsWith("\\")) return content;
833
+
834
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
835
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
836
+ return `\\text{${content}}`;
837
+ }
838
+
839
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
832
840
  if (content.includes("(") && content.includes(")")) {
833
841
  const parts = content.split(/(\([^)]+\))/);
834
842
  content = parts
835
843
  .map((part) => {
836
844
  if (part.startsWith("(") && part.endsWith(")")) {
837
- // Keep original characters in units
838
- return `\\mathrm{${part}}`;
845
+ return `\\text{${part}}`;
839
846
  }
840
847
  return part;
841
848
  })
@@ -887,9 +894,8 @@ function parseContainer(node, children) {
887
894
 
888
895
  function renderChildren(children) {
889
896
  const parts = [];
890
- let lefts = [];
897
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
891
898
 
892
- // PATCH: Special case for set-builder style: leading { ... trailing }
893
899
  if (
894
900
  children.length >= 3 &&
895
901
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -901,8 +907,9 @@ function renderChildren(children) {
901
907
  const innerContent = Array.prototype.slice
902
908
  .call(children, 1, -1)
903
909
  .map((child) => parse(child))
904
- .join("");
905
- return `\\left\\{${innerContent}\\right\\}`;
910
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
911
+ const result = `\\left\\{${innerContent}\\right\\}`;
912
+ return result;
906
913
  }
907
914
 
908
915
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -920,18 +927,19 @@ function renderChildren(children) {
920
927
  ) {
921
928
  // Only vars, nums, ops (possibly Greek); join with thin space
922
929
  const str = mrowKids.map((k) => parse(k)).join("\\,");
923
- parts.push(str);
930
+ if (str) {
931
+ // CHỈ PUSH NẾU KHÔNG RỖNG
932
+ parts.push(str);
933
+ }
924
934
  return;
925
935
  }
926
- }
927
- // END PATCH
936
+ } // END PATCH
928
937
  if (NodeTool.getNodeName(node) === "mo") {
929
938
  const op = NodeTool.getNodeText(node).trim();
930
939
  if (Brackets.contains(op)) {
931
940
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
932
941
  stretchy = ["", "true"].indexOf(stretchy) > -1;
933
942
 
934
- // Luôn escape dấu ngoặc nhọn cho LaTeX
935
943
  let escapedOp = op;
936
944
  if (op === "{" || op === "}") {
937
945
  escapedOp = `\\${op}`;
@@ -939,19 +947,27 @@ function renderChildren(children) {
939
947
 
940
948
  if (Brackets.isRight(op)) {
941
949
  const nearLeft = lefts[lefts.length - 1];
942
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
950
+ if (nearLeft) {
943
951
  const parentNode = node.parentNode;
944
952
  const isInPower =
945
953
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
946
954
 
955
+ let partToPush = "";
947
956
  if (stretchy && !isInPower) {
948
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
957
+ partToPush = `\\right${escapedOp}`;
949
958
  } else {
950
- parts.push(escapedOp); // Dùng escapedOp
959
+ partToPush = escapedOp;
960
+ }
961
+ if (partToPush) {
962
+ // CHỈ PUSH NẾU KHÔNG RỖNG
963
+ parts.push(partToPush);
951
964
  }
952
965
  lefts.pop();
953
966
  } else {
954
- parts.push(escapedOp); // Ngoặc phải không khớp
967
+ if (escapedOp) {
968
+ // CHỈ PUSH NẾU KHÔNG RỖNG
969
+ parts.push(escapedOp);
970
+ }
955
971
  }
956
972
  } else {
957
973
  // Là ngoặc trái
@@ -959,18 +975,71 @@ function renderChildren(children) {
959
975
  const isInPower =
960
976
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
961
977
 
978
+ let partToPush = "";
962
979
  if (stretchy && !isInPower) {
963
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
980
+ partToPush = `\\left${escapedOp}`;
964
981
  } else {
965
- parts.push(escapedOp); // Dùng escapedOp
982
+ partToPush = escapedOp;
983
+ }
984
+ if (partToPush) {
985
+ // CHỈ PUSH NẾU KHÔNG RỖNG
986
+ parts.push(partToPush);
987
+ }
988
+ lefts.push(op);
989
+ }
990
+ } else {
991
+ const parsedOperator = parseOperator(node);
992
+ if (parsedOperator) {
993
+ // CHỈ PUSH NẾU KHÔNG RỖNG
994
+ parts.push(parsedOperator);
995
+ }
996
+ }
997
+
998
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
999
+ } else if (NodeTool.getNodeName(node) === "msub") {
1000
+ const subChildren = Array.from(NodeTool.getChildren(node));
1001
+ if (
1002
+ subChildren.length === 2 &&
1003
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1004
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1005
+ ) {
1006
+ // ĐÚNG LÀ NGOẠI LỆ
1007
+
1008
+ const sub = parse(subChildren[1]);
1009
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1010
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1011
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1012
+ for (let i = parts.length - 1; i >= 0; i--) {
1013
+ if (parts[i] && parts[i].trim() === "\\left(") {
1014
+ parts.splice(i, 1); // Xóa "\left("
1015
+ break;
966
1016
  }
967
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1017
+ _;
1018
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1019
+
1020
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1021
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1022
+
1023
+ const nearLeft = lefts[lefts.length - 1];
1024
+ if (nearLeft) {
1025
+ lefts.pop();
968
1026
  }
969
1027
  } else {
970
- parts.push(parseOperator(node));
1028
+ // <msub> bình thường
1029
+ const parsed = parse(node);
1030
+ if (parsed) {
1031
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1032
+ parts.push(parsed);
1033
+ }
971
1034
  }
1035
+ // --- END PATCH V6 ---
972
1036
  } else {
973
- parts.push(parse(node));
1037
+ // Các node khác như <mtext>, #text, v.v.
1038
+ const parsed = parse(node);
1039
+ if (parsed) {
1040
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1041
+ parts.push(parsed);
1042
+ }
974
1043
  }
975
1044
  });
976
1045
  lefts = undefined;
@@ -817,11 +817,11 @@
817
817
  // Math Text
818
818
  function parseElementMtext(node) {
819
819
  let content = NodeTool.getNodeText(node)
820
- // Handle operators and spacing only
821
820
  .replace(/\s*=\s*/g, " = ")
822
821
  .replace(/\s*\.\s*/g, " \\cdot ")
823
822
  .trim();
824
823
 
824
+ // Map đặc biệt
825
825
  const specialMTextMap = {
826
826
  ℝ: "\\mathbb{R}",
827
827
  ℤ: "\\mathbb{Z}",
@@ -834,14 +834,21 @@
834
834
  };
835
835
  if (specialMTextMap[content]) return specialMTextMap[content];
836
836
 
837
- // Handle units with proper \mathrm formatting
837
+ // Nếu đã lệnh LaTeX thì giữ nguyên
838
+ if (content.startsWith("\\")) return content;
839
+
840
+ // Bọc token chữ/số liền nhau bằng \text{...} (hóa học: Cu, OH, NaOH, ...)
841
+ if (/^[A-Za-z][A-Za-z0-9]*$/.test(content)) {
842
+ return `\\text{${content}}`;
843
+ }
844
+
845
+ // (Giữ nguyên xử lý units nếu mtext chứa dấu ngoặc tròn)
838
846
  if (content.includes("(") && content.includes(")")) {
839
847
  const parts = content.split(/(\([^)]+\))/);
840
848
  content = parts
841
849
  .map((part) => {
842
850
  if (part.startsWith("(") && part.endsWith(")")) {
843
- // Keep original characters in units
844
- return `\\mathrm{${part}}`;
851
+ return `\\text{${part}}`;
845
852
  }
846
853
  return part;
847
854
  })
@@ -893,9 +900,8 @@
893
900
 
894
901
  function renderChildren(children) {
895
902
  const parts = [];
896
- let lefts = [];
903
+ let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
897
904
 
898
- // PATCH: Special case for set-builder style: leading { ... trailing }
899
905
  if (
900
906
  children.length >= 3 &&
901
907
  NodeTool.getNodeName(children[0]) === "mo" &&
@@ -907,8 +913,9 @@
907
913
  const innerContent = Array.prototype.slice
908
914
  .call(children, 1, -1)
909
915
  .map((child) => parse(child))
910
- .join("");
911
- return `\\left\\{${innerContent}\\right\\}`;
916
+ .join(""); // Chỉ trả về nếu nội dung không rỗng
917
+ const result = `\\left\\{${innerContent}\\right\\}`;
918
+ return result;
912
919
  }
913
920
 
914
921
  Array.prototype.forEach.call(children, (node, idx) => {
@@ -926,18 +933,19 @@
926
933
  ) {
927
934
  // Only vars, nums, ops (possibly Greek); join with thin space
928
935
  const str = mrowKids.map((k) => parse(k)).join("\\,");
929
- parts.push(str);
936
+ if (str) {
937
+ // CHỈ PUSH NẾU KHÔNG RỖNG
938
+ parts.push(str);
939
+ }
930
940
  return;
931
941
  }
932
- }
933
- // END PATCH
942
+ } // END PATCH
934
943
  if (NodeTool.getNodeName(node) === "mo") {
935
944
  const op = NodeTool.getNodeText(node).trim();
936
945
  if (Brackets.contains(op)) {
937
946
  let stretchy = NodeTool.getAttr(node, "stretchy", "true");
938
947
  stretchy = ["", "true"].indexOf(stretchy) > -1;
939
948
 
940
- // Luôn escape dấu ngoặc nhọn cho LaTeX
941
949
  let escapedOp = op;
942
950
  if (op === "{" || op === "}") {
943
951
  escapedOp = `\\${op}`;
@@ -945,19 +953,27 @@
945
953
 
946
954
  if (Brackets.isRight(op)) {
947
955
  const nearLeft = lefts[lefts.length - 1];
948
- if (nearLeft && Brackets.isPair(nearLeft, op)) {
956
+ if (nearLeft) {
949
957
  const parentNode = node.parentNode;
950
958
  const isInPower =
951
959
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
952
960
 
961
+ let partToPush = "";
953
962
  if (stretchy && !isInPower) {
954
- parts.push(`\\right${escapedOp}`); // Dùng escapedOp
963
+ partToPush = `\\right${escapedOp}`;
955
964
  } else {
956
- parts.push(escapedOp); // Dùng escapedOp
965
+ partToPush = escapedOp;
966
+ }
967
+ if (partToPush) {
968
+ // CHỈ PUSH NẾU KHÔNG RỖNG
969
+ parts.push(partToPush);
957
970
  }
958
971
  lefts.pop();
959
972
  } else {
960
- parts.push(escapedOp); // Ngoặc phải không khớp
973
+ if (escapedOp) {
974
+ // CHỈ PUSH NẾU KHÔNG RỖNG
975
+ parts.push(escapedOp);
976
+ }
961
977
  }
962
978
  } else {
963
979
  // Là ngoặc trái
@@ -965,18 +981,71 @@
965
981
  const isInPower =
966
982
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
967
983
 
984
+ let partToPush = "";
968
985
  if (stretchy && !isInPower) {
969
- parts.push(`\\left${escapedOp}`); // Dùng escapedOp
986
+ partToPush = `\\left${escapedOp}`;
970
987
  } else {
971
- parts.push(escapedOp); // Dùng escapedOp
988
+ partToPush = escapedOp;
989
+ }
990
+ if (partToPush) {
991
+ // CHỈ PUSH NẾU KHÔNG RỖNG
992
+ parts.push(partToPush);
993
+ }
994
+ lefts.push(op);
995
+ }
996
+ } else {
997
+ const parsedOperator = parseOperator(node);
998
+ if (parsedOperator) {
999
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1000
+ parts.push(parsedOperator);
1001
+ }
1002
+ }
1003
+
1004
+ // --- START PATCH V6 (Giữ nguyên logic V5) ---
1005
+ } else if (NodeTool.getNodeName(node) === "msub") {
1006
+ const subChildren = Array.from(NodeTool.getChildren(node));
1007
+ if (
1008
+ subChildren.length === 2 &&
1009
+ NodeTool.getNodeName(subChildren[0]) === "mo" &&
1010
+ NodeTool.getNodeText(subChildren[0]).trim() === ")"
1011
+ ) {
1012
+ // ĐÚNG LÀ NGOẠI LỆ
1013
+
1014
+ const sub = parse(subChildren[1]);
1015
+ // Mảng 'parts' lúc này "sạch" và là: ["\text{Cu}", "\left(", "\text{OH}"]
1016
+ const lastPart = parts.pop(); // lastPart = "\text{OH}"
1017
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\left("]
1018
+ for (let i = parts.length - 1; i >= 0; i--) {
1019
+ if (parts[i] && parts[i].trim() === "\\left(") {
1020
+ parts.splice(i, 1); // Xóa "\left("
1021
+ break;
972
1022
  }
973
- lefts.push(op); // Dùng `op` gốc để so sánh cặp
1023
+ _;
1024
+ } // Mảng 'parts' lúc này là: ["\text{Cu}"]
1025
+
1026
+ parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1027
+ // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1028
+
1029
+ const nearLeft = lefts[lefts.length - 1];
1030
+ if (nearLeft) {
1031
+ lefts.pop();
974
1032
  }
975
1033
  } else {
976
- parts.push(parseOperator(node));
1034
+ // <msub> bình thường
1035
+ const parsed = parse(node);
1036
+ if (parsed) {
1037
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1038
+ parts.push(parsed);
1039
+ }
977
1040
  }
1041
+ // --- END PATCH V6 ---
978
1042
  } else {
979
- parts.push(parse(node));
1043
+ // Các node khác như <mtext>, #text, v.v.
1044
+ const parsed = parse(node);
1045
+ if (parsed) {
1046
+ // CHỈ PUSH NẾU KHÔNG RỖNG
1047
+ parts.push(parsed);
1048
+ }
980
1049
  }
981
1050
  });
982
1051
  lefts = undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ed-mathml2tex",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Convert mathml to latex.",
5
5
  "author": "Mika",
6
6
  "license": "MIT",