v0.33.2 57f21d6

key.go RawHistoryFolder

1package favicon
2
3import (
4	"net"
5	"net/url"
6	"path"
7	"slices"
8	"strings"
9
10	"golang.org/x/net/idna"
11	"golang.org/x/net/publicsuffix"
12)
13
14type Key string
15
16func CanonicalKey(raw string) (Key, bool) {
17	candidates := Candidates(raw)
18	if len(candidates) == 0 {
19		return "", false
20	}
21	return candidates[0], true
22}
23
24func CanonicalHostKey(raw string) (Key, bool) {
25	candidates := hostCandidates(raw)
26	if len(candidates) == 0 {
27		return "", false
28	}
29	return candidates[0], true
30}
31
32func Candidates(raw string) []Key {
33	parsed := parseHost(raw)
34	if !parsed.ok {
35		return nil
36	}
37	return withPathCandidates(hostCandidatesForParsed(parsed), parsed.rawPath)
38}
39
40func hostCandidates(raw string) []Key {
41	parsed := parseHost(raw)
42	if !parsed.ok {
43		return nil
44	}
45	return hostCandidatesForParsed(parsed)
46}
47
48func hostCandidatesForParsed(parsed parsedHost) []Key {
49	host := normalizeHost(parsed.host)
50	if host == "" {
51		return nil
52	}
53
54	var candidates []Key
55	if isExactHost(host) {
56		if parsed.port != "" && (!parsed.hasScheme || !isDefaultPort(parsed.scheme, parsed.port)) {
57			return []Key{Key(net.JoinHostPort(host, parsed.port))}
58		}
59		return []Key{Key(host)}
60	}
61
62	ascii, err := idna.Lookup.ToASCII(host)
63	if err != nil {
64		return nil
65	}
66	host = strings.ToLower(strings.TrimSuffix(ascii, "."))
67	host = stripWWW(host)
68
69	if parsed.port != "" {
70		if !parsed.hasScheme || !isDefaultPort(parsed.scheme, parsed.port) {
71			return []Key{Key(net.JoinHostPort(host, parsed.port))}
72		}
73	}
74
75	labels := strings.Split(host, ".")
76	if slices.Contains(labels, "") {
77		return nil
78	}
79
80	registrable, err := publicsuffix.EffectiveTLDPlusOne(host)
81	if err != nil {
82		return []Key{Key(host)}
83	}
84
85	for i := range len(labels) {
86		candidate := strings.Join(labels[i:], ".")
87		candidates = append(candidates, Key(candidate))
88		if candidate == registrable {
89			break
90		}
91	}
92	return candidates
93}
94
95func withPathCandidates(hostCandidates []Key, rawPath string) []Key {
96	if len(hostCandidates) == 0 {
97		return nil
98	}
99	paths := pathCandidates(rawPath)
100	if len(paths) == 0 {
101		return hostCandidates
102	}
103
104	out := make([]Key, 0, len(paths)+len(hostCandidates))
105	base := string(hostCandidates[0])
106	for _, p := range paths {
107		out = append(out, Key(base+p))
108	}
109	out = append(out, hostCandidates...)
110	return out
111}
112
113func pathCandidates(rawPath string) []string {
114	p := normalizePath(rawPath)
115	if p == "" {
116		return nil
117	}
118
119	var out []string
120	for p != "" && p != "/" {
121		out = append(out, p)
122		idx := strings.LastIndex(p, "/")
123		if idx <= 0 {
124			break
125		}
126		p = p[:idx]
127	}
128	return out
129}
130
131func normalizePath(rawPath string) string {
132	if rawPath == "" {
133		return ""
134	}
135	if cut := strings.IndexAny(rawPath, "?#"); cut >= 0 {
136		rawPath = rawPath[:cut]
137	}
138	if rawPath == "" || rawPath == "/" {
139		return ""
140	}
141	if !strings.HasPrefix(rawPath, "/") {
142		rawPath = "/" + rawPath
143	}
144	cleaned := path.Clean(rawPath)
145	if cleaned == "." || cleaned == "/" {
146		return ""
147	}
148	return strings.TrimRight(cleaned, "/")
149}
150
151type parsedHost struct {
152	host      string
153	port      string
154	hasScheme bool
155	scheme    string
156	rawPath   string
157	ok        bool
158}
159
160func parseHost(raw string) parsedHost {
161	raw = strings.TrimSpace(raw)
162	if raw == "" {
163		return parsedHost{}
164	}
165
166	if strings.Contains(raw, "://") {
167		u, err := url.Parse(raw)
168		if err != nil || u.Scheme == "" {
169			return parsedHost{}
170		}
171		scheme := strings.ToLower(u.Scheme)
172		if scheme != "http" && scheme != "https" {
173			return parsedHost{hasScheme: true, scheme: scheme}
174		}
175		host := u.Hostname()
176		return parsedHost{
177			host:      host,
178			port:      u.Port(),
179			hasScheme: true,
180			scheme:    scheme,
181			rawPath:   u.EscapedPath(),
182			ok:        host != "",
183		}
184	}
185	if strings.HasPrefix(strings.ToLower(raw), "about:") {
186		return parsedHost{}
187	}
188
189	hostport := raw
190	rawPath := ""
191	if cut := strings.IndexAny(hostport, "/?#"); cut >= 0 {
192		rawPath = hostport[cut:]
193		hostport = hostport[:cut]
194	}
195	hostport = strings.TrimSuffix(hostport, ".")
196
197	if h, p, err := net.SplitHostPort(hostport); err == nil {
198		return parsedHost{host: h, port: p, rawPath: rawPath, ok: h != ""}
199	}
200	if strings.Count(hostport, ":") == 1 {
201		if h, p, ok := strings.Cut(hostport, ":"); ok && h != "" && p != "" {
202			return parsedHost{host: h, port: p, rawPath: rawPath, ok: true}
203		}
204	}
205	return parsedHost{host: hostport, rawPath: rawPath, ok: hostport != ""}
206}
207
208func normalizeHost(host string) string {
209	host = strings.TrimSpace(host)
210	host = strings.Trim(host, "[]")
211	host = strings.TrimSuffix(host, ".")
212	return strings.ToLower(host)
213}
214
215func stripWWW(host string) string {
216	return strings.TrimPrefix(host, "www.")
217}
218
219func isDefaultPort(scheme, port string) bool {
220	return (scheme == "http" && port == "80") || (scheme == "https" && port == "443")
221}
222
223func isExactHost(host string) bool {
224	return host == "localhost" || net.ParseIP(host) != nil
225}

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