| 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. |
| 13 | package deck |
| 14 | |
| 15 | import ( |
| 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. |
| 29 | type 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. |
| 39 | type 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. |
| 48 | type 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. |
| 61 | type 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. |
| 74 | type 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". |
| 87 | type 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. |
| 108 | func (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). |
| 125 | type 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". |
| 136 | type 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. |
| 145 | type 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. |
| 155 | type 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. |
| 159 | type 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. |
| 172 | func (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. |
| 186 | func 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 | |