oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The deck's shape over yjs: root names and the small set of accessors
2// ops, layout and view share — core/src/schema.rs's "roots and lookups"
3// and "JSON <-> yrs values" sections. Every accessor is as tolerant as
4// the core's: a key holding the wrong kind of value reads as absent, a
5// non-map entry in an array of maps is skipped, never trapped on.
6import * as Y from 'yjs'
7import { readText } from './schema.mjs'
8
9export const ROOT_META = 'meta'
10export const ROOT_SLIDES = 'slides'
11export const ROOT_ASSETS = 'assets'
12
13/** The three root types. */
14export function roots (doc) {
15 return {
16 meta: doc.getMap(ROOT_META),
17 slides: doc.getArray(ROOT_SLIDES),
18 assets: doc.getMap(ROOT_ASSETS)
19 }
20}
21
22export function getString (map, key) {
23 const v = map.get(key)
24 return typeof v === 'string' ? v : null
25}
26
27export function getNumber (map, key) {
28 const v = map.get(key)
29 if (typeof v === 'number') return v
30 if (typeof v === 'bigint') return Number(v)
31 return null
32}
33
34export function getMap (map, key) {
35 const v = map.get(key)
36 return v instanceof Y.Map ? v : null
37}
38
39export function getArray (map, key) {
40 const v = map.get(key)
41 return v instanceof Y.Array ? v : null
42}
43
44export function getFragment (map, key) {
45 const v = map.get(key)
46 return v instanceof Y.XmlFragment && !(v instanceof Y.XmlElement) ? v : null
47}
48
49/** The maps of an array of maps, in order, as [index, map] pairs. */
50export function mapsOf (array) {
51 const out = []
52 array.forEach((v, i) => { if (v instanceof Y.Map) out.push([i, v]) })
53 return out
54}
55
56/** The map in array whose id is id, as [index, map], or null. */
57export function findById (array, id) {
58 return mapsOf(array).find(([, m]) => getString(m, 'id') === id) ?? null
59}
60
61// --- reading order (schema.rs, "reading order"; spec §2.2, §2.3,
62// rulings 18 and 21) ---
63//
64// Both of the document's ordered arrays — the deck's slides and a
65// slide's blocks — are SETS; the reading order is the `order` key on
66// each map in them, with array index as the tie-break and as the rank
67// of an entry that carries none. Reordering by moving array entries
68// means deleting a map and inserting a copy, and a peer's edit in
69// flight against the original is lost on merge (codex 4, at both
70// levels); writing a number onto the map it already is loses nothing.
71
72/** The rank an entry sorts by: its order when a finite number, else its array index. */
73function rank (index, m) {
74 const n = getNumber(m, 'order')
75 return n !== null && Number.isFinite(n) ? n : index
76}
77
78/** The maps of an ordered array (slides, a slide's blocks) in reading order, as [arrayIndex, map] pairs. */
79export function orderedMaps (array) {
80 return mapsOf(array).sort((a, b) => (rank(a[0], a[1]) - rank(b[0], b[1])) || (a[0] - b[0]))
81}
82
83/** Whether any entry in the array carries an explicit order. */
84export function hasExplicitOrder (array) {
85 return mapsOf(array).some(([, m]) => { const n = getNumber(m, 'order'); return n !== null && Number.isFinite(n) })
86}
87
88/** Writes order 0..n-1 onto maps in the order given. */
89export function renumber (maps) {
90 maps.forEach((m, k) => m.set('order', k))
91}
92
93/** A plain object: a shared type rendered by content, a plain value as is. */
94function isPlainObject (v) {
95 return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Y.AbstractType)
96}
97
98/** `Some(json)` when the value is a map (shared or plain), else null. */
99export function getObject (map, key) {
100 const v = map.get(key)
101 if (v instanceof Y.Map) return mapToJSON(v)
102 if (isPlainObject(v)) return anyToJSON(v)
103 return null
104}
105
106export function anyToJSON (v) {
107 if (v === undefined) return null
108 if (typeof v === 'bigint') return Number(v)
109 if (Array.isArray(v)) return v.map(anyToJSON)
110 if (v instanceof Uint8Array) return Array.from(v)
111 if (isPlainObject(v)) {
112 const out = {}
113 for (const k of Object.keys(v)) out[k] = anyToJSON(v[k])
114 return out
115 }
116 return v
117}
118
119/**
120 * Any value the document holds, as JSON: a map as an object, an array
121 * as an array, a text fragment as its paragraphs, a text as its string.
122 */
123export function outToJSON (v) {
124 if (v instanceof Y.Map) return mapToJSON(v)
125 if (v instanceof Y.Array) return v.toArray().map(outToJSON)
126 if (v instanceof Y.XmlElement) return v.toString()
127 if (v instanceof Y.XmlFragment) return readText(v)
128 if (v instanceof Y.Text) return v.toString()
129 if (v instanceof Y.AbstractType) return null
130 return anyToJSON(v)
131}
132
133export function mapToJSON (map) {
134 const out = {}
135 map.forEach((v, k) => { out[k] = outToJSON(v) })
136 return out
137}
138
139/** A deep copy of a map ready to insert elsewhere (yrs's as_prelim). */
140export function cloneMap (map) {
141 return map.clone()
142}
143