1package html
2
3import (
4 "fmt"
5 "strconv"
6 "unicode/utf8"
7)
8
9// appendUpdateJSON appends the canonical JSON encoding of update to dst. The
10// bytes are identical to encoding/json.Marshal(update) (including HTML
11// escaping of text), but it writes into one caller-sized buffer instead of
12// going through reflection and an intermediate copy.
13func appendUpdateJSON(dst []byte, update Update) ([]byte, error) {
14 dst = append(dst, `{"schemaVersion":`...)
15 dst = strconv.AppendUint(dst, uint64(update.SchemaVersion), 10)
16 dst = append(dst, `,"width":`...)
17 dst = strconv.AppendInt(dst, int64(update.Width), 10)
18 dst = append(dst, `,"height":`...)
19 dst = strconv.AppendInt(dst, int64(update.Height), 10)
20 dst = append(dst, `,"snapshot":`...)
21 dst = strconv.AppendBool(dst, update.Snapshot)
22 dst = append(dst, `,"rows":`...)
23 if update.Rows == nil {
24 dst = append(dst, "null"...)
25 } else {
26 dst = append(dst, '[')
27 for i := range update.Rows {
28 if i > 0 {
29 dst = append(dst, ',')
30 }
31 dst = appendRowJSON(dst, &update.Rows[i])
32 }
33 dst = append(dst, ']')
34 }
35 dst = append(dst, `,"styles":`...)
36 if update.Styles == nil {
37 dst = append(dst, "null"...)
38 } else {
39 dst = append(dst, '[')
40 for i := range update.Styles {
41 if i > 0 {
42 dst = append(dst, ',')
43 }
44 var err error
45 if dst, err = appendStyleJSON(dst, &update.Styles[i]); err != nil {
46 return nil, err
47 }
48 }
49 dst = append(dst, ']')
50 }
51 dst = append(dst, `,"cursor":{"row":`...)
52 dst = strconv.AppendInt(dst, int64(update.Cursor.Row), 10)
53 dst = append(dst, `,"column":`...)
54 dst = strconv.AppendInt(dst, int64(update.Cursor.Column), 10)
55 dst = append(dst, `,"visible":`...)
56 dst = strconv.AppendBool(dst, update.Cursor.Visible)
57 dst = append(dst, `,"style":`...)
58 dst = strconv.AppendUint(dst, uint64(update.Cursor.Style), 10)
59 dst = append(dst, `,"styleSet":`...)
60 dst = strconv.AppendBool(dst, update.Cursor.StyleSet)
61 dst = append(dst, "}}"...)
62 return dst, nil
63}
64
65func appendRowJSON(dst []byte, row *RowUpdate) []byte {
66 dst = append(dst, `{"row":`...)
67 dst = strconv.AppendInt(dst, int64(row.Row), 10)
68 dst = append(dst, `,"cells":`...)
69 if row.Cells == nil {
70 return append(dst, "null}"...)
71 }
72 dst = append(dst, '[')
73 for i := range row.Cells {
74 cell := &row.Cells[i]
75 if i > 0 {
76 dst = append(dst, ',')
77 }
78 dst = append(dst, `{"column":`...)
79 dst = strconv.AppendInt(dst, int64(cell.Column), 10)
80 dst = append(dst, `,"width":`...)
81 dst = strconv.AppendInt(dst, int64(cell.Width), 10)
82 dst = append(dst, `,"text":`...)
83 dst = appendJSONString(dst, cell.Text)
84 dst = append(dst, `,"style":`...)
85 dst = strconv.AppendInt(dst, int64(cell.Style), 10)
86 dst = append(dst, '}')
87 }
88 return append(dst, "]}"...)
89}
90
91func appendStyleJSON(dst []byte, style *Style) ([]byte, error) {
92 dst = append(dst, '{')
93 first := true
94 flag := func(name string, set bool) {
95 if !set {
96 return
97 }
98 if !first {
99 dst = append(dst, ',')
100 }
101 first = false
102 dst = append(dst, '"')
103 dst = append(dst, name...)
104 dst = append(dst, `":true`...)
105 }
106 flag("bold", style.Bold)
107 flag("italic", style.Italic)
108 flag("inverse", style.Inverse)
109 flag("dim", style.Dim)
110 flag("blink", style.Blink)
111 flag("strikethrough", style.Strikethrough)
112 flag("underline", style.Underline)
113 if style.UnderlineStyle != 0 {
114 if !first {
115 dst = append(dst, ',')
116 }
117 first = false
118 dst = append(dst, `"underlineStyle":`...)
119 dst = strconv.AppendUint(dst, uint64(style.UnderlineStyle), 10)
120 }
121 if !first {
122 dst = append(dst, ',')
123 }
124 var err error
125 dst = append(dst, `"foreground":`...)
126 if dst, err = appendColorJSON(dst, style.Foreground); err != nil {
127 return nil, err
128 }
129 dst = append(dst, `,"background":`...)
130 if dst, err = appendColorJSON(dst, style.Background); err != nil {
131 return nil, err
132 }
133 dst = append(dst, `,"underlineColor":`...)
134 if dst, err = appendColorJSON(dst, style.UnderlineColor); err != nil {
135 return nil, err
136 }
137 return append(dst, '}'), nil
138}
139
140// appendColorJSON mirrors Color.MarshalJSON.
141func appendColorJSON(dst []byte, color Color) ([]byte, error) {
142 switch color.Kind {
143 case ColorDefault:
144 return append(dst, `{"kind":0}`...), nil
145 case ColorIndexed:
146 dst = append(dst, `{"kind":1,"index":`...)
147 dst = strconv.AppendUint(dst, uint64(color.Index), 10)
148 return append(dst, '}'), nil
149 case ColorRGB:
150 dst = append(dst, `{"kind":2,"rgb":{"r":`...)
151 dst = strconv.AppendUint(dst, uint64(color.RGB.R), 10)
152 dst = append(dst, `,"g":`...)
153 dst = strconv.AppendUint(dst, uint64(color.RGB.G), 10)
154 dst = append(dst, `,"b":`...)
155 dst = strconv.AppendUint(dst, uint64(color.RGB.B), 10)
156 return append(dst, "}}"...), nil
157 default:
158 return nil, fmt.Errorf("html: invalid color kind %d", color.Kind)
159 }
160}
161
162const hexDigits = "0123456789abcdef"
163
164// appendJSONString appends s as a JSON string using encoding/json's default
165// escaping: quotes, backslashes, control characters, <, >, & and U+2028/2029
166// are escaped. Cell text is always valid UTF-8; an invalid byte, should one
167// appear, is replaced by U+FFFD so the output stays valid JSON.
168func appendJSONString(dst []byte, s string) []byte {
169 dst = append(dst, '"')
170 start := 0
171 for i := 0; i < len(s); {
172 b := s[i]
173 if b < utf8.RuneSelf {
174 if b >= 0x20 && b != '"' && b != '\\' && b != '<' && b != '>' && b != '&' {
175 i++
176 continue
177 }
178 dst = append(dst, s[start:i]...)
179 switch b {
180 case '"', '\\':
181 dst = append(dst, '\\', b)
182 case '\b':
183 dst = append(dst, '\\', 'b')
184 case '\f':
185 dst = append(dst, '\\', 'f')
186 case '\n':
187 dst = append(dst, '\\', 'n')
188 case '\r':
189 dst = append(dst, '\\', 'r')
190 case '\t':
191 dst = append(dst, '\\', 't')
192 default:
193 dst = append(dst, '\\', 'u', '0', '0', hexDigits[b>>4], hexDigits[b&0xF])
194 }
195 i++
196 start = i
197 continue
198 }
199 r, size := utf8.DecodeRuneInString(s[i:])
200 if r == utf8.RuneError && size == 1 {
201 dst = append(dst, s[start:i]...)
202 dst = append(dst, "\uFFFD"...)
203 i += size
204 start = i
205 continue
206 }
207 if r == '\u2028' || r == '\u2029' {
208 dst = append(dst, s[start:i]...)
209 dst = append(dst, '\\', 'u', '2', '0', '2', hexDigits[r&0xF])
210 i += size
211 start = i
212 continue
213 }
214 i += size
215 }
216 dst = append(dst, s[start:]...)
217 return append(dst, '"')
218}
219
220// estimateUpdateJSONSize returns a capacity hint close to the encoded size so
221// the output buffer normally grows zero times.
222func estimateUpdateJSONSize(update Update) int {
223 size := 256 + 256*len(update.Styles)
224 for i := range update.Rows {
225 size += 32 + 48*len(update.Rows[i].Cells)
226 for j := range update.Rows[i].Cells {
227 size += len(update.Rows[i].Cells[j].Text)
228 }
229 }
230 return size
231}