Monorepo for Tangled
at master 237 lines 4.7 kB view raw
1package searchquery 2 3import ( 4 "strings" 5 "unicode" 6) 7 8type ItemKind int 9 10const ( 11 KindKeyword ItemKind = iota 12 KindQuoted 13 KindTagValue 14) 15 16type Item struct { 17 Kind ItemKind 18 Negated bool 19 Raw string 20 Key string 21 Value string 22} 23 24type Query struct { 25 items []Item 26} 27 28func Parse(input string) *Query { 29 q := &Query{} 30 runes := []rune(strings.TrimSpace(input)) 31 if len(runes) == 0 { 32 return q 33 } 34 35 i := 0 36 for i < len(runes) { 37 for i < len(runes) && unicode.IsSpace(runes[i]) { 38 i++ 39 } 40 if i >= len(runes) { 41 break 42 } 43 44 negated := false 45 if runes[i] == '-' && i+1 < len(runes) && runes[i+1] == '"' { 46 negated = true 47 i++ // skip '-' 48 } 49 50 if runes[i] == '"' { 51 start := i 52 if negated { 53 start-- // include the '-' in Raw 54 } 55 i++ // skip opening quote 56 inner := i 57 for i < len(runes) && runes[i] != '"' { 58 if runes[i] == '\\' && i+1 < len(runes) { 59 i++ 60 } 61 i++ 62 } 63 value := unescapeQuoted(runes[inner:i]) 64 if i < len(runes) { 65 i++ // skip closing quote 66 } 67 q.items = append(q.items, Item{ 68 Kind: KindQuoted, 69 Negated: negated, 70 Raw: string(runes[start:i]), 71 Value: value, 72 }) 73 continue 74 } 75 76 start := i 77 for i < len(runes) && !unicode.IsSpace(runes[i]) && runes[i] != '"' { 78 i++ 79 } 80 token := string(runes[start:i]) 81 82 negated = false 83 subject := token 84 if len(subject) > 1 && subject[0] == '-' { 85 negated = true 86 subject = subject[1:] 87 } 88 89 colonIdx := strings.Index(subject, ":") 90 if colonIdx > 0 { 91 key := subject[:colonIdx] 92 value := subject[colonIdx+1:] 93 q.items = append(q.items, Item{ 94 Kind: KindTagValue, 95 Negated: negated, 96 Raw: token, 97 Key: key, 98 Value: value, 99 }) 100 } else { 101 q.items = append(q.items, Item{ 102 Kind: KindKeyword, 103 Negated: negated, 104 Raw: token, 105 Value: subject, 106 }) 107 } 108 } 109 110 return q 111} 112 113func unescapeQuoted(runes []rune) string { 114 var b strings.Builder 115 for i := 0; i < len(runes); i++ { 116 if runes[i] == '\\' && i+1 < len(runes) { 117 i++ 118 } 119 b.WriteRune(runes[i]) 120 } 121 return b.String() 122} 123 124func (q *Query) Items() []Item { 125 return q.items 126} 127 128func (q *Query) Get(key string) *string { 129 for i, item := range q.items { 130 if item.Kind == KindTagValue && !item.Negated && item.Key == key { 131 return &q.items[i].Value 132 } 133 } 134 return nil 135} 136 137func (q *Query) GetAll(key string) []string { 138 var result []string 139 for _, item := range q.items { 140 if item.Kind == KindTagValue && !item.Negated && item.Key == key { 141 result = append(result, item.Value) 142 } 143 } 144 return result 145} 146 147func (q *Query) GetAllNegated(key string) []string { 148 var result []string 149 for _, item := range q.items { 150 if item.Kind == KindTagValue && item.Negated && item.Key == key { 151 result = append(result, item.Value) 152 } 153 } 154 return result 155} 156 157func (q *Query) Has(key string) bool { 158 return q.Get(key) != nil 159} 160 161// KnownTags is the set of tag keys with special system-defined handling. 162// Any tag:value pair whose key is not in this set is treated as a dynamic 163// label filter. 164var KnownTags = map[string]bool{ 165 "state": true, 166 "author": true, 167 "label": true, 168} 169 170// GetDynamicTags returns composite "key:value" strings for all non-negated 171// tag:value items whose key is not a known system tag. 172func (q *Query) GetDynamicTags() []string { 173 var result []string 174 for _, item := range q.items { 175 if item.Kind == KindTagValue && !item.Negated && !KnownTags[item.Key] { 176 result = append(result, item.Key+":"+item.Value) 177 } 178 } 179 return result 180} 181 182// GetNegatedDynamicTags returns composite "key:value" strings for all negated 183// tag:value items whose key is not a known system tag. 184func (q *Query) GetNegatedDynamicTags() []string { 185 var result []string 186 for _, item := range q.items { 187 if item.Kind == KindTagValue && item.Negated && !KnownTags[item.Key] { 188 result = append(result, item.Key+":"+item.Value) 189 } 190 } 191 return result 192} 193 194func (q *Query) Set(key, value string) { 195 raw := key + ":" + value 196 found := false 197 newItems := make([]Item, 0, len(q.items)) 198 199 for _, item := range q.items { 200 if item.Kind == KindTagValue && !item.Negated && item.Key == key { 201 if !found { 202 newItems = append(newItems, Item{ 203 Kind: KindTagValue, 204 Raw: raw, 205 Key: key, 206 Value: value, 207 }) 208 found = true 209 } 210 } else { 211 newItems = append(newItems, item) 212 } 213 } 214 215 if !found { 216 newItems = append(newItems, Item{ 217 Kind: KindTagValue, 218 Raw: raw, 219 Key: key, 220 Value: value, 221 }) 222 } 223 224 q.items = newItems 225} 226 227func (q *Query) String() string { 228 if len(q.items) == 0 { 229 return "" 230 } 231 232 parts := make([]string, len(q.items)) 233 for i, item := range q.items { 234 parts[i] = item.Raw 235 } 236 return strings.Join(parts, " ") 237}