1// Package url provides URL manipulation utilities for the browser.
2package url
3
4import (
5 "fmt"
6 "net/url"
7 "os"
8 "path/filepath"
9 "strings"
10)
11
12// expandHome expands ~ prefix to user's home directory.
13func expandHome(path string) string {
14 if strings.HasPrefix(path, "~/") {
15 if home, err := os.UserHomeDir(); err == nil {
16 return filepath.Join(home, path[2:])
17 }
18 }
19 return path
20}
21
22// looksLikeFilePath returns true if the input looks like a filesystem path.
23// This includes absolute paths (/path), relative paths (./path, ../path),
24// and home-relative paths (~/path).
25func looksLikeFilePath(input string) bool {
26 switch {
27 case strings.HasPrefix(input, "/"):
28 return true
29 case strings.HasPrefix(input, "./"):
30 return true
31 case strings.HasPrefix(input, "../"):
32 return true
33 case strings.HasPrefix(input, "~/"):
34 return true
35 }
36 return false
37}
38
39// Normalize adds https:// prefix if missing for URL-like inputs.
40// Returns the input unchanged if it already has a scheme or doesn't look like a URL.
41// If the input is an existing local file, returns a file:// URL.
42func Normalize(input string) string {
43 if input == "" {
44 return ""
45 }
46
47 // Already has scheme
48 switch {
49 case strings.HasPrefix(input, "http://"):
50 return input
51 case strings.HasPrefix(input, "https://"):
52 return input
53 case strings.HasPrefix(input, "dumb://"):
54 return input
55 case strings.HasPrefix(input, "file://"):
56 return input
57 case strings.HasPrefix(input, "about:"):
58 return input
59 }
60
61 // Check if input looks like a file path
62 if looksLikeFilePath(input) {
63 expanded := expandHome(input)
64 absPath, err := filepath.Abs(expanded)
65 if err == nil {
66 if _, statErr := os.Stat(absPath); statErr == nil {
67 // File exists, convert to file:// URL
68 return "file://" + absPath
69 }
70 }
71 // Path-like input but file doesn't exist - return unchanged
72 // Don't treat as URL to avoid malformed URLs like https:///path
73 return input
74 }
75
76 // Check if input is an existing file in current directory (relative without ./ prefix)
77 absPath, err := filepath.Abs(input)
78 if err == nil {
79 if _, statErr := os.Stat(absPath); statErr == nil {
80 return "file://" + absPath
81 }
82 }
83
84 // Looks like a URL (contains . and no spaces)
85 if input == "localhost" || strings.HasPrefix(input, "localhost:") || strings.HasPrefix(input, "localhost/") {
86 return "http://" + input
87 }
88 if strings.Contains(input, ".") && !strings.Contains(input, " ") {
89 return "https://" + input
90 }
91
92 return input
93}
94
95// LooksLikeURL checks if the input appears to be a URL (not a search query).
96// Returns true for strings like "github.com", "google.com/search", etc.
97// Also returns true for URLs with explicit schemes like "dumb://".
98func LooksLikeURL(input string) bool {
99 if input == "" {
100 return false
101 }
102
103 // Explicit schemes should always be treated as URLs.
104 switch {
105 case strings.HasPrefix(input, "http://"):
106 return true
107 case strings.HasPrefix(input, "https://"):
108 return true
109 case strings.HasPrefix(input, "dumb://"):
110 return true
111 case strings.HasPrefix(input, "file://"):
112 return true
113 case strings.HasPrefix(input, "about:"):
114 return true
115 }
116
117 // Contains a dot and no spaces = likely a URL
118 if input == "localhost" || strings.HasPrefix(input, "localhost:") || strings.HasPrefix(input, "localhost/") {
119 return true
120 }
121 return strings.Contains(input, ".") && !strings.Contains(input, " ")
122}
123
124// ExtractDomain extracts the normalized domain (host) from a URL string.
125// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
126// resolve to the same value.
127func ExtractDomain(rawURL string) string {
128 if rawURL == "" {
129 return ""
130 }
131 parsed, err := url.Parse(rawURL)
132 if err != nil || parsed.Host == "" {
133 return ""
134 }
135 return strings.TrimPrefix(parsed.Host, "www.")
136}
137
138// SanitizeDomainForFilename converts a domain to a safe filename with .ico extension.
139// Replaces unsafe filesystem characters with underscores.
140func SanitizeDomainForFilename(domain string) string {
141 return sanitizeDomain(domain) + ".ico"
142}
143
144// SanitizeDomainForPNG converts a domain to a safe filename with .png extension.
145// Used for favicon export for tools like rofi/fuzzel that require PNG format.
146func SanitizeDomainForPNG(domain string) string {
147 return sanitizeDomain(domain) + ".png"
148}
149
150// sanitizeDomain replaces unsafe filesystem characters with underscores.
151func sanitizeDomain(domain string) string {
152 replacer := strings.NewReplacer(
153 ":", "_",
154 "/", "_",
155 "\\", "_",
156 "*", "_",
157 "?", "_",
158 "\"", "_",
159 "<", "_",
160 ">", "_",
161 "|", "_",
162 )
163 return replacer.Replace(domain)
164}
165
166// SanitizeDomainForPNGSized converts a domain to a safe filename with size suffix.
167// Example: "google.com" with size 32 -> "google.com.32.png"
168// Used for normalized favicon export for tools like rofi/fuzzel.
169func SanitizeDomainForPNGSized(domain string, size int) string {
170 return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size)
171}