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