Monorepo for Tangled
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}