oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The JSON view of a deck — core/src/view.rs, key for key. What the drive
2// API returns and what the renderer reads; the vectors compare this to
3// the core's own view of the same document as parsed JSON, so key order
4// does not matter here (generate.mjs sorts keys when it writes a file).
5//
6// Every key has a default so a deck a peer left half-written still views
7// as something: an absent layout is flow, an absent template blank, an
8// absent z is 0. slides, and the blocks of one, are in reading order —
9// the order key sorted, array index for ties — and the key itself is
10// not in the view. A
11// block's text key is present for text-bearing blocks
12// (any block that carries a text fragment) and absent otherwise; a
13// table's cells appear as props.cells, a row-major array of texts.
14import * as Y from 'yjs'
15import { roots, getString, getNumber, getArray, getFragment, getObject, orderedMaps, outToJSON } from './ydoc.mjs'
16import { readText } from './schema.mjs'
17
18export const DEFAULT_THEME = 'paper'
19export const DEFAULT_ASPECT = '16:9'
20export const DEFAULT_TEMPLATE = 'blank'
21
22// --- typed leaves ---
23//
24// The server reads this view through typed structs, and a peer can put
25// any shape at any key (box.x = "a", background.kind = 5, fonts.body =
26// [], an asset that is a string). Passed through untyped, any one of
27// those fails the server's parse of the whole deck. So each leaf with a
28// fixed shape is rebuilt from the fields that have the right type, and
29// one that cannot be made is omitted — null, the default, a skipped
30// entry. core/src/view.rs applies the same rules, key for key; the
31// vectors compare the two, and view.test.mjs is the Rust test's twin.
32
33const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v)
34
35/** A present string field, or null; `undefined` when present and mistyped. */
36function stringField (o, key) {
37 const v = o[key]
38 if (v === undefined || v === null) return null
39 return typeof v === 'string' ? v : undefined
40}
41
42function numberField (o, key) {
43 const v = o[key]
44 if (v === undefined || v === null) return null
45 return typeof v === 'number' ? v : undefined
46}
47
48/** {x, y, w, h}, all four numbers, or null. */
49export function boxValue (v) {
50 if (!isObject(v)) return null
51 const out = {}
52 for (const key of ['x', 'y', 'w', 'h']) {
53 const n = numberField(v, key)
54 if (n === null || n === undefined) return null
55 out[key] = n
56 }
57 return out
58}
59
60/** {kind, value} with kind a string, else the theme default. */
61export function backgroundValue (v) {
62 const fallback = { kind: 'theme', value: '' }
63 if (!isObject(v)) return fallback
64 const kind = stringField(v, 'kind')
65 const value = stringField(v, 'value')
66 if (kind === null || kind === undefined || value === undefined) return fallback
67 return { kind, value: value ?? '' }
68}
69
70/** {heading, body}, both strings when present, else null. */
71export function fontsValue (v) {
72 if (!isObject(v)) return null
73 const heading = stringField(v, 'heading')
74 const body = stringField(v, 'body')
75 if (heading === undefined || body === undefined) return null
76 return { heading: heading ?? '', body: body ?? '' }
77}
78
79/** The §2.5 overrides that have the right type, field by field; order is a whole number. */
80export function phoneValue (v) {
81 if (!isObject(v)) return {}
82 const out = {}
83 if (typeof v.hidden === 'boolean') out.hidden = v.hidden
84 const order = numberField(v, 'order')
85 if (typeof order === 'number') out.order = Math.trunc(order)
86 const size = stringField(v, 'size')
87 if (typeof size === 'string') out.size = size
88 const box = boxValue(v.box)
89 if (box) out.box = box
90 return out
91}
92
93/** {ref, name, width, height, alt} for an entry with a string ref; null otherwise. */
94export function assetValue (v) {
95 if (!isObject(v)) return null
96 const ref = stringField(v, 'ref')
97 if (ref === null || ref === undefined) return null
98 const text = key => stringField(v, key) ?? ''
99 const whole = key => { const n = numberField(v, key); return typeof n === 'number' ? Math.trunc(n) : 0 }
100 return { ref, name: text('name'), width: whole('width'), height: whole('height'), alt: text('alt') }
101}
102
103function blockView (m) {
104 const props = getObject(m, 'props') ?? {}
105 const cells = getArray(m, 'cells')
106 if (cells !== null) {
107 props.cells = cells.toArray().map(c => (c instanceof Y.XmlFragment && !(c instanceof Y.XmlElement) ? readText(c) : outToJSON(c)))
108 }
109 const block = {
110 id: getString(m, 'id') ?? '',
111 type: getString(m, 'type') ?? '',
112 props,
113 region: getString(m, 'region'),
114 box: boxValue(getObject(m, 'box')),
115 z: getNumber(m, 'z') ?? 0,
116 phone: phoneValue(getObject(m, 'phone'))
117 }
118 const text = getFragment(m, 'text')
119 if (text !== null) block.text = readText(text)
120 return block
121}
122
123function slideView (m) {
124 const blocks = getArray(m, 'blocks')
125 const notes = getFragment(m, 'notes')
126 return {
127 id: getString(m, 'id') ?? '',
128 layout: getString(m, 'layout') ?? 'flow',
129 template: getString(m, 'template') ?? DEFAULT_TEMPLATE,
130 background: backgroundValue(getObject(m, 'background')),
131 phone: getString(m, 'phone') ?? 'inherit',
132 transition: getString(m, 'transition') ?? 'none',
133 notes: notes !== null ? readText(notes) : [],
134 // Reading order (ydoc.mjs orderedMaps); the order key itself
135 // stays out of the view, so every reader sees one sequence.
136 blocks: blocks !== null ? orderedMaps(blocks).map(([, b]) => blockView(b)) : []
137 }
138}
139
140/** The deck as JSON. Never empty: an untouched document views as the defaults. */
141export function view (doc) {
142 const r = roots(doc)
143 const assets = {}
144 r.assets.forEach((v, k) => {
145 const a = assetValue(outToJSON(v))
146 if (a) assets[k] = a
147 })
148 return {
149 meta: {
150 title: getString(r.meta, 'title') ?? '',
151 theme: getString(r.meta, 'theme') ?? DEFAULT_THEME,
152 accent: getString(r.meta, 'accent'),
153 fonts: fontsValue(getObject(r.meta, 'fonts')),
154 aspect: getString(r.meta, 'aspect') ?? DEFAULT_ASPECT
155 },
156 // Slides in reading order (ydoc.mjs orderedMaps), the same way the
157 // blocks of one are; the order key itself never reaches the view.
158 slides: orderedMaps(r.slides).map(([, m]) => slideView(m)),
159 assets
160 }
161}
162