oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package layout
2
3import (
4 "encoding/json"
5 "fmt"
6 "strconv"
7 "strings"
8
9 "amadan.net/oficina/slides/internal/slides/deck"
10)
11
12// props.go reads a block's free-form props, and turns the colours a
13// deck may name into the `#rrggbb` a paint list carries.
14//
15// Every read takes a default and every enumerated value is checked
16// against its vocabulary, for the reason render's own props section
17// gives: props arrive from a peer mid-edit and can hold anything at
18// all. A prop of the wrong type takes its default rather than
19// travelling any further.
20
21func stringProp(props map[string]any, key string) string {
22 s, _ := props[key].(string)
23 return s
24}
25
26func numProp(props map[string]any, key string, def float64) float64 {
27 switch v := props[key].(type) {
28 case float64:
29 return v
30 case int:
31 return float64(v)
32 case json.Number:
33 if f, err := v.Float64(); err == nil {
34 return f
35 }
36 }
37 return def
38}
39
40func boolProp(props map[string]any, key string) bool {
41 b, _ := props[key].(bool)
42 return b
43}
44
45// sizeProp is §2.4's s, m, l — the step, not the size.
46func sizeProp(props map[string]any, def string) string {
47 switch s, _ := props["size"].(string); s {
48 case "s", "m", "l":
49 return s
50 }
51 return def
52}
53
54// alignProp is a text block's own alignment, or the region's when it
55// has none. `justify` lays out as `start` here: justification changes
56// where the words sit on a line and not where the line breaks, so it
57// is a serialiser's job (SVG has no justified text at all) and this
58// package refusing to guess is better than it guessing differently
59// from the browser.
60func alignProp(props map[string]any, def Align) Align {
61 switch a, _ := props["align"].(string); a {
62 case "center":
63 return Center
64 case "end":
65 return End
66 case "start", "justify":
67 return Start
68 }
69 return def
70}
71
72// childIDs is a columns block's props.children (§2.4).
73func childIDs(b deck.Block) []string {
74 switch v := b.Props["children"].(type) {
75 case []string:
76 return v
77 case []any:
78 out := make([]string, 0, len(v))
79 for _, c := range v {
80 if s, ok := c.(string); ok {
81 out = append(out, s)
82 }
83 }
84 return out
85 }
86 return nil
87}
88
89// cellsProp reads a table's props.cells: a row-major array of texts
90// (core/src/view.rs). Back through JSON rather than walked by hand,
91// for render's reason — the view built it with encoding/json's shapes,
92// and a round trip is shorter and harder to get wrong than five type
93// assertions deep.
94func cellsProp(props map[string]any) []deck.Text {
95 raw, ok := props["cells"]
96 if !ok {
97 return nil
98 }
99 b, err := json.Marshal(raw)
100 if err != nil {
101 return nil
102 }
103 var out []deck.Text
104 if err := json.Unmarshal(b, &out); err != nil {
105 return nil
106 }
107 return out
108}
109
110// ── Colours ──────────────────────────────────────────────────────────
111
112// basicColors is CSS's sixteen original keywords, which is what a
113// person typing a colour by hand into a shape actually types. A
114// keyword outside it resolves to the fallback the caller names rather
115// than travelling into a PDF, where "rebeccapurple" is not a colour
116// anything can draw.
117var basicColors = map[string]Fill{
118 "black": "#000000", "silver": "#c0c0c0", "gray": "#808080", "grey": "#808080",
119 "white": "#ffffff", "maroon": "#800000", "red": "#ff0000", "purple": "#800080",
120 "fuchsia": "#ff00ff", "green": "#008000", "lime": "#00ff00", "olive": "#808000",
121 "yellow": "#ffff00", "navy": "#000080", "blue": "#0000ff", "teal": "#008080",
122 "aqua": "#00ffff",
123}
124
125// color resolves a colour a deck named into one this package can
126// paint: `#rgb` and `#rrggbb` verbatim, `currentColor` as the slide's
127// foreground, `none` and `transparent` as no colour at all, one of the
128// basic keywords as its hex, and anything else as def.
129//
130// An alpha channel is dropped, with the colour kept: a paint list has
131// no opacity (the SVG and the PDF would disagree about compositing it)
132// and dropping the channel loses less than dropping the colour.
133func (e *engine) color(v string, def Fill) Fill {
134 v = strings.TrimSpace(v)
135 switch strings.ToLower(v) {
136 case "":
137 return def
138 case "currentcolor":
139 return e.fg
140 case "none", "transparent":
141 return ""
142 }
143 if strings.HasPrefix(v, "#") {
144 if hex, ok := normalizeHex(v); ok {
145 return hex
146 }
147 return def
148 }
149 if c, ok := basicColors[strings.ToLower(v)]; ok {
150 return c
151 }
152 return def
153}
154
155// normalizeHex widens #rgb to #rrggbb, lowercases, and drops an alpha
156// channel.
157func normalizeHex(v string) (Fill, bool) {
158 h := strings.ToLower(v[1:])
159 switch len(h) {
160 case 4: // #rgba
161 h = h[:3]
162 case 8: // #rrggbbaa
163 h = h[:6]
164 }
165 if len(h) == 3 {
166 h = string([]byte{h[0], h[0], h[1], h[1], h[2], h[2]})
167 }
168 if len(h) != 6 {
169 return "", false
170 }
171 for _, c := range h {
172 if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
173 return "", false
174 }
175 }
176 return Fill("#" + h), true
177}
178
179// mix is the stylesheet's color-mix(in oklab, A p%, B), done in plain
180// sRGB.
181//
182// It is an approximation and the only one in this package that is not
183// arithmetic: oklab mixing is perceptual and this is not, so a panel
184// mixed here is a shade or two off the browser's. It is used for the
185// four greys the stylesheet builds that way — a code panel, a table
186// rule, a divider, a highlight — where being a shade off is invisible
187// and being absent is not. Getting it exact means an oklab conversion
188// nothing else here needs.
189func mix(a, b Fill, frac float64) Fill {
190 ca, ok1 := parseHex(a)
191 cb, ok2 := parseHex(b)
192 if !ok1 || !ok2 {
193 return b
194 }
195 var out [3]int
196 for i := range out {
197 v := float64(ca[i])*frac + float64(cb[i])*(1-frac)
198 out[i] = int(v + 0.5)
199 }
200 return Fill(fmt.Sprintf("#%02x%02x%02x", out[0], out[1], out[2]))
201}
202
203func parseHex(f Fill) ([3]int, bool) {
204 s := string(f)
205 if len(s) != 7 || s[0] != '#' {
206 return [3]int{}, false
207 }
208 var out [3]int
209 for i := range out {
210 v, err := strconv.ParseUint(s[1+i*2:3+i*2], 16, 8)
211 if err != nil {
212 return [3]int{}, false
213 }
214 out[i] = int(v)
215 }
216 return out, true
217}
218