json.mx raw
1 package helpers
2
3 // Minimal JSON serialization for Nostr events.
4 // No encoding/json dependency.
5
6 // JsonString returns a JSON-escaped string with surrounding quotes.
7 // Uses string concat to preserve non-ASCII in tinyjs.
8 func JsonString(s string) string {
9 result := "\""
10 start := 0
11 for i := 0; i < len(s); i++ {
12 c := s[i]
13 var esc string
14 switch c {
15 case '"':
16 esc = "\\\""
17 case '\\':
18 esc = "\\\\"
19 case '\n':
20 esc = "\\n"
21 case '\r':
22 esc = "\\r"
23 case '\t':
24 esc = "\\t"
25 case '\b':
26 esc = "\\b"
27 case '\f':
28 esc = "\\f"
29 default:
30 if c < 0x20 {
31 esc = "\\u00" + string(hexChars[c>>4]) + string(hexChars[c&0x0f])
32 } else {
33 continue
34 }
35 }
36 result += s[start:i] + esc
37 start = i + 1
38 }
39 return result + s[start:] + "\""
40 }
41
42 // JsonGetString extracts a string value for the given key from a JSON object.
43 // Returns empty string if not found. Handles basic escape sequences.
44 // Uses string concat (not []byte) to preserve non-ASCII characters in tinyjs.
45 func JsonGetString(s, key string) string {
46 kq := "\"" + key + "\""
47 kqLen := len(kq)
48 for i := 0; i <= len(s)-kqLen; i++ {
49 if s[i:i+kqLen] == kq {
50 j := i + kqLen
51 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
52 j++
53 }
54 if j >= len(s) || s[j] != ':' {
55 continue
56 }
57 j++
58 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
59 j++
60 }
61 if j >= len(s) || s[j] != '"' {
62 continue
63 }
64 j++
65 start := j
66 result := ""
67 for j < len(s) {
68 if s[j] == '\\' && j+1 < len(s) {
69 result += s[start:j]
70 j++
71 switch s[j] {
72 case '"', '\\', '/':
73 result += s[j : j+1]
74 case 'n':
75 result += "\n"
76 case 'r':
77 result += "\r"
78 case 't':
79 result += "\t"
80 default:
81 result += s[j : j+1]
82 }
83 j++
84 start = j
85 continue
86 }
87 if s[j] == '"' {
88 return result + s[start:j]
89 }
90 j++
91 }
92 }
93 }
94 return ""
95 }
96
97 // JsonGetValue extracts a raw JSON value for the given key from a JSON object.
98 // Works for any value type: objects, arrays, strings, numbers, bools, null.
99 // Returns the raw JSON substring. Returns empty string if not found.
100 func JsonGetValue(s, key string) string {
101 kq := "\"" + key + "\""
102 kqLen := len(kq)
103 for i := 0; i <= len(s)-kqLen; i++ {
104 if s[i:i+kqLen] != kq {
105 continue
106 }
107 j := i + kqLen
108 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
109 j++
110 }
111 if j >= len(s) || s[j] != ':' {
112 continue
113 }
114 j++
115 for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') {
116 j++
117 }
118 if j >= len(s) {
119 continue
120 }
121 start := j
122 switch s[j] {
123 case '{', '[':
124 open := s[j]
125 cls := byte('}')
126 if open == '[' {
127 cls = ']'
128 }
129 depth := 1
130 j++
131 for j < len(s) && depth > 0 {
132 if s[j] == open {
133 depth++
134 } else if s[j] == cls {
135 depth--
136 } else if s[j] == '"' {
137 j++
138 for j < len(s) && s[j] != '"' {
139 if s[j] == '\\' {
140 j++
141 }
142 j++
143 }
144 }
145 j++
146 }
147 return s[start:j]
148 case '"':
149 j++
150 for j < len(s) && s[j] != '"' {
151 if s[j] == '\\' {
152 j++
153 }
154 j++
155 }
156 return s[start : j+1]
157 default:
158 for j < len(s) && s[j] != ',' && s[j] != '}' && s[j] != ']' &&
159 s[j] != ' ' && s[j] != '\t' && s[j] != '\n' && s[j] != '\r' {
160 j++
161 }
162 return s[start:j]
163 }
164 }
165 return ""
166 }
167
168 // JsonGetBool returns the boolean value for the given key.
169 // Returns false if the key is missing or not a bool.
170 func JsonGetBool(s, key string) bool {
171 return JsonGetValue(s, key) == "true"
172 }
173
174 // JsonGetIntArray returns the integer array for the given key.
175 // Returns nil if the key is missing or not an array. Non-numeric elements
176 // are skipped. Handles negative numbers.
177 func JsonGetIntArray(s, key string) []int {
178 raw := JsonGetValue(s, key)
179 if len(raw) < 2 || raw[0] != '[' {
180 return nil
181 }
182 var result []int
183 i := 1
184 for i < len(raw) {
185 for i < len(raw) && (raw[i] == ' ' || raw[i] == '\t' || raw[i] == '\n' || raw[i] == '\r' || raw[i] == ',') {
186 i++
187 }
188 if i >= len(raw) || raw[i] == ']' {
189 break
190 }
191 neg := false
192 if raw[i] == '-' {
193 neg = true
194 i++
195 }
196 digStart := i
197 for i < len(raw) && raw[i] >= '0' && raw[i] <= '9' {
198 i++
199 }
200 if i == digStart {
201 // Not a number — skip this element.
202 for i < len(raw) && raw[i] != ',' && raw[i] != ']' {
203 i++
204 }
205 continue
206 }
207 n := 0
208 for p := digStart; p < i; p++ {
209 n = n*10 + int(raw[p]-'0')
210 }
211 if neg {
212 n = -n
213 }
214 result = append(result, n)
215 }
216 return result
217 }
218
219 // Itoa converts int64 to decimal string.
220 func Itoa(n int64) string {
221 if n == 0 {
222 return "0"
223 }
224 neg := false
225 if n < 0 {
226 neg = true
227 n = -n
228 }
229 var buf [20]byte
230 i := len(buf)
231 for n > 0 {
232 i--
233 buf[i] = byte('0' + n%10)
234 n /= 10
235 }
236 if neg {
237 i--
238 buf[i] = '-'
239 }
240 return string(buf[i:])
241 }
242