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 {