@@ -684,3 +684,246 @@ describe('m:sSubSup converter', () => {
684684 expect ( msubsup ! . children [ 0 ] ! . textContent ) . toBe ( 'x' ) ;
685685 } ) ;
686686} ) ;
687+
688+ describe ( 'm:func converter' , ( ) => {
689+ it ( 'converts m:func to function name + apply operator + argument' , ( ) => {
690+ const omml = {
691+ name : 'm:oMath' ,
692+ elements : [
693+ {
694+ name : 'm:func' ,
695+ elements : [
696+ {
697+ name : 'm:fName' ,
698+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'sin' } ] } ] } ] ,
699+ } ,
700+ {
701+ name : 'm:e' ,
702+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'x' } ] } ] } ] ,
703+ } ,
704+ ] ,
705+ } ,
706+ ] ,
707+ } ;
708+
709+ const result = convertOmmlToMathml ( omml , doc ) ;
710+ expect ( result ) . not . toBeNull ( ) ;
711+ expect ( result ! . textContent ) . toBe ( `sin${ '\u2061' } x` ) ;
712+
713+ const mrow = result ! . querySelector ( 'mrow' ) ;
714+ expect ( mrow ) . not . toBeNull ( ) ;
715+
716+ const functionIdentifier = mrow ! . querySelector ( 'mi' ) ;
717+ expect ( functionIdentifier ) . not . toBeNull ( ) ;
718+ expect ( functionIdentifier ! . textContent ) . toBe ( 'sin' ) ;
719+ expect ( functionIdentifier ! . getAttribute ( 'mathvariant' ) ) . toBe ( 'normal' ) ;
720+
721+ const applyOperator = mrow ! . querySelector ( 'mo' ) ;
722+ expect ( applyOperator ) . not . toBeNull ( ) ;
723+ expect ( applyOperator ! . textContent ) . toBe ( '\u2061' ) ;
724+ } ) ;
725+
726+ it ( 'ignores m:funcPr properties element' , ( ) => {
727+ const omml = {
728+ name : 'm:oMath' ,
729+ elements : [
730+ {
731+ name : 'm:func' ,
732+ elements : [
733+ { name : 'm:funcPr' , elements : [ { name : 'm:ctrlPr' } ] } ,
734+ {
735+ name : 'm:fName' ,
736+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'log' } ] } ] } ] ,
737+ } ,
738+ {
739+ name : 'm:e' ,
740+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : '10' } ] } ] } ] ,
741+ } ,
742+ ] ,
743+ } ,
744+ ] ,
745+ } ;
746+
747+ const result = convertOmmlToMathml ( omml , doc ) ;
748+ expect ( result ) . not . toBeNull ( ) ;
749+ expect ( result ! . textContent ) . toBe ( `log${ '\u2061' } 10` ) ;
750+ } ) ;
751+
752+ it ( 'renders single-character function names upright' , ( ) => {
753+ const omml = {
754+ name : 'm:oMath' ,
755+ elements : [
756+ {
757+ name : 'm:func' ,
758+ elements : [
759+ {
760+ name : 'm:fName' ,
761+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'f' } ] } ] } ] ,
762+ } ,
763+ {
764+ name : 'm:e' ,
765+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'x' } ] } ] } ] ,
766+ } ,
767+ ] ,
768+ } ,
769+ ] ,
770+ } ;
771+
772+ const result = convertOmmlToMathml ( omml , doc ) ;
773+ const firstMi = result ! . querySelector ( 'mi' ) ;
774+ expect ( firstMi ) . not . toBeNull ( ) ;
775+ expect ( firstMi ! . textContent ) . toBe ( 'f' ) ;
776+ expect ( firstMi ! . getAttribute ( 'mathvariant' ) ) . toBe ( 'normal' ) ;
777+ } ) ;
778+
779+ it ( 'wraps multi-part arguments in <mrow>' , ( ) => {
780+ const omml = {
781+ name : 'm:oMath' ,
782+ elements : [
783+ {
784+ name : 'm:func' ,
785+ elements : [
786+ {
787+ name : 'm:fName' ,
788+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'sin' } ] } ] } ] ,
789+ } ,
790+ {
791+ name : 'm:e' ,
792+ elements : [
793+ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'x' } ] } ] } ,
794+ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : '+' } ] } ] } ,
795+ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : '1' } ] } ] } ,
796+ ] ,
797+ } ,
798+ ] ,
799+ } ,
800+ ] ,
801+ } ;
802+
803+ const result = convertOmmlToMathml ( omml , doc ) ;
804+ expect ( result ) . not . toBeNull ( ) ;
805+
806+ const outerRow = result ! . querySelector ( 'math > mrow' ) ;
807+ expect ( outerRow ) . not . toBeNull ( ) ;
808+ expect ( outerRow ! . children . length ) . toBe ( 3 ) ;
809+ expect ( outerRow ! . children [ 0 ] ! . textContent ) . toBe ( 'sin' ) ;
810+ expect ( outerRow ! . children [ 1 ] ! . textContent ) . toBe ( '\u2061' ) ;
811+ expect ( outerRow ! . children [ 2 ] ! . textContent ) . toBe ( 'x+1' ) ;
812+ } ) ;
813+
814+ it ( 'renders only the argument when m:fName is missing' , ( ) => {
815+ const omml = {
816+ name : 'm:oMath' ,
817+ elements : [
818+ {
819+ name : 'm:func' ,
820+ elements : [
821+ {
822+ name : 'm:e' ,
823+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'x' } ] } ] } ] ,
824+ } ,
825+ ] ,
826+ } ,
827+ ] ,
828+ } ;
829+
830+ const result = convertOmmlToMathml ( omml , doc ) ;
831+ expect ( result ) . not . toBeNull ( ) ;
832+ expect ( result ! . textContent ) . toBe ( 'x' ) ;
833+
834+ // No apply operator when function name is missing
835+ const mo = result ! . querySelector ( 'mo' ) ;
836+ expect ( mo ) . toBeNull ( ) ;
837+ } ) ;
838+
839+ it ( 'renders only the function name when m:e is missing' , ( ) => {
840+ const omml = {
841+ name : 'm:oMath' ,
842+ elements : [
843+ {
844+ name : 'm:func' ,
845+ elements : [
846+ {
847+ name : 'm:fName' ,
848+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'sin' } ] } ] } ] ,
849+ } ,
850+ ] ,
851+ } ,
852+ ] ,
853+ } ;
854+
855+ const result = convertOmmlToMathml ( omml , doc ) ;
856+ expect ( result ) . not . toBeNull ( ) ;
857+ expect ( result ! . textContent ) . toBe ( 'sin' ) ;
858+
859+ // No apply operator when argument is missing
860+ const mo = result ! . querySelector ( 'mo' ) ;
861+ expect ( mo ) . toBeNull ( ) ;
862+
863+ // Function name should still be upright
864+ const mi = result ! . querySelector ( 'mi' ) ;
865+ expect ( mi ! . getAttribute ( 'mathvariant' ) ) . toBe ( 'normal' ) ;
866+ } ) ;
867+
868+ it ( 'returns null for empty m:func' , ( ) => {
869+ const omml = {
870+ name : 'm:oMath' ,
871+ elements : [
872+ {
873+ name : 'm:func' ,
874+ elements : [ ] ,
875+ } ,
876+ ] ,
877+ } ;
878+
879+ const result = convertOmmlToMathml ( omml , doc ) ;
880+ expect ( result ) . toBeNull ( ) ;
881+ } ) ;
882+
883+ it ( 'handles nested m:func (sin of cos x)' , ( ) => {
884+ const omml = {
885+ name : 'm:oMath' ,
886+ elements : [
887+ {
888+ name : 'm:func' ,
889+ elements : [
890+ {
891+ name : 'm:fName' ,
892+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'sin' } ] } ] } ] ,
893+ } ,
894+ {
895+ name : 'm:e' ,
896+ elements : [
897+ {
898+ name : 'm:func' ,
899+ elements : [
900+ {
901+ name : 'm:fName' ,
902+ elements : [
903+ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'cos' } ] } ] } ,
904+ ] ,
905+ } ,
906+ {
907+ name : 'm:e' ,
908+ elements : [ { name : 'm:r' , elements : [ { name : 'm:t' , elements : [ { type : 'text' , text : 'x' } ] } ] } ] ,
909+ } ,
910+ ] ,
911+ } ,
912+ ] ,
913+ } ,
914+ ] ,
915+ } ,
916+ ] ,
917+ } ;
918+
919+ const result = convertOmmlToMathml ( omml , doc ) ;
920+ expect ( result ) . not . toBeNull ( ) ;
921+ expect ( result ! . textContent ) . toBe ( `sin${ '\u2061' } cos${ '\u2061' } x` ) ;
922+
923+ // Both function names should be upright
924+ const mis = result ! . querySelectorAll ( 'mi[mathvariant="normal"]' ) ;
925+ expect ( mis . length ) . toBe ( 2 ) ;
926+ expect ( mis [ 0 ] ! . textContent ) . toBe ( 'sin' ) ;
927+ expect ( mis [ 1 ] ! . textContent ) . toBe ( 'cos' ) ;
928+ } ) ;
929+ } ) ;
0 commit comments