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 += "&#x" + ((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 += "&#x" + ((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]")]