ed-mathml2tex 0.2.0 → 0.2.2

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.
@@ -762,7 +762,7 @@ function parseOperator(node) {
762
762
  "÷": " \\div ",
763
763
  "∑": " \\sum ",
764
764
  "∏": " \\prod ",
765
- "∫": " \\int ",
765
+ "∫": "\\int",
766
766
  "−": "-",
767
767
  "≠": " \\neq ",
768
768
  ">": " > ",
@@ -812,7 +812,7 @@ function parseElementMs(node) {
812
812
 
813
813
  // Math Text
814
814
  function parseElementMtext(node) {
815
- let content = NodeTool.getNodeText(node)
815
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
816
816
  .replace(/\s*=\s*/g, " = ")
817
817
  .replace(/\s*\.\s*/g, " \\cdot ")
818
818
  .trim();
@@ -866,6 +866,14 @@ function parseElementMspace(node) {
866
866
  }
867
867
 
868
868
  function escapeSpecialChars(text) {
869
+ // Strip problematic Unicode characters:
870
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
871
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
872
+ // - Variation Selectors: \uFE00-\uFE0F
873
+ // - Private Use Area (PUA): \uE000-\uF8FF
874
+ // - Non-characters: \uFDD0-\uFDEF
875
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
876
+
869
877
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
870
878
  if (
871
879
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -896,7 +904,8 @@ function parseContainer(node, children) {
896
904
 
897
905
  function renderChildren(children) {
898
906
  const parts = [];
899
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
907
+ // lefts mảng object: { op: "(", index: 5 }
908
+ let lefts = [];
900
909
 
901
910
  if (
902
911
  children.length >= 3 &&
@@ -948,8 +957,8 @@ function renderChildren(children) {
948
957
  }
949
958
 
950
959
  if (Brackets.isRight(op)) {
951
- const nearLeft = lefts[lefts.length - 1];
952
- if (nearLeft) {
960
+ const nearLeftObj = lefts[lefts.length - 1];
961
+ if (nearLeftObj) {
953
962
  const parentNode = node.parentNode;
954
963
  const isInPower =
955
964
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -964,6 +973,7 @@ function renderChildren(children) {
964
973
  // CHỈ PUSH NẾU KHÔNG RỖNG
965
974
  parts.push(partToPush);
966
975
  }
976
+ // matched a left: remove corresponding left object
967
977
  lefts.pop();
968
978
  } else {
969
979
  if (escapedOp) {
@@ -987,7 +997,8 @@ function renderChildren(children) {
987
997
  // CHỈ PUSH NẾU KHÔNG RỖNG
988
998
  parts.push(partToPush);
989
999
  }
990
- lefts.push(op);
1000
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1001
+ lefts.push({ op: op, index: parts.length - 1 });
991
1002
  }
992
1003
  } else {
993
1004
  const parsedOperator = parseOperator(node);
@@ -1022,8 +1033,8 @@ function renderChildren(children) {
1022
1033
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1023
1034
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1024
1035
 
1025
- const nearLeft = lefts[lefts.length - 1];
1026
- if (nearLeft) {
1036
+ const nearLeftObj = lefts[lefts.length - 1];
1037
+ if (nearLeftObj) {
1027
1038
  lefts.pop();
1028
1039
  }
1029
1040
  } else {
@@ -1044,6 +1055,45 @@ function renderChildren(children) {
1044
1055
  }
1045
1056
  }
1046
1057
  });
1058
+
1059
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1060
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1061
+ if (lefts && lefts.length > 0) {
1062
+ const rightForLeft = (left) => {
1063
+ switch (left) {
1064
+ case "(":
1065
+ return ")";
1066
+ case "[":
1067
+ return "]";
1068
+ case "{":
1069
+ return "}";
1070
+ case "|":
1071
+ return "|";
1072
+ default:
1073
+ return ".";
1074
+ }
1075
+ };
1076
+
1077
+ lefts.forEach((leftObj) => {
1078
+ const idx = leftObj && leftObj.index;
1079
+ if (typeof idx !== "number" || !parts[idx]) return;
1080
+
1081
+ const left = leftObj.op;
1082
+ const right = rightForLeft(left);
1083
+
1084
+ // 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
1085
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1086
+ const hasMatchingRight =
1087
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1088
+
1089
+ if (!hasMatchingRight) {
1090
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1091
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1092
+ }
1093
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1094
+ });
1095
+ }
1096
+
1047
1097
  lefts = undefined;
1048
1098
  return parts;
1049
1099
  }
@@ -760,7 +760,7 @@ function parseOperator(node) {
760
760
  "÷": " \\div ",
761
761
  "∑": " \\sum ",
762
762
  "∏": " \\prod ",
763
- "∫": " \\int ",
763
+ "∫": "\\int",
764
764
  "−": "-",
765
765
  "≠": " \\neq ",
766
766
  ">": " > ",
@@ -810,7 +810,7 @@ function parseElementMs(node) {
810
810
 
811
811
  // Math Text
812
812
  function parseElementMtext(node) {
813
- let content = NodeTool.getNodeText(node)
813
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
814
814
  .replace(/\s*=\s*/g, " = ")
815
815
  .replace(/\s*\.\s*/g, " \\cdot ")
816
816
  .trim();
@@ -864,6 +864,14 @@ function parseElementMspace(node) {
864
864
  }
865
865
 
866
866
  function escapeSpecialChars(text) {
867
+ // Strip problematic Unicode characters:
868
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
869
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
870
+ // - Variation Selectors: \uFE00-\uFE0F
871
+ // - Private Use Area (PUA): \uE000-\uF8FF
872
+ // - Non-characters: \uFDD0-\uFDEF
873
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
874
+
867
875
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
868
876
  if (
869
877
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -894,7 +902,8 @@ function parseContainer(node, children) {
894
902
 
895
903
  function renderChildren(children) {
896
904
  const parts = [];
897
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
905
+ // lefts mảng object: { op: "(", index: 5 }
906
+ let lefts = [];
898
907
 
899
908
  if (
900
909
  children.length >= 3 &&
@@ -946,8 +955,8 @@ function renderChildren(children) {
946
955
  }
947
956
 
948
957
  if (Brackets.isRight(op)) {
949
- const nearLeft = lefts[lefts.length - 1];
950
- if (nearLeft) {
958
+ const nearLeftObj = lefts[lefts.length - 1];
959
+ if (nearLeftObj) {
951
960
  const parentNode = node.parentNode;
952
961
  const isInPower =
953
962
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -962,6 +971,7 @@ function renderChildren(children) {
962
971
  // CHỈ PUSH NẾU KHÔNG RỖNG
963
972
  parts.push(partToPush);
964
973
  }
974
+ // matched a left: remove corresponding left object
965
975
  lefts.pop();
966
976
  } else {
967
977
  if (escapedOp) {
@@ -985,7 +995,8 @@ function renderChildren(children) {
985
995
  // CHỈ PUSH NẾU KHÔNG RỖNG
986
996
  parts.push(partToPush);
987
997
  }
988
- lefts.push(op);
998
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
999
+ lefts.push({ op: op, index: parts.length - 1 });
989
1000
  }
990
1001
  } else {
991
1002
  const parsedOperator = parseOperator(node);
@@ -1020,8 +1031,8 @@ function renderChildren(children) {
1020
1031
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1021
1032
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1022
1033
 
1023
- const nearLeft = lefts[lefts.length - 1];
1024
- if (nearLeft) {
1034
+ const nearLeftObj = lefts[lefts.length - 1];
1035
+ if (nearLeftObj) {
1025
1036
  lefts.pop();
1026
1037
  }
1027
1038
  } else {
@@ -1042,6 +1053,45 @@ function renderChildren(children) {
1042
1053
  }
1043
1054
  }
1044
1055
  });
1056
+
1057
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1058
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1059
+ if (lefts && lefts.length > 0) {
1060
+ const rightForLeft = (left) => {
1061
+ switch (left) {
1062
+ case "(":
1063
+ return ")";
1064
+ case "[":
1065
+ return "]";
1066
+ case "{":
1067
+ return "}";
1068
+ case "|":
1069
+ return "|";
1070
+ default:
1071
+ return ".";
1072
+ }
1073
+ };
1074
+
1075
+ lefts.forEach((leftObj) => {
1076
+ const idx = leftObj && leftObj.index;
1077
+ if (typeof idx !== "number" || !parts[idx]) return;
1078
+
1079
+ const left = leftObj.op;
1080
+ const right = rightForLeft(left);
1081
+
1082
+ // 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
1083
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1084
+ const hasMatchingRight =
1085
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1086
+
1087
+ if (!hasMatchingRight) {
1088
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1089
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1090
+ }
1091
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1092
+ });
1093
+ }
1094
+
1045
1095
  lefts = undefined;
1046
1096
  return parts;
1047
1097
  }
@@ -766,7 +766,7 @@
766
766
  "÷": " \\div ",
767
767
  "∑": " \\sum ",
768
768
  "∏": " \\prod ",
769
- "∫": " \\int ",
769
+ "∫": "\\int",
770
770
  "−": "-",
771
771
  "≠": " \\neq ",
772
772
  ">": " > ",
@@ -816,7 +816,7 @@
816
816
 
817
817
  // Math Text
818
818
  function parseElementMtext(node) {
819
- let content = NodeTool.getNodeText(node)
819
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
820
820
  .replace(/\s*=\s*/g, " = ")
821
821
  .replace(/\s*\.\s*/g, " \\cdot ")
822
822
  .trim();
@@ -870,6 +870,14 @@
870
870
  }
871
871
 
872
872
  function escapeSpecialChars(text) {
873
+ // Strip problematic Unicode characters:
874
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
875
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
876
+ // - Variation Selectors: \uFE00-\uFE0F
877
+ // - Private Use Area (PUA): \uE000-\uF8FF
878
+ // - Non-characters: \uFDD0-\uFDEF
879
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
880
+
873
881
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
874
882
  if (
875
883
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -900,7 +908,8 @@
900
908
 
901
909
  function renderChildren(children) {
902
910
  const parts = [];
903
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
911
+ // lefts mảng object: { op: "(", index: 5 }
912
+ let lefts = [];
904
913
 
905
914
  if (
906
915
  children.length >= 3 &&
@@ -952,8 +961,8 @@
952
961
  }
953
962
 
954
963
  if (Brackets.isRight(op)) {
955
- const nearLeft = lefts[lefts.length - 1];
956
- if (nearLeft) {
964
+ const nearLeftObj = lefts[lefts.length - 1];
965
+ if (nearLeftObj) {
957
966
  const parentNode = node.parentNode;
958
967
  const isInPower =
959
968
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -968,6 +977,7 @@
968
977
  // CHỈ PUSH NẾU KHÔNG RỖNG
969
978
  parts.push(partToPush);
970
979
  }
980
+ // matched a left: remove corresponding left object
971
981
  lefts.pop();
972
982
  } else {
973
983
  if (escapedOp) {
@@ -991,7 +1001,8 @@
991
1001
  // CHỈ PUSH NẾU KHÔNG RỖNG
992
1002
  parts.push(partToPush);
993
1003
  }
994
- lefts.push(op);
1004
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1005
+ lefts.push({ op: op, index: parts.length - 1 });
995
1006
  }
996
1007
  } else {
997
1008
  const parsedOperator = parseOperator(node);
@@ -1026,8 +1037,8 @@
1026
1037
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1027
1038
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1028
1039
 
1029
- const nearLeft = lefts[lefts.length - 1];
1030
- if (nearLeft) {
1040
+ const nearLeftObj = lefts[lefts.length - 1];
1041
+ if (nearLeftObj) {
1031
1042
  lefts.pop();
1032
1043
  }
1033
1044
  } else {
@@ -1048,6 +1059,45 @@
1048
1059
  }
1049
1060
  }
1050
1061
  });
1062
+
1063
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1064
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1065
+ if (lefts && lefts.length > 0) {
1066
+ const rightForLeft = (left) => {
1067
+ switch (left) {
1068
+ case "(":
1069
+ return ")";
1070
+ case "[":
1071
+ return "]";
1072
+ case "{":
1073
+ return "}";
1074
+ case "|":
1075
+ return "|";
1076
+ default:
1077
+ return ".";
1078
+ }
1079
+ };
1080
+
1081
+ lefts.forEach((leftObj) => {
1082
+ const idx = leftObj && leftObj.index;
1083
+ if (typeof idx !== "number" || !parts[idx]) return;
1084
+
1085
+ const left = leftObj.op;
1086
+ const right = rightForLeft(left);
1087
+
1088
+ // 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
1089
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1090
+ const hasMatchingRight =
1091
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1092
+
1093
+ if (!hasMatchingRight) {
1094
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1095
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1096
+ }
1097
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1098
+ });
1099
+ }
1100
+
1051
1101
  lefts = undefined;
1052
1102
  return parts;
1053
1103
  }
@@ -762,7 +762,7 @@ function parseOperator(node) {
762
762
  "÷": " \\div ",
763
763
  "∑": " \\sum ",
764
764
  "∏": " \\prod ",
765
- "∫": " \\int ",
765
+ "∫": "\\int",
766
766
  "−": "-",
767
767
  "≠": " \\neq ",
768
768
  ">": " > ",
@@ -812,7 +812,7 @@ function parseElementMs(node) {
812
812
 
813
813
  // Math Text
814
814
  function parseElementMtext(node) {
815
- let content = NodeTool.getNodeText(node)
815
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
816
816
  .replace(/\s*=\s*/g, " = ")
817
817
  .replace(/\s*\.\s*/g, " \\cdot ")
818
818
  .trim();
@@ -866,6 +866,14 @@ function parseElementMspace(node) {
866
866
  }
867
867
 
868
868
  function escapeSpecialChars(text) {
869
+ // Strip problematic Unicode characters:
870
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
871
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
872
+ // - Variation Selectors: \uFE00-\uFE0F
873
+ // - Private Use Area (PUA): \uE000-\uF8FF
874
+ // - Non-characters: \uFDD0-\uFDEF
875
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
876
+
869
877
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
870
878
  if (
871
879
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -896,7 +904,8 @@ function parseContainer(node, children) {
896
904
 
897
905
  function renderChildren(children) {
898
906
  const parts = [];
899
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
907
+ // lefts mảng object: { op: "(", index: 5 }
908
+ let lefts = [];
900
909
 
901
910
  if (
902
911
  children.length >= 3 &&
@@ -948,8 +957,8 @@ function renderChildren(children) {
948
957
  }
949
958
 
950
959
  if (Brackets.isRight(op)) {
951
- const nearLeft = lefts[lefts.length - 1];
952
- if (nearLeft) {
960
+ const nearLeftObj = lefts[lefts.length - 1];
961
+ if (nearLeftObj) {
953
962
  const parentNode = node.parentNode;
954
963
  const isInPower =
955
964
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -964,6 +973,7 @@ function renderChildren(children) {
964
973
  // CHỈ PUSH NẾU KHÔNG RỖNG
965
974
  parts.push(partToPush);
966
975
  }
976
+ // matched a left: remove corresponding left object
967
977
  lefts.pop();
968
978
  } else {
969
979
  if (escapedOp) {
@@ -987,7 +997,8 @@ function renderChildren(children) {
987
997
  // CHỈ PUSH NẾU KHÔNG RỖNG
988
998
  parts.push(partToPush);
989
999
  }
990
- lefts.push(op);
1000
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1001
+ lefts.push({ op: op, index: parts.length - 1 });
991
1002
  }
992
1003
  } else {
993
1004
  const parsedOperator = parseOperator(node);
@@ -1022,8 +1033,8 @@ function renderChildren(children) {
1022
1033
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1023
1034
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1024
1035
 
1025
- const nearLeft = lefts[lefts.length - 1];
1026
- if (nearLeft) {
1036
+ const nearLeftObj = lefts[lefts.length - 1];
1037
+ if (nearLeftObj) {
1027
1038
  lefts.pop();
1028
1039
  }
1029
1040
  } else {
@@ -1044,6 +1055,45 @@ function renderChildren(children) {
1044
1055
  }
1045
1056
  }
1046
1057
  });
1058
+
1059
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1060
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1061
+ if (lefts && lefts.length > 0) {
1062
+ const rightForLeft = (left) => {
1063
+ switch (left) {
1064
+ case "(":
1065
+ return ")";
1066
+ case "[":
1067
+ return "]";
1068
+ case "{":
1069
+ return "}";
1070
+ case "|":
1071
+ return "|";
1072
+ default:
1073
+ return ".";
1074
+ }
1075
+ };
1076
+
1077
+ lefts.forEach((leftObj) => {
1078
+ const idx = leftObj && leftObj.index;
1079
+ if (typeof idx !== "number" || !parts[idx]) return;
1080
+
1081
+ const left = leftObj.op;
1082
+ const right = rightForLeft(left);
1083
+
1084
+ // 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
1085
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1086
+ const hasMatchingRight =
1087
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1088
+
1089
+ if (!hasMatchingRight) {
1090
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1091
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1092
+ }
1093
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1094
+ });
1095
+ }
1096
+
1047
1097
  lefts = undefined;
1048
1098
  return parts;
1049
1099
  }
@@ -760,7 +760,7 @@ function parseOperator(node) {
760
760
  "÷": " \\div ",
761
761
  "∑": " \\sum ",
762
762
  "∏": " \\prod ",
763
- "∫": " \\int ",
763
+ "∫": "\\int",
764
764
  "−": "-",
765
765
  "≠": " \\neq ",
766
766
  ">": " > ",
@@ -810,7 +810,7 @@ function parseElementMs(node) {
810
810
 
811
811
  // Math Text
812
812
  function parseElementMtext(node) {
813
- let content = NodeTool.getNodeText(node)
813
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
814
814
  .replace(/\s*=\s*/g, " = ")
815
815
  .replace(/\s*\.\s*/g, " \\cdot ")
816
816
  .trim();
@@ -864,6 +864,14 @@ function parseElementMspace(node) {
864
864
  }
865
865
 
866
866
  function escapeSpecialChars(text) {
867
+ // Strip problematic Unicode characters:
868
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
869
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
870
+ // - Variation Selectors: \uFE00-\uFE0F
871
+ // - Private Use Area (PUA): \uE000-\uF8FF
872
+ // - Non-characters: \uFDD0-\uFDEF
873
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
874
+
867
875
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
868
876
  if (
869
877
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -894,7 +902,8 @@ function parseContainer(node, children) {
894
902
 
895
903
  function renderChildren(children) {
896
904
  const parts = [];
897
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
905
+ // lefts mảng object: { op: "(", index: 5 }
906
+ let lefts = [];
898
907
 
899
908
  if (
900
909
  children.length >= 3 &&
@@ -946,8 +955,8 @@ function renderChildren(children) {
946
955
  }
947
956
 
948
957
  if (Brackets.isRight(op)) {
949
- const nearLeft = lefts[lefts.length - 1];
950
- if (nearLeft) {
958
+ const nearLeftObj = lefts[lefts.length - 1];
959
+ if (nearLeftObj) {
951
960
  const parentNode = node.parentNode;
952
961
  const isInPower =
953
962
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -962,6 +971,7 @@ function renderChildren(children) {
962
971
  // CHỈ PUSH NẾU KHÔNG RỖNG
963
972
  parts.push(partToPush);
964
973
  }
974
+ // matched a left: remove corresponding left object
965
975
  lefts.pop();
966
976
  } else {
967
977
  if (escapedOp) {
@@ -985,7 +995,8 @@ function renderChildren(children) {
985
995
  // CHỈ PUSH NẾU KHÔNG RỖNG
986
996
  parts.push(partToPush);
987
997
  }
988
- lefts.push(op);
998
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
999
+ lefts.push({ op: op, index: parts.length - 1 });
989
1000
  }
990
1001
  } else {
991
1002
  const parsedOperator = parseOperator(node);
@@ -1020,8 +1031,8 @@ function renderChildren(children) {
1020
1031
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1021
1032
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1022
1033
 
1023
- const nearLeft = lefts[lefts.length - 1];
1024
- if (nearLeft) {
1034
+ const nearLeftObj = lefts[lefts.length - 1];
1035
+ if (nearLeftObj) {
1025
1036
  lefts.pop();
1026
1037
  }
1027
1038
  } else {
@@ -1042,6 +1053,45 @@ function renderChildren(children) {
1042
1053
  }
1043
1054
  }
1044
1055
  });
1056
+
1057
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1058
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1059
+ if (lefts && lefts.length > 0) {
1060
+ const rightForLeft = (left) => {
1061
+ switch (left) {
1062
+ case "(":
1063
+ return ")";
1064
+ case "[":
1065
+ return "]";
1066
+ case "{":
1067
+ return "}";
1068
+ case "|":
1069
+ return "|";
1070
+ default:
1071
+ return ".";
1072
+ }
1073
+ };
1074
+
1075
+ lefts.forEach((leftObj) => {
1076
+ const idx = leftObj && leftObj.index;
1077
+ if (typeof idx !== "number" || !parts[idx]) return;
1078
+
1079
+ const left = leftObj.op;
1080
+ const right = rightForLeft(left);
1081
+
1082
+ // 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
1083
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1084
+ const hasMatchingRight =
1085
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1086
+
1087
+ if (!hasMatchingRight) {
1088
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1089
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1090
+ }
1091
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1092
+ });
1093
+ }
1094
+
1045
1095
  lefts = undefined;
1046
1096
  return parts;
1047
1097
  }
@@ -766,7 +766,7 @@
766
766
  "÷": " \\div ",
767
767
  "∑": " \\sum ",
768
768
  "∏": " \\prod ",
769
- "∫": " \\int ",
769
+ "∫": "\\int",
770
770
  "−": "-",
771
771
  "≠": " \\neq ",
772
772
  ">": " > ",
@@ -816,7 +816,7 @@
816
816
 
817
817
  // Math Text
818
818
  function parseElementMtext(node) {
819
- let content = NodeTool.getNodeText(node)
819
+ let content = escapeSpecialChars(NodeTool.getNodeText(node))
820
820
  .replace(/\s*=\s*/g, " = ")
821
821
  .replace(/\s*\.\s*/g, " \\cdot ")
822
822
  .trim();
@@ -870,6 +870,14 @@
870
870
  }
871
871
 
872
872
  function escapeSpecialChars(text) {
873
+ // Strip problematic Unicode characters:
874
+ // - Control characters: \u0000-\u0008, \u000B-\u000C, \u000E-\u001F, \u007F-\u009F
875
+ // - Zero-width & Invisible: \u200B-\u200F, \u202A-\u202E, \u2060-\u206F, \uFEFF
876
+ // - Variation Selectors: \uFE00-\uFE0F
877
+ // - Private Use Area (PUA): \uE000-\uF8FF
878
+ // - Non-characters: \uFDD0-\uFDEF
879
+ text = text.replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\uFEFF\uE000-\uF8FF\uFDD0-\uFDEF]/g, "");
880
+
873
881
  // Don't escape pi, Greek, or just a-z0-9 or Unicode Greek, or empty
874
882
  if (
875
883
  /^\\?[a-zA-Z0-9]+$/.test(text) ||
@@ -900,7 +908,8 @@
900
908
 
901
909
  function renderChildren(children) {
902
910
  const parts = [];
903
- let lefts = []; // PATCH: Special case for set-builder style: leading { ... trailing }
911
+ // lefts mảng object: { op: "(", index: 5 }
912
+ let lefts = [];
904
913
 
905
914
  if (
906
915
  children.length >= 3 &&
@@ -952,8 +961,8 @@
952
961
  }
953
962
 
954
963
  if (Brackets.isRight(op)) {
955
- const nearLeft = lefts[lefts.length - 1];
956
- if (nearLeft) {
964
+ const nearLeftObj = lefts[lefts.length - 1];
965
+ if (nearLeftObj) {
957
966
  const parentNode = node.parentNode;
958
967
  const isInPower =
959
968
  parentNode && NodeTool.getNodeName(parentNode) === "msup";
@@ -968,6 +977,7 @@
968
977
  // CHỈ PUSH NẾU KHÔNG RỖNG
969
978
  parts.push(partToPush);
970
979
  }
980
+ // matched a left: remove corresponding left object
971
981
  lefts.pop();
972
982
  } else {
973
983
  if (escapedOp) {
@@ -991,7 +1001,8 @@
991
1001
  // CHỈ PUSH NẾU KHÔNG RỖNG
992
1002
  parts.push(partToPush);
993
1003
  }
994
- lefts.push(op);
1004
+ // Lưu vị trí token left vừa push để có thể xử lý nếu không có right
1005
+ lefts.push({ op: op, index: parts.length - 1 });
995
1006
  }
996
1007
  } else {
997
1008
  const parsedOperator = parseOperator(node);
@@ -1026,8 +1037,8 @@
1026
1037
  parts.push(`${lastPart}_{${sub}}`); // Push "\text{OH}_{2}"
1027
1038
  // Mảng 'parts' lúc này là: ["\text{Cu}", "\text{OH}_{2}"]
1028
1039
 
1029
- const nearLeft = lefts[lefts.length - 1];
1030
- if (nearLeft) {
1040
+ const nearLeftObj = lefts[lefts.length - 1];
1041
+ if (nearLeftObj) {
1031
1042
  lefts.pop();
1032
1043
  }
1033
1044
  } else {
@@ -1048,6 +1059,45 @@
1048
1059
  }
1049
1060
  }
1050
1061
  });
1062
+
1063
+ // Nếu còn lefts (ngoặc trái) chưa được đóng, chỉ chuyển '\leftX' thành 'X'
1064
+ // khi không có đóng tương ứng phía sau — nếu có \right (hoặc dấu đóng) thì giữ \left
1065
+ if (lefts && lefts.length > 0) {
1066
+ const rightForLeft = (left) => {
1067
+ switch (left) {
1068
+ case "(":
1069
+ return ")";
1070
+ case "[":
1071
+ return "]";
1072
+ case "{":
1073
+ return "}";
1074
+ case "|":
1075
+ return "|";
1076
+ default:
1077
+ return ".";
1078
+ }
1079
+ };
1080
+
1081
+ lefts.forEach((leftObj) => {
1082
+ const idx = leftObj && leftObj.index;
1083
+ if (typeof idx !== "number" || !parts[idx]) return;
1084
+
1085
+ const left = leftObj.op;
1086
+ const right = rightForLeft(left);
1087
+
1088
+ // 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
1089
+ const tail = parts.slice(idx + 1).map(String).join(" ");
1090
+ const hasMatchingRight =
1091
+ (/\\right/.test(tail)) || (right !== "." && tail.indexOf(right) !== -1);
1092
+
1093
+ if (!hasMatchingRight) {
1094
+ // Không có đóng tương ứng: bỏ prefix '\left' để chỉ còn ký tự mở bình thường
1095
+ parts[idx] = String(parts[idx]).replace(/^\\left/, "");
1096
+ }
1097
+ // Nếu có đóng tương ứng thì giữ nguyên '\left...' để không sinh \right thừa
1098
+ });
1099
+ }
1100
+
1051
1101
  lefts = undefined;
1052
1102
  return parts;
1053
1103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ed-mathml2tex",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Convert mathml to latex.",
5
5
  "author": "Mika",
6
6
  "license": "MIT",