detached 696087a

feat(parser): add URL normalization for consistent history storage

commit
696087ad9f94ec3c6f8fbd843fa50be0fecd05d3
author
bnema · · 2025-11-19 18:23 UTC
parent
f101a25
signature
GPG Unverified2B82207D370127A0 The 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close