json.gno
3.12 Kb · 123 lines
1package safe
2
3import (
4 "strconv"
5 "strings"
6)
7
8// JSONString returns s as a quoted JSON string. Beyond the mandatory
9// escapes it also escapes U+2028/U+2029 (RA L-12) and '<', '>', '&', so the
10// output is safe to inline in HTML as well.
11func JSONString(s string) string {
12 const hexDigits = "0123456789abcdef"
13 var b strings.Builder
14 b.WriteByte('"')
15 for _, r := range s {
16 switch r {
17 case '"':
18 b.WriteString(`\"`)
19 case '\\':
20 b.WriteString(`\\`)
21 case '\n':
22 b.WriteString(`\n`)
23 case '\r':
24 b.WriteString(`\r`)
25 case '\t':
26 b.WriteString(`\t`)
27 case '<', '>', '&', 0x2028, 0x2029:
28 b.WriteString(`\u`)
29 b.WriteString(PadHex(int64(r), 4))
30 default:
31 if r < 0x20 || r == 0x7f {
32 b.WriteString(`\u00`)
33 b.WriteByte(hexDigits[(r>>4)&0xf])
34 b.WriteByte(hexDigits[r&0xf])
35 continue
36 }
37 b.WriteRune(r)
38 }
39 }
40 b.WriteByte('"')
41 return b.String()
42}
43
44// PadHex renders n in lowercase hex, zero-padded to width.
45func PadHex(n int64, width int) string {
46 s := strconv.FormatInt(n, 16)
47 if len(s) >= width {
48 return s
49 }
50 return strings.Repeat("0", width-len(s)) + s
51}
52
53// JSONWriter is the single JSON producer of every read API (design
54// §2.5: reads return primitives or JSON, never positional text). It tracks
55// commas itself, so callers only describe structure:
56//
57// w := &safe.JSONWriter{}
58// w.BeginObject(); w.Key("id"); w.Str("1"); w.EndObject()
59type JSONWriter struct {
60 b strings.Builder
61 hasItems []bool // one entry per open container
62 afterKey bool
63}
64
65func (w *JSONWriter) BeginObject() { w.open('{') }
66func (w *JSONWriter) EndObject() { w.close('}') }
67func (w *JSONWriter) BeginArray() { w.open('[') }
68func (w *JSONWriter) EndArray() { w.close(']') }
69
70// Key starts an object member; the next value call writes its value.
71func (w *JSONWriter) Key(k string) {
72 w.separate()
73 w.b.WriteString(JSONString(k))
74 w.b.WriteByte(':')
75 w.afterKey = true
76}
77
78func (w *JSONWriter) Str(v string) { w.value(JSONString(v)) }
79func (w *JSONWriter) Int(v int64) { w.value(strconv.FormatInt(v, 10)) }
80func (w *JSONWriter) Bool(v bool) { w.value(strconv.FormatBool(v)) }
81
82// Raw writes v verbatim. Only for JSON produced by another JSONWriter.
83func (w *JSONWriter) Raw(v string) { w.value(v) }
84
85// StrField and IntField are Key+value shorthands.
86func (w *JSONWriter) StrField(k, v string) { w.Key(k); w.Str(v) }
87func (w *JSONWriter) IntField(k string, v int64) { w.Key(k); w.Int(v) }
88func (w *JSONWriter) BoolField(k string, v bool) { w.Key(k); w.Bool(v) }
89
90func (w *JSONWriter) String() string { return w.b.String() }
91
92func (w *JSONWriter) open(c byte) {
93 w.separate()
94 w.b.WriteByte(c)
95 w.hasItems = append(w.hasItems, false)
96}
97
98func (w *JSONWriter) close(c byte) {
99 if len(w.hasItems) == 0 {
100 panic("JSONWriter: close without open")
101 }
102 w.hasItems = w.hasItems[:len(w.hasItems)-1]
103 w.b.WriteByte(c)
104}
105
106func (w *JSONWriter) value(v string) {
107 w.separate()
108 w.b.WriteString(v)
109}
110
111// separate writes the comma before an item, except right after a key.
112func (w *JSONWriter) separate() {
113 if w.afterKey {
114 w.afterKey = false
115 return
116 }
117 if n := len(w.hasItems); n > 0 {
118 if w.hasItems[n-1] {
119 w.b.WriteByte(',')
120 }
121 w.hasItems[n-1] = true
122 }
123}