diff --git a/src/SharpEquations.Avalonia/AvaloniaMathFont.cs b/src/SharpEquations.Avalonia/AvaloniaMathFont.cs
index 1538414..58bc98a 100644
--- a/src/SharpEquations.Avalonia/AvaloniaMathFont.cs
+++ b/src/SharpEquations.Avalonia/AvaloniaMathFont.cs
@@ -60,7 +60,20 @@ public AvaloniaMathFont(Typeface typeface)
public double GetAdvance(ushort glyph) => file.Advance(glyph);
///
- public GlyphBounds GetBounds(ushort glyph) => outlines.Value?.Bounds(glyph) ?? default;
+ public GlyphBounds GetBounds(ushort glyph) => outlines.Value?.Bounds(glyph) ?? LineBounds(glyph);
+
+ ///
+ /// The box of a glyph of a font without outlines (the bitmaps of a color emoji font, CBDT), which has no
+ /// ink box to read: its advance across, and the ascent and descent of the font.
+ ///
+ private GlyphBounds LineBounds(ushort glyph)
+ {
+ if (file.Table("hhea") is not { Length: >= 8 } hhea)
+ return default;
+ var ascent = (short)(hhea[4] << 8 | hhea[5]);
+ var descent = (short)(hhea[6] << 8 | hhea[7]);
+ return new GlyphBounds(0, descent, file.Advance(glyph), ascent);
+ }
///
public ushort GetScriptVariant(ushort glyph, int scriptLevel) => scriptStyles.Value?.Variant(glyph, scriptLevel) ?? glyph;
diff --git a/src/SharpEquations.Editing/BuildUp.cs b/src/SharpEquations.Editing/BuildUp.cs
index e9977a1..a1ec447 100644
--- a/src/SharpEquations.Editing/BuildUp.cs
+++ b/src/SharpEquations.Editing/BuildUp.cs
@@ -251,6 +251,26 @@ public static bool IsBalanced(string text, EquationFormat format)
return used.All(u => u) ? nodes : null;
}
+ ///
+ /// Where the nodes a run at an index of a row builds up with start: after the opening fence when the row is a pair
+ /// of fences and the run is between them, since the opening fence alone does not always parse (|x).
+ ///
+ public static int OperandStart(IReadOnlyList row, int index)
+ {
+ if (index <= 0 || index >= row.Count - 1)
+ return 0;
+ var open = row[0];
+ var close = row[row.Count - 1];
+ if (open.Name != "mo" || close.Name != "mo" || !IsFence(open.Text) || !IsFence(close.Text))
+ return 0;
+ for (var i = 1; i < row.Count - 1; i++)
+ {
+ if (row[i].Name == "mo" && (row[i].Text == open.Text || row[i].Text == close.Text))
+ return 0;
+ }
+ return 1;
+ }
+
/// The nodes of the row with every run built up where it parses.
public static MathNode BuildAll(MathNode node, EquationFormat format, bool display)
{
@@ -264,12 +284,13 @@ public static MathNode BuildAll(MathNode node, EquationFormat format, bool displ
{
if (!children[j].IsRun)
continue;
- var built = Parse(children.Take(j).ToList(), children[j].Text, format, display);
+ var start = OperandStart(children, j);
+ var built = Parse(children.Skip(start).Take(j - start).ToList(), children[j].Text, format, display);
if (built == null)
continue;
var after = children.Skip(j + 1).ToList();
- children = built.Concat(after).ToList();
- j = built.Count - 1;
+ children = children.Take(start).Concat(built).Concat(after).ToList();
+ j = start + built.Count - 1;
}
}
return node.WithChildren(children);
diff --git a/src/SharpEquations.Editing/MathEditor.cs b/src/SharpEquations.Editing/MathEditor.cs
index 405ec6a..ea2edd4 100644
--- a/src/SharpEquations.Editing/MathEditor.cs
+++ b/src/SharpEquations.Editing/MathEditor.cs
@@ -1008,7 +1008,8 @@ private bool TryBuildUpAtCaret(bool space, int held = 0, bool recordUndo = true)
if (!BuildUp.IsBalanced(text, InputFormat) || InputFormat == EquationFormat.LaTeX && BuildUp.AwaitsArguments(text))
return false;
- var nodes = BuildUp.Parse(row.Children.Take(j).ToList(), text, InputFormat, IsDisplay);
+ var start = BuildUp.OperandStart(row.Children, j);
+ var nodes = BuildUp.Parse(row.Children.Skip(start).Take(j - start).ToList(), text, InputFormat, IsDisplay);
if (nodes == null || space && BuildUp.EndsWithPendingCommand(text, nodes))
return false;
@@ -1017,14 +1018,14 @@ private bool TryBuildUpAtCaret(bool space, int held = 0, bool recordUndo = true)
undo.Push(Snapshot());
redo.Clear();
}
- var children = nodes.ToList();
+ var children = row.Children.Take(start).Concat(nodes).ToList();
if (after.Length > 0)
children.Add(MathNode.Run(after));
children.AddRange(row.Children.Skip(j + 1));
Root = MathTree.Replace(Root, rowPath, row.WithChildren(children));
Caret = Anchor = held > 0
- ? new MathPosition(MathTree.Append(rowPath, nodes.Count), held)
- : Canonical(new MathPosition(rowPath, nodes.Count));
+ ? new MathPosition(MathTree.Append(rowPath, start + nodes.Count), held)
+ : Canonical(new MathPosition(rowPath, start + nodes.Count));
return true;
}
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.Avalonia.Tests/Editing/CommandTests.cs b/tests/SharpEquations.Avalonia.Tests/Editing/CommandTests.cs
index ad07a88..5a45e46 100644
--- a/tests/SharpEquations.Avalonia.Tests/Editing/CommandTests.cs
+++ b/tests/SharpEquations.Avalonia.Tests/Editing/CommandTests.cs
@@ -51,6 +51,24 @@ public async Task FencesAroundTheSelection()
await Assert.That(editor.ToEquation()).IsEqualTo(U("[a+b]"));
}
+ /// What is typed between fences builds up without the opening fence, which alone does not parse (|x).
+ [Test]
+ [Arguments("|", "|", "|x+1/2|")]
+ [Arguments("β", "β", "βx+1/2β")]
+ [Arguments("(", ")", "(x+1/2)")]
+ [Arguments("[", "]", "[x+1/2]")]
+ [Arguments("β", "β", "βx+1/2β")]
+ public async Task TypedBetweenFencesBuildsUp(string open, string close, string expected)
+ {
+ var editor = new MathEditor();
+ editor.InsertFences(open, close);
+ editor.Insert("x+1/2 ");
+ await Assert.That(editor.ToEquation()).IsEqualTo(U(expected));
+ editor.Insert("-y");
+ editor.BuildUpAll();
+ await Assert.That(editor.ToEquation()).IsEqualTo(U(expected.Substring(0, expected.Length - close.Length) + "-y" + close));
+ }
+
[Test]
public async Task SelectedTextInEveryFormat()
{
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]")]