oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Package deck is the Go reading of the core's deck view — spec §2's
2// model as it arrives over the wasm boundary.
3//
4// The core is the contract, not this file: `core/src/view.rs` decides
5// the JSON, and every type here exists to receive it. The view fills a
6// default for nearly every key itself (an absent layout is `flow`, an
7// absent template is `blank`, an absent z is 0), so a struct of plain
8// fields is enough — this package validates nothing and repairs
9// nothing. The one exception is Block.UnmarshalJSON, and it says why.
10//
11// It is deliberately read-only. Writes go the other way, as op batches
12// the core applies (core/src/ops.rs); nothing here ever produces one.
13package deck
14
15import (
16 "encoding/json"
17 "fmt"
18 "strings"
19)
20
21// Deck is one deck's whole view.
22//
23// Slides arrive in reading order, the same way a slide's Blocks do: the
24// document keeps that order as an `order` key on each slide map rather
25// than as the array's own order (§2.2, §2.3, rulings 18 and 21), and the
26// view has already sorted by it, so nothing downstream of here — the
27// renderer, the layout engine, the viewer, the exporter — sees the key
28// or needs to.
29type Deck struct {
30 Meta Meta `json:"meta"`
31 Slides []Slide `json:"slides"`
32 Assets map[string]Asset `json:"assets"`
33}
34
35// Meta is the deck's own settings (§2.1). Accent arrives as null when
36// unset, which decodes to the empty string; Fonts arrives as null or as
37// one {heading, body} object, so it is a pointer — "no fonts chosen" and
38// "a pair of empty names" are different things to a renderer.
39type Meta struct {
40 Title string `json:"title"`
41 Theme string `json:"theme"`
42 Accent string `json:"accent"`
43 Fonts *Fonts `json:"fonts"`
44 Aspect string `json:"aspect"`
45}
46
47// Fonts is meta.fonts: the heading and body families, chosen together.
48type Fonts struct {
49 Heading string `json:"heading"`
50 Body string `json:"body"`
51}
52
53// Slide is one slide (§2.2). Template is meaningful on a flow slide and
54// ignored on a canvas one; Phone is meaningful the other way round.
55//
56// Blocks arrive in reading order. The document keeps that order as an
57// `order` key on each block map rather than as the array's own order
58// (§2.2, ruling 18), and the view has already sorted by it — so this
59// package, the renderer and the layout engine never see the key and
60// never need to.
61type Slide struct {
62 ID string `json:"id"`
63 Layout string `json:"layout"`
64 Template string `json:"template"`
65 Background Background `json:"background"`
66 Phone string `json:"phone"`
67 Transition string `json:"transition"`
68 Notes Text `json:"notes"`
69 Blocks []Block `json:"blocks"`
70}
71
72// Background is a slide's backdrop: a theme default, a colour, or an
73// asset id.
74type Background struct {
75 Kind string `json:"kind"`
76 Value string `json:"value"`
77}
78
79// Block is one block (§2.4). A flow block carries Region and no Box; a
80// canvas block carries Box and Z and the view sends its region as null,
81// which decodes to the empty string. Props is free-form because the
82// vocabulary differs per type and the core already checked it.
83//
84// Text is present only for text-bearing types — the view omits the key
85// entirely for the others, so an empty Text means "this type carries
86// none", not "somebody deleted the words".
87type Block struct {
88 ID string `json:"id"`
89 Type string `json:"type"`
90 Props map[string]any `json:"props"`
91 Region string `json:"region"`
92 Box *Box `json:"box"`
93 Z int `json:"z"`
94 Phone PhoneOverride `json:"phone"`
95 Text Text `json:"text"`
96}
97
98// UnmarshalJSON is the plain decoding with one repair: z is read as a
99// number and truncated, rather than decoded straight into Z.
100//
101// The core writes whole z values, and its view serialises a whole
102// number as an integer — but z lives in the CRDT, and every browser
103// peer writing into the same document (task 6 onwards) can put a
104// fraction there, which is the ordinary trick for inserting between two
105// neighbours. Decoded straight, one such value would fail the whole
106// deck's parse and take the viewer down with it; truncating costs a
107// z-order tie and nothing else.
108func (b *Block) UnmarshalJSON(data []byte) error {
109 // plain drops this method, so the embedded value decodes normally;
110 // the outer Z is shallower and therefore wins for the "z" key.
111 type plain Block
112 var raw struct {
113 plain
114 Z float64 `json:"z"`
115 }
116 if err := json.Unmarshal(data, &raw); err != nil {
117 return err
118 }
119 *b = Block(raw.plain)
120 b.Z = int(raw.Z)
121 return nil
122}
123
124// Box is a canvas block's frame, as fractions of the 16:9 stage (§2.3).
125type Box struct {
126 X float64 `json:"x"`
127 Y float64 `json:"y"`
128 W float64 `json:"w"`
129 H float64 `json:"h"`
130}
131
132// PhoneOverride is a block's phone layer (§2.5) — additions to the
133// desktop layout, never a replacement for it. Order is a pointer
134// because 0 is a real position and "unset" has to be distinguishable
135// from "first".
136type PhoneOverride struct {
137 Hidden bool `json:"hidden"`
138 Order *int `json:"order"`
139 Size string `json:"size"`
140 Box *Box `json:"box"`
141}
142
143// Asset is one registered image (§2.7). Ref is the object store
144// address; the bytes never pass through the CRDT.
145type Asset struct {
146 Ref string `json:"ref"`
147 Name string `json:"name"`
148 Width int `json:"width"`
149 Height int `json:"height"`
150 Alt string `json:"alt"`
151}
152
153// Text is the §2.6 text schema as the view sends it: paragraphs of
154// runs.
155type Text [][]Run
156
157// Run is one stretch of text under one set of marks. The view omits a
158// false mark and an absent href, so the zero value is an unmarked run.
159type Run struct {
160 T string `json:"t"`
161 B bool `json:"b"`
162 I bool `json:"i"`
163 C bool `json:"c"`
164 H bool `json:"h"`
165 Href string `json:"href"`
166}
167
168// Plain is the text with every mark dropped: runs concatenated inside a
169// paragraph, paragraphs joined by a newline. It is what the plain deck
170// page renders, and what search, exports and the speaker view read when
171// they want words rather than formatting.
172func (t Text) Plain() string {
173 var b strings.Builder
174 for i, para := range t {
175 if i > 0 {
176 b.WriteByte('\n')
177 }
178 for _, run := range para {
179 b.WriteString(run.T)
180 }
181 }
182 return b.String()
183}
184
185// Parse reads the core's view JSON (core.Handle.JSON) into a Deck.
186func Parse(b []byte) (*Deck, error) {
187 var d Deck
188 if err := json.Unmarshal(b, &d); err != nil {
189 return nil, fmt.Errorf("deck: parse view: %w", err)
190 }
191 return &d, nil
192}
193