oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "errors"
5
6 "amadan.net/oficina/sheets/internal/sheets/store"
7)
8
9// formatPatch is a change to the formatting of a rectangle of cells.
10//
11// **Every field is a pointer, and that is the whole design.** A toolbar
12// toggle sends `{"bold": false}` to un-bold and sends no `bold` key at
13// all to leave boldness alone, and those are different requests. With
14// value fields they would decode identically, and pressing "fill:
15// yellow" would silently un-bold the selection — the class of bug where
16// the control you did not touch is the one that changed.
17//
18// The client decides a toggle's TARGET STATE and sends it explicitly,
19// rather than sending "toggle" and letting the server work it out over
20// a range. Over a mixed selection those differ: Google and Excel both
21// make everything bold when anything is not, and a per-cell flip would
22// instead invert a checkerboard into its negative.
23type formatPatch struct {
24 R1 int `json:"r1"`
25 C1 int `json:"c1"`
26 R2 int `json:"r2"`
27 C2 int `json:"c2"`
28
29 Bold *bool `json:"bold,omitempty"`
30 Italic *bool `json:"italic,omitempty"`
31 Align *string `json:"align,omitempty"`
32 NumFmt *string `json:"numfmt,omitempty"`
33 // Fill and Ink carry a palette NAME, or "none" to clear. Never a
34 // hex — see palette.go for why the server owns the colours.
35 Fill *string `json:"fill,omitempty"`
36 Ink *string `json:"ink,omitempty"`
37
38 Client string `json:"client"`
39}
40
41var (
42 errBadAlign = errors.New("sheets: unknown alignment")
43 errBadSwatch = errors.New("sheets: unknown colour")
44 errBadNumFmt = errors.New("sheets: unknown number format")
45 errEmptyPatch = errors.New("sheets: patch changes nothing")
46)
47
48// alignments are the values Align accepts. Empty clears it back to the
49// engine's own choice, which right-aligns numbers and left-aligns text
50// — the behaviour a spreadsheet has when nobody has said otherwise, and
51// the reason "no alignment" is a real state rather than a synonym for
52// left.
53var alignments = map[string]bool{"left": true, "center": true, "right": true}
54
55// touchesAnything reports whether the patch would change a format at
56// all. A patch that names no field is rejected rather than applied,
57// because applying it would re-intern every cell in the range to a
58// format identical to the one it already has and publish a repaint for
59// no reason.
60func (p formatPatch) touchesAnything() bool {
61 return p.Bold != nil || p.Italic != nil || p.Align != nil ||
62 p.NumFmt != nil || p.Fill != nil || p.Ink != nil
63}
64
65// validate checks the patch's values before any cell is touched.
66//
67// Up front and over the whole patch, not per cell: a range write that
68// styles four hundred cells and then rejects the four hundred and first
69// has already half-applied something the caller asked for atomically.
70func (p formatPatch) validate() error {
71 if !p.touchesAnything() {
72 return errEmptyPatch
73 }
74 if p.Align != nil && *p.Align != "" && !alignments[*p.Align] {
75 return errBadAlign
76 }
77 // Against the offered list, not a length. See palette.go's
78 // numberFormat for why a byte cap was the wrong check.
79 if p.NumFmt != nil && !knownNumberFormat(*p.NumFmt) {
80 return errBadNumFmt
81 }
82 if p.Fill != nil && *p.Fill != noColour {
83 if _, ok := lookupSwatch(fillSwatches, *p.Fill); !ok {
84 return errBadSwatch
85 }
86 }
87 if p.Ink != nil && *p.Ink != noColour {
88 if _, ok := lookupSwatch(inkSwatches, *p.Ink); !ok {
89 return errBadSwatch
90 }
91 }
92 return nil
93}
94
95// applyTo merges the patch into one cell's existing format.
96//
97// f is taken by value and returned by value, with ID and Digest zeroed:
98// the result is CONTENT to be interned, not a row to be updated.
99// Updating the row in place would edit the format shared by every other
100// cell that happens to look the same, which is exactly what interning
101// makes possible and exactly what it must never be used to do — bolding
102// one cell would bold a thousand.
103func (p formatPatch) applyTo(f store.Format) store.Format {
104 f.ID, f.Digest = 0, ""
105
106 if p.Bold != nil {
107 f.Bold = *p.Bold
108 }
109 if p.Italic != nil {
110 f.Italic = *p.Italic
111 }
112 if p.Align != nil {
113 f.Align = *p.Align
114 }
115 if p.NumFmt != nil {
116 f.NumberFmt = *p.NumFmt
117 }
118 if p.Fill != nil {
119 applyFill(&f, *p.Fill)
120 }
121 if p.Ink != nil {
122 applyInk(&f, *p.Ink)
123 }
124 return f
125}
126
127// applyFill sets or clears a fill.
128//
129// **Setting one clears FillHex.** FillHex is the AUTHOR'S literal,
130// retained from import so a re-export is lossless (store/models.go).
131// Once somebody recolours the cell here it is no longer the author's
132// colour, and leaving the hex behind would make export write the old
133// one back — the file would come out the colour it went in, whatever
134// the screen showed.
135func applyFill(f *store.Format, name string) {
136 f.FillHex = ""
137 if name == noColour {
138 f.FillIntentH, f.FillIntentC = nil, nil
139 return
140 }
141 s, ok := lookupSwatch(fillSwatches, name)
142 if !ok {
143 // validate has already refused this; the guard is here so a
144 // future caller that forgets to validate clears the fill rather
145 // than writing a zero hue, which is red.
146 f.FillIntentH, f.FillIntentC = nil, nil
147 return
148 }
149 h, c := s.H, s.C
150 f.FillIntentH, f.FillIntentC = &h, &c
151}
152
153// applyInk is applyFill's twin for text colour — same rule, same reason.
154func applyInk(f *store.Format, name string) {
155 f.InkHex = ""
156 if name == noColour {
157 f.InkIntentH, f.InkIntentC = nil, nil
158 return
159 }
160 s, ok := lookupSwatch(inkSwatches, name)
161 if !ok {
162 f.InkIntentH, f.InkIntentC = nil, nil
163 return
164 }
165 h, c := s.H, s.C
166 f.InkIntentH, f.InkIntentC = &h, &c
167}
168
169// normalize orders and bounds the rectangle.
170//
171// A selection dragged up and to the left arrives with r2 < r1, which is
172// not an error — it is how a person selects. Ordering here means every
173// caller below can assume r1 <= r2 rather than each remembering to.
174func (p formatPatch) normalize(maxRows, maxCols int) (r1, c1, r2, c2 int, ok bool) {
175 r1, r2 = p.R1, p.R2
176 c1, c2 = p.C1, p.C2
177 if r1 > r2 {
178 r1, r2 = r2, r1
179 }
180 if c1 > c2 {
181 c1, c2 = c2, c1
182 }
183 if r1 < 0 || c1 < 0 {
184 return 0, 0, 0, 0, false
185 }
186 if r2 >= maxRows || c2 >= maxCols {
187 return 0, 0, 0, 0, false
188 }
189 return r1, c1, r2, c2, true
190}
191