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
161func (q *Query) Set(key, value string) {
162 raw := key + ":" + value
163 found := false
164 newItems := make([]Item, 0, len(q.items))
165
166 for _, item := range q.items {
167 if item.Kind == KindTagValue && !item.Negated && item.Key == key {
168 if !found {
169 newItems = append(newItems, Item{
170 Kind: KindTagValue,
171 Raw: raw,
172 Key: key,
173 Value: value,
174 })
175 found = true
176 }
177 } else {
178 newItems = append(newItems, item)
179 }
180 }
181
182 if !found {
183 newItems = append(newItems, Item{
184 Kind: KindTagValue,
185 Raw: raw,
186 Key: key,
187 Value: value,
188 })
189 }
190
191 q.items = newItems
192}
193
194func (q *Query) String() string {
195 if len(q.items) == 0 {
196 return ""
197 }
198
199 parts := make([]string, len(q.items))
200 for i, item := range q.items {
201 parts[i] = item.Raw
202 }
203 return strings.Join(parts, " ")
204}