diff --git a/CSharpMath.Core.Tests/Atom/MathListTest.cs b/CSharpMath.Core.Tests/Atom/MathListTest.cs index 1e55a6bc..68de5ebb 100644 --- a/CSharpMath.Core.Tests/Atom/MathListTest.cs +++ b/CSharpMath.Core.Tests/Atom/MathListTest.cs @@ -173,9 +173,8 @@ static void CheckListContents(MathList? list) { CheckAtomNucleusAndRange("", 13, 1), CheckAtomNucleusAndRange("∫", 14, 1), CheckAtomNucleusAndRange("θ", 15, 1), - // Comments are not given ranges as they won't affect typesetting - CheckAtomNucleusAndRange(":)", Range.UndefinedInt, Range.UndefinedInt), - CheckAtomNucleusAndRange(",", 16, 1) + CheckAtomNucleusAndRange(":)", 16, 1), + CheckAtomNucleusAndRange(",", 17, 1) ); Assert.Collection(list.Atoms[2].Superscript, CheckAtomNucleusAndRange("13", 0, 2), diff --git a/CSharpMath.Core.Tests/Display/TypesetterTests.cs b/CSharpMath.Core.Tests/Display/TypesetterTests.cs index fde7acae..cd1b7ea4 100644 --- a/CSharpMath.Core.Tests/Display/TypesetterTests.cs +++ b/CSharpMath.Core.Tests/Display/TypesetterTests.cs @@ -16,11 +16,11 @@ internal static ListDisplay ParseLaTeXToDisplay(string latex) => private static readonly TFont _font = new TFont(20); private static readonly TypesettingContext _context = BackEnd.TestTypesettingContext.Instance; - System.Action?> TestList(int rangeMax, double ascent, double descent, double width, double x, double y, + static System.Action?> TestList((int, int) range, double ascent, double descent, double width, double x, double y, LinePosition linePos, int indexInParent, params System.Action>[] inspectors) => d => { var list = Assert.IsType>(d); Assert.False(list.HasScript); - Assert.Equal(new Range(0, rangeMax), list.Range); + Assert.Equal(new Range(range.Item1, range.Item2), list.Range); Approximately.Equal(ascent, list.Ascent); Approximately.Equal(descent, list.Descent); Approximately.Equal(width, list.Width); @@ -29,10 +29,17 @@ internal static ListDisplay ParseLaTeXToDisplay(string latex) => Assert.Equal(indexInParent, list.IndexInParent); Assert.Collection(list.Displays, inspectors); }; - void TestOuter(string latex, int rangeMax, double ascent, double descent, double width, + static System.Action?> TestList(int rangeMax, double ascent, double descent, double width, double x, double y, + LinePosition linePos, int indexInParent, params System.Action>[] inspectors) => + TestList((0, rangeMax), ascent, descent, width, x, y, linePos, indexInParent, inspectors); + static void TestOuter(string latex, int rangeMax, double ascent, double descent, double width, params System.Action>[] inspectors) => TestList(rangeMax, ascent, descent, width, 0, 0, LinePosition.Regular, Range.UndefinedInt, inspectors) (ParseLaTeXToDisplay(latex)); + static void TestOuter(string latex, (int, int) range, double ascent, double descent, double width, + params System.Action>[] inspectors) => + TestList(range, ascent, descent, width, 0, 0, LinePosition.Regular, Range.UndefinedInt, inspectors) + (ParseLaTeXToDisplay(latex)); /// Makes sure that a single codepoint of various atom types have the same measured size. [Theory, InlineData("x"), InlineData("2"), InlineData(","), InlineData("+"), InlineData("Σ"), InlineData("𝑥")] @@ -67,18 +74,18 @@ public void TestVariablesNumbersAndOrdinaries(string latex) => Assert.Equal(40, line.Width); }); [Theory] - [InlineData("%\n1234", "1234")] - [InlineData("12.b% comment ", "12.𝑏")] - [InlineData("|`% \\notacommand \u2028@/", "|`@/")] - public void TestIgnoreComments(string latex, string text) => - TestOuter(latex, 4, 14, 4, 40, + [InlineData("%\n1234", "1234", 1, 4)] + [InlineData("12.b% comment ", "12.𝑏", 0, 4)] + [InlineData("|`% \\notacommand \u2028@/", "|`@/", 0, 5)] + public void TestIgnoreComments(string latex, string text, int rangeStart, int rangeEnd) => + TestOuter(latex, (rangeStart, rangeEnd), 14, 4, 40, d => { var line = Assert.IsType>(d); Assert.Equal(4, line.Atoms.Count); Assert.All(line.Atoms, Assert.IsNotType); Assert.Equal(text, string.Concat(line.Text)); Assert.Equal(new PointF(), line.Position); - Assert.Equal(new Range(0, 4), line.Range); + Assert.Equal(new Range(rangeStart, rangeEnd), line.Range); Assert.False(line.HasScript); Assert.Equal(14, line.Ascent); @@ -533,5 +540,81 @@ public void TestColor() => Assert.Null(line.TextColor); } ); + [Fact] + public void Issue213() { + float charWidth = 10; + float mathUnit = 20f / 18f; + foreach (var space in new[] { "", "\\," }) + // 5 mu spacing between = (Relation) and - (Unary Operator, aka Ordinary) + TestOuter($"{space}=-1", (space.Length / 2, 3), 14, 4, 3 * charWidth + 5 * mathUnit, // expected: - becomes UnaryOperator then typesetted as Ordinary + d => { + var textBefore = Assert.IsType>(d); + Assert.Equal(3 * charWidth + 5 * mathUnit, textBefore.Width); + Assert.Equal("=\u22121", string.Concat(textBefore.Text)); + Assert.Equal(new Range(space.Length / 2, 3), textBefore.Range); + Assert.Collection(textBefore.Atoms, + a => Assert.Equal("=", Assert.IsType(a).Nucleus), + a => Assert.Equal("\u2212", Assert.IsType(a).Nucleus), + a => Assert.Equal("1", Assert.IsType(a).Nucleus)); + }); + foreach (var (nonDisplayedAtomThatCreatesNewDisplayLine, additionalWidth) in new[] { (@"\, ", 3 * mathUnit), (@"\displaystyle ", 0) }) + TestOuter($@"={nonDisplayedAtomThatCreatesNewDisplayLine}-1", 4, 14, 4, 3 * charWidth + 5 * mathUnit + additionalWidth, // issue 213: inserting a space between them should still work + eq => { + var textBefore = Assert.IsType>(eq); + Assert.Equal(new PointF(), textBefore.Position); + Assert.Equal(charWidth, textBefore.Width); + Assert.Equal("=", string.Concat(textBefore.Text)); + Assert.Equal(new Range(0, 1), textBefore.Range); + Assert.Equal("=", Assert.IsType(Assert.Single(textBefore.Atoms)).Nucleus); + }, m1 => { + var textAfter = Assert.IsType>(m1); + Assert.Equal(new PointF(10 + 5 * mathUnit + additionalWidth, 0), textAfter.Position); + Assert.Equal(2 * charWidth, textAfter.Width); + Assert.Equal("\u22121", string.Concat(textAfter.Text)); + Assert.Equal(new Range(2, 2), textAfter.Range); + Assert.Collection(textAfter.Atoms, + a => Assert.Equal("\u2212", Assert.IsType(a).Nucleus), + a => Assert.Equal("1", Assert.IsType(a).Nucleus)); + }); + TestOuter("=%comment\n-1", 4, 14, 4, 3 * charWidth + 5 * mathUnit, // same should apply to Comment atoms + d => { + var textBefore = Assert.IsType>(d); + Assert.Equal(3 * charWidth + 5 * mathUnit, textBefore.Width); + Assert.Equal("=\u22121", string.Concat(textBefore.Text)); + Assert.Equal(new Range(0, 4), textBefore.Range); + Assert.Collection(textBefore.Atoms, + a => Assert.Equal("=", Assert.IsType(a).Nucleus), + a => Assert.Equal("\u2212", Assert.IsType(a).Nucleus), + a => Assert.Equal("1", Assert.IsType(a).Nucleus)); + }); + } + [Fact] + public void SpacingBetweenNumbers() { + // Numbers that have non-display atoms between them should not be fused together. + foreach (var (nonDisplayedAtomThatCreatesNewDisplayLine, additionalWidth) in new[] { (@"\quad ", 20), (@"\displaystyle ", 0) }) + TestOuter($"2{nonDisplayedAtomThatCreatesNewDisplayLine}3", 3, 14, 4, 10 + additionalWidth + 10, + d => { + var line = Assert.IsType>(d); + Assert.Equal(new PointF(), line.Position); + Assert.Equal("2", Assert.IsType(Assert.Single(line.Atoms)).Nucleus); + Assert.Equal("2", string.Concat(line.Text)); + Assert.Equal(new Range(0, 1), line.Range); + Assert.False(line.HasScript); + Assert.Equal(14, line.Ascent); + Assert.Equal(4, line.Descent); + Assert.Equal(10, line.Width); + }, + d => { + var line = Assert.IsType>(d); + Assert.Equal(new PointF(10 + additionalWidth, 0), line.Position); + Assert.Equal("3", Assert.IsType(Assert.Single(line.Atoms)).Nucleus); + Assert.Equal("3", string.Concat(line.Text)); + Assert.Equal(new Range(2, 1), line.Range); + Assert.False(line.HasScript); + Assert.Equal(14, line.Ascent); + Assert.Equal(4, line.Descent); + Assert.Equal(10, line.Width); + }); + } } } \ No newline at end of file diff --git a/CSharpMath.Rendering.Tests/MathDisplay/Abs.png b/CSharpMath.Rendering.Tests/MathDisplay/Abs.png index c00fd6ce..f3369c8c 100644 Binary files a/CSharpMath.Rendering.Tests/MathDisplay/Abs.png and b/CSharpMath.Rendering.Tests/MathDisplay/Abs.png differ diff --git a/CSharpMath.Rendering.Tests/MathInline/Abs.png b/CSharpMath.Rendering.Tests/MathInline/Abs.png index c00fd6ce..f3369c8c 100644 Binary files a/CSharpMath.Rendering.Tests/MathInline/Abs.png and b/CSharpMath.Rendering.Tests/MathInline/Abs.png differ diff --git a/CSharpMath/Atom/MathAtom.cs b/CSharpMath/Atom/MathAtom.cs index 9426bac5..4b060e15 100644 --- a/CSharpMath/Atom/MathAtom.cs +++ b/CSharpMath/Atom/MathAtom.cs @@ -72,8 +72,7 @@ public void Fuse(MathAtom otherAtom) { FusedAtoms.Add(otherAtom); } Nucleus += otherAtom.Nucleus; - IndexRange = new Range(IndexRange.Location, - IndexRange.Length + otherAtom.IndexRange.Length); + IndexRange += otherAtom.IndexRange; Subscript = otherAtom.Subscript; Superscript = otherAtom.Superscript; } diff --git a/CSharpMath/Atom/MathList.cs b/CSharpMath/Atom/MathList.cs index 2282e6fb..d7fa5f4e 100644 --- a/CSharpMath/Atom/MathList.cs +++ b/CSharpMath/Atom/MathList.cs @@ -53,32 +53,28 @@ public MathList Clone(bool finalize) { foreach (var atom in Atoms) newList.Add(atom.Clone(finalize)); } else { + MathAtom? prevNode = null; + int prevDisplayedIndex = -1; foreach (var atom in Atoms) { - if (atom is Comment) { - var newComment = atom.Clone(finalize); - newComment.IndexRange = Range.NotFound; - newList.Add(newComment); - continue; - } - var prevNode = newList.Last; var newNode = atom.Clone(finalize); - if (atom.IndexRange == Range.Zero) { - int prevIndex = - prevNode?.IndexRange.Location + prevNode?.IndexRange.Length ?? 0; - newNode.IndexRange = new Range(prevIndex, 1); - } - switch (prevNode, newNode) { + if (newNode.IndexRange == Range.Zero) + newNode.IndexRange = new Range(prevNode is { } prev ? prev.IndexRange.Location + prev.IndexRange.Length : 0, 1); + switch (prevDisplayedIndex == -1 ? null : newList[prevDisplayedIndex], newNode) { + // NOTE: The left pattern does not include UnaryOperator. Just try "1+++2" and "1++++2" in any LaTeX rendering engine. case (null or BinaryOperator or Relation or Open or Punctuation or LargeOperator, BinaryOperator b): newNode = b.ToUnaryOperator(); break; case (BinaryOperator b, Relation or Punctuation or Close): - newList.Last = b.ToUnaryOperator(); + newList[prevDisplayedIndex] = b.ToUnaryOperator(); break; - case (Number n, Number _) when n.Superscript.IsEmpty() && n.Subscript.IsEmpty(): - n.Fuse(newNode); - continue; // do not add the new node; we fused it instead. } + if ((prevNode, newNode) is (Number { Superscript.Count: 0, Subscript.Count: 0 } n, Number)) { + n.Fuse(newNode); + continue; // do not add the new node; we fused it instead. + } + if (newNode is not (Comment or Space or Style)) prevDisplayedIndex = newList.Count; // Corresponds to atom types that use continue; in Typesetter.CreateLine newList.Add(newNode); + prevNode = newNode; } } return newList; diff --git a/CSharpMath/Atom/Range.cs b/CSharpMath/Atom/Range.cs index b5b94a55..b4395543 100644 --- a/CSharpMath/Atom/Range.cs +++ b/CSharpMath/Atom/Range.cs @@ -10,7 +10,7 @@ namespace CSharpMath.Atom { /// /// Corresponds to a range of s before finalization. /// This value is tracked in finalized s and s, - /// for utilization in CSharpMath.Editor to construct MathListIndexes from s. + /// for use in . /// public readonly struct Range : IEquatable { public const int UndefinedInt = int.MinValue; diff --git a/CSharpMath/Display/InterElementSpaces.cs b/CSharpMath/Display/InterElementSpaces.cs index b3d48d83..62808b9c 100644 --- a/CSharpMath/Display/InterElementSpaces.cs +++ b/CSharpMath/Display/InterElementSpaces.cs @@ -52,7 +52,7 @@ static int GetInterElementSpaceArrayIndexForType(MathAtom atomType, bool row) => var multiplier = Spaces[leftIndex, rightIndex] switch { Invalid => throw new InvalidCodePathException - ($"Invalid space between {left.TypeName} and {right.TypeName}"), + ($"Invalid space between {left.TypeName} and {right.TypeName}. The {nameof(Atoms.BinaryOperator)} should have been converted to a {nameof(Atoms.UnaryOperator)} during {nameof(MathList)} {nameof(MathList.Clone)}, then converted to an {nameof(Atoms.Ordinary)} during {nameof(Typesetter)} preparation."), None => 0, Thin => 3, NsThin => style < LineStyle.Script ? 3 : 0, diff --git a/CSharpMath/Display/Typesetter.cs b/CSharpMath/Display/Typesetter.cs index 280e86bc..6fb0b719 100644 --- a/CSharpMath/Display/Typesetter.cs +++ b/CSharpMath/Display/Typesetter.cs @@ -125,6 +125,9 @@ internal Typesetter(TFont font, TypesettingContext context, internal static ListDisplay CreateLine( MathList list, TFont font, TypesettingContext context, LineStyle style, bool cramped, bool spaced = false) { + // NOTE: The 3 atom types that use continue; below, aka [Comment, Space, Style], correspond to non-displayed atom types + // in MathList.Clone(true). Update that if-condition and add a test in Issue213() if more such atom types are added. + // Otherwise, using these atoms between = (Relation) and - (BinaryOperator) will cause an exception from invalid spacing. List _PreprocessMathList() { MathAtom? prevAtom = null; @@ -338,8 +341,7 @@ private void CreateDisplayAtoms(List preprocessedAtoms) { if (_currentLineIndexRange.Location == Range.UndefinedInt) _currentLineIndexRange = atom.IndexRange; else - _currentLineIndexRange = new Range(_currentLineIndexRange.Location, - _currentLineIndexRange.Length + atom.IndexRange.Length); + _currentLineIndexRange += atom.IndexRange; // add the fused atoms if (atom.FusedAtoms != null) _currentAtoms.AddRange(atom.FusedAtoms);