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