package icon import ( "encoding/json" "errors" "fmt" "net/url" "sort" "strings" "time" "unicode" ) var iconifyBase = "https://api.iconify.design" // SetSearchEndpoint mengarahkan pencarian ikon ke endpoint lain (dipakai tes). func SetSearchEndpoint(base string) { iconifyBase = base } // Result adalah satu ikon dari pencarian online. type Result struct { Name string // contoh: "logos:whatsapp-icon" URL string // URL SVG siap diunduh } // ShortName mengembalikan nama ikon tanpa prefix koleksi. func (r Result) ShortName() string { _, name, ok := strings.Cut(r.Name, ":") if !ok { return r.Name } return name } // Search mencari ikon di Iconify berdasarkan kata kunci. func Search(query string, limit int) ([]Result, error) { query = strings.TrimSpace(query) if query == "" { return nil, errors.New("search query is empty") } if limit <= 0 { limit = 32 } endpoint := fmt.Sprintf("%s/search?query=%s&limit=%d", iconifyBase, url.QueryEscape(query), limit) data, err := get(endpoint) if err != nil { return nil, err } var payload struct { Icons []string `json:"icons"` } if err := json.Unmarshal(data, &payload); err != nil { return nil, err } results := make([]Result, 0, len(payload.Icons)) for _, name := range payload.Icons { prefix, icon, ok := strings.Cut(name, ":") if !ok { continue } // height=256 supaya SVG-nya punya ukuran eksplisit (bukan "1em") results = append(results, Result{ Name: name, URL: fmt.Sprintf("%s/%s/%s.svg?height=256", iconifyBase, url.PathEscape(prefix), url.PathEscape(icon)), }) } if len(results) == 0 { return nil, errors.New("no icon found for " + query) } return results, nil } // SearchBest mencoba beberapa variasi kata kunci supaya nama seperti // "web.whatsapp.com" atau "WhatsApp Web" tetap menemukan ikon. func SearchBest(query string, limit int) ([]Result, error) { for _, candidate := range queryVariants(query) { if results, err := Search(candidate, limit); err == nil { return results, nil } } return nil, errors.New("no icon found for " + strings.TrimSpace(query)) } // Find mengambil hasil pertama yang namanya cocok dengan kata kunci, // supaya ikon acak tidak dipasang ke aplikasi yang namanya tidak dikenal. func Find(query string) (Result, bool) { for _, candidate := range queryVariants(query) { results, err := Search(candidate, 32) if err != nil { continue } want := slug(candidate) for _, r := range results { name := slug(r.ShortName()) if name == want || strings.Contains(name, want) || strings.Contains(want, name) { return r, true } } } return Result{}, false } // QueryHint mengembalikan kata kunci paling bermakna dari sebuah nama, // dipakai untuk mengisi kolom pencarian ikon. func QueryHint(query string) string { variants := queryVariants(query) if len(variants) > 1 { return variants[1] } return strings.TrimSpace(query) } // queryVariants menyusun kandidat kata kunci: teks asli dulu, lalu kata-kata // yang lebih sederhana (kata terpanjang didahulukan). Kata < 4 huruf dibuang // supaya pencarian tidak jatuh ke ikon acak. func queryVariants(query string) []string { query = strings.TrimSpace(query) if query == "" { return nil } out := []string{query} base := query // host seperti "web.whatsapp.com" → buang TLD-nya if !strings.Contains(base, " ") && strings.Contains(base, ".") { parts := strings.Split(base, ".") if len(parts) > 1 { base = strings.Join(parts[:len(parts)-1], " ") } } words := strings.FieldsFunc(base, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }) sort.SliceStable(words, func(i, j int) bool { return len(words[i]) > len(words[j]) }) for _, word := range words { if len(word) < 4 || strings.EqualFold(word, query) { continue } out = append(out, word) } return out } // FromURL mengunduh ikon dari URL dan menebak ekstensinya dari isi file. func FromURL(rawURL string) ([]byte, string, error) { data, err := get(rawURL) if err != nil { // sekali coba lagi: API ikon publik kadang membatasi permintaan beruntun time.Sleep(600 * time.Millisecond) data, err = get(rawURL) } if err != nil { return nil, "", err } ext := sniffExt(data) if ext == "" { return nil, "", fmt.Errorf("unsupported icon format at %s", rawURL) } return data, ext, nil } // slug menyamakan nama untuk perbandingan (huruf kecil, tanpa pemisah). func slug(s string) string { var b strings.Builder for _, r := range strings.ToLower(s) { if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' { b.WriteRune(r) } } return b.String() }