diff --git a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs index bb6a4b32..98fdd519 100644 --- a/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs +++ b/CSharpMath.Core.Tests/Atom/LaTeXParserTest.cs @@ -1633,5 +1633,67 @@ public void TestErrorSurrogates() { Assert.Equal(expected.Replace("\r", null), actual); } } + + [Fact] + public void MiddleDelimiterSplitsInnerAndRoundTrips() { + var list = ParseLaTeX(@"\left\{a\middle|b\right\}"); + var inner = Assert.IsType(Assert.Single(list)); + Assert.Equal(2, inner.Segments.Count); + Assert.Equal("|", Assert.Single(inner.MiddleBoundaries).Nucleus); + Assert.Equal(@"\left\{ a\middle| b\right\} ", + LaTeXParser.MathListToLaTeX(list).ToString()); + } + + [Fact] + public void MiddleDelimiterOutsideInnerIsRejected() { + var (_, error) = LaTeXParser.MathListFromLaTeX(@"a\middle|b"); + Assert.NotNull(error); + Assert.Contains("Missing \\left", error); + } + + [Theory] + [InlineData(@"\left(a\middle|b\middle|c\right)", 3, 2)] + [InlineData(@"\left.\frac{a}{b}\middle.\begin{matrix}x\\y\end{matrix}\right.", 2, 1)] + [InlineData(@"\left(\left\{a\middle|b\right\}\middle|c\right)", 2, 1)] + [InlineData(@"\left. a\middle. b\right.", 2, 1)] + public void MiddleDelimiterSupportsMultipleNestedNullAndCompositeSegments(string input, + int segmentCount, int middleCount) { + var parsed = ParseLaTeX(input); + var inner = Assert.IsType(Assert.Single(parsed)); + Assert.Equal(segmentCount, inner.Segments.Count); + Assert.Equal(middleCount, inner.MiddleBoundaries.Count); + Assert.Equal(segmentCount, ((IMathListContainer)inner).InnerLists.Count()); + var roundTrip = LaTeXParser.MathListToLaTeX(new MathList(inner)).ToString(); + var reparsed = Assert.IsType(Assert.Single(ParseLaTeX(roundTrip))); + Assert.Equal(inner, reparsed); + } + + [Fact] + public void MiddleDelimiterErrorsIdentifySourceAndMissingRight() { + var (_, unsupported) = LaTeXParser.MathListFromLaTeX(@"\left(a\middle\bogus b\right)"); + Assert.NotNull(unsupported); + Assert.Contains("pos", unsupported); + var (_, missingRight) = LaTeXParser.MathListFromLaTeX(@"\left(a\middle|b"); + Assert.Contains("Missing \\right", missingRight); + var (_, missingDelimiter) = LaTeXParser.MathListFromLaTeX(@"\left(a\middle"); + Assert.Contains("Missing delimiter", missingDelimiter); + } + + [Fact] + public void MiddleDelimiterInnerConstructorEnforcesInvariantAndSnapshots() { + Assert.Throws(() => new Inner(Boundary.Empty, + new[] { new MathList() }, new[] { new Boundary("|") }, Boundary.Empty)); + var segments = new List { new MathList(), new MathList(new Number("2")) }; + var middles = new List { new Boundary("|") }; + var inner = new Inner(Boundary.Empty, segments, middles, Boundary.Empty); + segments.Clear(); + middles.Clear(); + Assert.Equal(2, inner.Segments.Count); + Assert.Equal(inner, inner.Clone(true)); + Assert.Throws(() => ((IList)inner.Segments).Add(new MathList())); + Assert.Throws(() => ((IList)inner.MiddleBoundaries).Clear()); + var set = new HashSet { inner }; + Assert.Contains(inner.Clone(true), set); + } } -} \ No newline at end of file +} diff --git a/CSharpMath.Core.Tests/Display/TypesetterTests.cs b/CSharpMath.Core.Tests/Display/TypesetterTests.cs index 23e3d77b..9b3857a9 100644 --- a/CSharpMath.Core.Tests/Display/TypesetterTests.cs +++ b/CSharpMath.Core.Tests/Display/TypesetterTests.cs @@ -311,6 +311,43 @@ public void TestInner(string left, string right) => Assert.False(glyph2.HasScript); Assert.Equal(right.EnumerateRunes().Last(), glyph2.Glyph); }); + + [Theory] + [InlineData(LineStyle.Display)] + [InlineData(LineStyle.Text)] + [InlineData(LineStyle.Script)] + public void TestMiddleDelimitersShareExtentAndRetainAssemblies(LineStyle style) { + var line = Typesetter.CreateLine( + AtomTests.LaTeXParserTest.ParseLaTeX( + @"\left\{\frac{1}{2}\middle|\begin{matrix}x\\y\end{matrix}\middle\Vert z\right\}^2"), + _font, _context, style); + var inner = line.Displays.OfType>().Single(); + Assert.True(inner.HasScript); + Assert.Equal(5, inner.Inner.Displays.Count); + var middle = Assert.IsType>(inner.Inner.Displays[1]); + var middle2 = Assert.IsType>(inner.Inner.Displays[3]); + Assert.Equal('|', middle.Glyph.ToString()[0]); + Assert.Equal('‖', middle2.Glyph.ToString()[0]); + Assert.NotEqual(middle.Glyph, middle2.Glyph); + Assert.NotEqual(0, middle.Ascent + middle.Descent); + Assert.NotEqual(0, middle2.Ascent + middle2.Descent); + Assert.True(inner.Ascent >= inner.Inner.Ascent); + Assert.True(inner.Descent >= inner.Inner.Descent); + Assert.NotNull(inner.Left); + Assert.NotNull(inner.Right); + Assert.True(inner.Left.Ascent + inner.Left.Descent > 0); + Assert.True(inner.Right.Ascent + inner.Right.Descent > 0); + var outerExtent = System.Math.Max(inner.Left.Ascent + inner.Left.Descent, inner.Right.Ascent + inner.Right.Descent); + Assert.InRange(middle.Ascent + middle.Descent, outerExtent * .75f, outerExtent * 1.25f); + Assert.InRange(middle2.Ascent + middle2.Descent, outerExtent * .75f, outerExtent * 1.25f); + var nullMiddle = Typesetter.CreateLine( + AtomTests.LaTeXParserTest.ParseLaTeX(@"\left. a\middle. b\right."), + _font, _context, style).Displays.OfType>().Single(); + Assert.Equal(2, nullMiddle.Inner.Displays.Count); + Assert.DoesNotContain(nullMiddle.Inner.Displays, d => d is GlyphDisplay); + // All parts are laid out in one shared group coordinate system. + Assert.Equal(inner.Inner.Position.Y, middle.Position.Y); + } [Theory, InlineData("\\sqrt2", "", "2"), InlineData("\\sqrt[3]2", "3", "2")] public void TestRadical(string latex, string degree, string radicand) => TestOuter(latex, 1, 18.56, 4, degree.IsEmpty() ? 20 : 21.44, d => { @@ -673,4 +710,4 @@ public void SpacingBetweenNumbers() { }); } } -} \ No newline at end of file +} diff --git a/CSharpMath.Rendering.Tests/TestRendering.cs b/CSharpMath.Rendering.Tests/TestRendering.cs index e281aa21..555b445e 100644 --- a/CSharpMath.Rendering.Tests/TestRendering.cs +++ b/CSharpMath.Rendering.Tests/TestRendering.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using CSharpMath.Display; +using CSharpMath.Display.Displays; +using CSharpMath.Rendering.BackEnd; using Xunit; namespace CSharpMath.Rendering.Tests { @@ -68,12 +71,73 @@ public abstract class TestRendering protected abstract double FileSizeTolerance { get; } protected abstract void DrawToStream(Painter painter, Stream stream, float textPainterCanvasWidth, TextAlignment alignment) where TContent : class; + private static InnerDisplay? FindInner(IDisplay display) => display switch { + InnerDisplay inner when inner.Inner.Displays.Count > 1 + && (inner.Inner.Displays.Any(d => d is GlyphDisplay) + || inner.Inner.Displays.Any(d => d is ListDisplay)) => inner, + InnerDisplay inner => FindInner(inner.Inner), + ListDisplay list => list.Displays.Select(FindInner).FirstOrDefault(i => i != null), + _ => null + }; + private static InnerDisplay? FindAnyInner(IDisplay display) => display switch { + InnerDisplay inner => inner, + ListDisplay list => list.Displays.Select(FindAnyInner).FirstOrDefault(i => i != null), + _ => null + }; [Theory, ClassData(typeof(TestRenderingMathData))] public void MathDisplay(string file, string latex) => Run(file, latex, new TMathPainter { LineStyle = Atom.LineStyle.Display }); [Theory, ClassData(typeof(TestRenderingMathData))] public void MathInline(string file, string latex) => Run(file, latex, new TMathPainter { LineStyle = Atom.LineStyle.Text }); + + [Theory] + [InlineData(Atom.LineStyle.Display)] + [InlineData(Atom.LineStyle.Text)] + [InlineData(Atom.LineStyle.Script)] + public void MiddleDelimiterRenderingSmoke(Atom.LineStyle lineStyle) { + // Inspect the backend display tree and independently resolved delimiter glyphs. + foreach (var latex in new[] { + @"\left\{\frac{x_i}{i}\middle|i\in\mathcal{I}\right\}", + @"\left. a\middle|b\right.", + @"\left. a\middle. b\right.", + @"\left( a\middle|b\middle\Vert c\right)", + @"\left\{\left[\frac{a}{b}\middle|\begin{pmatrix}x&y\\z&w\end{pmatrix}\right]\right\}", + @"\left(\middle|x\right)^2" + }) { + var painter = new TMathPainter { LineStyle = lineStyle, LaTeX = latex }; + var measure = painter.Measure(); + Assert.True(measure.Width > 0 && measure.Height > 0, latex); + var inner = Assert.IsType>(FindInner(painter.Display!)); + var parts = inner.Inner.Displays; + if (latex.Contains(@"\middle.")) { + Assert.Equal(2, parts.Count); + Assert.DoesNotContain(parts, part => part is GlyphDisplay); + } else { + Assert.Contains(parts, part => part is GlyphDisplay); + } + if (latex.Contains(@"\middle|b\middle\Vert")) { + var middles = parts.OfType>().ToArray(); + Assert.Equal(2, middles.Length); + var barPainter = new TMathPainter { LineStyle = lineStyle, LaTeX = @"\left|a\right|" }; + _ = barPainter.Measure(); + var bar = Assert.IsType>(FindAnyInner(barPainter.Display!)); + var vertPainter = new TMathPainter { LineStyle = lineStyle, LaTeX = @"\left\|a\right\|" }; + _ = vertPainter.Measure(); + var vert = Assert.IsType>(FindAnyInner(vertPainter.Display!)); + Assert.Equal(bar.Left!.GetType(), middles[0].GetType()); + Assert.Equal(vert.Left!.GetType(), middles[1].GetType()); + Assert.Equal(((GlyphDisplay)bar.Left).Glyph.Info.GlyphIndex, middles[0].Glyph.Info.GlyphIndex); + Assert.Equal(((GlyphDisplay)vert.Left).Glyph.Info.GlyphIndex, middles[1].Glyph.Info.GlyphIndex); + var outerExtent = Math.Max(inner.Left!.Ascent + inner.Left.Descent, inner.Right!.Ascent + inner.Right.Descent); + Assert.All(middles, glyph => Assert.InRange(glyph.Ascent + glyph.Descent, outerExtent * 0.75f, outerExtent * 1.25f)); + Assert.All(middles, glyph => Assert.True(glyph.Ascent + glyph.Descent > 0)); + Assert.InRange(Math.Abs(middles[0].ShiftDown - middles[1].ShiftDown), 0, 1e-4f); + Assert.InRange(Math.Abs(inner.Left!.ShiftDown - middles[0].ShiftDown), 0, 1e-4f); + Assert.InRange(Math.Abs(inner.Right!.ShiftDown - middles[1].ShiftDown), 0, 1e-4f); + } + } + } [Theory, ClassData(typeof(TestRenderingTextData))] public void TextLeft(string file, string latex) => Run(file, latex, new TTextPainter()); @@ -202,4 +266,4 @@ public virtual void MathPainterSettings(string file, TMathPainter painter) => public void TextPainterSettings(string file, TTextPainter painter) => Run(file, @"Inline \color{red}{Maths}: $\int_{a_1^2}^{a_2^2}\color{green}\sqrt\frac x2dx$Display \color{red}{Maths}: $$\int_{a_1^2}^{a_2^2}\color{green}\sqrt\frac x2dx$$", painter); } -} \ No newline at end of file +} diff --git a/CSharpMath/Atom/Atoms/Inner.cs b/CSharpMath/Atom/Atoms/Inner.cs index 6772fc3c..c955cf13 100644 --- a/CSharpMath/Atom/Atoms/Inner.cs +++ b/CSharpMath/Atom/Atoms/Inner.cs @@ -1,32 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; namespace CSharpMath.Atom.Atoms { /// An inner atom, i.e. embedded math list public sealed class Inner : MathAtom, IMathListContainer { - public Inner(Boundary left, MathList innerList, Boundary right) => - (LeftBoundary, InnerList, RightBoundary) = (left, innerList, right); - public MathList InnerList { get; } + public Inner(Boundary left, MathList innerList, Boundary right) + : this(left, new[] { innerList }, Array.Empty(), right) { } + public Inner(Boundary left, IReadOnlyList segments, + IReadOnlyList middleBoundaries, Boundary right) { + if (segments is null) throw new ArgumentNullException(nameof(segments)); + if (middleBoundaries is null) throw new ArgumentNullException(nameof(middleBoundaries)); + if (segments.Count != middleBoundaries.Count + 1) + throw new ArgumentException("An inner atom must have one more segment than middle delimiters."); + (LeftBoundary, Segments, MiddleBoundaries, RightBoundary) = + (left, Array.AsReadOnly(segments.ToArray()), + Array.AsReadOnly(middleBoundaries.ToArray()), right); + } + public MathList InnerList => Segments[0]; + public IReadOnlyList Segments { get; } + public IReadOnlyList MiddleBoundaries { get; } public Boundary LeftBoundary { get; } public Boundary RightBoundary { get; } - System.Collections.Generic.IEnumerable IMathListContainer.InnerLists => - new[] { InnerList }; + System.Collections.Generic.IEnumerable IMathListContainer.InnerLists => Segments; public override bool ScriptsAllowed => true; public new Inner Clone(bool finalize) => (Inner)base.Clone(finalize); protected override MathAtom CloneInside(bool finalize) => - new Inner(LeftBoundary, InnerList.Clone(finalize), RightBoundary); + new Inner(LeftBoundary, Segments.Select(s => s.Clone(finalize)).ToArray(), + MiddleBoundaries, RightBoundary); public bool EqualsInner(Inner otherInner) => EqualsAtom(otherInner) - && InnerList.NullCheckingStructuralEquality(otherInner.InnerList) + && Segments.Count == otherInner.Segments.Count + && Segments.Zip(otherInner.Segments, (a, b) => a.NullCheckingStructuralEquality(b)).All(x => x) + && MiddleBoundaries.SequenceEqual(otherInner.MiddleBoundaries) && LeftBoundary.NullCheckingStructuralEquality(otherInner.LeftBoundary) && RightBoundary.NullCheckingStructuralEquality(otherInner.RightBoundary); public override bool Equals(object obj) => obj is Inner i ? EqualsInner(i) : false; public override int GetHashCode() => - (base.GetHashCode(), InnerList, LeftBoundary, RightBoundary).GetHashCode(); - public override string DebugString => - new StringBuilder(@"\inner") - .AppendInBracesOrEmptyBraces(LeftBoundary.Nucleus) - .AppendInBracesOrLiteralNull(InnerList.DebugString) - .AppendInBracesOrEmptyBraces(RightBoundary.Nucleus) - .AppendDebugStringOfScripts(this).ToString(); + (base.GetHashCode(), string.Join("\u001f", Segments.Select(s => s.DebugString)), + string.Join("\u001f", MiddleBoundaries.Select(b => b.Nucleus)), + LeftBoundary, RightBoundary).GetHashCode(); + public override string DebugString => MiddleBoundaries.Count == 0 + ? new StringBuilder(@"\inner").AppendInBracesOrEmptyBraces(LeftBoundary.Nucleus) + .AppendInBracesOrLiteralNull(InnerList.DebugString) + .AppendInBracesOrEmptyBraces(RightBoundary.Nucleus) + .AppendDebugStringOfScripts(this).ToString() + : new StringBuilder(@"\inner").AppendInBracesOrEmptyBraces(LeftBoundary.Nucleus) + .AppendInBracesOrLiteralNull(string.Join("|", Segments.Select(s => s.DebugString))) + .AppendInBracesOrLiteralNull(string.Join(",", MiddleBoundaries.Select(b => b.Nucleus))) + .AppendInBracesOrEmptyBraces(RightBoundary.Nucleus) + .AppendDebugStringOfScripts(this).ToString(); } -} \ No newline at end of file +} diff --git a/CSharpMath/Atom/LaTeXParser.cs b/CSharpMath/Atom/LaTeXParser.cs index 1250d8e7..38e45d5a 100644 --- a/CSharpMath/Atom/LaTeXParser.cs +++ b/CSharpMath/Atom/LaTeXParser.cs @@ -24,6 +24,8 @@ public class TableEnvironment : IEnvironment { } public class InnerEnvironment : IEnvironment { public Boundary? RightBoundary { get; set; } + public List Segments { get; } = new List(); + public List MiddleBoundaries { get; } = new List(); } #pragma warning restore CA1034 // Nested types should not be visible public string Chars { get; } @@ -539,6 +541,18 @@ static bool MathAtomToLaTeX(MathAtom atom, StringBuilder builder, MathListToLaTeX(radical.Radicand, builder, currentFontStyle); builder.Append('}'); break; + case Inner { + MiddleBoundaries.Count: > 0, LeftBoundary: var left, + Segments: var segments, RightBoundary: var right + }: + builder.Append(@"\left").Append(BoundaryToLaTeX(left)).Append(' '); + for (var i = 0; i < segments.Count; i++) { + MathListToLaTeX(segments[i], builder, currentFontStyle); + if (i < segments.Count - 1) + builder.Append(@"\middle").Append(BoundaryToLaTeX(((Inner)atom).MiddleBoundaries[i])).Append(' '); + } + builder.Append(@"\right").Append(BoundaryToLaTeX(right)).Append(' '); + break; case Inner { LeftBoundary: { Nucleus: null }, InnerList: var list, RightBoundary: { Nucleus: null } }: MathListToLaTeX(list, builder, currentFontStyle); break; @@ -731,4 +745,4 @@ public static StringBuilder MathListToLaTeX(MathList mathList, StringBuilder? sb return sb; } } -} \ No newline at end of file +} diff --git a/CSharpMath/Atom/LaTeXSettings.cs b/CSharpMath/Atom/LaTeXSettings.cs index 3ef5599a..e641bdbc 100644 --- a/CSharpMath/Atom/LaTeXSettings.cs +++ b/CSharpMath/Atom/LaTeXSettings.cs @@ -146,15 +146,28 @@ public static class LaTeXSettings { Ok(new Radical(degree ?? new MathList(), radicand)))) }, { @"\left", (parser, accumulate, stopChar) => parser.ReadDelimiter("left").Bind(left => { - parser.Environments.Push(new LaTeXParser.InnerEnvironment()); - return parser.ReadUntil(stopChar).Bind(innerList => { - if (!(parser.Environments.PeekOrDefault() is - LaTeXParser.InnerEnvironment { RightBoundary: { } right })) { + var environment = new LaTeXParser.InnerEnvironment(); + parser.Environments.Push(environment); + var completed = false; + try { + while (true) { + var result = parser.ReadUntil(stopChar); + if (result.Error != null) return result.Error; + environment.Segments.Add(result._value); + if (environment.RightBoundary is Boundary right) { + parser.Environments.Pop(); + completed = true; + return Ok(new Inner(left, environment.Segments.ToArray(), + environment.MiddleBoundaries.ToArray(), right)); + } + if (environment.MiddleBoundaries.Count == environment.Segments.Count) + continue; return Err($@"Missing \right for \left with delimiter {left}"); } - parser.Environments.Pop(); - return Ok(new Inner(left, innerList, right)); - }); + } finally { + if (!completed && parser.Environments.PeekOrDefault() == environment) + parser.Environments.Pop(); + } }) }, { @"\overline", (parser, accumulate, stopChar) => parser.ReadArgument().Bind(mathList => Ok(new Overline(mathList))) }, @@ -321,6 +334,14 @@ public static class LaTeXSettings { inner.RightBoundary = boundary; return OkStop(accumulate); } }, + { @"\middle", (parser, accumulate, stopChar) => { + if (!(parser.Environments.PeekOrDefault() is LaTeXParser.InnerEnvironment inner)) + return "Missing \\left"; + var (boundary, error) = parser.ReadDelimiter("middle"); + if (error != null) return error; + inner.MiddleBoundaries.Add(boundary); + return OkStop(accumulate); + } }, { @"\\", @"\cr", (parser, accumulate, stopChar) => { if (!(parser.Environments.PeekOrDefault() is LaTeXParser.TableEnvironment environment)) { return parser.ReadTable(null, accumulate, true, stopChar).Bind(table => OkStop(new MathList(table))); @@ -1189,4 +1210,4 @@ atom is Accent accent // \varsupsetneqq -> ⫌ + U+FE00 (Variation Selector 1) Not dealing with variation selectors, thank you very much }; } -} \ No newline at end of file +} diff --git a/CSharpMath/Display/Displays/InnerDisplay.cs b/CSharpMath/Display/Displays/InnerDisplay.cs index b176366d..d57ad145 100644 --- a/CSharpMath/Display/Displays/InnerDisplay.cs +++ b/CSharpMath/Display/Displays/InnerDisplay.cs @@ -19,6 +19,7 @@ public InnerDisplay(ListDisplay inner, IGlyphDisplayA display representing the right delimiter. ///Its position is relative to the parent and is not treated as a sub-display. public IGlyphDisplay? Right { get; } + internal int MiddleBoundariesCount { get; set; } public float Ascent => System.Math.Max(Left?.Ascent ?? 0, System.Math.Max(Right?.Ascent ?? 0, Inner.Ascent)); public float Descent => System.Math.Max(Left?.Descent ?? 0, System.Math.Max(Right?.Descent ?? 0, Inner.Descent)); @@ -58,4 +59,4 @@ public void SetTextColorRecursive(Color? textColor) { public override string ToString() => $@"\inner[{Left}][{Right}]{{{Inner}}}"; } -} \ No newline at end of file +} diff --git a/CSharpMath/Display/Typesetter.cs b/CSharpMath/Display/Typesetter.cs index 8c58f6bf..aaf57c00 100644 --- a/CSharpMath/Display/Typesetter.cs +++ b/CSharpMath/Display/Typesetter.cs @@ -232,7 +232,8 @@ private void CreateDisplayAtoms(List preprocessedAtoms) { AddDisplayLine(false); AddInterElementSpace(prevAtom, inner); IDisplay innerDisplay; - if (inner.LeftBoundary != Boundary.Empty || inner.RightBoundary != Boundary.Empty) { + if (inner.LeftBoundary != Boundary.Empty || inner.RightBoundary != Boundary.Empty + || inner.MiddleBoundaries.Count > 0) { innerDisplay = MakeInner(inner, atom.IndexRange); } else { innerDisplay = CreateLine(inner.InnerList, _font, _context, _style, _cramped); @@ -694,20 +695,39 @@ float _FractionDelimiterHeight() => } private InnerDisplay MakeInner(Inner inner, Range range) { - if (inner.LeftBoundary == Boundary.Empty && inner.RightBoundary == Boundary.Empty) { + if (inner.LeftBoundary == Boundary.Empty && inner.RightBoundary == Boundary.Empty + && inner.MiddleBoundaries.Count == 0) { throw new InvalidCodePathException("Inner should have a boundary to call this function."); } var innerListDisplay = CreateLine(inner.InnerList, _font, _context, _style, _cramped, true); + List> segmentDisplays = new List> { innerListDisplay }; + if (inner.MiddleBoundaries.Count > 0) + for (var i = 1; i < inner.Segments.Count; i++) + segmentDisplays.Add(CreateLine(inner.Segments[i], _font, _context, _style, _cramped, true)); float axisHeight = _mathTable.AxisHeight(_styleFont); // delta is the max distance from the axis. float delta = - Math.Max(innerListDisplay.Ascent - axisHeight, innerListDisplay.Descent + axisHeight); + Math.Max(segmentDisplays.Max(d => d.Ascent) - axisHeight, + segmentDisplays.Max(d => d.Descent) + axisHeight); var d1 = delta / 500 * _delimiterFactor; // This represents atleast 90% of the formula float d2 = 2 * delta - _delimiterShortfallPoints; // This represents a shortfall of 5pt // The size of the delimiter glyph should cover at least 90% of the formula or // be at most 5pt short. float glyphHeight = Math.Max(d1, d2); + if (inner.MiddleBoundaries.Count > 0) { + var displays = new List>(); + for (var i = 0; i < inner.Segments.Count; i++) { + if (i > 0) { + var middle = inner.MiddleBoundaries[i - 1]; + if (middle.Nucleus?.Length > 0) + displays.Add(FindGlyphForBoundary(middle.Nucleus, glyphHeight)); + } + displays.Add(segmentDisplays[i]); + } + innerListDisplay = new ListDisplay(displays); + } + var leftGlyph = inner.LeftBoundary is Boundary { Nucleus: var left } && left?.Length > 0 ? FindGlyphForBoundary(left, glyphHeight) @@ -717,7 +737,10 @@ private InnerDisplay MakeInner(Inner inner, Range range) { inner.RightBoundary is Boundary { Nucleus: var right } && right?.Length > 0 ? FindGlyphForBoundary(right, glyphHeight) : null; - return new InnerDisplay(innerListDisplay, leftGlyph, rightGlyph, range); + var result = new InnerDisplay(innerListDisplay, leftGlyph, rightGlyph, range) { + MiddleBoundariesCount = inner.MiddleBoundaries.Count + }; + return result; } private IGlyphDisplay FindGlyphForBoundary( @@ -1133,4 +1156,4 @@ private IDisplay AddLimitsToDisplay(IDisplay displ return display; } } -} \ No newline at end of file +} diff --git a/CSharpMath/Editor/Extensions/InnerDisplay.cs b/CSharpMath/Editor/Extensions/InnerDisplay.cs index 4b4e7a16..4e487da7 100644 --- a/CSharpMath/Editor/Extensions/InnerDisplay.cs +++ b/CSharpMath/Editor/Extensions/InnerDisplay.cs @@ -16,6 +16,8 @@ partial class Extensions { : point.X > self.Position.X + self.Width - (self.Right?.Width / 2 ?? 0) //We are after the inner ? new(self.Range.End) + : self.MiddleBoundariesCount > 0 + ? new(self.Range.Location) : self.Inner.IndexForPoint(context, point)?.Wrap(self.Range.Location, MathListSubIndexType.Inner); public static PointF? PointForIndex( @@ -44,4 +46,4 @@ public static void Highlight( this InnerDisplay self, Color color) where TFont : IFont => self.Inner.Highlight(color); } -} \ No newline at end of file +} diff --git a/CSharpMath/Editor/Extensions/MathList.cs b/CSharpMath/Editor/Extensions/MathList.cs index 3b60f896..43b3b092 100644 --- a/CSharpMath/Editor/Extensions/MathList.cs +++ b/CSharpMath/Editor/Extensions/MathList.cs @@ -63,6 +63,8 @@ when self.Atoms[index.AtomIndex] is Atoms.Fraction frac ? true case (MathListSubIndexType.Inner, var subIndex) when self.Atoms[index.AtomIndex] is Atoms.Inner inner ? true : throw new SubIndexTypeMismatchException(nameof(Atoms.Inner), index.AtomIndex): + if (inner.MiddleBoundaries.Count > 0) + throw new NotSupportedException("Editing multi-segment inner atoms is not supported."); return inner.InnerList.InsertAndAdvance(subIndex, atom, advanceType).Wrap(index.AtomIndex, MathListSubIndexType.Inner); case (var type, _): throw new ArgumentOutOfRangeException(nameof(index), type, "Index type out of valid range."); @@ -145,6 +147,8 @@ when self.Atoms[index.AtomIndex] is Atoms.Fraction frac ? true case (MathListSubIndexType.Inner, var subIndex) when self.Atoms[index.AtomIndex] is Atoms.Inner inner ? true : throw new SubIndexTypeMismatchException(nameof(Atoms.Inner), index.AtomIndex): + if (inner.MiddleBoundaries.Count > 0) + throw new NotSupportedException("Editing multi-segment inner atoms is not supported."); index = inner.InnerList.RemoveAt(subIndex).Wrap(index.AtomIndex, MathListSubIndexType.Inner); break; case (var type, _): @@ -199,6 +203,8 @@ when self.Atoms[start.AtomIndex] is Atoms.Fraction frac ? true case (MathListSubIndexType.Inner, _) when self.Atoms[start.AtomIndex] is Atoms.Inner inner ? true : throw new SubIndexTypeMismatchException(nameof(Atoms.Inner), start.AtomIndex): + if (inner.MiddleBoundaries.Count > 0) + throw new NotSupportedException("Editing multi-segment inner atoms is not supported."); inner.InnerList.RemoveAtoms(range.SubIndexRange); break; } @@ -216,9 +222,9 @@ when self.Atoms[start.AtomIndex] is Atoms.Inner inner ? true (MathListSubIndexType.Radicand, var subIndex) => atom is Atoms.Radical radical ? radical.Radicand.AtomAt(subIndex) : null, (MathListSubIndexType.Numerator, var subIndex) => atom is Atoms.Fraction frac ? frac.Numerator.AtomAt(subIndex) : null, (MathListSubIndexType.Denominator, var subIndex) => atom is Atoms.Fraction frac ? frac.Denominator.AtomAt(subIndex) : null, - (MathListSubIndexType.Inner, var subIndex) => atom is Atoms.Inner inner ? inner.InnerList.AtomAt(subIndex) : null, + (MathListSubIndexType.Inner, var subIndex) => atom is Atoms.Inner { MiddleBoundaries.Count: 0 } inner ? inner.InnerList.AtomAt(subIndex) : null, (var type, _) => throw new ArgumentOutOfRangeException(nameof(index), type, "Index type out of valid range."), }; } } -} \ No newline at end of file +} diff --git a/CSharpMath/Editor/MathKeyboard.cs b/CSharpMath/Editor/MathKeyboard.cs index 0e9305a3..bb17b575 100644 --- a/CSharpMath/Editor/MathKeyboard.cs +++ b/CSharpMath/Editor/MathKeyboard.cs @@ -203,7 +203,7 @@ void MoveCursorLeft() { _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Radicand, rad.Radicand.Count); else if (MathList.AtomAt(levelDown) is Atoms.Fraction frac && frac.Denominator.IsNonEmpty()) _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Denominator, frac.Denominator.Count); - else if (MathList.AtomAt(levelDown) is Atoms.Inner inner && inner.InnerList.IsNonEmpty()) + else if (MathList.AtomAt(levelDown) is Atoms.Inner { MiddleBoundaries.Count: 0 } inner && inner.InnerList.IsNonEmpty()) _insertionIndex = levelDown.LevelUpWithSubIndex(MathListSubIndexType.Inner, inner.InnerList.Count); else goto case MathListSubIndexType.Radicand; break; @@ -233,7 +233,7 @@ void MoveCursorLeft() { case { Subscript: var s } when s.IsNonEmpty(): _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Subscript, s.Count); break; - case Atoms.Inner { InnerList: var l }: + case Atoms.Inner { MiddleBoundaries.Count: 0, InnerList: var l }: _insertionIndex = prev.LevelUpWithSubIndex(MathListSubIndexType.Inner, l.Count); break; case Atoms.Radical { Radicand: var r }: @@ -318,7 +318,7 @@ void MoveCursorRight() { _insertionIndex = levelDown.LevelUpWithSubIndex( a.Subscript.IsNonEmpty() ? MathListSubIndexType.Subscript : MathListSubIndexType.Superscript, 0); break; - case Atoms.Inner _: + case Atoms.Inner { MiddleBoundaries.Count: 0 }: _insertionIndex = _insertionIndex.LevelUpWithSubIndex(MathListSubIndexType.Inner, 0); break; case Atoms.Fraction _: @@ -828,4 +828,4 @@ public void Dispose() { ((IDisposable)blinkTimer).Dispose(); } } -} \ No newline at end of file +} diff --git a/CSharpMath/PublicAPI.Unshipped.txt b/CSharpMath/PublicAPI.Unshipped.txt index e69de29b..a9f96e4e 100644 --- a/CSharpMath/PublicAPI.Unshipped.txt +++ b/CSharpMath/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +CSharpMath.Atom.Atoms.Inner.Inner(CSharpMath.Atom.Boundary left, System.Collections.Generic.IReadOnlyList! segments, System.Collections.Generic.IReadOnlyList! middleBoundaries, CSharpMath.Atom.Boundary right) -> void +CSharpMath.Atom.Atoms.Inner.MiddleBoundaries.get -> System.Collections.Generic.IReadOnlyList! +CSharpMath.Atom.Atoms.Inner.Segments.get -> System.Collections.Generic.IReadOnlyList! +CSharpMath.Atom.LaTeXParser.InnerEnvironment.MiddleBoundaries.get -> System.Collections.Generic.List! +CSharpMath.Atom.LaTeXParser.InnerEnvironment.Segments.get -> System.Collections.Generic.List!