// Package search implements the multi-provider search router. // // Three search providers are supported out of the box: // // - ud → Urban Dictionary // - gh → GitHub // - steam → Steam Store // // Each provider has a stable ID, a display name, and a URL template // where {q} is substituted with the URL-encoded query. package search import ( "net/url" "strings" "go.bigb.es/auxilia/culpa" ) // Provider describes a single upstream search engine. type Provider struct { ID string // short prefix, e.g. "ud" Name string // human-readable name, e.g. "Urban Dictionary" Description string // shown in OpenSearch description XML SearchURL string // template containing the literal "{q}" placeholder HomeURL string // upstream homepage, used as fallback IconURL string // 16x16 favicon, optional } // SearchURLFor returns the upstream search URL for the given raw query. // // The query is trimmed and percent-encoded; an empty query returns the // provider's HomeURL so a stray /search?q= still lands somewhere useful. func (p Provider) SearchURLFor(q string) string { q = strings.TrimSpace(q) if q == "" { return p.HomeURL } return strings.ReplaceAll(p.SearchURL, "{q}", url.QueryEscape(q)) } // AllProviders returns the registered providers in stable order. // // The order is also the order they appear in /providers and OpenSearch // listings, so it's intentional rather than a map iteration. func AllProviders() []Provider { return []Provider{ { ID: "ud", Name: "Urban Dictionary", Description: "Search slang definitions on Urban Dictionary", SearchURL: "https://www.urbandictionary.com/define.php?term={q}", HomeURL: "https://www.urbandictionary.com/", IconURL: "https://www.urbandictionary.com/favicon.ico", }, { ID: "gh", Name: "GitHub", Description: "Search repositories, code, and issues on GitHub", SearchURL: "https://github.com/search?q={q}", HomeURL: "https://github.com/", IconURL: "https://github.com/favicon.ico", }, { ID: "steam", Name: "Steam Store", Description: "Search games on the Steam store", SearchURL: "https://store.steampowered.com/search/?term={q}", HomeURL: "https://store.steampowered.com/", IconURL: "https://store.steampowered.com/favicon.ico", }, } } // providerByID indexes AllProviders by ID for O(1) lookup. func providerByID() map[string]Provider { all := AllProviders() m := make(map[string]Provider, len(all)) for _, p := range all { m[p.ID] = p } return m } // Lookup returns the provider with the given ID, or false if none matches. func Lookup(id string) (Provider, bool) { p, ok := providerByID()[id] return p, ok } // Route inspects a raw user query and decides which provider should handle it. // // The first whitespace-delimited token is treated as a possible prefix. // Both "ud foo" and "ud:foo" route to the urbandictionary provider with // the query "foo". If no token matches a provider ID, the default provider // receives the original query unchanged. // // Returns the matched provider and the cleaned query to forward. func Route(raw string, providers map[string]Provider, defaultID string) (Provider, string, error) { def, ok := providers[defaultID] if !ok { return Provider{}, "", culpa.WithCode( culpa.Errorf("default provider %q is not registered", defaultID), "UNKNOWN_PROVIDER", ) } trimmed := strings.TrimSpace(raw) if trimmed == "" { return def, "", nil } // "ud:foo bar" → prefix "ud", rest "foo bar" if i := strings.IndexByte(trimmed, ':'); i > 0 { prefix := strings.ToLower(trimmed[:i]) if p, ok := providers[prefix]; ok { return p, strings.TrimSpace(trimmed[i+1:]), nil } } // "ud foo bar" → prefix "ud", rest "foo bar" if i := strings.IndexByte(trimmed, ' '); i > 0 { prefix := strings.ToLower(trimmed[:i]) if p, ok := providers[prefix]; ok { return p, strings.TrimSpace(trimmed[i+1:]), nil } } // Bare prefix like "ud" with no query — go to that provider's homepage. if p, ok := providers[strings.ToLower(trimmed)]; ok { return p, "", nil } return def, trimmed, nil }