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
42 changes: 38 additions & 4 deletions Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,45 @@ public static List<StyledSegment> ToStyledSegments (IReadOnlyList<InlineRun> run
return segments;
}

private static Attribute MakeLinkAttribute (Attribute normal, StyledSegment segment)
private static Attribute MakeLinkAttribute (Attribute normal, StyledSegment segment) =>
IsMarkdownLinkTarget (segment.Url) ? normal with { Style = normal.Style | TextStyle.Underline } : normal;

internal static bool IsMarkdownLinkTarget (string? url)
{
if (string.IsNullOrWhiteSpace (url))
{
return false;
}

if (url.StartsWith ('#'))
{
return true;
}

if (TryCreateSafeAbsoluteUri (url, out _))
{
return true;
}

return Uri.TryCreate (url, UriKind.Relative, out _);
}

internal static bool TryCreateSafeAbsoluteUri (string? url, out Uri? absoluteUri)
{
bool isClickable = !string.IsNullOrWhiteSpace (segment.Url)
&& (Uri.IsWellFormedUriString (segment.Url, UriKind.Absolute) || segment.Url!.StartsWith ('#'));
absoluteUri = null;

if (!Uri.TryCreate (url, UriKind.Absolute, out Uri? parsed) || parsed is null)
{
return false;
}

if (!Link.SafeSchemes.Contains (parsed.Scheme))
{
return false;
}

absoluteUri = parsed;

return isClickable ? normal with { Style = normal.Style | TextStyle.Underline } : normal;
return true;
}
}
3 changes: 2 additions & 1 deletion Terminal.Gui/Views/Markdown/Markdown.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ bool IDesignable.EnableForDesign ()
## Links

* [Markdown API docs](https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.Views.Markdown.html) for more info.
* [Relative local link](docs/getting-started.md) renders as a link without opening a URI handler by default.

## Checklist

Expand Down Expand Up @@ -600,4 +601,4 @@ And this text is after. Thematic breaks are rendered as full-width horizontal li

That's all folks! 👋
""";
}
}
2 changes: 1 addition & 1 deletion Terminal.Gui/Views/Markdown/MarkdownTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ private void DrawWrappedRow (List<StyledSegment> [] cellSegments, Alignment [] a
}

bool hasUrl = !string.IsNullOrWhiteSpace (seg.Url);
bool isAbsoluteUrl = hasUrl && Uri.IsWellFormedUriString (seg.Url, UriKind.Absolute);
bool isAbsoluteUrl = hasUrl && MarkdownAttributeHelper.TryCreateSafeAbsoluteUri (seg.Url, out _);
bool isActive = hasUrl && HasFocus && IsActiveLinkAt (rowIndex, col, seg.Url!);

if (isActive)
Expand Down
7 changes: 4 additions & 3 deletions Terminal.Gui/Views/Markdown/MarkdownView.Drawing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,9 @@ private void DrawRenderedLine (RenderedLine line, int contentRow, int drawRow)
{
Attribute linkAttr = GetAttributeForSegment (segment);
Attribute reversed = new (linkAttr.Background, linkAttr.Foreground, linkAttr.Style);
bool isSafeAbsoluteLink = MarkdownAttributeHelper.TryCreateSafeAbsoluteUri (segment.Url, out _);

if (!string.IsNullOrWhiteSpace (segment.Url) && Uri.IsWellFormedUriString (segment.Url, UriKind.Absolute) && Driver is { })
if (isSafeAbsoluteLink && Driver is { })
{
Driver.CurrentUrl = segment.Url;

Expand Down Expand Up @@ -276,7 +277,7 @@ private void DrawGrapheme (StyledSegment segment, string grapheme, int x, int y)
{
Attribute attr = GetAttributeForSegment (segment);

if (!string.IsNullOrWhiteSpace (segment.Url) && Uri.IsWellFormedUriString (segment.Url, UriKind.Absolute) && Driver is { })
if (MarkdownAttributeHelper.TryCreateSafeAbsoluteUri (segment.Url, out _) && Driver is { })
{
Driver.CurrentUrl = segment.Url;

Expand Down Expand Up @@ -337,4 +338,4 @@ private void TryQueueSixel (string imageSource, Point viewPosition)
_sixelRenderMap [queueId] = newRender;
Driver.GetSixels ().Enqueue (newRender);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,37 @@ public void Mouse_Click_On_Link_Raises_LinkClicked ()
window.Dispose ();
}

// Codex - GPT-5
Comment thread
tig marked this conversation as resolved.
[Fact]
public void Mouse_Click_On_Relative_Link_Raises_LinkClicked ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);

Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None };

Terminal.Gui.Views.Markdown markdownView = new () { Text = "[Local](docs/readme.md)", Width = 20, Height = 3 };

window.Add (markdownView);

string clickedUrl = "";

markdownView.LinkClicked += (_, e) =>
{
clickedUrl = e.Url;
e.Handled = true;
};

app.Begin (window);
app.LayoutAndDraw ();

markdownView.NewMouseEvent (new Mouse { Position = new Point (0, 0), Flags = MouseFlags.LeftButtonClicked });

Assert.Equal ("docs/readme.md", clickedUrl);

window.Dispose ();
}

// Copilot
[Fact]
public void Mouse_Click_On_Link_In_Table_Cell_Raises_LinkClicked ()
Expand Down Expand Up @@ -256,6 +287,45 @@ public void Mouse_Click_On_Link_In_Table_Cell_Raises_LinkClicked ()
window.Dispose ();
}

// Codex - GPT-5
Comment thread
tig marked this conversation as resolved.
[Fact]
public void Mouse_Click_On_Relative_Link_In_Table_Cell_Raises_LinkClicked ()
{
using IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);

Runnable window = new () { Width = 80, Height = 20, BorderStyle = LineStyle.None };

string markdown = """
| Name | Description |
|------|-------------|
| [local](docs/readme.md) | Pick one item |
""";

Terminal.Gui.Views.Markdown markdownView = new () { Text = markdown, Width = 60, Height = 15 };
window.Add (markdownView);

string clickedUrl = "";

markdownView.LinkClicked += (_, e) =>
{
clickedUrl = e.Url;
e.Handled = true;
};

app.Begin (window);
app.LayoutAndDraw ();

MarkdownTable? tableView = markdownView.SubViews.OfType<MarkdownTable> ().FirstOrDefault ();
Assert.NotNull (tableView);

tableView.NewMouseEvent (new Mouse { Position = new Point (2, 3), Flags = MouseFlags.LeftButtonClicked });

Assert.Equal ("docs/readme.md", clickedUrl);

window.Dispose ();
}

// Copilot
[Fact]
public void Tab_Navigation_Cycles_Through_Table_Links ()
Expand Down Expand Up @@ -749,11 +819,11 @@ public void Style_Link_Absolute_Underline_With_OSC8 ()
}

[Fact]
public void Style_Link_Relative_No_Underline_No_OSC8 ()
public void Style_Link_Relative_Underline_No_OSC8 ()
{
(IApplication app, Runnable window) = SetupStyleTest ("[Go](foo.md)");

DriverAssert.AssertDriverOutputIs (@"\x1b[30m\x1b[107mGo", output, app.Driver);
DriverAssert.AssertDriverOutputIs (@"\x1b[30m\x1b[107m\x1b[4mGo\x1b[30m\x1b[107m\x1b[24m", output, app.Driver);

window.Dispose ();
app.Dispose ();
Expand Down
Loading