package html import ( "fmt" "strconv" "unicode/utf8" ) // appendUpdateJSON appends the canonical JSON encoding of update to dst. The // bytes are identical to encoding/json.Marshal(update) (including HTML // escaping of text), but it writes into one caller-sized buffer instead of // going through reflection and an intermediate copy. func appendUpdateJSON(dst []byte, update Update) ([]byte, error) { dst = append(dst, `{"schemaVersion":`...) dst = strconv.AppendUint(dst, uint64(update.SchemaVersion), 10) dst = append(dst, `,"width":`...) dst = strconv.AppendInt(dst, int64(update.Width), 10) dst = append(dst, `,"height":`...) dst = strconv.AppendInt(dst, int64(update.Height), 10) dst = append(dst, `,"snapshot":`...) dst = strconv.AppendBool(dst, update.Snapshot) dst = append(dst, `,"rows":`...) if update.Rows == nil { dst = append(dst, "null"...) } else { dst = append(dst, '[') for i := range update.Rows { if i > 0 { dst = append(dst, ',') } dst = appendRowJSON(dst, &update.Rows[i]) } dst = append(dst, ']') } dst = append(dst, `,"styles":`...) if update.Styles == nil { dst = append(dst, "null"...) } else { dst = append(dst, '[') for i := range update.Styles { if i > 0 { dst = append(dst, ',') } var err error if dst, err = appendStyleJSON(dst, &update.Styles[i]); err != nil { return nil, err } } dst = append(dst, ']') } dst = append(dst, `,"cursor":{"row":`...) dst = strconv.AppendInt(dst, int64(update.Cursor.Row), 10) dst = append(dst, `,"column":`...) dst = strconv.AppendInt(dst, int64(update.Cursor.Column), 10) dst = append(dst, `,"visible":`...) dst = strconv.AppendBool(dst, update.Cursor.Visible) dst = append(dst, `,"style":`...) dst = strconv.AppendUint(dst, uint64(update.Cursor.Style), 10) dst = append(dst, `,"styleSet":`...) dst = strconv.AppendBool(dst, update.Cursor.StyleSet) dst = append(dst, "}}"...) return dst, nil } func appendRowJSON(dst []byte, row *RowUpdate) []byte { dst = append(dst, `{"row":`...) dst = strconv.AppendInt(dst, int64(row.Row), 10) dst = append(dst, `,"cells":`...) if row.Cells == nil { return append(dst, "null}"...) } dst = append(dst, '[') for i := range row.Cells { cell := &row.Cells[i] if i > 0 { dst = append(dst, ',') } dst = append(dst, `{"column":`...) dst = strconv.AppendInt(dst, int64(cell.Column), 10) dst = append(dst, `,"width":`...) dst = strconv.AppendInt(dst, int64(cell.Width), 10) dst = append(dst, `,"text":`...) dst = appendJSONString(dst, cell.Text) dst = append(dst, `,"style":`...) dst = strconv.AppendInt(dst, int64(cell.Style), 10) dst = append(dst, '}') } return append(dst, "]}"...) } func appendStyleJSON(dst []byte, style *Style) ([]byte, error) { dst = append(dst, '{') first := true flag := func(name string, set bool) { if !set { return } if !first { dst = append(dst, ',') } first = false dst = append(dst, '"') dst = append(dst, name...) dst = append(dst, `":true`...) } flag("bold", style.Bold) flag("italic", style.Italic) flag("inverse", style.Inverse) flag("dim", style.Dim) flag("blink", style.Blink) flag("strikethrough", style.Strikethrough) flag("underline", style.Underline) if style.UnderlineStyle != 0 { if !first { dst = append(dst, ',') } first = false dst = append(dst, `"underlineStyle":`...) dst = strconv.AppendUint(dst, uint64(style.UnderlineStyle), 10) } if !first { dst = append(dst, ',') } var err error dst = append(dst, `"foreground":`...) if dst, err = appendColorJSON(dst, style.Foreground); err != nil { return nil, err } dst = append(dst, `,"background":`...) if dst, err = appendColorJSON(dst, style.Background); err != nil { return nil, err } dst = append(dst, `,"underlineColor":`...) if dst, err = appendColorJSON(dst, style.UnderlineColor); err != nil { return nil, err } return append(dst, '}'), nil } // appendColorJSON mirrors Color.MarshalJSON. func appendColorJSON(dst []byte, color Color) ([]byte, error) { switch color.Kind { case ColorDefault: return append(dst, `{"kind":0}`...), nil case ColorIndexed: dst = append(dst, `{"kind":1,"index":`...) dst = strconv.AppendUint(dst, uint64(color.Index), 10) return append(dst, '}'), nil case ColorRGB: dst = append(dst, `{"kind":2,"rgb":{"r":`...) dst = strconv.AppendUint(dst, uint64(color.RGB.R), 10) dst = append(dst, `,"g":`...) dst = strconv.AppendUint(dst, uint64(color.RGB.G), 10) dst = append(dst, `,"b":`...) dst = strconv.AppendUint(dst, uint64(color.RGB.B), 10) return append(dst, "}}"...), nil default: return nil, fmt.Errorf("html: invalid color kind %d", color.Kind) } } const hexDigits = "0123456789abcdef" // appendJSONString appends s as a JSON string using encoding/json's default // escaping: quotes, backslashes, control characters, <, >, & and U+2028/2029 // are escaped. Cell text is always valid UTF-8; an invalid byte, should one // appear, is replaced by U+FFFD so the output stays valid JSON. func appendJSONString(dst []byte, s string) []byte { dst = append(dst, '"') start := 0 for i := 0; i < len(s); { b := s[i] if b < utf8.RuneSelf { if b >= 0x20 && b != '"' && b != '\\' && b != '<' && b != '>' && b != '&' { i++ continue } dst = append(dst, s[start:i]...) switch b { case '"', '\\': dst = append(dst, '\\', b) case '\b': dst = append(dst, '\\', 'b') case '\f': dst = append(dst, '\\', 'f') case '\n': dst = append(dst, '\\', 'n') case '\r': dst = append(dst, '\\', 'r') case '\t': dst = append(dst, '\\', 't') default: dst = append(dst, '\\', 'u', '0', '0', hexDigits[b>>4], hexDigits[b&0xF]) } i++ start = i continue } r, size := utf8.DecodeRuneInString(s[i:]) if r == utf8.RuneError && size == 1 { dst = append(dst, s[start:i]...) dst = append(dst, "\uFFFD"...) i += size start = i continue } if r == '\u2028' || r == '\u2029' { dst = append(dst, s[start:i]...) dst = append(dst, '\\', 'u', '2', '0', '2', hexDigits[r&0xF]) i += size start = i continue } i += size } dst = append(dst, s[start:]...) return append(dst, '"') } // estimateUpdateJSONSize returns a capacity hint close to the encoded size so // the output buffer normally grows zero times. func estimateUpdateJSONSize(update Update) int { size := 256 + 256*len(update.Styles) for i := range update.Rows { size += 32 + 48*len(update.Rows[i].Cells) for j := range update.Rows[i].Cells { size += len(update.Rows[i].Cells[j].Text) } } return size }