From 973331db7976f3c52ba69459b03f5bce0b79ffe1 Mon Sep 17 00:00:00 2001 From: Emre Koca <110906681+kocaemre@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:21:57 +0200 Subject: [PATCH] feat(markdown): hide duplicate link destinations Signed-off-by: Emre Koca <110906681+kocaemre@users.noreply.github.com> --- .../output-1_implementation-summary.md | 14 ++++ .../prompt-1_issue-76-markdown-links.md | 14 ++++ internal/cli/repl/markdown/hyperlink_test.go | 34 +++++++++ internal/cli/repl/markdown/links.go | 76 +++++++++++++++++++ internal/cli/repl/markdown/markdown.go | 5 +- 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 .ai-interactions/tasks/issue-76-markdown-links/output-1_implementation-summary.md create mode 100644 .ai-interactions/tasks/issue-76-markdown-links/prompt-1_issue-76-markdown-links.md create mode 100644 internal/cli/repl/markdown/links.go diff --git a/.ai-interactions/tasks/issue-76-markdown-links/output-1_implementation-summary.md b/.ai-interactions/tasks/issue-76-markdown-links/output-1_implementation-summary.md new file mode 100644 index 00000000..f04772f1 --- /dev/null +++ b/.ai-interactions/tasks/issue-76-markdown-links/output-1_implementation-summary.md @@ -0,0 +1,14 @@ +Plan and implementation summary: + +1. Reproduced the issue with failing tests in internal/cli/repl/markdown/hyperlink_test.go: + - A relative Markdown link like [internal/cli/repl/repl.go:413](/internal/cli/repl/repl.go#L413) should not show the destination. + - An external Markdown link like [docs](https://example.com/docs) should keep the URL as the OSC 8 target while hiding it from visible text. + +2. Added a small Markdown link rewrite layer in internal/cli/repl/markdown/links.go: + - Relative destinations and anchors are rewritten to label-only Markdown before rendering. + - External http/https Markdown links are rewritten to label-only Markdown and recorded. + - After Glamour renders the label, the rendered label is wrapped once with OSC 8 escapes. + +3. Preserved existing bare URL behavior by leaving bare URLs untouched and continuing to run makeURLsClickable after Markdown link processing. + +4. Verified with targeted markdown tests, broader REPL tests, gofmt, go mod tidy, and the repository-required race test command. diff --git a/.ai-interactions/tasks/issue-76-markdown-links/prompt-1_issue-76-markdown-links.md b/.ai-interactions/tasks/issue-76-markdown-links/prompt-1_issue-76-markdown-links.md new file mode 100644 index 00000000..8214a04b --- /dev/null +++ b/.ai-interactions/tasks/issue-76-markdown-links/prompt-1_issue-76-markdown-links.md @@ -0,0 +1,14 @@ +User prompt: Continue the OSS PR sprint by implementing a focused fix for mochow13/keen-code issue #76, "Render Markdown links without duplicate destinations". + +Requirements from the issue: +- Relative repository source references and anchors should display their label only. +- External http/https Markdown links should render the label as an OSC 8 terminal hyperlink without printing a duplicate destination. +- Bare URLs should retain existing behavior. +- Add focused tests for relative source references and external URLs. + +Repository instructions followed: +- Minimal comments only when strictly necessary. +- Run gofmt on modified Go files. +- Run go mod tidy after the change. +- Run go test -race ./... after finalising the change. +- Include interaction files under .ai-interactions/tasks/. diff --git a/internal/cli/repl/markdown/hyperlink_test.go b/internal/cli/repl/markdown/hyperlink_test.go index 6a9e2b0d..c8335fa4 100644 --- a/internal/cli/repl/markdown/hyperlink_test.go +++ b/internal/cli/repl/markdown/hyperlink_test.go @@ -75,3 +75,37 @@ func TestRendererRenderLinkIsClickable(t *testing.T) { t.Fatalf("expected OSC 8 hyperlink escape in %q", rendered) } } + +func TestRendererRenderRelativeMarkdownLinkShowsLabelOnly(t *testing.T) { + renderer, err := New(80) + if err != nil { + t.Fatalf("New() error = %v", err) + } + + rendered := stripANSI(renderer.Render("[internal/cli/repl/repl.go:413](/internal/cli/repl/repl.go#L413)")) + if !strings.Contains(rendered, "internal/cli/repl/repl.go:413") { + t.Fatalf("expected link label in %q", rendered) + } + if strings.Contains(rendered, "/internal/cli/repl/repl.go#L413") { + t.Fatalf("expected relative destination to be hidden, got %q", rendered) + } +} + +func TestRendererRenderExternalMarkdownLinkUsesOSC8Label(t *testing.T) { + renderer, err := New(80) + if err != nil { + t.Fatalf("New() error = %v", err) + } + + rendered := renderer.Render("[docs](https://example.com/docs)") + if !strings.Contains(rendered, osc8Open+"https://example.com/docs"+osc8ST) { + t.Fatalf("expected markdown link destination as OSC 8 target in %q", rendered) + } + if strings.Contains(stripEscapes(rendered), "https://example.com/docs") { + t.Fatalf("expected external destination to be hidden from visible text, got %q", rendered) + } +} + +func stripEscapes(value string) string { + return ansiEscape.ReplaceAllString(stripANSI(value), "") +} diff --git a/internal/cli/repl/markdown/links.go b/internal/cli/repl/markdown/links.go new file mode 100644 index 00000000..a60707a4 --- /dev/null +++ b/internal/cli/repl/markdown/links.go @@ -0,0 +1,76 @@ +package markdown + +import "strings" + +type markdownLink struct { + label string + dest string +} + +func rewriteMarkdownLinks(markdown string) (string, []markdownLink) { + if !strings.Contains(markdown, "](") { + return markdown, nil + } + + var links []markdownLink + var b strings.Builder + b.Grow(len(markdown)) + for i := 0; i < len(markdown); { + if markdown[i] != '[' { + b.WriteByte(markdown[i]) + i++ + continue + } + + labelEnd := strings.Index(markdown[i+1:], "](") + if labelEnd == -1 { + b.WriteByte(markdown[i]) + i++ + continue + } + labelEnd += i + 1 + destStart := labelEnd + 2 + destEnd := strings.IndexByte(markdown[destStart:], ')') + if destEnd == -1 { + b.WriteByte(markdown[i]) + i++ + continue + } + destEnd += destStart + + label := markdown[i+1 : labelEnd] + dest := markdown[destStart:destEnd] + switch { + case strings.HasPrefix(dest, "http://") || strings.HasPrefix(dest, "https://"): + links = append(links, markdownLink{label: label, dest: dest}) + b.WriteString(label) + case isRelativeMarkdownLink(dest): + b.WriteString(label) + default: + b.WriteString(markdown[i : destEnd+1]) + } + i = destEnd + 1 + } + + return b.String(), links +} + +func makeMarkdownLinksClickable(rendered string, links []markdownLink) string { + for _, link := range links { + if link.label == "" || link.dest == "" { + continue + } + rendered = strings.Replace(rendered, link.label, osc8Open+link.dest+osc8ST+link.label+osc8Close, 1) + } + return rendered +} + +func isRelativeMarkdownLink(dest string) bool { + if dest == "" { + return false + } + if strings.HasPrefix(dest, "#") || strings.HasPrefix(dest, "/") || strings.HasPrefix(dest, "./") || strings.HasPrefix(dest, "../") { + return true + } + return !strings.Contains(dest, ":") +} diff --git a/internal/cli/repl/markdown/markdown.go b/internal/cli/repl/markdown/markdown.go index a6a875a0..34f71545 100644 --- a/internal/cli/repl/markdown/markdown.go +++ b/internal/cli/repl/markdown/markdown.go @@ -50,12 +50,15 @@ func (r *Renderer) Render(markdown string) string { return "" } + markdown, links := rewriteMarkdownLinks(markdown) tables := markdownTableBlocks(markdown) rendered, err := r.renderer.Render(markdown) if err != nil { return markdown } - return makeURLsClickable(addTableOuterBorders(rendered, tables)) + rendered = addTableOuterBorders(rendered, tables) + rendered = makeMarkdownLinksClickable(rendered, links) + return makeURLsClickable(rendered) } func (r *Renderer) UpdateWidth(width int) error {