diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
index 42bcb67..c4b0d88 100644
--- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
+++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
@@ -507,8 +507,9 @@ private string Binary(XElement node, string op)
if (retd.Length == 0)
retd = "γγ";
- // a scripted base (π_π under ^π) is grouped, as π_π^π is one subsup
- if ((op == "^" || op == "_" || IsPrime(Text(Last(node)))) && IsScripted(First(node)) && functionName == null)
+ // a scripted base (π_π under ^π) is grouped, as π_π^π is one subsup, and so is a matrix in brackets (β¨(β¦)),
+ // which a script after it would not apply to
+ if ((op == "^" || op == "_" || IsPrime(Text(Last(node)))) && (IsScripted(First(node)) || IsBracketedMatrix(First(node))) && functionName == null)
ret = "γ" + ret.TrimEnd(' ') + "γ";
if (op == "^" && Name(Last(node)) == "msub" || op == "_" && Name(Last(node)) == "msup")
return ret + op + "(" + retd + ")";
@@ -522,6 +523,14 @@ private string Binary(XElement node, string op)
// a script that applies a function (log_(πβ‘π)): the space before the function name would end the script
if ((op == "^" || op == "_") && Name(Last(node)) == "mrow" && Role(Last(node)) == ":function")
retd = "(" + retd.Trim(' ') + ")";
+ // a denominator that applies a function (π/sinβ‘πΌ): a space after the fraction bar would end the fraction
+ if (Name(node) == "mfrac" && Name(Last(node)) == "mrow" && Role(Last(node)) == ":function")
+ retd = retd.TrimStart(' ');
+ // a denominator that ends with a script, before an operand that follows the fraction (1/(4ππβ) πβπβ/πΒ²): the
+ // space after it would end the script, not the denominator (an operator after it ends both)
+ if (Name(node) == "mfrac" && op == "/" && EndsWithSpacedScript(Last(node)) && node.ElementsAfterSelf().FirstOrDefault() is { } next && Name(next) != "mo" &&
+ !(Ch(retd, 0) == "(" && RemoveOuterParens(retd) != retd))
+ retd = "(" + retd.Trim(' ') + ")";
if (IsMathMLObject(node, false) && ChildCount(node) > 0)
{
var attr = Role(Last(node));
@@ -757,10 +766,20 @@ private static bool EndsWithScript(XElement node)
return Name(node) is "msub" or "msup" or "msubsup" or "mover" or "munder" or "munderover";
}
+ private static bool IsBracketedMatrix(XElement node) => Name(node) == "mrow" && MatrixIntents.Any(m => m.Intent == Attr(node, "intent"));
+
+ // a script that a space after it ends, but not what is before it: all but the superscript digits (π₯Β²)
+ private static bool EndsWithSpacedScript(XElement node)
+ {
+ while (Name(node) == "mrow" && ChildCount(node) > 0 && !HasAttr(node, "intent"))
+ node = Last(node);
+ return IsScripted(node) && !(Name(node) == "msup" && IsDigitArg(node));
+ }
+
private string ScriptBase(XElement node)
{
var ret = Dump(node);
- if (IsScripted(node) || Name(node) == "mrow" && ChildCount(node) > 1 && !HasAttr(node, "intent"))
+ if (IsScripted(node) || Name(node) == "mrow" && ChildCount(node) > 1 && !HasAttr(node, "intent") || IsBracketedMatrix(node))
ret = "γ" + ret.TrimEnd(' ') + "γ";
return ret;
}
diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
index 89391c8..4045c88 100644
--- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
+++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
@@ -634,6 +634,13 @@ private static JsObject TransformSpaces(object value)
foreach (var space in spaceString)
spaces += "" + ((int)space).ToString("x", CultureInfo.InvariantCulture) + ";";
}
+ // a space of a width in eighteenths of an em (the Β½ em of ", "), as the character of that width; none for
+ // the width 0 of an empty cell
+ else if (value is JsObject width && Get(width, "space") is { } eighteenths && System.Convert.ToInt32(eighteenths, CultureInfo.InvariantCulture) > 0)
+ {
+ foreach (var space in UnicodeMathSerializer.UniSpaces[System.Convert.ToInt32(eighteenths, CultureInfo.InvariantCulture)])
+ spaces += "" + ((int)space).ToString("x", CultureInfo.InvariantCulture) + ";";
+ }
return O("mo", NoAttr(spaces));
}
diff --git a/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs b/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
index 458b869..c71470a 100644
--- a/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
+++ b/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
@@ -35,6 +35,36 @@ public async Task UnicodeMathWritesWhatItReads(string mathML, string unicodeMath
await Assert.That(RoundTrip(back, EquationFormat.UnicodeMath)).IsEqualTo(back);
}
+ ///
+ /// UnicodeMath that reads to an equation the writer wrote as text that read back otherwise: a script of a matrix
+ /// in brackets (β¨(β¦)Β²), a function in a denominator (π/ sinβ‘πΌ), a denominator that ends with a script before
+ /// what follows the fraction (1/4ππβ πβπβ/πΒ²), and the space after a comma that two spaces type (π, π).
+ ///
+ [Test]
+ [Arguments("(β (a&b@c&d))^2")]
+ [Arguments("(β (a&b@c&d))^(-1)=1/(ad-bc) (β (d&-b@-c&a))")]
+ [Arguments("[β (a&b@c&d)]_ij")]
+ [Arguments("|β (a&b@c&d)|^2")]
+ [Arguments("a/sinβ‘Ξ±")]
+ [Arguments("a/sinβ‘Ξ±=b/sinβ‘Ξ²=c/sinβ‘Ξ³")]
+ [Arguments("(sinβ‘Ξ±)/a")]
+ [Arguments("1/(4ΟΞ΅_0) (q_1 q_2)/r^2")]
+ [Arguments("1/(x_1) y")]
+ [Arguments("1/(ab^2) c/d")]
+ [Arguments("A=βs, s=2")]
+ [Arguments("f(x, y)")]
+ public async Task UnicodeMathReadsBackWhatItWrites(string unicodeMath)
+ {
+ var equation = Equation.Parse(unicodeMath, EquationFormat.UnicodeMath);
+ await Assert.That(RoundTrip(equation, EquationFormat.UnicodeMath)).IsEqualTo(equation).Because(equation.ToString(EquationFormat.UnicodeMath));
+ }
+
+ [Test]
+ public async Task UnicodeMathReadsTwoSpacesAfterACommaAsASpace()
+ {
+ await Assert.That(Equation.Parse("a, b", EquationFormat.UnicodeMath)).IsEqualTo(Equation.Parse("a,\u2002b", EquationFormat.UnicodeMath));
+ }
+
[Test]
[Arguments("[a|b]")]
[Arguments("[aβb]")]