ed-mathml2tex 0.0.9 → 0.1.1

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.
@@ -584,74 +584,75 @@ T.createMarker = function() {
584
584
  }
585
585
  };
586
586
 
587
- function getRender_default(template) {
588
- return function(node, children) {
589
- const parts = renderChildren(children);
590
- return renderTemplate(template, parts);
591
- };
592
- }
593
-
594
587
  function getRender_joinSeparator(template, separator = '') {
595
- return function(node, children) {
588
+ return function (node, children) {
596
589
  const parts = renderChildren(children);
597
590
  return template.replace('@content', parts.join(separator));
598
591
  };
599
592
  }
600
593
 
601
- function convert(mathmlHtml){
594
+ function convert(mathmlHtml) {
602
595
  const math = NodeTool.parseMath(mathmlHtml);
603
596
  let result = toLatex(parse(math));
604
-
605
- // Last-chance post-processing for specific patterns
606
- if (mathmlHtml.includes("<munder>") &&
607
- mathmlHtml.includes("<mo>→</mo>") &&
608
- mathmlHtml.includes("<mrow/>")) {
597
+
598
+ // Xử sau cuối cho các mẫu đặc biệt
599
+ result = result.replace(/\\right\.\./g, "\\right."); // Loại bỏ dấu chấm trùng lặp
600
+ result = result.replace(/\.\s*\./g, "."); // Loại bỏ dấu chấm trùng lặp
601
+
602
+ if (mathmlHtml.includes("<munder>") &&
603
+ mathmlHtml.includes("<mo>→</mo>") &&
604
+ mathmlHtml.includes("<mrow/>")) {
609
605
  if (result.includes("\\limits")) {
610
606
  result = "\\underset{}{\\rightarrow}";
611
607
  }
612
608
  }
613
-
609
+
610
+ // Thêm xử lý cho các thẻ MathML khác
611
+ result = result
612
+ .replace(/∞/g, "\\infty") // Vô cực
613
+ .replace(/∑/g, "\\sum") // Tổng
614
+ .replace(/∏/g, "\\prod") // Tích
615
+ .replace(/∫/g, "\\int"); // Tích phân
616
+
614
617
  return result;
615
618
  }
616
619
 
617
620
  function toLatex(result) {
618
- // binomial coefficients
621
+ // Xử lý binomial coefficients
619
622
  result = result.replace(/\\left\(\\DELETE_BRACKET_L/g, '');
620
623
  result = result.replace(/\\DELETE_BRACKET_R\\right\)/g, '');
621
624
  result = result.replace(/\\DELETE_BRACKET_L/g, '');
622
625
  result = result.replace(/\\DELETE_BRACKET_R/g, '');
623
-
624
- // Fix all cases of arrows with limits
625
- // Case 1: munder - arrow with empty subscript
626
+
627
+ // Xử các trường hợp mũi tên với giới hạn
626
628
  result = result.replace(/→\\limits_{}/g, "\\underset{}{\\rightarrow}");
627
629
  result = result.replace(/→\\limits_{(\s*)}/g, "\\underset{}{\\rightarrow}");
628
630
  result = result.replace(/\\rightarrow\\limits_{}/g, "\\underset{}{\\rightarrow}");
629
631
  result = result.replace(/\\rightarrow\\limits_{(\s*)}/g, "\\underset{}{\\rightarrow}");
630
-
631
- // Case 2: munder - arrow with non-empty subscript
632
+
632
633
  result = result.replace(/→\\limits_\{([^}]*)\}/g, "\\underset{$1}{\\rightarrow}");
633
634
  result = result.replace(/\\rightarrow\\limits_\{([^}]*)\}/g, "\\underset{$1}{\\rightarrow}");
634
-
635
- // Case 3: munderover - arrow with both subscript and superscript
635
+
636
636
  result = result.replace(/→\\limits_\{([^}]*)\}\^\{([^}]*)\}/g, "\\overset{$2}{\\underset{$1}{\\rightarrow}}");
637
637
  result = result.replace(/\\rightarrow\\limits_\{([^}]*)\}\^\{([^}]*)\}/g, "\\overset{$2}{\\underset{$1}{\\rightarrow}}");
638
-
639
- // Case 4: mover - fix expressions with arrow superscript
640
- // Simple expression with arrow superscript: expr^{\rightarrow} → \overrightarrow{expr}
638
+
639
+ // Xử vector các hiệu đặc biệt
641
640
  result = result.replace(/([^{}\s]+)\^\{\\rightarrow\}/g, "\\overrightarrow{$1}");
642
641
  result = result.replace(/\{([^{}]+)\}\^\{\\rightarrow\}/g, "\\overrightarrow{$1}");
643
-
644
- // Complex expressions with subscripts and arrow: expr_{sub}^{\rightarrow} → \overrightarrow{expr_{sub}}
645
642
  result = result.replace(/([A-Za-z0-9]+)_\{([^{}]+)\}\^\{\\rightarrow\}/g, "\\overrightarrow{$1_{$2}}");
646
643
  result = result.replace(/([A-Za-z0-9]+)_([0-9])\^\{\\rightarrow\}/g, "\\overrightarrow{$1_$2}");
647
-
648
- // Very complex expressions: (expr)^{\rightarrow} → \overrightarrow{(expr)}
649
644
  result = result.replace(/(\([^()]+\))\^\{\\rightarrow\}/g, "\\overrightarrow{$1}");
650
-
651
- // Also match if there are spaces
652
- result = result.replace(/→\s*\\limits\s*_\s*{\s*}/g, "\\underset{}{\\rightarrow}");
653
- result = result.replace(/\\rightarrow\s*\\limits\s*_\s*{\s*}/g, "\\underset{}{\\rightarrow}");
654
-
645
+
646
+ // Thêm xử các hiệu toán học phổ biến
647
+ result = result.replace(/≤/g, "\\leq");
648
+ result = result.replace(/≥/g, "\\geq");
649
+ result = result.replace(/≠/g, "\\neq");
650
+ result = result.replace(/≈/g, "\\approx");
651
+ result = result.replace(/π/g, "\\pi");
652
+ result = result.replace(/α/g, "\\alpha");
653
+ result = result.replace(/β/g, "\\beta");
654
+ result = result.replace(/γ/g, "\\gamma");
655
+
655
656
  return result;
656
657
  }
657
658
 
@@ -665,15 +666,15 @@ function parse(node) {
665
666
  }
666
667
 
667
668
  // @see https://www.w3.org/TR/MathML3/chapter7.html
669
+ // Cải tiến parseLeaf để hỗ trợ thêm các ký hiệu
668
670
  function parseLeaf(node) {
669
671
  let r = '';
670
672
  const nodeName = NodeTool.getNodeName(node);
671
-
672
- // Special case for empty mrow
673
+
673
674
  if (nodeName === "mrow" && NodeTool.getNodeText(node).trim() === "") {
674
675
  return "";
675
676
  }
676
-
677
+
677
678
  switch (nodeName) {
678
679
  case 'mi':
679
680
  r = parseElementMi(node);
@@ -684,53 +685,76 @@ function parseLeaf(node) {
684
685
  case 'mo':
685
686
  r = parseOperator(node);
686
687
  break;
687
- case 'ms': r = parseElementMs(node);
688
+ case 'ms':
689
+ r = parseElementMs(node);
688
690
  break;
689
- case 'mtext': r = parseElementMtext(node);
691
+ case 'mtext':
692
+ r = parseElementMtext(node);
690
693
  break;
691
- case 'mglyph': r = parseElementMglyph(node);
694
+ case 'mglyph':
695
+ r = parseElementMglyph(node);
692
696
  break;
693
- case 'mprescripts': r = '';
697
+ case 'mprescripts':
698
+ r = '';
694
699
  break;
695
- case 'mspace': r = parseElementMspace();
696
- case 'none': r = '\\:';
697
- //TODO other usecase of 'none' ?
700
+ case 'mspace':
701
+ r = parseElementMspace();
698
702
  break;
699
- default: r = escapeSpecialChars(NodeTool.getNodeText(node).trim());
703
+ case 'none':
704
+ r = '\\:';
705
+ break;
706
+ default:
707
+ r = escapeSpecialChars(NodeTool.getNodeText(node).trim());
700
708
  break;
701
709
  }
702
710
  return r;
703
711
  }
704
712
 
705
- // operator token, mathematical operators
713
+ // Cải tiến parseOperator để hỗ trợ thêm toán tử
706
714
  function parseOperator(node) {
707
715
  let it = NodeTool.getNodeText(node).trim();
708
-
709
- // Special case for arrow (→) with proper spacing
710
- if (it === "→") {
711
- return " \\rightarrow "; // Add spaces around arrow
712
- }
713
-
714
- it = MathSymbol.parseOperator(it);
715
- return escapeSpecialChars(it);
716
+
717
+ const operatorMap = {
718
+ "→": " \\rightarrow ",
719
+ "←": " \\leftarrow ",
720
+ "↔": " \\leftrightarrow ",
721
+ "⇒": " \\Rightarrow ",
722
+ "⇐": " \\Leftarrow ",
723
+ "⇔": " \\Leftrightarrow ",
724
+ "±": " \\pm ",
725
+ "×": " \\times ",
726
+ "÷": " \\div ",
727
+ "∑": " \\sum ",
728
+ "∏": " \\prod ",
729
+ "∫": " \\int ",
730
+ "−": "-",
731
+ "≠": " \\neq ",
732
+ ">": " > ",
733
+ "=": " = ",
734
+ ",": ", ", // Dấu phẩy trong tập hợp
735
+ ";": ";", // Dấu chấm phẩy không cần khoảng trắng
736
+ "Ω": "\\Omega" // Thêm ký hiệu Omega
737
+ };
738
+
739
+ return operatorMap[it] || escapeSpecialChars(MathSymbol.parseOperator(it));
716
740
  }
717
741
 
718
742
  // Math identifier
719
743
  function parseElementMi(node) {
720
744
  let it = NodeTool.getNodeText(node).trim();
721
-
745
+
722
746
  // Handle vectors (e.g. AB', AI)
723
747
  if (it.includes("'")) {
724
748
  return it; // Return as is to handle in mrow
725
749
  }
726
-
750
+
727
751
  // Handle subscripts (e.g. n₂)
728
- if (it.match(/[a-zA-Z]\d/)) {
752
+ if (it.match(/[a-zA-z]\d/)) {
729
753
  const base = it[0];
730
754
  const sub = it[1];
731
755
  return `${base}_{${sub}}`;
732
756
  }
733
-
757
+
734
758
  it = MathSymbol.parseIdentifier(it);
735
759
  return escapeSpecialChars(it);
736
760
  }
@@ -742,33 +766,33 @@ function parseElementMn(node) {
742
766
  }
743
767
 
744
768
  // Math String
745
- function parseElementMs(node){
769
+ function parseElementMs(node) {
746
770
  const content = NodeTool.getNodeText(node).trimRight();
747
771
  const it = escapeSpecialChars(content);
748
772
  return ['"', it, '"'].join('');
749
773
  }
750
774
 
751
775
  // Math Text
752
- function parseElementMtext(node){
776
+ function parseElementMtext(node) {
753
777
  const content = NodeTool.getNodeText(node);
754
778
  const it = escapeSpecialChars(content);
755
779
  return `\\text{${it}}`;
756
780
  }
757
781
 
758
782
  // Math glyph (image)
759
- function parseElementMglyph(node){
783
+ function parseElementMglyph(node) {
760
784
  const it = ['"', NodeTool.getAttr(node, 'alt', ''), '"'].join('');
761
785
  return escapeSpecialChars(it);
762
786
  }
763
787
 
764
788
  // TODO need or not
765
- function parseElementMspace(node){
789
+ function parseElementMspace(node) {
766
790
  return '';
767
791
  }
768
792
 
769
793
  function escapeSpecialChars(text) {
770
794
  const specialChars = /\$|%|_|&|#|\{|\}/g;
771
- text = text.replace(specialChars, char => `\\${ char }`);
795
+ text = text.replace(specialChars, char => `\\${char}`);
772
796
  return text;
773
797
  }
774
798
 
@@ -786,31 +810,33 @@ function renderChildren(children) {
786
810
  const parts = [];
787
811
  let lefts = [];
788
812
  Array.prototype.forEach.call(children, (node) => {
789
- if(NodeTool.getNodeName(node) === 'mo'){
813
+ if (NodeTool.getNodeName(node) === 'mo') {
790
814
  const op = NodeTool.getNodeText(node).trim();
791
- if(Brackets.contains(op)){
815
+ if (Brackets.contains(op)) {
792
816
  let stretchy = NodeTool.getAttr(node, 'stretchy', 'true');
793
817
  stretchy = ['', 'true'].indexOf(stretchy) > -1;
794
- if(Brackets.isRight(op)){
818
+
819
+ if (Brackets.isRight(op)) {
795
820
  const nearLeft = lefts[lefts.length - 1];
796
- if(nearLeft){
797
- if(Brackets.isPair(nearLeft, op)){
821
+ if (nearLeft) {
822
+ if (Brackets.isPair(nearLeft, op)) {
798
823
  parts.push(Brackets.parseRight(op, stretchy));
799
824
  lefts.pop();
800
825
  } else {
801
- if(Brackets.isLeft(op)) {
802
- parts.push(Brackets.parseLeft(op, stretchy));
803
- lefts.push(op);
804
- }
805
- }
806
- }else {
807
- if(Brackets.isLeft(op)) {
808
- parts.push(Brackets.parseLeft(op, stretchy));
809
- lefts.push(op);
826
+ parts.push(`\\right${op}`);
810
827
  }
828
+ } else {
829
+ parts.push(op); // Chỉ thêm dấu ngoặc đóng khi đứng một mình
811
830
  }
812
831
  } else {
813
- parts.push(Brackets.parseLeft(op, stretchy));
832
+ // Xử lý dấu ngoặc mở
833
+ if (op === '{' && node.parentNode &&
834
+ Array.from(NodeTool.getChildren(node.parentNode)).some(child =>
835
+ NodeTool.getNodeName(child) === 'mtable')) {
836
+ parts.push('\\left\\{');
837
+ } else {
838
+ parts.push(Brackets.parseLeft(op, stretchy));
839
+ }
814
840
  lefts.push(op);
815
841
  }
816
842
  } else {
@@ -820,345 +846,444 @@ function renderChildren(children) {
820
846
  parts.push(parse(node));
821
847
  }
822
848
  });
823
- if(lefts.length > 0){
824
- for(let i=0; i < lefts.length; i++){
825
- parts.push("\\right.");
826
- }
849
+
850
+ // Chỉ thêm \right. nếu có dấu ngoặc mở chưa được đóng
851
+ if (lefts.length > 0 && !parts.some(p => p.includes('\\right'))) {
852
+ parts.push("\\right.");
827
853
  }
828
854
  lefts = undefined;
829
855
  return parts;
830
856
  }
831
857
 
832
-
833
858
  function getRender(node) {
834
859
  let render = undefined;
835
860
  const nodeName = NodeTool.getNodeName(node);
836
- switch(nodeName){
861
+
862
+ switch (nodeName) {
863
+ case 'mrow':
864
+ render = function (node, children) {
865
+ const childrenArray = Array.from(children);
866
+ if (childrenArray.length >= 2 &&
867
+ NodeTool.getNodeName(childrenArray[0]) === 'mo' &&
868
+ NodeTool.getNodeName(childrenArray[childrenArray.length - 1]) === 'mo') {
869
+ const firstOp = NodeTool.getNodeText(childrenArray[0]).trim();
870
+ const lastOp = NodeTool.getNodeText(childrenArray[childrenArray.length - 1]).trim();
871
+
872
+ // Xử lý đặc biệt cho dấu ngoặc nhọn chứa mtable
873
+ if (firstOp === '{' && childrenArray.some(child =>
874
+ NodeTool.getNodeName(child) === 'mtable')) {
875
+ const innerContent = childrenArray
876
+ .slice(1, -1)
877
+ .map(child => parse(child))
878
+ .join('');
879
+ return `\\left\\{${innerContent}\\right.`;
880
+ }
881
+
882
+ // Xử lý cho trường hợp [a;b) và [a,b)
883
+ if (firstOp === '[' && lastOp === ')') {
884
+ const innerContent = childrenArray
885
+ .slice(1, -1)
886
+ .map(child => parse(child))
887
+ .join('');
888
+ return `\\left[${innerContent}\\right)`;
889
+ }
890
+
891
+ // Xử lý ngoặc nhọn bình thường
892
+ if (firstOp === '{' && lastOp === '}') {
893
+ const innerContent = childrenArray
894
+ .slice(1, -1)
895
+ .map(child => parse(child))
896
+ .join('');
897
+ return `\\{${innerContent}\\}`;
898
+ }
899
+
900
+ // Bỏ qua nếu firstOp là rỗng (trường hợp <mo></mo>)
901
+ if (!firstOp && !lastOp) {
902
+ return getRender_joinSeparator("@content")(node, childrenArray);
903
+ }
904
+
905
+ // Xử lý đặc biệt cho dấu ngoặc vuông chứa mtable
906
+ if (firstOp === '[') {
907
+ const innerContent = childrenArray
908
+ .slice(1, -1) // Bỏ dấu ngoặc mở và đóng
909
+ .map(child => {
910
+ const parsed = parse(child);
911
+ // Nếu child là mtable, trả về nội dung đã được định dạng với \begin{array}{l} ... \end{array}
912
+ if (NodeTool.getNodeName(child) === 'mtable') {
913
+ const rows = Array.from(NodeTool.getChildren(child)).map(row => {
914
+ const rowChildren = Array.from(NodeTool.getChildren(row));
915
+ return rowChildren.map(cell => parse(cell)).join('');
916
+ });
917
+ return `\\begin{array}{l} ${rows.join(' \\\\ ')} \\end{array}`;
918
+ }
919
+ // Nếu child là mrow chứa mtable, xử lý tương tự
920
+ if (NodeTool.getNodeName(child) === 'mrow' &&
921
+ Array.from(NodeTool.getChildren(child)).some(c => NodeTool.getNodeName(c) === 'mtable')) {
922
+ const mtableChild = Array.from(NodeTool.getChildren(child)).find(c => NodeTool.getNodeName(c) === 'mtable');
923
+ const rows = Array.from(NodeTool.getChildren(mtableChild)).map(row => {
924
+ const rowChildren = Array.from(NodeTool.getChildren(row));
925
+ return rowChildren.map(cell => parse(cell)).join('');
926
+ });
927
+ return `\\begin{array}{l} ${rows.join(' \\\\ ')} \\end{array}`;
928
+ }
929
+ return parsed;
930
+ })
931
+ .join('');
932
+
933
+ // Giữ nguyên dấu ngoặc vuông lớn
934
+ return `\\left[${innerContent}\\right.`;
935
+ }
936
+
937
+ if (Brackets.isPair(firstOp, lastOp)) {
938
+ const innerContent = childrenArray
939
+ .slice(1, -1)
940
+ .map(child => parse(child))
941
+ .join('');
942
+ return `\\left${firstOp}${innerContent}\\right${lastOp}`;
943
+ }
944
+
945
+ // Giữ nguyên dấu ngoặc nhọn trong MathML
946
+ if (firstOp === '{' || lastOp === '}') {
947
+ const innerContent = childrenArray
948
+ .slice(1, lastOp === '}' ? -1 : undefined)
949
+ .map(child => parse(child))
950
+ .join('');
951
+ return `${firstOp === '{' ? '\\{' : ''}${innerContent}${lastOp === '}' ? '\\}' : ''}`;
952
+ }
953
+ }
954
+ return getRender_joinSeparator("@content")(node, childrenArray);
955
+ };
956
+ break;
957
+
837
958
  case 'msub':
838
- render = function(node, children) {
839
- if (!children || children.length < 2) return '';
840
-
841
- const base = parse(children[0]);
842
- if (!base) return '';
843
-
844
- const sub = children[1];
959
+ render = function (node, children) {
960
+ const childrenArray = Array.from(children);
961
+ if (!childrenArray || childrenArray.length < 2) return '';
962
+ const base = parse(childrenArray[0]) || '';
963
+ const sub = childrenArray[1];
845
964
  if (!sub) return base;
846
965
 
847
- // Handle nested subscript with empty mrow
848
- if (NodeTool.getNodeName(sub) === 'msub' &&
849
- sub.firstChild &&
850
- NodeTool.getNodeName(sub.firstChild) === 'mrow' &&
851
- (!NodeTool.getNodeText(sub.firstChild) || NodeTool.getNodeText(sub.firstChild).trim() === '')) {
966
+ if (NodeTool.getNodeName(sub) === 'msub' &&
967
+ sub.firstChild &&
968
+ NodeTool.getNodeName(sub.firstChild) === 'mrow' &&
969
+ (!NodeTool.getNodeText(sub.firstChild) || NodeTool.getNodeText(sub.firstChild).trim() === '')) {
852
970
  const lastChild = sub.lastChild;
853
- if (!lastChild) return base;
854
- return `${base}_${parse(lastChild)}`;
971
+ return lastChild ? `${base}_${parse(lastChild)}` : base;
855
972
  }
856
-
857
- // Regular subscript
858
973
  return `${base}_{${parse(sub)}}`;
859
974
  };
860
975
  break;
976
+
861
977
  case 'msup':
862
- render = getRender_default("@1^{@2}");
978
+ render = function (node, children) {
979
+ const childrenArray = Array.from(children);
980
+ if (!childrenArray || childrenArray.length < 2) return '';
981
+ const base = parse(childrenArray[0]) || '';
982
+ const sup = parse(childrenArray[1]) || '';
983
+ return `${base}^{${sup}}`;
984
+ };
863
985
  break;
986
+
864
987
  case 'msubsup':
865
- render = getRender_default("@1_{@2}^{@3}");
988
+ render = function (node, children) {
989
+ const childrenArray = Array.from(children);
990
+ if (!childrenArray || childrenArray.length < 3) return '';
991
+ const base = parse(childrenArray[0]);
992
+ const sub = parse(childrenArray[1]);
993
+ const sup = parse(childrenArray[2]);
994
+
995
+ const lastChild = childrenArray[0].lastElementChild;
996
+ if (lastChild && NodeTool.getNodeName(lastChild) === 'mo' &&
997
+ NodeTool.getNodeText(lastChild).trim() === '|') {
998
+ const content = Array.from(childrenArray[0].children)
999
+ .slice(0, -1)
1000
+ .map(child => parse(child))
1001
+ .join('');
1002
+ return `\\left.${content}\\right|_{${sub}}^{${sup}}`;
1003
+ }
1004
+ return `${base}_{${sub}}^{${sup}}`;
1005
+ };
866
1006
  break;
1007
+
867
1008
  case 'mover':
868
- render = renderMover;
1009
+ render = function (node, children) {
1010
+ const childrenArray = Array.from(children);
1011
+ if (!childrenArray || childrenArray.length < 2) return '';
1012
+ const base = parse(childrenArray[0]) || '';
1013
+ const over = parse(childrenArray[1]) || '';
1014
+ const overText = NodeTool.getNodeText(childrenArray[1])?.trim() || '';
1015
+ const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1016
+
1017
+ if (overText === "→" && isAccent) return `\\vec{${base}}`;
1018
+ if (overText === "¯" && isAccent) return `\\overline{${base}}`;
1019
+ if (overText === "^" && isAccent) return `\\hat{${base}}`;
1020
+ return `\\overset{${over}}{${base}}`;
1021
+ };
869
1022
  break;
1023
+
870
1024
  case 'munder':
871
- render = renderMunder;
1025
+ render = function (node, children) {
1026
+ const childrenArray = Array.from(children);
1027
+ if (!childrenArray || childrenArray.length < 2) return '';
1028
+ const base = parse(childrenArray[0]) || '';
1029
+ const under = parse(childrenArray[1]) || '';
1030
+ const isUnderAccent = NodeTool.getAttr(node, "accentunder", "false") === "true";
1031
+
1032
+ if (base === "∫") return `\\int_{${under}}`;
1033
+ return `\\underset{${under}}{${base}}`;
1034
+ };
872
1035
  break;
1036
+
873
1037
  case 'munderover':
874
- render = renderMunderover;
1038
+ render = function (node, children) {
1039
+ const childrenArray = Array.from(children);
1040
+ if (!childrenArray || childrenArray.length < 3) return '';
1041
+ const base = parse(childrenArray[0]);
1042
+ const under = parse(childrenArray[1]);
1043
+ const over = parse(childrenArray[2]);
1044
+ const baseText = NodeTool.getNodeText(childrenArray[0]).trim();
1045
+
1046
+ if (baseText === '∫') return `\\int_{${under}}^{${over}}`;
1047
+ if (baseText === '∑') return `\\sum_{${under}}^{${over}}`;
1048
+ if (baseText === '∏') return `\\prod_{${under}}^{${over}}`;
1049
+ if (baseText === '|') return `\\big|_{${under}}^{${over}}`;
1050
+ return `${base}_{${under}}^{${over}}`;
1051
+ };
875
1052
  break;
1053
+
876
1054
  case 'mmultiscripts':
877
- render = renderMmultiscripts;
1055
+ render = function (node, children) {
1056
+ const childrenArray = Array.from(children);
1057
+ if (!childrenArray || childrenArray.length < 1) return '';
1058
+ const base = parse(childrenArray[0]);
1059
+ let prescripts = '';
1060
+ let postscripts = '';
1061
+ let i = 1;
1062
+
1063
+ while (i < childrenArray.length) {
1064
+ if (NodeTool.getNodeName(childrenArray[i]) === 'mprescripts') {
1065
+ i++;
1066
+ if (i + 1 < childrenArray.length) {
1067
+ prescripts = `_{${parse(childrenArray[i])}}^{${parse(childrenArray[i + 1])}}`;
1068
+ i += 2;
1069
+ }
1070
+ } else {
1071
+ if (i + 1 < childrenArray.length) {
1072
+ postscripts += `_{${parse(childrenArray[i])}}^{${parse(childrenArray[i + 1])}}`;
1073
+ i += 2;
1074
+ } else break;
1075
+ }
1076
+ }
1077
+ return `${base}${prescripts}${postscripts}`;
1078
+ };
1079
+ break;
1080
+
1081
+ case 'mlongdiv':
1082
+ render = function (node, children) {
1083
+ const childrenArray = Array.from(children);
1084
+ if (!childrenArray || childrenArray.length < 2) return '';
1085
+ return `\\longdiv{${parse(childrenArray[0])}}{${parse(childrenArray[1])}}`;
1086
+ };
878
1087
  break;
1088
+
879
1089
  case 'mroot':
880
- render = getRender_default("\\sqrt[@2]{@1}");
1090
+ render = function (node, children) {
1091
+ const childrenArray = Array.from(children);
1092
+ if (!childrenArray || childrenArray.length < 2) return '';
1093
+ const base = parse(childrenArray[0]);
1094
+ const index = parse(childrenArray[1]);
1095
+ return `\\sqrt[${index}]{${base}}`;
1096
+ };
881
1097
  break;
1098
+
882
1099
  case 'msqrt':
883
- render = getRender_joinSeparator("\\sqrt{@content}");
1100
+ render = function (node, children) {
1101
+ const childrenArray = Array.from(children);
1102
+ const content = renderChildren(childrenArray).join('');
1103
+ return `\\sqrt{${content}}`;
1104
+ };
884
1105
  break;
1106
+
885
1107
  case 'mtable':
886
- render = renderTable;
1108
+ render = function (node, children) {
1109
+ const childrenArray = Array.from(children);
1110
+
1111
+ // Kiểm tra xem mtable có phải là mtable con không
1112
+ let isNestedTable = false;
1113
+ let parent = node.parentNode;
1114
+ while (parent) {
1115
+ if (NodeTool.getNodeName(parent) === 'mtable') {
1116
+ isNestedTable = true;
1117
+ break;
1118
+ }
1119
+ parent = parent.parentNode;
1120
+ }
1121
+
1122
+ // Xử lý mỗi mtr như một hàng
1123
+ const rows = childrenArray.map(row => {
1124
+ const rowChildren = Array.from(NodeTool.getChildren(row));
1125
+ return rowChildren.map(cell => parse(cell)).join('');
1126
+ });
1127
+
1128
+ // Nếu là mtable con, chỉ trả về các hàng mà không bao bọc trong \begin{array}...\end{array}
1129
+ if (isNestedTable) {
1130
+ return rows.join(' \\\\ ');
1131
+ }
1132
+
1133
+ // Nếu mtable nằm trong mrow với dấu ngoặc vuông, sẽ được xử lý ở mrow
1134
+ let isInsideSquareBrackets = false;
1135
+ parent = node.parentNode;
1136
+ while (parent) {
1137
+ if (NodeTool.getNodeName(parent) === 'mrow') {
1138
+ const childrenOfParent = Array.from(NodeTool.getChildren(parent));
1139
+ if (childrenOfParent.length >= 2 &&
1140
+ NodeTool.getNodeName(childrenOfParent[0]) === 'mo' &&
1141
+ NodeTool.getNodeText(childrenOfParent[0]).trim() === '[') {
1142
+ isInsideSquareBrackets = true;
1143
+ break;
1144
+ }
1145
+ }
1146
+ parent = parent.parentNode;
1147
+ }
1148
+
1149
+ if (isInsideSquareBrackets) {
1150
+ return rows.join(' \\\\ ');
1151
+ }
1152
+
1153
+ // Nếu là mtable chính, bao bọc trong \begin{array}...\end{array}
1154
+ const arrayContent = rows.join(' \\\\ ');
1155
+ return `\\begin{array}{l} ${arrayContent} \\end{array}`;
1156
+ };
887
1157
  break;
1158
+
888
1159
  case 'mtr':
889
- render = getRender_joinSeparator("@content \\\\ ", ' & ');
1160
+ render = getRender_joinSeparator("@content", " & ");
890
1161
  break;
1162
+
891
1163
  case 'mtd':
892
1164
  render = getRender_joinSeparator("@content");
893
1165
  break;
1166
+
894
1167
  case 'mfrac':
895
- render = renderMfrac;
1168
+ render = function (node, children) {
1169
+ const childrenArray = Array.from(children);
1170
+ if (!childrenArray || childrenArray.length < 2) return '';
1171
+ const num = parse(childrenArray[0]);
1172
+ const den = parse(childrenArray[1]);
1173
+ const linethickness = NodeTool.getAttr(node, 'linethickness', 'medium');
1174
+ if (linethickness === '0') return `\\binom{${num}}{${den}}`;
1175
+ return `\\frac{${num}}{${den}}`;
1176
+ };
896
1177
  break;
1178
+
897
1179
  case 'mfenced':
898
- render = renderMfenced;
1180
+ render = function (node, children) {
1181
+ const childrenArray = Array.from(children);
1182
+ const open = NodeTool.getAttr(node, 'open', '(');
1183
+ const close = NodeTool.getAttr(node, 'close', ')');
1184
+ const separators = NodeTool.getAttr(node, 'separators', ',').split('');
1185
+
1186
+ // Xử lý đặc biệt cho mfenced chứa mtable
1187
+ if (open === '{' && !close) {
1188
+ // Kiểm tra xem có mtable trong cấu trúc không
1189
+ const hasMtable = childrenArray.some(child => {
1190
+ // Kiểm tra trực tiếp mtable
1191
+ if (NodeTool.getNodeName(child) === 'mtable') return true;
1192
+ // Kiểm tra mtable trong mrow
1193
+ if (NodeTool.getNodeName(child) === 'mrow') {
1194
+ return Array.from(NodeTool.getChildren(child)).some(
1195
+ grandChild => NodeTool.getNodeName(grandChild) === 'mtable'
1196
+ );
1197
+ }
1198
+ return false;
1199
+ });
1200
+
1201
+ if (hasMtable) {
1202
+ const content = childrenArray.map(child => parse(child)).join('');
1203
+ return `\\left\\{${content}\\right.`;
1204
+ }
1205
+ }
1206
+
1207
+ // Xử lý cho trường hợp [a;b)
1208
+ if (open === '[' && close === ')') {
1209
+ const parts = [];
1210
+ childrenArray.forEach((child, index) => {
1211
+ parts.push(parse(child));
1212
+ if (index < childrenArray.length - 1 && separators[index % separators.length]) {
1213
+ parts.push(separators[index % separators.length]);
1214
+ }
1215
+ });
1216
+ return `\\left[${parts.join('')}\\right)`;
1217
+ }
1218
+
1219
+ // Giữ nguyên xử lý cho các trường hợp khác
1220
+ const parts = [];
1221
+ childrenArray.forEach((child, index) => {
1222
+ parts.push(parse(child));
1223
+ if (index < childrenArray.length - 1 && separators[index % separators.length]) {
1224
+ parts.push(separators[index % separators.length]);
1225
+ }
1226
+ });
1227
+ const content = parts.join('');
1228
+
1229
+ if (open === '{' && close === '}') return `\\{${content}\\}`;
1230
+ if (open === '|' && close === '|') return `\\left|${content}\\right|`;
1231
+ if (!close) return `\\left${open}${content}\\right.`;
1232
+ if (!open) return `${content}\\right${close}`;
1233
+ return `\\left${open}${content}\\right${close}`;
1234
+ };
1235
+ break;
1236
+
1237
+ case 'menclose':
1238
+ render = function (node, children) {
1239
+ const childrenArray = Array.from(children);
1240
+ const notation = NodeTool.getAttr(node, 'notation', 'longdiv');
1241
+ const content = renderChildren(childrenArray).join('');
1242
+ switch (notation) {
1243
+ case 'box': return `\\boxed{${content}}`;
1244
+ case 'circle': return `\\enclose{circle}{${content}}`;
1245
+ case 'roundedbox': return `\\fbox{${content}}`;
1246
+ default: return content;
1247
+ }
1248
+ };
899
1249
  break;
1250
+
900
1251
  case 'mi':
901
1252
  case 'mn':
902
1253
  case 'mo':
903
1254
  case 'ms':
904
1255
  case 'mtext':
905
- // they may contains <mglyph>
906
1256
  render = getRender_joinSeparator("@content");
907
1257
  break;
1258
+
908
1259
  case 'mphantom':
909
- render = renderMphantom;
1260
+ render = function (node, children) {
1261
+ const childrenArray = Array.from(children);
1262
+ const content = renderChildren(childrenArray).join('');
1263
+ return `\\phantom{${content}}`;
1264
+ };
1265
+ break;
1266
+
1267
+ case 'mstyle':
1268
+ render = function (node, children) {
1269
+ const childrenArray = Array.from(children);
1270
+ const mathsize = NodeTool.getAttr(node, 'mathsize', 'normal');
1271
+ const content = renderChildren(childrenArray).join('');
1272
+ switch (mathsize) {
1273
+ case 'big': return `\\large{${content}}`;
1274
+ case 'small': return `\\small{${content}}`;
1275
+ default: return content;
1276
+ }
1277
+ };
910
1278
  break;
1279
+
911
1280
  default:
912
- // math, mstyle, mrow
913
1281
  render = getRender_joinSeparator("@content");
914
1282
  break;
915
1283
  }
916
1284
  return render;
917
1285
  }
918
1286
 
919
- function renderTable(node, children) {
920
- const template = "\\begin{array}{l}@content\\end{array}";
921
- // Remove extra backslash and add proper spacing
922
- const render = getRender_joinSeparator(template, " \\\\ ");
923
- return render(node, children);
924
- }
925
-
926
- function renderMover(node, children) {
927
- // Check if children exists and has enough elements
928
- if (!children || children.length < 2) return '';
929
-
930
- const baseNode = children[0];
931
- const overNode = children[1];
932
-
933
- // Check if nodes exist before accessing
934
- if (!baseNode || !overNode) return '';
935
-
936
- const overText = NodeTool.getNodeText(overNode)?.trim() || '';
937
- const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
938
-
939
- // Handle vector notation
940
- if (overText === "→" && isAccent) {
941
- return `\\vec{${parse(baseNode)}}`;
942
- }
943
-
944
- return parse(baseNode);
945
- }
946
-
947
- function renderMfenced(node, children) {
948
- const [open, close, separatorsStr] = [
949
- NodeTool.getAttr(node, 'open', '('),
950
- NodeTool.getAttr(node, 'close', ')'),
951
- NodeTool.getAttr(node, 'separators', ',')
952
- ];
953
-
954
- const parts = renderChildren(children);
955
- const content = parts.join(separatorsStr === '|' ? ',' : separatorsStr).trim();
956
-
957
- // Handle system of equations with curly brace
958
- if (open === '{') {
959
- // If no closing brace specified, use \right.
960
- return `\\left\\{${content}${close ? '\\right\\}' : '\\right.'}`;
961
- }
962
-
963
- // Handle absolute value notation
964
- if (open === '|' && (close === '|' || close === '')) {
965
- return `\\left|${content}\\right|`;
966
- }
967
-
968
- // Regular cases with proper closing
969
- return `\\left${open}${content}\\right${close || '.'}`;
970
- }
971
-
972
- function renderMfrac(node, children){
973
- const [linethickness, bevelled] = [
974
- NodeTool.getAttr(node, 'linethickness', 'medium'),
975
- NodeTool.getAttr(node, 'bevelled', 'false')
976
- ];
977
-
978
- let render = null;
979
- if(bevelled === 'true') {
980
- render = getRender_default("{}^{@1}/_{@2}");
981
- } else if(['0', '0px'].indexOf(linethickness) > -1) {
982
- const [prevNode, nextNode] = [
983
- NodeTool.getPrevNode(node),
984
- NodeTool.getNextNode(node)
985
- ];
986
- if((prevNode && NodeTool.getNodeText(prevNode).trim() === '(') &&
987
- (nextNode && NodeTool.getNodeText(nextNode).trim() === ')')
988
- ) {
989
- render = getRender_default("\\binom{@1}{@2}");
990
- } else {
991
- render = getRender_default("\\frac{@1}{@2}");
992
- }
993
- } else {
994
- render = getRender_default("\\frac{@1}{@2}");
995
- }
996
- return render(node, children);
997
- }
998
-
999
- function renderMmultiscripts(node, children) {
1000
- if(children.length === 0) { return '' }
1001
- let sepIndex = -1;
1002
- let mprescriptsNode = null;
1003
- Array.prototype.forEach.call(children, (node) => {
1004
- if(NodeTool.getNodeName(node) === 'mprescripts'){
1005
- mprescriptsNode = node;
1006
- }
1007
- });
1008
- if(mprescriptsNode) {
1009
- sepIndex = Array.prototype.indexOf.call(children, mprescriptsNode);
1010
- }
1011
- const parts = renderChildren(children);
1012
-
1013
- const splitArray = (arr, index) => {
1014
- return [arr.slice(0, index), arr.slice(index + 1, arr.length)]
1015
- };
1016
- const renderScripts = (items) => {
1017
- if(items.length > 0) {
1018
- const subs = [];
1019
- const sups = [];
1020
- items.forEach((item, index) => {
1021
- // one render as sub script, one as super script
1022
- if((index + 1) % 2 === 0){
1023
- sups.push(item);
1024
- } else {
1025
- subs.push(item);
1026
- }
1027
- });
1028
- return [
1029
- (subs.length > 0 ? `_{${subs.join(' ')}}` : ''),
1030
- (sups.length > 0 ? `^{${sups.join(' ')}}` : '')
1031
- ].join('');
1032
- } else {
1033
- return '';
1034
- }
1035
- };
1036
- const base = parts.shift();
1037
- let prevScripts = [];
1038
- let backScripts = [];
1039
- if(sepIndex === -1){
1040
- backScripts = parts;
1041
- } else {
1042
- [backScripts, prevScripts] = splitArray(parts, sepIndex - 1);
1043
- }
1044
- return [renderScripts(prevScripts), base, renderScripts(backScripts)].join('');
1045
- }
1046
-
1047
- function renderMunder(node, children){
1048
- const nodes = flattenNodeTreeByNodeName(node, 'munder');
1049
- let result = undefined;
1050
- for(let i = 0; i < nodes.length - 1; i++) {
1051
- if(!result){ result = parse(nodes[i]); }
1052
-
1053
- const underNode = nodes[i + 1];
1054
- const underText = NodeTool.getNodeText(underNode).trim();
1055
- const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1056
-
1057
- // Special handling for arrow accent
1058
- if (underText === "→" && isAccent) {
1059
- return `\\underset{${result}}{\\rightarrow}`;
1060
- }
1061
-
1062
- const under = parse(underNode);
1063
- const template = getMatchValueByChar({
1064
- decimals: MathSymbol.underScript.decimals,
1065
- values: MathSymbol.underScript.templates,
1066
- judgeChar: underText,
1067
- defaultValue: "@1_{@2}"
1068
- });
1069
- result = renderTemplate(template.replace("@v", "@1"), [result, under]);
1070
- }
1071
- return result;
1072
- }
1073
-
1074
- function renderMunderover(node, children){
1075
- const nodes = flattenNodeTreeByNodeName(node, 'munderover');
1076
- let result = undefined;
1077
- for(let i = 0; i < nodes.length - 1; i++) {
1078
- if(!result){ result = parse(nodes[i]); }
1079
-
1080
- const overNode = nodes[i + 1];
1081
- const overText = NodeTool.getNodeText(overNode).trim();
1082
- const underNode = nodes[i + 2];
1083
- const underText = NodeTool.getNodeText(underNode).trim();
1084
- const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1085
-
1086
- // Special handling for arrow accent
1087
- if (overText === "→" && isAccent) {
1088
- return `\\overset{${result}}{\\underset{${underText}}{\\rightarrow}}`;
1089
- }
1090
-
1091
- const over = parse(overNode);
1092
- const under = parse(underNode);
1093
- const template = getMatchValueByChar({
1094
- decimals: MathSymbol.underoverScript.decimals,
1095
- values: MathSymbol.underoverScript.templates,
1096
- judgeChar: overText,
1097
- defaultValue: "@1_{@2}^{@3}"
1098
- });
1099
- result = renderTemplate(template.replace("@v", "@1"), [over, under]);
1100
- }
1101
- return result;
1102
- }
1103
-
1104
- function renderMphantom(node, children){
1105
- const nodes = flattenNodeTreeByNodeName(node, 'mphantom');
1106
- let result = undefined;
1107
- for(let i = 0; i < nodes.length - 1; i++) {
1108
- if(!result){ result = parse(nodes[i]); }
1109
-
1110
- const phantomNode = nodes[i + 1];
1111
- const phantomText = NodeTool.getNodeText(phantomNode).trim();
1112
- const isAccent = NodeTool.getAttr(node, "accent", "false") === "true";
1113
-
1114
- // Special handling for arrow accent
1115
- if (phantomText === "→" && isAccent) {
1116
- return `\\overrightarrow{${result}}`;
1117
- }
1118
-
1119
- const phantom = parse(phantomNode);
1120
- const template = getMatchValueByChar({
1121
- decimals: MathSymbol.phantomScript.decimals,
1122
- values: MathSymbol.phantomScript.templates,
1123
- judgeChar: phantomText,
1124
- defaultValue: "@1^{@2}"
1125
- });
1126
- result = renderTemplate(template.replace("@v", "@1"), [result, phantom]);
1127
- }
1128
- return result;
1129
- }
1130
-
1131
- function renderTemplate(template, args) {
1132
- return template.replace(/@(\d+)/g, (match, index) => {
1133
- const arg = args[index - 1];
1134
- return arg || match;
1135
- });
1136
- }
1137
-
1138
- function getMatchValueByChar(options) {
1139
- const { decimals, values, judgeChar, defaultValue } = options;
1140
- const match = values.find(value => value.judgeChar === judgeChar);
1141
- return match || defaultValue;
1142
- }
1143
-
1144
- function flattenNodeTreeByNodeName(node, nodeName) {
1145
- const nodes = [];
1146
- const children = NodeTool.getChildren(node);
1147
- if (children && children.length > 0) {
1148
- // Convert HTMLCollection to Array before using forEach
1149
- Array.from(children).forEach(child => {
1150
- if (NodeTool.getNodeName(child) === nodeName) {
1151
- nodes.push(child);
1152
- } else {
1153
- // Recursively search in child nodes
1154
- const childNodes = flattenNodeTreeByNodeName(child, nodeName);
1155
- nodes.push(...childNodes);
1156
- }
1157
- });
1158
- }
1159
- return nodes;
1160
- }
1161
-
1162
1287
  // Export the convert function
1163
1288
  var mathml2latex = {
1164
1289
  convert: convert