diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs index fe960b295..7faf0fd39 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs @@ -231,11 +231,10 @@ public void Should_Not_Apply_Link_To_Text_After_Link_Close_Tag() var console = new TestConsole() .EmitAnsiSequences(); - // When - text after the [/] closing tag should NOT have the link + // When console.Markup("Before [link=https://example.com]LINK[/] After"); // Then - // The link should only wrap "LINK", not " After" var output = console.Output; output.ShouldMatch(@"Before \e\]8;id=\d+;https://example\.com\e\\LINK\e\]8;;\e\\ After"); } @@ -247,17 +246,38 @@ public void Should_Properly_Handle_Nested_Link_With_Styles() var console = new TestConsole() .EmitAnsiSequences(); - // When - link with styled text inside + // When console.Markup("[link=https://example.com][bold]Bold Link[/][/] Plain"); // Then - // The link should only wrap "Bold Link", not " Plain" var output = console.Output; - - // Check that "Plain" is NOT inside a link - var linkEndIndex = output.LastIndexOf("\u001b]8;;\u001b\\"); - var plainIndex = output.IndexOf("Plain"); + var linkEndIndex = output.LastIndexOf("\e]8;;\e\\", StringComparison.Ordinal); + var plainIndex = output.IndexOf("Plain", StringComparison.Ordinal); plainIndex.ShouldBeGreaterThan(linkEndIndex, "Plain should appear after the link ends"); } + + [Fact] + [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2124")] + public void Should_Preserve_Link_When_Wrapped_Inside_Grid_Cell() + { + // Given + var console = new TestConsole() + .Width(10) + .SupportsAnsi(true) + .EmitAnsiSequences(); + + var grid = new Grid(); + grid.AddColumn(); + grid.AddRow("[link=https://example.com/readme.md]pneumonoultramicroscopicsilicovolcanoconiosis[/]"); + + // When + console.Write(grid); + + // Then + Regex.Matches( + console.Output.NormalizeLineEndings(), + "https://example.com/readme.md") + .Count.ShouldBeGreaterThan(1); + } } } \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Unit/Rendering/SegmentTests.cs b/src/Spectre.Console.Tests/Unit/Rendering/SegmentTests.cs index d759a4a2e..ac29b8979 100644 --- a/src/Spectre.Console.Tests/Unit/Rendering/SegmentTests.cs +++ b/src/Spectre.Console.Tests/Unit/Rendering/SegmentTests.cs @@ -30,7 +30,8 @@ public void Should_Split_Segment_Correctly(string text, int offset, string expec { // Given var style = new Style(Color.Red, Color.Green, Decoration.Bold); - var segment = new Segment(text, style); + var link = new Link("https://example.com"); + var segment = new Segment(text, style, link); // When var (first, second) = segment.Split(offset); @@ -38,8 +39,10 @@ public void Should_Split_Segment_Correctly(string text, int offset, string expec // Then first.Text.ShouldBe(expectedFirst); first.Style.ShouldBe(style); + first.Link.ShouldBe(link); second?.Text.ShouldBe(expectedSecond); second?.Style.ShouldBe(style); + second?.Link.ShouldBe(link); } } @@ -268,4 +271,93 @@ public void Should_Handle_Fullwidth_Text_When_Using_Crop() result[0].Text.EndsWith("…", StringComparison.Ordinal).ShouldBeFalse(); } } + + public sealed class LinkPreservation + { + private static readonly Link _link = new("https://example.com/readme.md"); + + [Fact] + [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2124")] + public void Should_Preserve_Link_When_Splitting_Lines_By_Width() + { + // Given + var segment = new Segment("abcdefghijklmnop", Style.Plain, _link); + + // When + var lines = Segment.SplitLines([segment], maxWidth: 4); + + // Then + lines.Count.ShouldBeGreaterThan(1); + lines.ShouldAllBe(line => line.All(p => p.Link == _link)); + } + + [Fact] + public void Should_Preserve_Link_When_Folding_Overflow() + { + // Given + var segment = new Segment("abcdefghijklmnop", Style.Plain, _link); + + // When + var result = Segment.SplitOverflow(segment, Overflow.Fold, maxWidth: 4); + + // Then + result.ShouldAllBe(part => part.Link == _link); + } + + [Fact] + public void Should_Preserve_Link_On_Ellipsis_When_Overflowing() + { + // Given + var segment = new Segment("abcdefghijklmnop", Style.Plain, _link); + + // When + var result = Segment.SplitOverflow(segment, Overflow.Ellipsis, maxWidth: 5); + + // Then + result.Count.ShouldBe(1); + result[0].Text.ShouldEndWith("…"); + result[0].Link.ShouldBe(_link); + } + + [Fact] + public void Should_Preserve_Link_When_Truncating() + { + // Given + var segment = new Segment("abcdefghijklmnop", Style.Plain, _link); + + // When + var truncated = Segment.Truncate(segment, maxWidth: 4); + + // Then + truncated.ShouldNotBeNull(); + truncated.Text.ShouldBe("abcd"); + truncated.Link.ShouldBe(_link); + } + + [Fact] + public void Should_Preserve_Link_When_Cloning() + { + // Given + var segment = new Segment("abc", Style.Plain, _link); + + // When + var result = segment.Clone(); + + // Then + result.Link.ShouldBe(_link); + } + + [Fact] + public void Should_Preserve_Link_When_Stripping_Line_Endings() + { + // Given + var segment = new Segment("abc\n", Style.Plain, _link); + + // When + var result = segment.StripLineEndings(); + + // Then + result.Link.ShouldBe(_link); + } + } } \ No newline at end of file diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs index 114949065..f15854a0f 100644 --- a/src/Spectre.Console/Rendering/Segment.cs +++ b/src/Spectre.Console/Rendering/Segment.cs @@ -141,7 +141,7 @@ public static int CellCount(IEnumerable segments) /// A new segment without any trailing line endings. public Segment StripLineEndings() { - return new Segment(Text.TrimEnd('\n').TrimEnd('\r'), Style); + return new Segment(Text.TrimEnd('\n').TrimEnd('\r'), Style, Link); } /// @@ -179,8 +179,8 @@ public Segment StripLineEndings() } return ( - new Segment(Text.Substring(0, index), Style), - new Segment(Text.Substring(index, Text.Length - index), Style)); + new Segment(Text.Substring(0, index), Style, Link), + new Segment(Text.Substring(index, Text.Length - index), Style, Link)); } /// @@ -189,7 +189,7 @@ public Segment StripLineEndings() /// A new segment that's identical to this one. public Segment Clone() { - return new Segment(Text, Style); + return new Segment(Text, Style, Link); } /// @@ -268,7 +268,7 @@ public static List SplitLines(IEnumerable segments, int ma { if (parts[0].Length > 0) { - line.Add(new Segment(parts[0], segment.Style)); + line.Add(new Segment(parts[0], segment.Style, segment.Link)); } } @@ -347,32 +347,32 @@ public static List SplitOverflow(Segment segment, Overflow? overflow, i var splitted = SplitSegment(segment.Text, maxWidth); foreach (var str in splitted) { - result.Add(new Segment(str, segment.Style)); + result.Add(new Segment(str, segment.Style, segment.Link)); } } else if (overflow == Overflow.Crop) { if (maxWidth <= 0) { - result.Add(new Segment(string.Empty, segment.Style)); + result.Add(new Segment(string.Empty, segment.Style, segment.Link)); } else { var truncated = Truncate(segment, maxWidth); - result.Add(truncated ?? new Segment(string.Empty, segment.Style)); + result.Add(truncated ?? new Segment(string.Empty, segment.Style, segment.Link)); } } else if (overflow == Overflow.Ellipsis) { if (Math.Max(0, maxWidth - 1) == 0) { - result.Add(new Segment("…", segment.Style)); + result.Add(new Segment("…", segment.Style, segment.Link)); } else { var truncated = Truncate(segment, maxWidth - 1); var prefix = truncated?.Text ?? string.Empty; - result.Add(new Segment(prefix + "…", segment.Style)); + result.Add(new Segment(prefix + "…", segment.Style, segment.Link)); } } @@ -455,7 +455,7 @@ public static List Truncate(IEnumerable segments, int maxWidth return null; } - return new Segment(builder.ToString(), segment.Style); + return new Segment(builder.ToString(), segment.Style, segment.Link); } internal static IEnumerable Merge(IEnumerable segments) @@ -512,7 +512,7 @@ internal static List TruncateWithEllipsis(IEnumerable segments } var result = new List(segments); - result.Add(new Segment("…", result.Last().Style)); + result.Add(new Segment("…", result.Last().Style, result.Last().Link)); return result; }