Skip to content
Open
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
15 changes: 14 additions & 1 deletion src/SharpEquations.Avalonia/AvaloniaMathFont.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,20 @@ public AvaloniaMathFont(Typeface typeface)
public double GetAdvance(ushort glyph) => file.Advance(glyph);

/// <inheritdoc/>
public GlyphBounds GetBounds(ushort glyph) => outlines.Value?.Bounds(glyph) ?? default;
public GlyphBounds GetBounds(ushort glyph) => outlines.Value?.Bounds(glyph) ?? LineBounds(glyph);

/// <summary>
/// The box of a glyph of a font without outlines (the bitmaps of a color emoji font, <c>CBDT</c>), which has no
/// ink box to read: its advance across, and the ascent and descent of the font.
/// </summary>
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);
}

/// <inheritdoc/>
public ushort GetScriptVariant(ushort glyph, int scriptLevel) => scriptStyles.Value?.Variant(glyph, scriptLevel) ?? glyph;
Expand Down
27 changes: 24 additions & 3 deletions src/SharpEquations.Editing/BuildUp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,26 @@ public static bool IsBalanced(string text, EquationFormat format)
return used.All(u => u) ? nodes : null;
}

/// <summary>
/// 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 (<c>|x</c>).
/// </summary>
public static int OperandStart(IReadOnlyList<MathNode> 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;
}

/// <summary>The nodes of the row with every run built up where it parses.</summary>
public static MathNode BuildAll(MathNode node, EquationFormat format, bool display)
{
Expand All @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions src/SharpEquations.Editing/MathEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}

Expand Down
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
18 changes: 18 additions & 0 deletions tests/SharpEquations.Avalonia.Tests/Editing/CommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ public async Task FencesAroundTheSelection()
await Assert.That(editor.ToEquation()).IsEqualTo(U("[a+b]"));
}

/// <summary>What is typed between fences builds up without the opening fence, which alone does not parse (<c>|x</c>).</summary>
[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()
{
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