tagliatelle

Simple self-hosted tagging filesystem webapp
Log | Files | Refs

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 }