// Package url provides URL manipulation utilities for the browser. package url import ( "fmt" "net/url" "os" "path/filepath" "strings" ) // expandHome expands ~ prefix to user's home directory. func expandHome(path string) string { if strings.HasPrefix(path, "~/") { if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, path[2:]) } } return path } // looksLikeFilePath returns true if the input looks like a filesystem path. // This includes absolute paths (/path), relative paths (./path, ../path), // and home-relative paths (~/path). func looksLikeFilePath(input string) bool { switch { case strings.HasPrefix(input, "/"): return true case strings.HasPrefix(input, "./"): return true case strings.HasPrefix(input, "../"): return true case strings.HasPrefix(input, "~/"): return true } return false } // Normalize adds https:// prefix if missing for URL-like inputs. // Returns the input unchanged if it already has a scheme or doesn't look like a URL. // If the input is an existing local file, returns a file:// URL. func Normalize(input string) string { if input == "" { return "" } // Already has scheme switch { case strings.HasPrefix(input, "http://"): return input case strings.HasPrefix(input, "https://"): return input case strings.HasPrefix(input, "dumb://"): return input case strings.HasPrefix(input, "file://"): return input case strings.HasPrefix(input, "about:"): return input } // Check if input looks like a file path if looksLikeFilePath(input) { expanded := expandHome(input) absPath, err := filepath.Abs(expanded) if err == nil { if _, statErr := os.Stat(absPath); statErr == nil { // File exists, convert to file:// URL return "file://" + absPath } } // Path-like input but file doesn't exist - return unchanged // Don't treat as URL to avoid malformed URLs like https:///path return input } // Check if input is an existing file in current directory (relative without ./ prefix) absPath, err := filepath.Abs(input) if err == nil { if _, statErr := os.Stat(absPath); statErr == nil { return "file://" + absPath } } // Looks like a URL (contains . and no spaces) if strings.Contains(input, ".") && !strings.Contains(input, " ") { return "https://" + input } return input } // LooksLikeURL checks if the input appears to be a URL (not a search query). // Returns true for strings like "github.com", "google.com/search", etc. // Also returns true for URLs with explicit schemes like "dumb://". func LooksLikeURL(input string) bool { if input == "" { return false } // Explicit schemes should always be treated as URLs. switch { case strings.HasPrefix(input, "http://"): return true case strings.HasPrefix(input, "https://"): return true case strings.HasPrefix(input, "dumb://"): return true case strings.HasPrefix(input, "file://"): return true case strings.HasPrefix(input, "about:"): return true } // Contains a dot and no spaces = likely a URL return strings.Contains(input, ".") && !strings.Contains(input, " ") } // ExtractDomain extracts the normalized domain (host) from a URL string. // Normalizes by stripping "www." prefix so youtube.com and www.youtube.com // resolve to the same value. func ExtractDomain(rawURL string) string { if rawURL == "" { return "" } parsed, err := url.Parse(rawURL) if err != nil || parsed.Host == "" { return "" } return strings.TrimPrefix(parsed.Host, "www.") } // SanitizeDomainForFilename converts a domain to a safe filename with .ico extension. // Replaces unsafe filesystem characters with underscores. func SanitizeDomainForFilename(domain string) string { return sanitizeDomain(domain) + ".ico" } // SanitizeDomainForPNG converts a domain to a safe filename with .png extension. // Used for favicon export for tools like rofi/fuzzel that require PNG format. func SanitizeDomainForPNG(domain string) string { return sanitizeDomain(domain) + ".png" } // sanitizeDomain replaces unsafe filesystem characters with underscores. func sanitizeDomain(domain string) string { replacer := strings.NewReplacer( ":", "_", "/", "_", "\\", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_", ) return replacer.Replace(domain) } // SanitizeDomainForPNGSized converts a domain to a safe filename with size suffix. // Example: "google.com" with size 32 -> "google.com.32.png" // Used for normalized favicon export for tools like rofi/fuzzel. func SanitizeDomainForPNGSized(domain string, size int) string { return fmt.Sprintf("%s.%d.png", sanitizeDomain(domain), size) }