| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "strconv" |
| 6 | "strings" |
| 7 | |
| 8 | "amadan.net/oficina/sheets/internal/sheets/store" |
| 9 | ) |
| 10 | |
| 11 | // cellPaint is a cell's formatting, rendered for the table view. |
| 12 | // |
| 13 | // The grid gets this from paint.js, which sets data- attributes on the |
| 14 | // td and writes the OKLCH components into custom properties. The table |
| 15 | // view has no script, so the server does the same job — and the result |
| 16 | // has to be the SAME attributes and the SAME properties, because |
| 17 | // sheets.css already styles them and a second vocabulary here would be |
| 18 | // a second implementation of "what bold looks like". |
| 19 | // |
| 20 | // PRODUCT.md's second principle is the reason this exists at all: the |
| 21 | // scriptless path is the product, not the fallback, and a screen that |
| 22 | // can set bold but cannot show it is the worse half of the feature. |
| 23 | type cellPaint struct { |
| 24 | Bold bool |
| 25 | Italic bool |
| 26 | Align string // "", "left", "center", "right" |
| 27 | // Fill and Ink say which of the two colour mechanisms applies. |
| 28 | // Fill is a colour THIS app applied, stored as OKLCH intent and |
| 29 | // resolved per theme; FillHex is a colour an imported FILE |
| 30 | // authored, painted exactly as written (spec §11.1). They are |
| 31 | // mutually exclusive in practice and are kept as separate flags |
| 32 | // rather than one enum, because the CSS distinguishes them too. |
| 33 | Fill bool |
| 34 | Ink bool |
| 35 | FillHex bool |
| 36 | InkHex bool |
| 37 | // LowContrast mirrors the grid's data-contrast="low", so the cells |
| 38 | // the contrast notice names are the ones marked on both screens. |
| 39 | LowContrast bool |
| 40 | // Vars is the inline custom-property declaration. template.CSS |
| 41 | // because it is interpolated into a style attribute — and it is |
| 42 | // only ever built by paintOf below, from numbers formatted by |
| 43 | // strconv and hex strings already validated on the way in. |
| 44 | Vars template.CSS |
| 45 | } |
| 46 | |
| 47 | // Any reports whether there is anything to paint. The template uses it |
| 48 | // to skip the whole attribute block for the overwhelmingly common |
| 49 | // unformatted cell. |
| 50 | func (p cellPaint) Any() bool { |
| 51 | return p.Bold || p.Italic || p.Align != "" || p.Fill || p.Ink || |
| 52 | p.FillHex || p.InkHex |
| 53 | } |
| 54 | |
| 55 | // paintOf converts a stored Format into the table view's paint. |
| 56 | // |
| 57 | // It deliberately reads store.Format directly rather than going through |
| 58 | // styleOf/wireStyle. wireStyle is the JSON the GRID receives, with short |
| 59 | // keys and omitempty, shaped for a stream that pays per keystroke; the |
| 60 | // table view is a rendered page and needs Go values. Routing one through |
| 61 | // the other would make a change to the wire format silently a change to |
| 62 | // this page. |
| 63 | func paintOf(f *store.Format) cellPaint { |
| 64 | if f == nil { |
| 65 | return cellPaint{} |
| 66 | } |
| 67 | p := cellPaint{ |
| 68 | Bold: f.Bold, Italic: f.Italic, Align: f.Align, |
| 69 | LowContrast: lowContrast(f), |
| 70 | } |
| 71 | var vars strings.Builder |
| 72 | if f.FillIntentH != nil && f.FillIntentC != nil { |
| 73 | p.Fill = true |
| 74 | vars.WriteString("--sheet-h:" + num(*f.FillIntentH) + ";") |
| 75 | vars.WriteString("--sheet-c:" + num(*f.FillIntentC) + ";") |
| 76 | } |
| 77 | if f.InkIntentH != nil && f.InkIntentC != nil { |
| 78 | p.Ink = true |
| 79 | vars.WriteString("--sheet-kh:" + num(*f.InkIntentH) + ";") |
| 80 | vars.WriteString("--sheet-kc:" + num(*f.InkIntentC) + ";") |
| 81 | } |
| 82 | // An imported literal colour, through previewsvg.go's safeHex — |
| 83 | // the same check the thumbnail already applies to the same field, |
| 84 | // and reused rather than rewritten so the two screens cannot come |
| 85 | // to different conclusions about what a colour is. |
| 86 | // |
| 87 | // It is not belt and braces here. This string came out of somebody |
| 88 | // else's .xlsx and is about to be interpolated into a style |
| 89 | // attribute, and html/template cannot look inside a template.CSS |
| 90 | // value: declaring that type is a promise the caller makes, so the |
| 91 | // caller has to be the one that keeps it. |
| 92 | if hex := safeHex(f.FillHex); hex != "" { |
| 93 | p.FillHex = true |
| 94 | vars.WriteString("--sheet-fx:" + hex + ";") |
| 95 | } |
| 96 | if hex := safeHex(f.InkHex); hex != "" { |
| 97 | p.InkHex = true |
| 98 | vars.WriteString("--sheet-k:" + hex + ";") |
| 99 | } |
| 100 | p.Vars = template.CSS(vars.String()) |
| 101 | return p |
| 102 | } |
| 103 | |
| 104 | // num renders an OKLCH component. 'g' with -1 precision, matching how |
| 105 | // the grid's JSON carries these, so a fill looks identical on the two |
| 106 | // screens rather than differing in the last decimal place. |
| 107 | func num(f float64) string { return strconv.FormatFloat(f, 'g', -1, 64) } |
| 108 | |