magiclinks.go (3023B)
1 package app 2 3 import ( 4 "net/url" 5 "regexp" 6 "sort" 7 "strings" 8 ) 9 10 const maxMagicLinksPerRule = 10 11 12 type MagicLinkRule struct { 13 Name string `json:"name"` 14 Category string `json:"category"` 15 Value string `json:"value"` 16 Template string `json:"template"` 17 } 18 19 type MagicLink struct { 20 Label string 21 URL string 22 } 23 24 var magicLinkVarPattern = regexp.MustCompile(`\$(?:\{([^}]+)\}|([A-Za-z0-9_]+))`) 25 26 func magicLinkVarName(m []string) string { 27 if m[1] != "" { 28 return strings.TrimSpace(m[1]) 29 } 30 return m[2] 31 } 32 33 // magic links must start with http(s) 34 func validMagicLinkTemplate(tmpl string) bool { 35 lower := strings.ToLower(tmpl) 36 return strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") 37 } 38 39 func tagValuesFold(tags map[string][]string, name string) []string { 40 var values []string 41 for cat, vals := range tags { 42 if strings.EqualFold(cat, name) { 43 values = append(values, vals...) 44 } 45 } 46 sort.Strings(values) 47 return values 48 } 49 50 func ruleMatches(rule MagicLinkRule, tags map[string][]string) bool { 51 accepted := expandTagWithAliases(rule.Category, rule.Value) 52 for _, have := range tagValuesFold(tags, rule.Category) { 53 for _, want := range accepted { 54 if strings.EqualFold(have, want) { 55 return true 56 } 57 } 58 } 59 return false 60 } 61 62 // Skip when nothing matches, build multiple with multiple matches 63 func buildMagicLinks(rules []MagicLinkRule, tags map[string][]string) []MagicLink { 64 var links []MagicLink 65 seen := make(map[string]bool) 66 67 for _, rule := range rules { 68 if !ruleMatches(rule, tags) { 69 continue 70 } 71 72 // Collect each distinct variable used by the template, in order. 73 var varNames []string 74 varValues := make(map[string][]string) 75 missing := false 76 for _, m := range magicLinkVarPattern.FindAllStringSubmatch(rule.Template, -1) { 77 name := magicLinkVarName(m) 78 key := strings.ToLower(name) 79 if _, done := varValues[key]; done { 80 continue 81 } 82 vals := tagValuesFold(tags, name) 83 if len(vals) == 0 { 84 missing = true 85 break 86 } 87 varNames = append(varNames, key) 88 varValues[key] = vals 89 } 90 if missing { 91 continue 92 } 93 94 // Expand the product of variable values. 95 combos := []map[string]string{{}} 96 for _, key := range varNames { 97 var next []map[string]string 98 for _, combo := range combos { 99 for _, v := range varValues[key] { 100 c := make(map[string]string, len(combo)+1) 101 for k, val := range combo { 102 c[k] = val 103 } 104 c[key] = v 105 next = append(next, c) 106 if len(next) >= maxMagicLinksPerRule { 107 break 108 } 109 } 110 if len(next) >= maxMagicLinksPerRule { 111 break 112 } 113 } 114 combos = next 115 } 116 117 for _, combo := range combos { 118 built := magicLinkVarPattern.ReplaceAllStringFunc(rule.Template, func(s string) string { 119 m := magicLinkVarPattern.FindStringSubmatch(s) 120 return url.PathEscape(combo[strings.ToLower(magicLinkVarName(m))]) 121 }) 122 if seen[built] { 123 continue 124 } 125 seen[built] = true 126 links = append(links, MagicLink{Label: rule.Name, URL: built}) 127 } 128 } 129 130 return links 131 }