better logic for relative urls

This commit is contained in:
Evan Jarrett
2025-12-20 16:48:08 -06:00
parent 023efb05aa
commit 8e31137c62
2 changed files with 92 additions and 9 deletions
+50 -9
View File
@@ -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
}
+42
View File
@@ -145,6 +145,48 @@ func TestRewriteRelativeURLs(t *testing.T) {
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com//cdn.example.com/image.png">`,
},
{
name: "bare relative src (no ./ prefix)",
html: `<img src="image.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/image.png">`,
},
{
name: "bare relative href (no ./ prefix)",
html: `<a href="page.html">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="https://example.com/docs/page.html">link</a>`,
},
{
name: "bare relative with path",
html: `<img src="images/logo.png">`,
baseURL: "https://example.com/docs/",
expected: `<img src="https://example.com/docs/images/logo.png">`,
},
{
name: "anchor links unchanged",
html: `<a href="#section">link</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="#section">link</a>`,
},
{
name: "data URLs unchanged",
html: `<img src="data:image/png;base64,abc123">`,
baseURL: "https://example.com/docs/",
expected: `<img src="data:image/png;base64,abc123">`,
},
{
name: "mailto links unchanged",
html: `<a href="mailto:test@example.com">email</a>`,
baseURL: "https://example.com/docs/",
expected: `<a href="mailto:test@example.com">email</a>`,
},
{
name: "mixed bare and prefixed relative URLs",
html: `<img src="slices_and_lucy.png"><a href="./other.md">link</a>`,
baseURL: "https://github.com/user/repo/blob/main/",
expected: `<img src="https://github.com/user/repo/blob/main/slices_and_lucy.png"><a href="https://github.com/user/repo/blob/main/other.md">link</a>`,
},
}
for _, tt := range tests {