package gojq import ( "bytes" "encoding/json" "fmt" "io" "math" "math/big" "sort" "strconv" "strings" "unicode/utf8" ) // Marshal returns the jq-flavored JSON encoding of v. // // This method accepts only limited types (nil, bool, int, float64, *big.Int, // json.Number, string, []any, and map[string]any) because these are the // possible types a gojq iterator can emit. This method marshals NaN to null, // truncates infinities to (+|-) math.MaxFloat64, uses \b and \f in strings, // and does not escape '<', '>', '&', '\u2028', and '\u2029'. These behaviors // are based on the marshaler of jq command, and different from json.Marshal in // the Go standard library. Note that the result is not safe to embed in HTML. func Marshal(v any) ([]byte, error) { var b bytes.Buffer (&encoder{w: &b}).encode(v) return b.Bytes(), nil } func jsonMarshal(v any) string { var sb strings.Builder (&encoder{w: &sb}).encode(v) return sb.String() } func jsonEncodeString(sb *strings.Builder, v string) { (&encoder{w: sb}).encodeString(v) } type encoder struct { w interface { io.Writer io.ByteWriter io.StringWriter } buf [64]byte } func (e *encoder) encode(v any) { switch v := v.(type) { case nil: e.w.WriteString("null") case bool: if v { e.w.WriteString("true") } else { e.w.WriteString("false") } case int: e.w.Write(strconv.AppendInt(e.buf[:0], int64(v), 10)) case float64: e.encodeFloat64(v) case *big.Int: e.w.Write(v.Append(e.buf[:0], 10)) case json.Number: e.w.WriteString(v.String()) case string: e.encodeString(v) case []any: e.encodeArray(v) case map[string]any: e.encodeObject(v) default: panic(fmt.Sprintf("invalid type: %[1]T (%[1]v)", v)) } } // ref: floatEncoder in encoding/json func (e *encoder) encodeFloat64(f float64) { if math.IsNaN(f) { e.w.WriteString("null") return } f = min(max(f, -math.MaxFloat64), math.MaxFloat64) format := byte('f') if x := math.Abs(f); x != 0 && x < 1e-6 || x >= 1e21 { format = 'e' } buf := strconv.AppendFloat(e.buf[:0], f, format, -1, 64) if format == 'e' { // clean up e-09 to e-9 if n := len(buf); n >= 4 && buf[n-4] == 'e' && buf[n-3] == '-' && buf[n-2] == '0' { buf[n-2] = buf[n-1] buf = buf[:n-1] } } e.w.Write(buf) } // ref: encodeState#string in encoding/json func (e *encoder) encodeString(s string) { e.w.WriteByte('"') start := 0 for i := 0; i < len(s); { if b := s[i]; b < utf8.RuneSelf { if ' ' <= b && b <= '~' && b != '"' && b != '\\' { i++ continue } if start < i { e.w.WriteString(s[start:i]) } switch b { case '"': e.w.WriteString(`\"`) case '\\': e.w.WriteString(`\\`) case '\b': e.w.WriteString(`\b`) case '\f': e.w.WriteString(`\f`) case '\n': e.w.WriteString(`\n`) case '\r': e.w.WriteString(`\r`) case '\t': e.w.WriteString(`\t`) default: const hex = "0123456789abcdef" e.w.WriteString(`\u00`) e.w.WriteByte(hex[b>>4]) e.w.WriteByte(hex[b&0xF]) } i++ start = i continue } c, size := utf8.DecodeRuneInString(s[i:]) if c == utf8.RuneError && size == 1 { if start < i { e.w.WriteString(s[start:i]) } e.w.WriteString(`\ufffd`) i += size start = i continue } i += size } if start < len(s) { e.w.WriteString(s[start:]) } e.w.WriteByte('"') } func (e *encoder) encodeArray(vs []any) { e.w.WriteByte('[') for i, v := range vs { if i > 0 { e.w.WriteByte(',') } e.encode(v) } e.w.WriteByte(']') } func (e *encoder) encodeObject(vs map[string]any) { e.w.WriteByte('{') type keyVal struct { key string val any } kvs := make([]keyVal, len(vs)) var i int for k, v := range vs { kvs[i] = keyVal{k, v} i++ } sort.Slice(kvs, func(i, j int) bool { return kvs[i].key < kvs[j].key }) for i, kv := range kvs { if i > 0 { e.w.WriteByte(',') } e.encodeString(kv.key) e.w.WriteByte(':') e.encode(kv.val) } e.w.WriteByte('}') }