// The JSON view of a deck — core/src/view.rs, key for key. What the drive // API returns and what the renderer reads; the vectors compare this to // the core's own view of the same document as parsed JSON, so key order // does not matter here (generate.mjs sorts keys when it writes a file). // // Every key has a default so a deck a peer left half-written still views // as something: an absent layout is flow, an absent template blank, an // absent z is 0. slides, and the blocks of one, are in reading order — // the order key sorted, array index for ties — and the key itself is // not in the view. A // block's text key is present for text-bearing blocks // (any block that carries a text fragment) and absent otherwise; a // table's cells appear as props.cells, a row-major array of texts. import * as Y from 'yjs' import { roots, getString, getNumber, getArray, getFragment, getObject, orderedMaps, outToJSON } from './ydoc.mjs' import { readText } from './schema.mjs' export const DEFAULT_THEME = 'paper' export const DEFAULT_ASPECT = '16:9' export const DEFAULT_TEMPLATE = 'blank' // --- typed leaves --- // // The server reads this view through typed structs, and a peer can put // any shape at any key (box.x = "a", background.kind = 5, fonts.body = // [], an asset that is a string). Passed through untyped, any one of // those fails the server's parse of the whole deck. So each leaf with a // fixed shape is rebuilt from the fields that have the right type, and // one that cannot be made is omitted — null, the default, a skipped // entry. core/src/view.rs applies the same rules, key for key; the // vectors compare the two, and view.test.mjs is the Rust test's twin. const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v) /** A present string field, or null; `undefined` when present and mistyped. */ function stringField (o, key) { const v = o[key] if (v === undefined || v === null) return null return typeof v === 'string' ? v : undefined } function numberField (o, key) { const v = o[key] if (v === undefined || v === null) return null return typeof v === 'number' ? v : undefined } /** {x, y, w, h}, all four numbers, or null. */ export function boxValue (v) { if (!isObject(v)) return null const out = {} for (const key of ['x', 'y', 'w', 'h']) { const n = numberField(v, key) if (n === null || n === undefined) return null out[key] = n } return out } /** {kind, value} with kind a string, else the theme default. */ export function backgroundValue (v) { const fallback = { kind: 'theme', value: '' } if (!isObject(v)) return fallback const kind = stringField(v, 'kind') const value = stringField(v, 'value') if (kind === null || kind === undefined || value === undefined) return fallback return { kind, value: value ?? '' } } /** {heading, body}, both strings when present, else null. */ export function fontsValue (v) { if (!isObject(v)) return null const heading = stringField(v, 'heading') const body = stringField(v, 'body') if (heading === undefined || body === undefined) return null return { heading: heading ?? '', body: body ?? '' } } /** The §2.5 overrides that have the right type, field by field; order is a whole number. */ export function phoneValue (v) { if (!isObject(v)) return {} const out = {} if (typeof v.hidden === 'boolean') out.hidden = v.hidden const order = numberField(v, 'order') if (typeof order === 'number') out.order = Math.trunc(order) const size = stringField(v, 'size') if (typeof size === 'string') out.size = size const box = boxValue(v.box) if (box) out.box = box return out } /** {ref, name, width, height, alt} for an entry with a string ref; null otherwise. */ export function assetValue (v) { if (!isObject(v)) return null const ref = stringField(v, 'ref') if (ref === null || ref === undefined) return null const text = key => stringField(v, key) ?? '' const whole = key => { const n = numberField(v, key); return typeof n === 'number' ? Math.trunc(n) : 0 } return { ref, name: text('name'), width: whole('width'), height: whole('height'), alt: text('alt') } } function blockView (m) { const props = getObject(m, 'props') ?? {} const cells = getArray(m, 'cells') if (cells !== null) { props.cells = cells.toArray().map(c => (c instanceof Y.XmlFragment && !(c instanceof Y.XmlElement) ? readText(c) : outToJSON(c))) } const block = { id: getString(m, 'id') ?? '', type: getString(m, 'type') ?? '', props, region: getString(m, 'region'), box: boxValue(getObject(m, 'box')), z: getNumber(m, 'z') ?? 0, phone: phoneValue(getObject(m, 'phone')) } const text = getFragment(m, 'text') if (text !== null) block.text = readText(text) return block } function slideView (m) { const blocks = getArray(m, 'blocks') const notes = getFragment(m, 'notes') return { id: getString(m, 'id') ?? '', layout: getString(m, 'layout') ?? 'flow', template: getString(m, 'template') ?? DEFAULT_TEMPLATE, background: backgroundValue(getObject(m, 'background')), phone: getString(m, 'phone') ?? 'inherit', transition: getString(m, 'transition') ?? 'none', notes: notes !== null ? readText(notes) : [], // Reading order (ydoc.mjs orderedMaps); the order key itself // stays out of the view, so every reader sees one sequence. blocks: blocks !== null ? orderedMaps(blocks).map(([, b]) => blockView(b)) : [] } } /** The deck as JSON. Never empty: an untouched document views as the defaults. */ export function view (doc) { const r = roots(doc) const assets = {} r.assets.forEach((v, k) => { const a = assetValue(outToJSON(v)) if (a) assets[k] = a }) return { meta: { title: getString(r.meta, 'title') ?? '', theme: getString(r.meta, 'theme') ?? DEFAULT_THEME, accent: getString(r.meta, 'accent'), fonts: fontsValue(getObject(r.meta, 'fonts')), aspect: getString(r.meta, 'aspect') ?? DEFAULT_ASPECT }, // Slides in reading order (ydoc.mjs orderedMaps), the same way the // blocks of one are; the order key itself never reaches the view. slides: orderedMaps(r.slides).map(([, m]) => slideView(m)), assets } }