1package sheet23import (4 "encoding/json"5 "fmt"6 "strconv"7)89// fileCell is a cell with formatting or a note as the file writes it; a10// cell with neither is just its input (see fileFormat).11type fileCell struct {12 Input string `json:"input,omitempty"`13 // Text says how the input reads when that isn't what the format14 // says: true for text typed into a Plain text cell whose format15 // changed since, false for a number or formula typed before the cell16 // was formatted as Plain text. It needs no version bump: earlier17 // builds read the input as the format says.18 Text *bool `json:"text,omitempty"`19 Format string `json:"format,omitempty"`20 Decimals *int `json:"decimals,omitempty"`21 Pattern string `json:"pattern,omitempty"`22 Bold bool `json:"bold,omitempty"`23 Italic bool `json:"italic,omitempty"`24 Underline bool `json:"underline,omitempty"`25 Strikethrough bool `json:"strikethrough,omitempty"`26 Align string `json:"align,omitempty"`27 Own bool `json:"own,omitempty"` // not taking its column's or row's formatting28 // Wrap and Borders need no version bump: earlier builds ignore them29 // and show the text overflowing, without lines.30 Wrap string `json:"wrap,omitempty"` // "wrap" or "clip"31 Borders *fileBorders `json:"borders,omitempty"`32 // VAlign needs no version bump either: earlier builds show the text33 // at the bottom.34 VAlign string `json:"valign,omitempty"` // "top", "middle" or "bottom"35 // Note needs no version bump: earlier builds ignore it and drop the36 // note.37 Note string `json:"note,omitempty"`38}3940func encodeCell(c *Cell) (json.RawMessage, error) {41 text := c.readsAgainstFormat()42 if c.Format.IsZero() && c.Style.IsZero() && c.Note == "" && text == nil {43 return json.Marshal(c.Input)44 }45 fc := fileCell{46 Input: c.Input,47 Text: text,48 Pattern: c.Format.Pattern,49 Bold: c.Style.Bold,50 Italic: c.Style.Italic,51 Underline: c.Style.Underline,52 Strikethrough: c.Style.Strikethrough,53 Align: c.Style.Align.String(),54 Own: c.Style.own,55 Wrap: c.Style.Wrap.String(),56 VAlign: c.Style.VAlign.String(),57 Borders: encodeBorders(c.Style.Borders),58 Note: c.Note,59 }60 if !c.Format.IsZero() {61 fc.Format = c.Format.Kind.String()62 }63 if c.Format.Kind.HasDecimals() {64 d := c.Format.Decimals65 fc.Decimals = &d66 }67 return json.Marshal(fc)68}6970// readsAgainstFormat is the file's text field for c: whether its entry71// is text, when its format says otherwise (see fileCell), else nil.72func (c *Cell) readsAgainstFormat() *bool {73 text := c.Format.Kind == FmtText74 switch {75 case text && c.expr != nil, !text && c.typedText:76 v := c.typedText77 return &v78 }79 return nil80}8182// decodeCell reads a cell's entry and formatting, as line formats are83// stored too.84func decodeCell(raw json.RawMessage) (string, Format, Style, error) {85 e, err := decodeNoted(raw)86 return e.input, e.f, e.st, err87}8889// fileEntry is a cell as the file has it.90type fileEntry struct {91 input string92 f Format93 st Style94 note string95 text *bool // see fileCell.Text96}9798// cell builds the cell, as newCell does, reading its entry as text or99// not as the file says.100func (e fileEntry) cell(implied bool) (*Cell, error) {101 asText := e.f.Kind == FmtText102 if e.text != nil {103 asText = *e.text104 }105 c, err := readEntry(e.input, e.f, e.st, implied, asText)106 return c.withNote(CleanNote(e.note)), err107}108109// decodeNoted is decodeCell with the cell's note and how it reads.110func decodeNoted(raw json.RawMessage) (fileEntry, error) {111 var input string112 if err := json.Unmarshal(raw, &input); err == nil {113 return fileEntry{input: input}, nil114 }115 var fc fileCell116 if err := json.Unmarshal(raw, &fc); err != nil {117 return fileEntry{}, err118 }119 input, f, st, err := decodeFormatted(fc)120 return fileEntry{input, f, st, fc.Note, fc.Text}, err121}122123// decodeFormatted reads the entry and formatting of a cell written as an124// object.125func decodeFormatted(fc fileCell) (string, Format, Style, error) {126 var f Format127 if fc.Format != "" {128 k, ok := ParseFormatKind(fc.Format)129 if !ok {130 return "", Format{}, Style{}, fmt.Errorf("unknown format %q", fc.Format)131 }132 f = Format{Kind: k, Pattern: fc.Pattern}133 if fc.Decimals != nil {134 f.Decimals = clampInt(*fc.Decimals, 0, MaxDecimals)135 }136 }137 al, ok := ParseAlign(fc.Align)138 if !ok {139 return "", Format{}, Style{}, fmt.Errorf("unknown alignment %q", fc.Align)140 }141 wr, ok := ParseWrap(fc.Wrap)142 if !ok {143 return "", Format{}, Style{}, fmt.Errorf("unknown wrapping %q", fc.Wrap)144 }145 va, ok := ParseVAlign(fc.VAlign)146 if !ok {147 return "", Format{}, Style{}, fmt.Errorf("unknown vertical alignment %q", fc.VAlign)148 }149 b, err := decodeBorders(fc.Borders)150 if err != nil {151 return "", Format{}, Style{}, err152 }153 st := Style{Bold: fc.Bold, Italic: fc.Italic, Underline: fc.Underline, Strikethrough: fc.Strikethrough, Align: al, Wrap: wr, VAlign: va, Borders: b, own: fc.Own}154 return fc.Input, f, st, nil155}156157// fileBorders are a cell's borders in the file, each edge's line by name158// and, when it has one, its color: {"top": "thin", "left": "double",159// "leftColor": "red"}. Earlier builds ignore the colors and draw the160// lines in the text's ink.161type fileBorders struct {162 Top string `json:"top,omitempty"`163 Bottom string `json:"bottom,omitempty"`164 Left string `json:"left,omitempty"`165 Right string `json:"right,omitempty"`166 TopColor string `json:"topColor,omitempty"`167 BottomColor string `json:"bottomColor,omitempty"`168 LeftColor string `json:"leftColor,omitempty"`169 RightColor string `json:"rightColor,omitempty"`170}171172func encodeBorders(b Borders) *fileBorders {173 if b.IsZero() {174 return nil175 }176 return &fileBorders{Top: b.Top().String(), Bottom: b.Bottom().String(), Left: b.Left().String(), Right: b.Right().String(),177 TopColor: b.Color(EdgeTop).String(), BottomColor: b.Color(EdgeBottom).String(),178 LeftColor: b.Color(EdgeLeft).String(), RightColor: b.Color(EdgeRight).String()}179}180181func decodeBorders(fb *fileBorders) (Borders, error) {182 var b Borders183 if fb == nil {184 return b, nil185 }186 lines := [...]string{EdgeTop: fb.Top, EdgeBottom: fb.Bottom, EdgeLeft: fb.Left, EdgeRight: fb.Right}187 colors := [...]string{EdgeTop: fb.TopColor, EdgeBottom: fb.BottomColor, EdgeLeft: fb.LeftColor, EdgeRight: fb.RightColor}188 for edge, name := range lines {189 l, ok := ParseLine(name)190 if !ok {191 return 0, fmt.Errorf("unknown border line %q", name)192 }193 c, ok := ParseColor(colors[edge])194 if !ok {195 return 0, fmt.Errorf("unknown border color %q", colors[edge])196 }197 if l != LineNone {198 b = b.WithStroke(Edge(edge), Stroke{l, c})199 }200 }201 return b, nil202}203204// appendSaved appends the cell at a, which holds one, as the file205// writes it, or reports false for a cell the file leaves out (a pivot's206// or a spill's result). A plain cell without formatting is written from207// its slot, without making a Cell.208func (st *cellStore) appendSaved(buf []byte, a Addr) ([]byte, bool, error) {209 b, i := st.find(a)210 if sl := b.vals[i]; sl.kind != slotRich && sl.kind&slotDerived == 0 && sl.look == 0 {211 return st.appendInput(buf, sl), true, nil212 }213 c := st.get(a).saved()214 if c == nil {215 return buf, false, nil216 }217 raw, err := encodeCell(c)218 return append(buf, raw...), true, err219}220221// appendInput appends a plain slot's entry as a JSON string.222func (st *cellStore) appendInput(buf []byte, sl slot) []byte {223 switch {224 case sl.kind == slotBlank:225 return append(buf, '"', '"')226 case sl.ref != 0:227 return appendJSONString(buf, st.strs.strs[sl.ref])228 case sl.kind == slotBool:229 return append(append(append(buf, '"'), boolText(sl.num != 0)...), '"')230 }231 buf = strconv.AppendFloat(append(buf, '"'), sl.num, 'f', int(sl.dec)-1, 64)232 return append(buf, '"')233}234235// appendJSONString appends s as json.Marshal writes it: as it is between236// quotes when it is printable ASCII that needs no escape (HTML's237// characters included, which json.Marshal escapes), and through238// json.Marshal otherwise.239func appendJSONString(buf []byte, s string) []byte {240 for i := 0; i < len(s); i++ {241 if c := s[i]; c < 0x20 || c > 0x7e || c == '"' || c == '\\' || c == '<' || c == '>' || c == '&' {242 raw, _ := json.Marshal(s)243 return append(buf, raw...)244 }245 }246 return append(append(append(buf, '"'), s...), '"')247}248249// appendAddr appends a's A1 name, as a.String() writes it.250func appendAddr(buf []byte, a Addr) []byte {251 var col [4]byte252 i := len(col)253 for c := a.Col + 1; c > 0; c = (c - 1) / 26 {254 i--255 col[i] = byte('A' + (c-1)%26)256 }257 return strconv.AppendInt(append(buf, col[i:]...), int64(a.Row+1), 10)258}