Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ")";
Expand All @@ -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));
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
30 changes: 30 additions & 0 deletions tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,36 @@ public async Task UnicodeMathWritesWhatItReads(string mathML, string unicodeMath
await Assert.That(RoundTrip(back, EquationFormat.UnicodeMath)).IsEqualTo(back);
}

/// <summary>
/// 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 (𝑎, 𝑏).
/// </summary>
[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("<mrow><mo>[</mo><mi>a</mi><mo>|</mo><mi>b</mi><mo>]</mo></mrow>")]
[Arguments("<mrow><mo>[</mo><mi>a</mi><mo>‖</mo><mi>b</mi><mo>]</mo></mrow>")]
Expand Down
Loading