Skip to content
Merged
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
73 changes: 73 additions & 0 deletions ConsoleMarkdownRenderer.Tests/RendererTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,79 @@ public void RendererTests_AutolinkTest(int index, string expectedContent, string
Assert.IsFalse(link.IsImage, $"IsImage should be false at index {index}");
}

[TestMethod]
public void RendererTests_TerminalHyperlinks_DefaultEnabled()
{
// By default, UseTerminalHyperlinks is true so OSC 8 escape sequences should be
// emitted around link text in supported terminals.
Assert.IsTrue(new DisplayOptions().UseTerminalHyperlinks,
"UseTerminalHyperlinks should default to true.");

var output = RenderMarkdownWithLinkCapableConsole("[two](http://two.example/)", new DisplayOptions());

// OSC 8 hyperlink wraps with ESC ] 8 ; <params> ; <url> ESC \ ... ESC ] 8 ; ; ESC \
Assert.Contains("\u001B]8;", output, "Expected OSC 8 open sequence in output");
Assert.Contains("http://two.example/", output, "Expected URL in OSC 8 sequence");
Assert.Contains("\u001B]8;;\u001B\\", output, "Expected OSC 8 close sequence in output");
}

[TestMethod]
public void RendererTests_TerminalHyperlinks_OptOut()
{
// When UseTerminalHyperlinks is false, no OSC 8 escape sequences should be emitted.
var options = new DisplayOptions { UseTerminalHyperlinks = false };
var output = RenderMarkdownWithLinkCapableConsole("[two](http://two.example/)", options);

Assert.DoesNotContain("\u001B]8;", output,
$"Expected no OSC 8 sequences when UseTerminalHyperlinks is false. Output: {output}");
// Visible link text/URL should still be present in the rendered output.
Assert.Contains("http://two.example/", output, "URL should still be rendered as visible text");
}

[TestMethod]
public void RendererTests_TerminalHyperlinks_AutolinkEmitsOsc8()
{
// Autolinks should also be wrapped in OSC 8 hyperlinks by default.
var output = RenderMarkdownWithLinkCapableConsole("<https://auto.example/>", new DisplayOptions());

Assert.Contains("\u001B]8;", output, "Expected OSC 8 open sequence for autolink");
Assert.Contains("https://auto.example/", output, "Expected autolink URL in output");
Assert.Contains("\u001B]8;;\u001B\\", output, "Expected OSC 8 close sequence for autolink");
}

[TestMethod]
public void RendererTests_TerminalHyperlinks_UrlWithBracketsIsEscaped()
{
// URLs may contain '[' or ']'. The link tag's URL parameter must be escaped via
// Markup.Escape so it doesn't break Spectre's markup parser.
const string url = "http://example.com/path[1]";
var output = RenderMarkdownWithLinkCapableConsole($"[label](<{url}>)", new DisplayOptions());

// The URL should appear unmodified inside the OSC 8 escape sequence.
Assert.Contains($"\u001B]8;id=", output, "Expected OSC 8 open sequence");
Assert.Contains(url, output, $"Expected the unescaped URL '{url}' in the output");
}

[TestMethod]
public void RendererTests_TerminalHyperlinks_ClonePreservesOptOut()
{
var options = new DisplayOptions { UseTerminalHyperlinks = false };
var clone = options.Clone();
Assert.IsFalse(clone.UseTerminalHyperlinks, "Clone() should preserve UseTerminalHyperlinks");
}

private string RenderMarkdownWithLinkCapableConsole(string markdown, DisplayOptions options)
{
// Configure the test console to actually emit ANSI sequences and advertise OSC 8
// hyperlink support, so the rendered Output contains the escape sequences we are
// asserting on.
var console = NewConsole();
console.EmitAnsiSequences = true;
console.Profile.Capabilities.Links = true;
console.Write(Renderer(markdown, options));
return console.Output;
}

[TestMethod]
[DataRow("quote 2." , Decoration.Italic)]
[DataRow("should even" , Decoration.Italic | Decoration.Bold)]
Expand Down
11 changes: 11 additions & 0 deletions DisplayOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ public sealed class DisplayOptions

// When set to true wrap Headers with '#'s
public bool WrapHeader { get; set; } = true;

/// <summary>
/// When <see langword="true"/> (the default), links rendered for
/// <see cref="Markdig.Syntax.Inlines.LinkInline"/> and <see cref="Markdig.Syntax.Inlines.AutolinkInline"/>
/// are wrapped with Spectre.Console's <c>[link=...]...[/]</c> markup so that
/// supported terminals (iTerm2, Windows Terminal, GNOME Terminal, etc.) render them as
/// clickable <a href="https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda">OSC 8 hyperlinks</a>.
/// Set to <see langword="false"/> to disable for terminals that render the escape sequences as garbage.
/// </summary>
public bool UseTerminalHyperlinks { get; set; } = true;
Comment thread
boxofyellow marked this conversation as resolved.

// When set to true the content structure is displayed and detail of unsupported markdown is displayed
public bool IncludeDebug = false;
Expand Down Expand Up @@ -158,6 +168,7 @@ public sealed class DisplayOptions
Superscript = this.Superscript,
UnknownDelimiterChar = this.UnknownDelimiterChar,
UnknownDelimiterContent = this.UnknownDelimiterContent,
UseTerminalHyperlinks = this.UseTerminalHyperlinks,
WrapHeader = this.WrapHeader,
};

Expand Down
15 changes: 14 additions & 1 deletion ObjectRenderers/ConsoleRendererBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,30 @@ public T PopLink(string url, bool isImage = false)

public T WriteLink(Action<T> writeDisplay, string url, bool isImage = false)
{
// When enabled (and the URL is non-empty), wrap the rendered link text with
// Spectre.Console's [link=...]...[/] markup so that supported terminals emit
// an OSC 8 hyperlink, making the link clickable inline.
var useHyperlink = Options.UseTerminalHyperlinks && !string.IsNullOrEmpty(url);
if (useHyperlink)
{
AddInLine($"[link={Markup.Escape(url)}]");
}
if (isImage)
{
AddInLine("!");
}
WriteEscape("[")
.PushLink();
writeDisplay(CastThis);
return PopLink(url, isImage)
PopLink(url, isImage)
.WriteEscape("](")
.WriteEscape(url)
.AddInLine(")");
if (useHyperlink)
{
AddInLine("[/]");
}
return CastThis;
}

public T NewListBlockFrame(ListBlock list)
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<img width="314" height="22" alt="Image" src="https://github.com/user-attachments/assets/17f529a2-17c3-4a18-bd47-145befff5acb" />
- After
<img width="338" height="58" alt="Image" src="https://github.com/user-attachments/assets/6b66b0fd-9cfa-4b40-8733-236ed5ab4b39" />
- [#133](https://github.com/boxofyellow/ConsoleMarkdownRenderer/pull/133): Emit OSC 8 terminal hyperlinks from WriteLink via Spectre Markup

### :wrench: Internal Improvements :wrench:
- [#129](https://github.com/boxofyellow/ConsoleMarkdownRenderer/pull/129): Use ConfigureAwait(false) on awaits in published library code
Expand Down
Loading