package helpers import "git.smesh.lol/moxie/pkg/mxutil" // Minimal JSON serialization for Nostr events. // No encoding/json dependency. // JsonString returns a JSON-escaped string with surrounding quotes. // Uses string concat to preserve non-ASCII in tinyjs. func JsonString(s string) (sv string) { if len(s) == 0 { return "\"\"" } result := "\"" start := 0 for i := 0; i < len(s); i++ { c := s[i] var esc string switch c { case '"': esc = "\\\"" case '\\': esc = "\\\\" case '\n': esc = "\\n" case '\r': esc = "\\r" case '\t': esc = "\\t" case '\b': esc = "\\b" case '\f': esc = "\\f" default: if c < 0x20 { esc = "\\u00" | string(hexChars[c>>4]) | string(hexChars[c&0x0f]) } else { continue } } result |= s[start:i] | esc start = i + 1 } return result | s[start:] | "\"" } // JsonGetString extracts a string value for the given key from a JSON object. // Returns empty string if not found. Handles basic escape sequences. // Properly skips over string values to avoid false-matching keys inside values. func JsonGetString(s, key string) (result string) { kLen := len(key) kqLen := kLen + 2 i := 0 for i <= len(s)-kqLen { keyOk := s[i] == '"' && s[i+1+kLen] == '"' if keyOk { for j := 0; j < kLen; j++ { if s[i+1+j] != key[j] { keyOk = false } } } if keyOk { j := i + kqLen for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') { j++ } if j < len(s) && s[j] == ':' { j++ for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') { j++ } if j < len(s) && s[j] == '"' { j++ start := j out := "" for j < len(s) { if s[j] == '\\' && j+1 < len(s) { out |= s[start:j] j++ switch s[j] { case '"', '\\', '/': out |= s[j : j+1] case 'n': out |= "\n" case 'r': out |= "\r" case 't': out |= "\t" case 'u': if j+4 < len(s) { cp := jsonHexNibble(s[j+1])<<12 | jsonHexNibble(s[j+2])<<8 | jsonHexNibble(s[j+3])<<4 | jsonHexNibble(s[j+4]) out |= jsonUTF8(cp) j += 4 } default: out |= s[j : j+1] } j++ start = j continue } if s[j] == '"' { return out | s[start:j] } j++ } i = j continue } } // Key didn't match pattern "key": "value" - skip past this string. i++ for i < len(s) && s[i] != '"' { if s[i] == '\\' { i++ } i++ } if i < len(s) { i++ } continue } if s[i] == '"' { i++ for i < len(s) && s[i] != '"' { if s[i] == '\\' { i++ } i++ } if i < len(s) { i++ } continue } i++ } return "" } // JsonGetValue extracts a raw JSON value for the given key from a JSON object. // Works for any value type: objects, arrays, strings, numbers, bools, null. // Properly skips over string values to avoid false-matching keys inside values. func JsonGetValue(s, key string) (result string) { kLen := len(key) kqLen := kLen + 2 i := 0 for i <= len(s)-kqLen { keyOk := s[i] == '"' && s[i+1+kLen] == '"' if keyOk { for j := 0; j < kLen; j++ { if s[i+1+j] != key[j] { keyOk = false } } } if keyOk { j := i + kqLen for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') { j++ } if j < len(s) && s[j] == ':' { j++ for j < len(s) && (s[j] == ' ' || s[j] == '\t' || s[j] == '\n' || s[j] == '\r') { j++ } if j >= len(s) { return "" } start := j switch s[j] { case '{', '[': open := s[j] cls := byte('}') if open == '[' { cls = ']' } depth := 1 j++ for j < len(s) && depth > 0 { if s[j] == open { depth++ } else if s[j] == cls { depth-- } else if s[j] == '"' { j++ for j < len(s) && s[j] != '"' { if s[j] == '\\' { j++ } j++ } } j++ } return s[start:j] case '"': j++ for j < len(s) && s[j] != '"' { if s[j] == '\\' { j++ } j++ } return s[start : j+1] default: for j < len(s) && s[j] != ',' && s[j] != '}' && s[j] != ']' && s[j] != ' ' && s[j] != '\t' && s[j] != '\n' && s[j] != '\r' { j++ } return s[start:j] } } // Key found but no colon - skip past this string. i++ for i < len(s) && s[i] != '"' { if s[i] == '\\' { i++ } i++ } if i < len(s) { i++ } continue } if s[i] == '"' { i++ for i < len(s) && s[i] != '"' { if s[i] == '\\' { i++ } i++ } if i < len(s) { i++ } continue } i++ } return "" } // JsonGetBool returns the boolean value for the given key. // Returns false if the key is missing or not a bool. func JsonGetBool(s, key string) (ok bool) { return JsonGetValue(s, key) == "true" } // JsonGetIntArray returns the integer array for the given key. // Returns nil if the key is missing or not an array. Non-numeric elements // are skipped. Handles negative numbers. func JsonGetIntArray(s, key string) (ss []int32) { raw := JsonGetValue(s, key) if len(raw) < 2 || raw[0] != '[' { return nil } var result []int32 i := 1 for i < len(raw) { for i < len(raw) && (raw[i] == ' ' || raw[i] == '\t' || raw[i] == '\n' || raw[i] == '\r' || raw[i] == ',') { i++ } if i >= len(raw) || raw[i] == ']' { break } neg := false if raw[i] == '-' { neg = true i++ } digStart := i for i < len(raw) && raw[i] >= '0' && raw[i] <= '9' { i++ } if i == digStart { // Not a number - skip this element. for i < len(raw) && raw[i] != ',' && raw[i] != ']' { i++ } continue } n := 0 for p := digStart; p < i; p++ { n = n*10 + int32(raw[p]-'0') } if neg { n = -n } result = mxutil.Ensure(result, 1) result = push(result, n) } return result } func jsonHexNibble(c byte) (n int32) { if c >= '0' && c <= '9' { return int32(c - '0') } if c >= 'a' && c <= 'f' { return int32(c-'a') + 10 } if c >= 'A' && c <= 'F' { return int32(c-'A') + 10 } return 0 } func jsonUTF8(cp int32) (s string) { if cp < 0x80 { return string([]byte{byte(cp)}) } if cp < 0x800 { return string([]byte{byte(0xC0 | (cp >> 6)), byte(0x80 | (cp & 0x3F))}) } return string([]byte{byte(0xE0 | (cp >> 12)), byte(0x80 | ((cp >> 6) & 0x3F)), byte(0x80 | (cp & 0x3F))}) } // Itoa converts int64 to decimal string. func Itoa(n int64) (s string) { if n == 0 { return "0" } neg := false if n < 0 { neg = true n = -n } var buf [20]byte i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } if neg { i-- buf[i] = '-' } return string(buf[i:]) }