// The deck's shape over yjs: root names and the small set of accessors // ops, layout and view share — core/src/schema.rs's "roots and lookups" // and "JSON <-> yrs values" sections. Every accessor is as tolerant as // the core's: a key holding the wrong kind of value reads as absent, a // non-map entry in an array of maps is skipped, never trapped on. import * as Y from 'yjs' import { readText } from './schema.mjs' export const ROOT_META = 'meta' export const ROOT_SLIDES = 'slides' export const ROOT_ASSETS = 'assets' /** The three root types. */ export function roots (doc) { return { meta: doc.getMap(ROOT_META), slides: doc.getArray(ROOT_SLIDES), assets: doc.getMap(ROOT_ASSETS) } } export function getString (map, key) { const v = map.get(key) return typeof v === 'string' ? v : null } export function getNumber (map, key) { const v = map.get(key) if (typeof v === 'number') return v if (typeof v === 'bigint') return Number(v) return null } export function getMap (map, key) { const v = map.get(key) return v instanceof Y.Map ? v : null } export function getArray (map, key) { const v = map.get(key) return v instanceof Y.Array ? v : null } export function getFragment (map, key) { const v = map.get(key) return v instanceof Y.XmlFragment && !(v instanceof Y.XmlElement) ? v : null } /** The maps of an array of maps, in order, as [index, map] pairs. */ export function mapsOf (array) { const out = [] array.forEach((v, i) => { if (v instanceof Y.Map) out.push([i, v]) }) return out } /** The map in array whose id is id, as [index, map], or null. */ export function findById (array, id) { return mapsOf(array).find(([, m]) => getString(m, 'id') === id) ?? null } // --- reading order (schema.rs, "reading order"; spec §2.2, §2.3, // rulings 18 and 21) --- // // Both of the document's ordered arrays — the deck's slides and a // slide's blocks — are SETS; the reading order is the `order` key on // each map in them, with array index as the tie-break and as the rank // of an entry that carries none. Reordering by moving array entries // means deleting a map and inserting a copy, and a peer's edit in // flight against the original is lost on merge (codex 4, at both // levels); writing a number onto the map it already is loses nothing. /** The rank an entry sorts by: its order when a finite number, else its array index. */ function rank (index, m) { const n = getNumber(m, 'order') return n !== null && Number.isFinite(n) ? n : index } /** The maps of an ordered array (slides, a slide's blocks) in reading order, as [arrayIndex, map] pairs. */ export function orderedMaps (array) { return mapsOf(array).sort((a, b) => (rank(a[0], a[1]) - rank(b[0], b[1])) || (a[0] - b[0])) } /** Whether any entry in the array carries an explicit order. */ export function hasExplicitOrder (array) { return mapsOf(array).some(([, m]) => { const n = getNumber(m, 'order'); return n !== null && Number.isFinite(n) }) } /** Writes order 0..n-1 onto maps in the order given. */ export function renumber (maps) { maps.forEach((m, k) => m.set('order', k)) } /** A plain object: a shared type rendered by content, a plain value as is. */ function isPlainObject (v) { return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Y.AbstractType) } /** `Some(json)` when the value is a map (shared or plain), else null. */ export function getObject (map, key) { const v = map.get(key) if (v instanceof Y.Map) return mapToJSON(v) if (isPlainObject(v)) return anyToJSON(v) return null } export function anyToJSON (v) { if (v === undefined) return null if (typeof v === 'bigint') return Number(v) if (Array.isArray(v)) return v.map(anyToJSON) if (v instanceof Uint8Array) return Array.from(v) if (isPlainObject(v)) { const out = {} for (const k of Object.keys(v)) out[k] = anyToJSON(v[k]) return out } return v } /** * Any value the document holds, as JSON: a map as an object, an array * as an array, a text fragment as its paragraphs, a text as its string. */ export function outToJSON (v) { if (v instanceof Y.Map) return mapToJSON(v) if (v instanceof Y.Array) return v.toArray().map(outToJSON) if (v instanceof Y.XmlElement) return v.toString() if (v instanceof Y.XmlFragment) return readText(v) if (v instanceof Y.Text) return v.toString() if (v instanceof Y.AbstractType) return null return anyToJSON(v) } export function mapToJSON (map) { const out = {} map.forEach((v, k) => { out[k] = outToJSON(v) }) return out } /** A deep copy of a map ready to insert elsewhere (yrs's as_prelim). */ export function cloneMap (map) { return map.clone() }