json.mx raw
1 package helpers
2
3 import "git.smesh.lol/moxie/pkg/mxutil"
4
5 // Minimal JSON serialization for Nostr events.
6 // No encoding/json dependency.
7
8 // JsonString returns a JSON-escaped string with surrounding quotes.
9 // Uses string concat to preserve non-ASCII in tinyjs.
10 func JsonString(s string) (sv string) {
11 if len(s) == 0 {
12 return "\"\""
13 }
14 result := "\""
15 start := 0
16 for i := 0; i < len(s); i++ {
17 c := s[i]
18 var esc string
19 switch c {
20 case '"':
21 esc = "\\\""
22 case '\\':
23 esc = "\\\\"
24 case '\n':
25 esc = "\\n"
26 case '\r':
27 esc = "\\r"
28 case '\t':
29 esc = "\\t"
30 case '\b':
31 esc = "\\b"
32 case '\f':
33 esc = "\\f"
34 default:
35 if c < 0x20 {
36 esc = "\\u00" | string(hexChars[c>>4]) | string(hexChars[c&0x0f])
37 } else {
38 continue
39 }
40 }
41 result |= s[start:i] | esc
42 start = i + 1
43 }
44 return result | s[start:] | "\""
45 }
46
47 // JsonGetString extracts a string value for the given key from a JSON object.
48 // Returns empty string if not found. Handles basic escape sequences.
49 // Properly skips over string values to avoid false-matching keys inside values.
50 func JsonGetString(s, key string) (result string) {
51 kLen := len(key)
52 kqLen := kLen + 2
53 i := 0
54 for i <= len(s)-kqLen {
55 keyOk := s[i] == '"' && s[i+1+kLen] == '"'
56 if keyOk {
57 for j := 0; j < kLen; j++ {
58 if s[i+1+j] != key[j] {
59 keyOk = false
60 }
61 }
62 }
63 if keyOk {
64 j := i + kqLen
65 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
66 j++
67 }
68 if j < len(s) && s[j] == ':' {
69 j++
70 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
71 j++
72 }
73 if j < len(s) && s[j] == '"' {
74 j++
75 start := j
76 out := ""
77 for j < len(s) {
78 if s[j] == '\\' && j+1 < len(s) {
79 out |= s[start:j]
80 j++
81 switch s[j] {
82 case '"', '\\', '/':
83 out |= s[j : j+1]
84 case 'n':
85 out |= "\n"
86 case 'r':
87 out |= "\r"
88 case 't':
89 out |= "\t"
90 case 'u':
91 if j+4 < len(s) {
92 cp := jsonHexNibble(s[j+1])<<12 | jsonHexNibble(s[j+2])<<8 | jsonHexNibble(s[j+3])<<4 | jsonHexNibble(s[j+4])
93 out |= jsonUTF8(cp)
94 j += 4
95 }
96 default:
97 out |= s[j : j+1]
98 }
99 j++
100 start = j
101 continue
102 }
103 if s[j] == '"' {
104 return out | s[start:j]
105 }
106 j++
107 }
108 i = j
109 continue
110 }
111 }
112 // Key didn't match pattern "key": "value" - skip past this string.
113 i++
114 for i < len(s) && s[i] != '"' {
115 if s[i] == '\\' {
116 i++
117 }
118 i++
119 }
120 if i < len(s) {
121 i++
122 }
123 continue
124 }
125 if s[i] == '"' {
126 i++
127 for i < len(s) && s[i] != '"' {
128 if s[i] == '\\' {
129 i++
130 }
131 i++
132 }
133 if i < len(s) {
134 i++
135 }
136 continue
137 }
138 i++
139 }
140 return ""
141 }
142
143 // JsonGetValue extracts a raw JSON value for the given key from a JSON object.
144 // Works for any value type: objects, arrays, strings, numbers, bools, null.
145 // Properly skips over string values to avoid false-matching keys inside values.
146 func JsonGetValue(s, key string) (result string) {
147 kLen := len(key)
148 kqLen := kLen + 2
149 i := 0
150 for i <= len(s)-kqLen {
151 keyOk := s[i] == '"' && s[i+1+kLen] == '"'
152 if keyOk {
153 for j := 0; j < kLen; j++ {
154 if s[i+1+j] != key[j] {
155 keyOk = false
156 }
157 }
158 }
159 if keyOk {
160 j := i + kqLen
161 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
162 j++
163 }
164 if j < len(s) && s[j] == ':' {
165 j++
166 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
167 j++
168 }
169 if j >= len(s) {
170 return ""
171 }
172 start := j
173 switch s[j] {
174 case '{', '[':
175 open := s[j]
176 cls := byte('}')
177 if open == '[' {
178 cls = ']'
179 }
180 depth := 1
181 j++
182 for j < len(s) && depth > 0 {
183 if s[j] == open {
184 depth++
185 } else if s[j] == cls {
186 depth--
187 } else if s[j] == '"' {
188 j++
189 for j < len(s) && s[j] != '"' {
190 if s[j] == '\\' {
191 j++
192 }
193 j++
194 }
195 }
196 j++
197 }
198 return s[start:j]
199 case '"':
200 j++
201 for j < len(s) && s[j] != '"' {
202 if s[j] == '\\' {
203 j++
204 }
205 j++
206 }
207 return s[start : j+1]
208 default:
209 for j < len(s) && s[j] != ',' && s[j] != '}' && s[j] != ']' &&
210 s[j] != ' ' && s[j] != '\t' && s[j] != '\n' && s[j] != '\r' {
211 j++
212 }
213 return s[start:j]
214 }
215 }
216 // Key found but no colon - skip past this string.
217 i++
218 for i < len(s) && s[i] != '"' {
219 if s[i] == '\\' {
220 i++
221 }
222 i++
223 }
224 if i < len(s) {
225 i++
226 }
227 continue
228 }
229 if s[i] == '"' {
230 i++
231 for i < len(s) && s[i] != '"' {
232 if s[i] == '\\' {
233 i++
234 }
235 i++
236 }
237 if i < len(s) {
238 i++
239 }
240 continue
241 }
242 i++
243 }
244 return ""
245 }
246
247 // JsonGetBool returns the boolean value for the given key.
248 // Returns false if the key is missing or not a bool.
249 func JsonGetBool(s, key string) (ok bool) {
250 return JsonGetValue(s, key) == "true"
251 }
252
253 // JsonGetIntArray returns the integer array for the given key.
254 // Returns nil if the key is missing or not an array. Non-numeric elements
255 // are skipped. Handles negative numbers.
256 func JsonGetIntArray(s, key string) (ss []int32) {
257 raw := JsonGetValue(s, key)
258 if len(raw) < 2 || raw[0] != '[' {
259 return nil
260 }
261 var result []int32
262 i := 1
263 for i < len(raw) {
264 for i < len(raw) && (raw[i] == ' ' || raw[i] == '\t' || raw[i] == '\n' || raw[i] == '\r' || raw[i] == ',') {
265 i++
266 }
267 if i >= len(raw) || raw[i] == ']' {
268 break
269 }
270 neg := false
271 if raw[i] == '-' {
272 neg = true
273 i++
274 }
275 digStart := i
276 for i < len(raw) && raw[i] >= '0' && raw[i] <= '9' {
277 i++
278 }
279 if i == digStart {
280 // Not a number - skip this element.
281 for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
282 i++
283 }
284 continue
285 }
286 n := 0
287 for p := digStart; p < i; p++ {
288 n = n*10 + int32(raw[p]-'0')
289 }
290 if neg {
291 n = -n
292 }
293 result = mxutil.Ensure(result, 1)
294 result = push(result, n)
295 }
296 return result
297 }
298
299 func jsonHexNibble(c byte) (n int32) {
300 if c >= '0' && c <= '9' {
301 return int32(c - '0')
302 }
303 if c >= 'a' && c <= 'f' {
304 return int32(c-'a') + 10
305 }
306 if c >= 'A' && c <= 'F' {
307 return int32(c-'A') + 10
308 }
309 return 0
310 }
311
312 func jsonUTF8(cp int32) (s string) {
313 if cp < 0x80 {
314 return string([]byte{byte(cp)})
315 }
316 if cp < 0x800 {
317 return string([]byte{byte(0xC0 | (cp >> 6)), byte(0x80 | (cp & 0x3F))})
318 }
319 return string([]byte{byte(0xE0 | (cp >> 12)), byte(0x80 | ((cp >> 6) & 0x3F)), byte(0x80 | (cp & 0x3F))})
320 }
321
322 // Itoa converts int64 to decimal string.
323 func Itoa(n int64) (s string) {
324 if n == 0 {
325 return "0"
326 }
327 neg := false
328 if n < 0 {
329 neg = true
330 n = -n
331 }
332 var buf [20]byte
333 i := len(buf)
334 for n > 0 {
335 i--
336 buf[i] = byte('0' + n%10)
337 n /= 10
338 }
339 if neg {
340 i--
341 buf[i] = '-'
342 }
343 return string(buf[i:])
344 }
345