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 "html/template"
5 "regexp"
6 "strconv"
7 "strings"
8)
9
10// The thumbnail's geometry.
11//
12// 26 by 34, matching the Docs app's slot exactly and by agreement. A
13// spreadsheet is a landscape thing and this box is not, which is a
14// deliberate trade: the two apps' file lists have to have the same row
15// height or they stop reading as one product, and that matters more
16// than the aspect of a picture two centimetres tall.
17const (
18 previewW = 26.0
19 previewH = 34.0
20 // The bar inside a cell, as a fraction of the cell. A mark, not a
21 // fill: the cell is where the data is, the bar is that there is
22 // some.
23 previewInsetX = 0.14
24 previewBarTop = 0.28
25 previewBarH = 0.44
26)
27
28// safeHex accepts a fill hex from a document and returns a CSS colour,
29// or "".
30//
31// Six or eight hex digits and nothing else. The value came out of a
32// spreadsheet somebody uploaded and is about to be interpolated into an
33// SVG attribute, so this is the check that keeps a crafted "colour"
34// from being anything but a colour. html/template would escape the
35// attribute anyway; this makes the value correct rather than merely
36// harmless.
37var hexOnly = regexp.MustCompile(`^[0-9A-Fa-f]{6}$|^[0-9A-Fa-f]{8}$`)
38
39func safeHex(v string) string {
40 if !hexOnly.MatchString(v) {
41 return ""
42 }
43 if len(v) == 8 {
44 v = v[2:] // XLSX writes ARGB; the alpha is always FF here
45 }
46 return "#" + v
47}
48
49// trimFloat renders a number for a CSS function without an exponent or
50// a trailing run of zeroes, so the markup is stable and short.
51func trimFloat(f float64) string {
52 s := strconv.FormatFloat(f, 'f', 4, 64)
53 s = strings.TrimRight(s, "0")
54 return strings.TrimSuffix(s, ".")
55}
56
57// previewSVG renders a thumbnail.
58//
59// Inline SVG, built server-side, on read. Not a raster and not a file:
60// a rendered preview in the bucket would be an object derived from a
61// sealed sheet, and the argument about whether it counts as content is
62// one this app should never have to have. It also lets the SVG use the
63// page's own custom properties — --sheet-fill-l resolves per theme, so
64// the thumbnail follows light and dark the way the grid does.
65//
66// The returned value is template.HTML because it IS markup. Every
67// value interpolated into it is a number this file formatted or a
68// colour safeHex validated; nothing from a document reaches it
69// unchecked.
70func previewSVG(p preview) template.HTML {
71 var b strings.Builder
72 b.WriteString(`<svg viewBox="0 0 26 34" width="26" height="34" `)
73 b.WriteString(`role="img" aria-hidden="true" focusable="false" data-preview>`)
74 b.WriteString(`<rect x="0.5" y="0.5" width="25" height="33" rx="1.5" `)
75 b.WriteString(`fill="var(--rst-surface)" stroke="var(--rst-line)"/>`)
76
77 if !p.Empty && p.Rows > 0 && p.Cols > 0 {
78 cw := previewW / float64(p.Cols)
79 ch := previewH / float64(p.Rows)
80 for _, c := range p.Cells {
81 x := float64(c.C) * cw
82 y := float64(c.R) * ch
83
84 // The fill covers the whole cell, because that is what a
85 // fill does. It is drawn first so the bar sits on it.
86 if c.Fill != "" {
87 rect(&b, x, y, cw, ch, c.Fill, 0)
88 }
89 // The bar stands in for content. Bold cells are darker,
90 // which is what makes a header row read as a header row at
91 // this size — the one piece of formatting that survives
92 // being two millimetres tall.
93 ink := "var(--rst-text-faint)"
94 if c.Bold {
95 ink = "var(--rst-text-muted)"
96 }
97 rect(&b,
98 x+cw*previewInsetX, y+ch*previewBarTop,
99 cw*(1-2*previewInsetX), ch*previewBarH,
100 ink, 0.4)
101 }
102 }
103
104 b.WriteString(`</svg>`)
105 return template.HTML(b.String())
106}
107
108func rect(b *strings.Builder, x, y, w, h float64, fill string, rx float64) {
109 b.WriteString(`<rect x="` + trimFloat(x) + `" y="` + trimFloat(y) +
110 `" width="` + trimFloat(w) + `" height="` + trimFloat(h) + `"`)
111 if rx > 0 {
112 b.WriteString(` rx="` + trimFloat(rx) + `"`)
113 }
114 b.WriteString(` fill="` + fill + `"/>`)
115}
116