diff --git a/src/Spectre.Console.Ansi.Tests/AnsiMarkupTests.cs b/src/Spectre.Console.Ansi.Tests/AnsiMarkupTests.cs
index a503b9786..a18be6063 100644
--- a/src/Spectre.Console.Ansi.Tests/AnsiMarkupTests.cs
+++ b/src/Spectre.Console.Ansi.Tests/AnsiMarkupTests.cs
@@ -238,8 +238,8 @@ public void Should_Escape_Markup_Blocks_As_Expected()
}
[Theory]
- [InlineData("[yellow]Hello[/]", "[93mHello[0m")]
- [InlineData("[yellow]Hello [italic]World[/]![/]", "[93mHello [0m[3;93mWorld[0m[93m![0m")]
+ [InlineData("[yellow]Hello[/]", "\e[93mHello\e[0m")]
+ [InlineData("[yellow]Hello [italic]World[/]![/]", "\e[93mHello \e[0m\e[3;93mWorld\e[0m\e[93m!\e[0m")]
public void Should_Output_Expected_Ansi_For_Markup(string text, string expected)
{
// Given
@@ -265,7 +265,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Url_And_Text()
// Then
fixture.Output.ShouldMatch(
- "]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\\\\Click to visit my blog]8;;\\\\");
+ "\e]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\e\\\\Click to visit my blog\e]8;;\e\\\\");
}
[Fact]
@@ -279,7 +279,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Only_Url()
// Then
fixture.Output.ShouldMatch(
- "]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\\\\https:\\/\\/patriksvensson\\.se]8;;\\\\");
+ "\e]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\e\\\\https:\\/\\/patriksvensson\\.se\e]8;;\e\\\\");
}
[Fact]
@@ -294,7 +294,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Bracket_In_Url_Only()
// Then
fixture.Output.ShouldMatch(
- "]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt]8;;\\\\");
+ "\e]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\e\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt\e]8;;\e\\\\");
}
[Fact]
@@ -310,7 +310,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Bracket_In_Url()
// Then
fixture.Output.ShouldMatch(
- "]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt]8;;\\\\");
+ "\e]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\e\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt\e]8;;\e\\\\");
}
[Theory]
diff --git a/src/Spectre.Console.Ansi/Link.cs b/src/Spectre.Console.Ansi/Link.cs
index 373de5434..0217d493b 100644
--- a/src/Spectre.Console.Ansi/Link.cs
+++ b/src/Spectre.Console.Ansi/Link.cs
@@ -4,7 +4,7 @@ namespace Spectre.Console;
/// Represents a link.
///
/// The link URL.
-public sealed class Link(string url)
+public sealed class Link(string url) : IEquatable
{
///
/// Gets the link ID.
@@ -15,4 +15,35 @@ public sealed class Link(string url)
/// Gets the url.
///
public string Url { get; } = url;
+
+ ///
+ public bool Equals(Link? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return Id == other.Id && Url == other.Url;
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return ReferenceEquals(this, obj) || obj is Link other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Id.GetHashCode() * 397) ^ Url.GetHashCode();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Ansi.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Ansi.cs
index 206fc6f8a..7983ed6c2 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Ansi.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Ansi.cs
@@ -14,11 +14,11 @@ public void Should_Write_Ansi_Codes_To_Console_If_Supported()
.EmitAnsiSequences();
// When
- console.WriteAnsi("[101mHello[0m");
+ console.WriteAnsi("\e[101mHello\e[0m");
// Then
console.Output.NormalizeLineEndings()
- .ShouldBe("[101mHello[0m");
+ .ShouldBe("\e[101mHello\e[0m");
}
[Fact]
@@ -31,7 +31,7 @@ public void Should_Not_Write_Ansi_Codes_To_Console_If_Not_Supported()
.EmitAnsiSequences();
// When
- console.WriteAnsi("[101mHello[0m");
+ console.WriteAnsi("\e[101mHello\e[0m");
// Then
console.Output.NormalizeLineEndings()
@@ -49,7 +49,7 @@ public void Should_Return_Ansi_For_Renderable()
var result = console.ToAnsi(markup);
// Then
- result.ShouldBe("[38;5;11mHello [0m[38;5;12mWorld[0m[38;5;11m![0m");
+ result.ShouldBe("\e[38;5;11mHello \e[0m\e[38;5;12mWorld\e[0m\e[38;5;11m!\e[0m");
}
}
}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs
index 8d63c28e1..4381e30cf 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Cursor.cs
@@ -7,10 +7,10 @@ public sealed class Cursor
public sealed class TheMoveMethod
{
[Theory]
- [InlineData(CursorDirection.Up, "Hello[2AWorld")]
- [InlineData(CursorDirection.Down, "Hello[2BWorld")]
- [InlineData(CursorDirection.Right, "Hello[2CWorld")]
- [InlineData(CursorDirection.Left, "Hello[2DWorld")]
+ [InlineData(CursorDirection.Up, "Hello\e[2AWorld")]
+ [InlineData(CursorDirection.Down, "Hello\e[2BWorld")]
+ [InlineData(CursorDirection.Right, "Hello\e[2CWorld")]
+ [InlineData(CursorDirection.Left, "Hello\e[2DWorld")]
public void Should_Return_Correct_Ansi_Code(CursorDirection direction, string expected)
{
// Given
@@ -40,7 +40,7 @@ public void Should_Return_Correct_Ansi_Code()
console.Write("World");
// Then
- console.Output.ShouldBe("Hello[3;5HWorld");
+ console.Output.ShouldBe("Hello\e[3;5HWorld");
}
}
}
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
index 4d40666f4..a3d156c80 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs
@@ -5,8 +5,8 @@ public partial class AnsiConsoleTests
public sealed class Markup
{
[Theory]
- [InlineData("[yellow]Hello[/]", "[93mHello[0m")]
- [InlineData("[yellow]Hello [italic]World[/]![/]", "[93mHello [0m[3;93mWorld[0m[93m![0m")]
+ [InlineData("[yellow]Hello[/]", "\e[93mHello\e[0m")]
+ [InlineData("[yellow]Hello [italic]World[/]![/]", "\e[93mHello \e[0m\e[3;93mWorld\e[0m\e[93m!\e[0m")]
public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected)
{
// Given
@@ -32,7 +32,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Url_And_Text()
console.Markup("[link=https://patriksvensson.se]Click to visit my blog[/]");
// Then
- console.Output.ShouldMatch("]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\\\\Click to visit my blog]8;;\\\\");
+ console.Output.ShouldMatch("\e]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\e\\\\Click to visit my blog\e]8;;\e\\\\");
}
[Fact]
@@ -46,7 +46,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Only_Url()
console.Markup("[link]https://patriksvensson.se[/]");
// Then
- console.Output.ShouldMatch("]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\\\\https:\\/\\/patriksvensson\\.se]8;;\\\\");
+ console.Output.ShouldMatch("\e]8;id=[0-9]*;https:\\/\\/patriksvensson\\.se\e\\\\https:\\/\\/patriksvensson\\.se\e]8;;\e\\\\");
}
[Fact]
@@ -61,7 +61,7 @@ public void Should_Output_Expected_Ansi_For_Link_With_Bracket_In_Url_Only()
console.Markup($"[link]{Path.EscapeMarkup()}[/]");
// Then
- console.Output.ShouldMatch("]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt]8;;\\\\");
+ console.Output.ShouldMatch("\e]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\e\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt\e]8;;\e\\\\");
}
[Fact]
@@ -76,11 +76,11 @@ public void Should_Output_Expected_Ansi_For_Link_With_Bracket_In_Url()
console.Markup($"[link={Path.EscapeMarkup()}]{Path.EscapeMarkup()}[/]");
// Then
- console.Output.ShouldMatch("]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt]8;;\\\\");
+ console.Output.ShouldMatch("\e]8;id=[0-9]*;file:\\/\\/c:\\/temp\\/\\[x\\].txt\e\\\\file:\\/\\/c:\\/temp\\/\\[x\\].txt\e]8;;\e\\\\");
}
[Theory]
- [InlineData("[yellow]Hello [[ World[/]", "[93mHello [ World[0m")]
+ [InlineData("[yellow]Hello [[ World[/]", "\e[93mHello [ World\e[0m")]
public void Should_Be_Able_To_Escape_Tags(string markup, string expected)
{
// Given
@@ -174,5 +174,50 @@ public void Should_Not_Fail_As_In_GH1024(string markup, string expected)
// Then
console.Output.ShouldBe(expected);
}
+
+ [Fact]
+ [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2083")]
+ [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2078")]
+ public void Should_Preserve_Links_When_Multiple_Segments_Are_Merged()
+ {
+ // Given
+ var console = new TestConsole()
+ .Width(8)
+ .SupportsAnsi(true)
+ .EmitAnsiSequences();
+
+ // When
+ console.Write(
+ Align.Center(
+ new Spectre.Console.Markup(
+ "[link=https://example.com/readme.md]Docs[/]")));
+
+ // Then
+ console.Output.ShouldMatch(
+ " \e]8;id=[0-9]*;https:\\/\\/example\\.com\\/readme.md\e\\\\Docs\e]8;;\e\\\\ ");
+ }
+
+ [Fact]
+ [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2083")]
+ [GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/2078")]
+ public void Should_Preserve_Links_Over_Line_Breaks_When_Multiple_Segments_Are_Merged()
+ {
+ // Given
+ var console = new TestConsole()
+ .Width(8)
+ .SupportsAnsi(true)
+ .EmitAnsiSequences();
+
+ // When
+ console.Write(
+ Align.Center(
+ new Spectre.Console.Markup(
+ "[link=https://example.com/readme.md]Foo and Bar[/]")));
+
+ // Then
+ console.Output.ShouldMatch(
+ "\e]8;id=[0-9]*;https:\\/\\/example\\.com\\/readme.md\e\\\\Foo and \e]8;;\e\\\\\n" +
+ " \e]8;id=[0-9]*;https:\\/\\/example\\.com\\/readme.md\e\\\\Bar\e]8;;\e\\\\ ");
+ }
}
}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.MarkupInterpolated.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.MarkupInterpolated.cs
index df6854b21..ed4b0c917 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.MarkupInterpolated.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.MarkupInterpolated.cs
@@ -17,7 +17,7 @@ public void Should_Print_Simple_Interpolated_Strings()
console.MarkupInterpolated($"[Green]{Path}[/]");
// Then
- console.Output.ShouldBe($"[32m{Path}[0m");
+ console.Output.ShouldBe($"\e[32m{Path}\e[0m");
}
[Fact]
@@ -34,7 +34,7 @@ public void Should_Not_Throw_Error_On_Links_Brackets()
// Then
var pathAsRegEx = Regex.Replace(Path, "([/\\[\\]\\\\])", "\\$1", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- console.Output.ShouldMatch($"\\]8;id=[0-9]+;{pathAsRegEx}\\\\{pathAsRegEx}\\]8;;\\\\");
+ console.Output.ShouldMatch($"\e\\]8;id=[0-9]+;{pathAsRegEx}\e\\\\{pathAsRegEx}\e\\]8;;\e\\\\");
}
}
}
\ No newline at end of file
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
index 127a721f0..ab6f719d6 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs
@@ -3,15 +3,15 @@ namespace Spectre.Console.Tests.Unit;
public partial class AnsiConsoleTests
{
[Theory]
- [InlineData(Decoration.Bold, "\u001b[1mHello World[0m")]
- [InlineData(Decoration.Dim, "\u001b[2mHello World[0m")]
- [InlineData(Decoration.Italic, "\u001b[3mHello World[0m")]
- [InlineData(Decoration.Underline, "\u001b[4mHello World[0m")]
- [InlineData(Decoration.Invert, "\u001b[7mHello World[0m")]
- [InlineData(Decoration.Conceal, "\u001b[8mHello World[0m")]
- [InlineData(Decoration.SlowBlink, "\u001b[5mHello World[0m")]
- [InlineData(Decoration.RapidBlink, "\u001b[6mHello World[0m")]
- [InlineData(Decoration.Strikethrough, "\u001b[9mHello World[0m")]
+ [InlineData(Decoration.Bold, "\u001b[1mHello World\e[0m")]
+ [InlineData(Decoration.Dim, "\u001b[2mHello World\e[0m")]
+ [InlineData(Decoration.Italic, "\u001b[3mHello World\e[0m")]
+ [InlineData(Decoration.Underline, "\u001b[4mHello World\e[0m")]
+ [InlineData(Decoration.Invert, "\u001b[7mHello World\e[0m")]
+ [InlineData(Decoration.Conceal, "\u001b[8mHello World\e[0m")]
+ [InlineData(Decoration.SlowBlink, "\u001b[5mHello World\e[0m")]
+ [InlineData(Decoration.RapidBlink, "\u001b[6mHello World\e[0m")]
+ [InlineData(Decoration.Strikethrough, "\u001b[9mHello World\e[0m")]
public void Should_Write_Decorated_Text_Correctly(Decoration decoration, string expected)
{
// Given
@@ -26,8 +26,8 @@ public void Should_Write_Decorated_Text_Correctly(Decoration decoration, string
}
[Theory]
- [InlineData(Decoration.Bold | Decoration.Underline, "\u001b[1;4mHello World[0m")]
- [InlineData(Decoration.Bold | Decoration.Underline | Decoration.Conceal, "\u001b[1;4;8mHello World[0m")]
+ [InlineData(Decoration.Bold | Decoration.Underline, "\u001b[1;4mHello World\e[0m")]
+ [InlineData(Decoration.Bold | Decoration.Underline | Decoration.Conceal, "\u001b[1;4;8mHello World\e[0m")]
public void Should_Write_Text_With_Multiple_Decorations_Correctly(Decoration decoration, string expected)
{
// Given
diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
index a05b59bc2..b985bc3d3 100644
--- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
+++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs
@@ -5,8 +5,8 @@ public partial class AnsiConsoleTests
public sealed class Clear
{
[Theory]
- [InlineData(false, "Hello[2J[3JWorld")]
- [InlineData(true, "Hello[2J[3J[1;1HWorld")]
+ [InlineData(false, "Hello\e[2J\e[3JWorld")]
+ [InlineData(true, "Hello\e[2J\e[3J\e[1;1HWorld")]
public void Should_Clear_Screen(bool home, string expected)
{
// Given
@@ -123,7 +123,7 @@ public void Should_Reset_Colors_Correctly_After_Line_Break()
// Then
console.Output.NormalizeLineEndings()
- .ShouldBe("[101mHello[0m\n[102mWorld[0m\n");
+ .ShouldBe("\e[101mHello\e[0m\n\e[102mWorld\e[0m\n");
}
[Fact]
@@ -139,7 +139,7 @@ public void Should_Reset_Colors_Correctly_After_Line_Break_In_Text()
// Then
console.Output.NormalizeLineEndings()
- .ShouldBe("[101mHello[0m\n[101mWorld[0m\n");
+ .ShouldBe("\e[101mHello\e[0m\n\e[101mWorld\e[0m\n");
}
}
diff --git a/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs b/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs
index 848f77e12..89fcaf916 100644
--- a/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs
+++ b/src/Spectre.Console.Tests/Unit/Live/Progress/ProgressTests.cs
@@ -24,11 +24,11 @@ public void Should_Render_Task_Correctly()
console.Output
.NormalizeLineEndings()
.ShouldBe(
- "[?25l" + // Hide cursor
+ "\e[?25l" + // Hide cursor
" \n" + // Top padding
- "[38;5;8m━━━━━━━━━━[0m\n" + // Task
+ "\e[38;5;8m━━━━━━━━━━\e[0m\n" + // Task
" " + // Bottom padding
- "[2K[1A[2K[1A[2K[?25h"); // Clear + show cursor
+ "\e[2K\e[1A\e[2K\e[1A\e[2K\e[?25h"); // Clear + show cursor
}
[Fact]
@@ -52,11 +52,11 @@ public void Should_Not_Auto_Clear_If_Specified()
console.Output
.NormalizeLineEndings()
.ShouldBe(
- "[?25l" + // Hide cursor
+ "\e[?25l" + // Hide cursor
" \n" + // Top padding
- "[38;5;8m━━━━━━━━━━[0m\n" + // Task
+ "\e[38;5;8m━━━━━━━━━━\e[0m\n" + // Task
" \n" + // Bottom padding
- "[?25h"); // show cursor
+ "\e[?25h"); // show cursor
}
[Fact]
@@ -249,12 +249,12 @@ public void Should_Hide_Completed_Tasks()
console.Output
.NormalizeLineEndings()
.ShouldBe(
- "[?25l" + // Hide cursor
+ "\e[?25l" + // Hide cursor
" \n" + // top padding
- "[38;5;8m━━━━━━━━━━[0m\n" + // taskInProgress1
- "[38;5;11m━━[0m[38;5;8m━━━━━━━━[0m\n" + // taskInProgress2
+ "\e[38;5;8m━━━━━━━━━━\e[0m\n" + // taskInProgress1
+ "\e[38;5;11m━━\e[0m\e[38;5;8m━━━━━━━━\e[0m\n" + // taskInProgress2
" \n" + // bottom padding
- "[?25h"); // show cursor
+ "\e[?25h"); // show cursor
}
[Fact]
@@ -316,8 +316,8 @@ public void Should_Render_Tasks_Added_Before_And_After_Correctly()
console.Output.SplitLines().Select(x => x.Trim()).ToArray()
.ShouldBeEquivalentTo(new[]
{
- "[?25l", "foo1", "afterFoo1", "foo2", "beforeFoo3", "foo3",
- "[2K[1A[2K[1A[2K[1A[2K[1A[2K[1A[2K[1A[2K[?25h",
+ "\e[?25l", "foo1", "afterFoo1", "foo2", "beforeFoo3", "foo3",
+ "\e[2K\e[1A\e[2K\e[1A\e[2K\e[1A\e[2K\e[1A\e[2K\e[1A\e[2K\e[1A\e[2K\e[?25h",
});
}
@@ -350,7 +350,7 @@ public void Should_Render_Tasks_At_Specified_Indexes_Correctly()
console.Output.SplitLines().Select(x => x.Trim()).ToArray()
.ShouldBeEquivalentTo(new[]
{
- "[?25l", "foo1", "afterFoo1", "foo2", "beforeFoo3", "foo3",
+ "\e[?25l", "foo1", "afterFoo1", "foo2", "beforeFoo3", "foo3",
"[2K[1A[2K[1A[2K[1A[2K[1A[2K[1A[2K[1A[2K[?25h",
});
}
diff --git a/src/Spectre.Console/Rendering/Segment.cs b/src/Spectre.Console/Rendering/Segment.cs
index 6f30e9970..5f82b56b8 100644
--- a/src/Spectre.Console/Rendering/Segment.cs
+++ b/src/Spectre.Console/Rendering/Segment.cs
@@ -469,9 +469,9 @@ internal static IEnumerable Merge(IEnumerable segments)
continue;
}
- // Same style?
+ // Same style and link?
if (segmentBuilder.StyleEquals(segment.Style) && !segmentBuilder.IsLineBreak() &&
- !segmentBuilder.IsControlCode())
+ !segmentBuilder.IsControlCode() && segmentBuilder.HasSameLink(segment.Link))
{
segmentBuilder.Append(segment.Text);
continue;
@@ -625,5 +625,15 @@ public void Reset(Segment segment)
_textBuilder.Append(segment.Text);
_originalSegment = segment;
}
+
+ public bool HasSameLink(Link? link)
+ {
+ if (link == null && _originalSegment.Link == null)
+ {
+ return true;
+ }
+
+ return _originalSegment.Link?.Equals(link) ?? false;
+ }
}
}
\ No newline at end of file