feat(parser): add URL normalization for consistent history storage
- commit
- 696087ad9f94ec3c6f8fbd843fa50be0fecd05d3
- author
- bnema · · 2025-11-19 18:23 UTC
- parent
- f101a25
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
2 files · +63 -0
A internal/parser/url_helpers.go +31 -0
| @@ -0,0 +1,31 @@ | |||
| 1 | + | package parser | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | neturl "net/url" | |
| 5 | + | "strings" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // NormalizeHistoryURL ensures history URLs have a canonical form so lookups match. | |
| 9 | + | // Currently it only guarantees that http/https URLs always include a trailing slash | |
| 10 | + | // for root paths, matching how WebKit reports final navigation URLs. | |
| 11 | + | func NormalizeHistoryURL(raw string) string { | |
| 12 | + | trimmed := strings.TrimSpace(raw) | |
| 13 | + | if trimmed == "" { | |
| 14 | + | return trimmed | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | u, err := neturl.Parse(trimmed) | |
| 18 | + | if err != nil { | |
| 19 | + | return trimmed | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | if u.Scheme != "http" && u.Scheme != "https" { | |
| 23 | + | return trimmed | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | if u.Path == "" { | |
| 27 | + | u.Path = "/" | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | return u.String() | |
| 31 | + | } | |
| @@ -0,0 +1,31 @@ | |||||
| 1 | + | package parser | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | neturl "net/url" | |||
| 5 | + | "strings" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // NormalizeHistoryURL ensures history URLs have a canonical form so lookups match. | |||
| 9 | + | // Currently it only guarantees that http/https URLs always include a trailing slash | |||
| 10 | + | // for root paths, matching how WebKit reports final navigation URLs. | |||
| 11 | + | func NormalizeHistoryURL(raw string) string { | |||
| 12 | + | trimmed := strings.TrimSpace(raw) | |||
| 13 | + | if trimmed == "" { | |||
| 14 | + | return trimmed | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | u, err := neturl.Parse(trimmed) | |||
| 18 | + | if err != nil { | |||
| 19 | + | return trimmed | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | if u.Scheme != "http" && u.Scheme != "https" { | |||
| 23 | + | return trimmed | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | if u.Path == "" { | |||
| 27 | + | u.Path = "/" | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | return u.String() | |||
| 31 | + | } | |||
A internal/parser/url_helpers_test.go +32 -0
| @@ -0,0 +1,32 @@ | |||
| 1 | + | package parser | |
| 2 | + | ||
| 3 | + | import "testing" | |
| 4 | + | ||
| 5 | + | func TestNormalizeHistoryURL(t *testing.T) { | |
| 6 | + | t.Parallel() | |
| 7 | + | ||
| 8 | + | tests := []struct { | |
| 9 | + | name string | |
| 10 | + | input string | |
| 11 | + | expected string | |
| 12 | + | }{ | |
| 13 | + | {"addsTrailingSlash", "https://duckduckgo.com", "https://duckduckgo.com/"}, | |
| 14 | + | {"keepsExistingSlash", "https://duckduckgo.com/", "https://duckduckgo.com/"}, | |
| 15 | + | {"keepsPath", "https://example.com/foo", "https://example.com/foo"}, | |
| 16 | + | {"keepsQuery", "https://example.com?q=1", "https://example.com/?q=1"}, | |
| 17 | + | {"httpScheme", "http://example.com", "http://example.com/"}, | |
| 18 | + | {"nonHttpScheme", "ftp://example.com", "ftp://example.com"}, | |
| 19 | + | {"invalidURL", "not a url", "not a url"}, | |
| 20 | + | {"empty", "", ""}, | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | for _, tt := range tests { | |
| 24 | + | tt := tt | |
| 25 | + | t.Run(tt.name, func(t *testing.T) { | |
| 26 | + | t.Parallel() | |
| 27 | + | if got := NormalizeHistoryURL(tt.input); got != tt.expected { | |
| 28 | + | t.Fatalf("NormalizeHistoryURL(%q) = %q, want %q", tt.input, got, tt.expected) | |
| 29 | + | } | |
| 30 | + | }) | |
| 31 | + | } | |
| 32 | + | } | |
| @@ -0,0 +1,32 @@ | |||||
| 1 | + | package parser | |||
| 2 | + | ||||
| 3 | + | import "testing" | |||
| 4 | + | ||||
| 5 | + | func TestNormalizeHistoryURL(t *testing.T) { | |||
| 6 | + | t.Parallel() | |||
| 7 | + | ||||
| 8 | + | tests := []struct { | |||
| 9 | + | name string | |||
| 10 | + | input string | |||
| 11 | + | expected string | |||
| 12 | + | }{ | |||
| 13 | + | {"addsTrailingSlash", "https://duckduckgo.com", "https://duckduckgo.com/"}, | |||
| 14 | + | {"keepsExistingSlash", "https://duckduckgo.com/", "https://duckduckgo.com/"}, | |||
| 15 | + | {"keepsPath", "https://example.com/foo", "https://example.com/foo"}, | |||
| 16 | + | {"keepsQuery", "https://example.com?q=1", "https://example.com/?q=1"}, | |||
| 17 | + | {"httpScheme", "http://example.com", "http://example.com/"}, | |||
| 18 | + | {"nonHttpScheme", "ftp://example.com", "ftp://example.com"}, | |||
| 19 | + | {"invalidURL", "not a url", "not a url"}, | |||
| 20 | + | {"empty", "", ""}, | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | for _, tt := range tests { | |||
| 24 | + | tt := tt | |||
| 25 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 26 | + | t.Parallel() | |||
| 27 | + | if got := NormalizeHistoryURL(tt.input); got != tt.expected { | |||
| 28 | + | t.Fatalf("NormalizeHistoryURL(%q) = %q, want %q", tt.input, got, tt.expected) | |||
| 29 | + | } | |||
| 30 | + | }) | |||
| 31 | + | } | |||
| 32 | + | } | |||