package country import ( "sort" "strings" ) type entry struct { Code string Name string Keywords []string } var ( sortedKeywords []keywordMatch nameByCode map[string]string ) type keywordMatch struct { keyword string code string } func init() { nameByCode = make(map[string]string, len(catalog)) for _, e := range catalog { nameByCode[e.Code] = e.Name for _, kw := range e.Keywords { sortedKeywords = append(sortedKeywords, keywordMatch{keyword: strings.ToLower(kw), code: e.Code}) } } sort.Slice(sortedKeywords, func(i, j int) bool { return len(sortedKeywords[i].keyword) > len(sortedKeywords[j].keyword) }) } // Detect returns ISO-like country code from node name, or empty string. func Detect(name string) string { lower := strings.ToLower(strings.TrimSpace(name)) if lower == "" { return "" } normalized := strings.NewReplacer( "_", " ", "-", " ", "|", " ", "/", " ", "[", " ", "]", " ", "(", " ", ")", " ", "【", " ", "】", " ", "(", " ", ")", " ", ).Replace(lower) padded := " " + normalized + " " for _, m := range sortedKeywords { kw := m.keyword if len(kw) <= 2 { continue } if keywordMatches(padded, kw) { return m.code } } for _, tok := range strings.Fields(normalized) { tok = strings.TrimSpace(tok) if tok == "" { continue } if len(tok) == 2 { if _, ok := nameByCode[strings.ToUpper(tok)]; ok { return strings.ToUpper(tok) } continue } if len(tok) > 2 { prefix := strings.ToUpper(tok[:2]) if _, ok := nameByCode[prefix]; !ok { continue } suffix := tok[2:] if suffix == "" { continue } allDigits := true for _, r := range suffix { if r < '0' || r > '9' { allDigits = false break } } if allDigits { return prefix } } } return "" } // Name returns Chinese display name for a country code. func Name(code string) string { if n, ok := nameByCode[strings.ToUpper(code)]; ok { return n } return code } // GroupRuntime returns Mihomo proxy-group name for a country pool. func GroupRuntime(code string) string { return "country_" + strings.ToUpper(strings.TrimSpace(code)) } // TargetAlias returns rule target alias stored in DB, e.g. country:HK. func TargetAlias(code string) string { return "country:" + strings.ToUpper(strings.TrimSpace(code)) } // ParseTargetAlias parses country:XX to code, ok false if not a country target. func ParseTargetAlias(target string) (string, bool) { if !strings.HasPrefix(strings.ToLower(target), "country:") { return "", false } code := strings.TrimSpace(target[len("country:"):]) if code == "" { return "", false } return strings.ToUpper(code), true } // All returns catalog entries for UI. func All() []struct{ Code, Name string } { out := make([]struct{ Code, Name string }, 0, len(catalog)) for _, e := range catalog { out = append(out, struct{ Code, Name string }{Code: e.Code, Name: e.Name}) } return out } func keywordMatches(text, kw string) bool { if !strings.Contains(text, kw) { return false } if containsHan(kw) { return true } start := 0 for { idx := strings.Index(text[start:], kw) if idx < 0 { return false } pos := start + idx before := pos == 0 || !isASCIIAlphaNum(text[pos-1]) afterEnd := pos + len(kw) after := afterEnd >= len(text) || !isASCIIAlphaNum(text[afterEnd]) if before && after { return true } start = pos + 1 } } func isASCIIAlphaNum(b byte) bool { return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') } func containsHan(s string) bool { for _, r := range s { if r >= 0x4e00 && r <= 0x9fff { return true } } return false }