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 strings.Contains(input, ".") && !strings.Contains(input, " ") {
86 return "https://" + input
87 }
88
89 return input
90}
91
92// LooksLikeURL checks if the input appears to be a URL (not a search query).
93// Returns true for strings like "github.com", "google.com/search", etc.
94// Also returns true for URLs with explicit schemes like "dumb://".
95func LooksLikeURL(input string) bool {
96 if input == "" {
97 return false
98 }
99
100 // Explicit schemes should always be treated as URLs.
101 switch {
102 case strings.HasPrefix(input, "http://"):
103 return true
104 case strings.HasPrefix(input, "https://"):
105 return true
106 case strings.HasPrefix(input, "dumb://"):
107 return true
108 case strings.HasPrefix(input, "file://"):
109 return true
110 case strings.HasPrefix(input, "about:"):
111 return true
112 }
113
114 // Contains a dot and no spaces = likely a URL
115 return strings.Contains(input, ".") && !strings.Contains(input, " ")
116}
117
118// ExtractDomain extracts the normalized domain (host) from a URL string.
119// Normalizes by stripping "www." prefix so youtube.com and www.youtube.com
120// resolve to the same value.
121func ExtractDomain(rawURL string) string {
122 if rawURL == "" {
123 return ""
124 }
125 parsed, err := url.Parse(rawURL)
126 if err != nil || parsed.Host == "" {
127 return ""
128 }
129 return strings.TrimPrefix(parsed.Host, "www.")
130}
131
132// SanitizeDomainForFilename converts a domain to a safe filename with .ico extension.
133// Replaces unsafe filesystem characters with underscores.
134func SanitizeDomainForFilename(domain string) string {
135 return sanitizeDomain(domain) + ".ico"
136}
137
138// SanitizeDomainForPNG converts a domain to a safe filename with .png extension.
139// Used for favicon export for tools like rofi/fuzzel that require PNG format.
140func SanitizeDomainForPNG(domain string) string {
141 return sanitizeDomain(domain) + ".png"
142}
143
144// sanitizeDomain replaces unsafe filesystem characters with underscores.
145func sanitizeDomain(domain string) string {
146 replacer := strings.NewReplacer(
147 ":", "_",
148 "/", "_",
149 "\\", "_",
150 "*", "_",
151 "?", "_",
152 "\"", "_",
153 "<", "_",
154 ">", "_",
155 "|", "_",
156 )
157 return replacer.Replace(domain)
158}
159
160// SanitizeDomainForPNGSized converts a domain to a safe filename with size suffix.
161// Example: "google.com" with size 32 -> "google.com.32.png"
162// Used for normalized favicon export for tools like rofi/fuzzel.
163func SanitizeDomainForPNGSized(domain string, size int) string {
164 return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size)
165}