diff --git a/pkg/appview/readme/fetcher.go b/pkg/appview/readme/fetcher.go
index 1f97f9b..47d81f0 100644
--- a/pkg/appview/readme/fetcher.go
+++ b/pkg/appview/readme/fetcher.go
@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
+ "regexp"
"strings"
"time"
@@ -192,6 +193,15 @@ func (f *Fetcher) RenderMarkdown(content []byte) (string, error) {
return f.renderMarkdown(content, "")
}
+// Regex patterns for matching relative URLs that need rewriting
+// These match src="..." or href="..." where the URL is relative (not absolute, not data:, not #anchor)
+var (
+ // Match src="filename" where filename doesn't start with http://, https://, //, /, #, data:, or mailto:
+ relativeSrcPattern = regexp.MustCompile(`src="([^"/:][^"]*)"`)
+ // Match href="filename" where filename doesn't start with http://, https://, //, /, #, data:, or mailto:
+ relativeHrefPattern = regexp.MustCompile(`href="([^"/:][^"]*)"`)
+)
+
// rewriteRelativeURLs converts relative URLs to absolute URLs
func rewriteRelativeURLs(html, baseURL string) string {
if baseURL == "" {
@@ -203,20 +213,51 @@ func rewriteRelativeURLs(html, baseURL string) string {
return html
}
- // Simple string replacement for common patterns
- // This is a basic implementation - for production, consider using an HTML parser
+ // Handle root-relative URLs (starting with /) first
+ // Must be done before bare relative URLs to avoid double-processing
+ if base.Scheme != "" && base.Host != "" {
+ root := fmt.Sprintf("%s://%s/", base.Scheme, base.Host)
+ // Replace src="/" and href="/" but not src="//" (protocol-relative URLs)
+ html = strings.ReplaceAll(html, `src="/`, fmt.Sprintf(`src="%s`, root))
+ html = strings.ReplaceAll(html, `href="/`, fmt.Sprintf(`href="%s`, root))
+ }
+
+ // Handle explicit relative paths (./something and ../something)
html = strings.ReplaceAll(html, `src="./`, fmt.Sprintf(`src="%s`, baseURL))
html = strings.ReplaceAll(html, `href="./`, fmt.Sprintf(`href="%s`, baseURL))
html = strings.ReplaceAll(html, `src="../`, fmt.Sprintf(`src="%s../`, baseURL))
html = strings.ReplaceAll(html, `href="../`, fmt.Sprintf(`href="%s../`, baseURL))
- // Handle root-relative URLs (starting with /)
- if base.Scheme != "" && base.Host != "" {
- root := fmt.Sprintf("%s://%s/", base.Scheme, base.Host)
- // Replace src="/" and href="/" but not src="//" (absolute URLs)
- html = strings.ReplaceAll(html, `src="/`, fmt.Sprintf(`src="%s`, root))
- html = strings.ReplaceAll(html, `href="/`, fmt.Sprintf(`href="%s`, root))
- }
+ // Handle bare relative URLs (e.g., src="image.png" without ./ prefix)
+ // Skip URLs that are already absolute (start with http://, https://, or //)
+ // Skip anchors (#), data URLs (data:), and mailto links
+ html = relativeSrcPattern.ReplaceAllStringFunc(html, func(match string) string {
+ // Extract the URL from src="..."
+ url := match[5 : len(match)-1] // Remove 'src="' and '"'
+
+ // Skip if already processed or is a special URL type
+ if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") ||
+ strings.HasPrefix(url, "//") || strings.HasPrefix(url, "#") ||
+ strings.HasPrefix(url, "data:") || strings.HasPrefix(url, "mailto:") {
+ return match
+ }
+
+ return fmt.Sprintf(`src="%s%s"`, baseURL, url)
+ })
+
+ html = relativeHrefPattern.ReplaceAllStringFunc(html, func(match string) string {
+ // Extract the URL from href="..."
+ url := match[6 : len(match)-1] // Remove 'href="' and '"'
+
+ // Skip if already processed or is a special URL type
+ if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") ||
+ strings.HasPrefix(url, "//") || strings.HasPrefix(url, "#") ||
+ strings.HasPrefix(url, "data:") || strings.HasPrefix(url, "mailto:") {
+ return match
+ }
+
+ return fmt.Sprintf(`href="%s%s"`, baseURL, url)
+ })
return html
}
diff --git a/pkg/appview/readme/fetcher_test.go b/pkg/appview/readme/fetcher_test.go
index a07d7d3..d929c72 100644
--- a/pkg/appview/readme/fetcher_test.go
+++ b/pkg/appview/readme/fetcher_test.go
@@ -145,6 +145,48 @@ func TestRewriteRelativeURLs(t *testing.T) {
baseURL: "https://example.com/docs/",
expected: `
`,
},
+ {
+ name: "bare relative src (no ./ prefix)",
+ html: `
`,
+ baseURL: "https://example.com/docs/",
+ expected: `
`,
+ },
+ {
+ name: "bare relative href (no ./ prefix)",
+ html: `link`,
+ baseURL: "https://example.com/docs/",
+ expected: `link`,
+ },
+ {
+ name: "bare relative with path",
+ html: `
`,
+ baseURL: "https://example.com/docs/",
+ expected: `
`,
+ },
+ {
+ name: "anchor links unchanged",
+ html: `link`,
+ baseURL: "https://example.com/docs/",
+ expected: `link`,
+ },
+ {
+ name: "data URLs unchanged",
+ html: `
`,
+ baseURL: "https://example.com/docs/",
+ expected: `
`,
+ },
+ {
+ name: "mailto links unchanged",
+ html: `email`,
+ baseURL: "https://example.com/docs/",
+ expected: `email`,
+ },
+ {
+ name: "mixed bare and prefixed relative URLs",
+ html: `
link`,
+ baseURL: "https://github.com/user/repo/blob/main/",
+ expected: `
link`,
+ },
}
for _, tt := range tests {