Expanding my comment into a concrete answer: Using GeneralUtilities`PrintDefinitions to look at the functions responsible for the "MathML" export1, it looks like the core function is System`Convert`MathMLDump`BoxesToSMML:
(* remove limit on number of definitions *)
DownValues@GeneralUtilities`PrintDefinitions =
DownValues@GeneralUtilities`PrintDefinitions /. 256 -> Infinity;
(* print definitions of the function *)
GeneralUtilities`PrintDefinitions@System`Convert`MathMLDump`BoxesToSMML;
The following sections outline how to fix the following three issues:
ExportString[Row[{1}], "MathML"] doesn't work before version 12.1 due to missing transformation rules for TemplateBox
ExportString[ToBoxes[a==b],"MathML"] exports the == as ⩵, since the character mappings are not set properly
ExportString[a==b,"MathML"] exports the == as  (even with the fix for 2.), since there is no mapping for \[LongEqual] (the TraditionalForm version of ==) in the default mappings
Adding rules for box structures
In version 12.1, there are two definitions that seem to be responsible for the conversion of TemplateBox expressions (which is what Row is converted to by ToBoxes) - extracting them and adding them in e.g. 11.3 yields the following result:
Begin["System`Convert`MathMLDump`"];
BoxesToSMML[whole : TemplateBox[{__}, _, ___]] /; ! alreadyWrapped := Block[
{alreadyWrapped = True},
XMLElement[
"semantics",
{},
{
BoxesToSMML@BoxForm`TemplateBoxToDisplayBoxes@whole,
BoxesToSMML@annotationWrapper@TagBox[whole, "MathMLMathematicaTag"]
}
]
];
BoxesToSMML[whole : TemplateBox[{___}, _, ___]] :=
BoxesToSMML@BoxForm`TemplateBoxToDisplayBoxes@whole;
End[];
ExportString[Row[{1}], "MathML"]
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<semantics>
<mrow>
<mn>1</mn>
</mrow>
<annotation encoding='Mathematica'>TemplateBox[List["1"], "RowDefault"]</annotation>
</semantics>
</math>
The definitions effectively use BoxForm`TemplateBoxToDisplayBoxes to convert arbitrary TemplateBox expressions to their resulting boxes, so this should work for (almost) all TemplateBox expressions, not just Row.
Specifying the mappings for special characters
It looks like special characters are not exported properly, even though there is a list for special character mappings for MathML2:
ExportString[ToBoxes[a == b], "MathML"]
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow>
<mi>a</mi>
<mo>⩵</mo>
<mi>b</mi>
</mrow>
</math>
The list of mappings (which is not used by default for some reason) does include a case for \[Equal]:
System`Convert`MLStringDataDump`$UnicodeToMathML2Entities
(* {..., "\[Equal]" -> "⩵" ,...} *)
Luckily, the necessary option for System`Convert`XMLDump`WriteSymbolicXML is passed through from the top-level, so we can just add it:
ExportString[ToBoxes[a == b], "MathML", "Entities" -> "MathML"]
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow>
<mi>a</mi>
<mo>⩵</mo>
<mi>b</mi>
</mrow>
</math>
Possible values for "Entities" can be found in the definition of System`Convert`XMLDump`addEntitiesToExportFunction:
"HTML"
"MathML"
"MathML1"
"MathML2"
_String->_String
_String:>_String (* equivalent to the previous, the delayed evaluation doesn't work *)
{...} (* list of allowed values *)
Adding custom rules to the character mappings
Unfortunately, the above fix is not enough for a==b:
ExportString[a == b, "MathML", "Entities" -> "MathML"]
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow>
<mi>a</mi>
<mo></mo>
<mi>b</mi>
</mrow>
</math>
The issue is that by default, the boxes are created using MakeBoxes[expr,TraditionalForm], which formats == as \[LongEqual], which is not in the list of mappings. Since System`Convert`XMLDump`determineEntityExportFunction memoizes the conversion functions and since the first call to the MathML exporter resets the list of mappings, we have to be a bit creative to get the mapping in there:
(* needs fresh kernel session *)
(* load the MathML exporter, but without setting "Entities" to avoid memoization *)
ExportString[a == b, "MathML", "Entities" -> {"x" -> "x"}];
(* add custom character mappings *)
System`Convert`MLStringDataDump`$UnicodeToMathML2Entities =
System`Convert`MLStringDataDump`$UnicodeToMathML2Entities~Union~{
"\[LongEqual]" -> "⩵"
};
(* now it works *)
ExportString[a == b, "MathML", "Entities" -> "MathML"]
<math xmlns='http://www.w3.org/1998/Math/MathML'>
<mrow>
<mi>a</mi>
<mo>⩵</mo>
<mi>b</mi>
</mrow>
</math>
1For the curious: The call tree essentially looks like this:
ExportString to Export (which is used to export to a temporary file):
ExportString
System`ConvertersDump`ExportStringTest
Export
Export to System`ConvertersDump`export (this is the function which calls the correct function for a given format)
Export
System`ConvertersDump`ExportTest
System`ConvertersDump`ExportInternal
System`ConvertersDump`export
System`ConvertersDump`export to System`Convert`MathMLDump`BoxesToSMML (which is the function we're interested in)
System`ConvertersDump`export
System`Convert`MathMLDump`mathMLExport
System`Convert`MathMLDump`exportMathMLAux
System`Convert`MathMLDump`exportFunction
XML`MathML`BoxesToSymbolicMathML
System`Convert`MathMLDump`includeFormat
System`Convert`MathMLDump`BoxesToSMML
2The relevant part of the call tree here is (starting from above):
- see above until
System`Convert`MathMLDump`exportMathMLAux
System`Convert`XMLDump`WriteSymbolicXML
System`Convert`XMLDump`determineEntityExportFunction
System`Convert`XMLDump`addEntitiesToExportFunction
System`Convert`MathMLDump`BoxesToSMML- if you want to look at it withGeneralUtiities`PrintDefinitions, you'll probably need to remove the limit on the number of definitions withDownValues@GeneralUtilities`PrintDefinitions = DownValues@GeneralUtilities`PrintDefinitions /. 256 -> \[Infinity];or similar $\endgroup$