oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1// The shape pass of applyOps — what serde does for core/src/ops.rs's Op
2// enum: required fields, the kinds of value each takes, and the difference
3// between an absent key and an explicit null where it matters (`after`
4// absent appends, `after: null` puts first; `accent: null` clears). The
5// result is one op in the enum's shape: absent fields undefined, nulls
6// kept only where they mean something. Every check here is one the core
7// makes too, so a batch this module accepts the core accepts.
8
9const fail = msg => { throw new Error(msg) }
10
11const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v)
12const isInt = v => Number.isInteger(v)
13
14function field (o, key, check, what) {
15 const v = o[key]
16 if (v === undefined) fail(`missing field \`${key}\``)
17 if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`)
18 return v
19}
20function optional (o, key, check, what) {
21 const v = o[key]
22 if (v === undefined || v === null) return undefined
23 if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`)
24 return v
25}
26/** undefined when absent, null when explicitly null (so `after: null` and `accent: null` mean something). */
27function nullable (o, key, check, what) {
28 const v = o[key]
29 if (v === undefined) return undefined
30 if (v === null) return null
31 if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`)
32 return v
33}
34const str = (o, k) => field(o, k, v => typeof v === 'string', 'a string')
35const optStr = (o, k) => optional(o, k, v => typeof v === 'string', 'a string')
36const nullStr = (o, k) => nullable(o, k, v => typeof v === 'string', 'a string')
37const optNum = (o, k) => optional(o, k, v => typeof v === 'number' && Number.isFinite(v), 'a number')
38const optProps = (o, k) => optional(o, k, isObject, 'a map')
39
40function boxSpec (v) {
41 if (!isObject(v)) fail('invalid type for `box`, expected a map')
42 for (const k of ['x', 'y', 'w', 'h']) field(v, k, n => typeof n === 'number' && Number.isFinite(n), 'a number')
43 return { x: v.x, y: v.y, w: v.w, h: v.h }
44}
45
46/** One op as the core's enum: absent fields undefined, explicit nulls kept where they mean something. */
47export function parseOp (raw) {
48 if (!isObject(raw)) fail('an op must be an object')
49 const op = raw.op
50 if (typeof op !== 'string') fail('missing field `op`')
51 switch (op) {
52 case 'set_meta': {
53 let fonts = nullable(raw, 'fonts', isObject, 'a map')
54 if (fonts) fonts = { heading: str(fonts, 'heading'), body: str(fonts, 'body') }
55 return { op, title: optStr(raw, 'title'), theme: optStr(raw, 'theme'), accent: nullStr(raw, 'accent'), fonts }
56 }
57 case 'add_slide':
58 return { op, id: str(raw, 'id'), after: nullStr(raw, 'after'), layout: str(raw, 'layout'), template: optStr(raw, 'template') }
59 case 'remove_slide':
60 case 'remove_asset':
61 return { op, id: str(raw, 'id') }
62 case 'move_slide':
63 return { op, id: str(raw, 'id'), after: optStr(raw, 'after') }
64 case 'duplicate_slide': {
65 const ids = field(raw, 'new_block_ids', v => Array.isArray(v) && v.every(x => typeof x === 'string'), 'a list of strings')
66 return { op, id: str(raw, 'id'), new_id: str(raw, 'new_id'), new_block_ids: ids }
67 }
68 case 'set_slide': {
69 let background = optional(raw, 'background', isObject, 'a map')
70 if (background) background = { kind: str(background, 'kind'), value: optStr(background, 'value') ?? '' }
71 return {
72 op, id: str(raw, 'id'), layout: optStr(raw, 'layout'), template: optStr(raw, 'template'), background,
73 phone: optStr(raw, 'phone'), transition: optStr(raw, 'transition')
74 }
75 }
76 case 'set_notes':
77 return { op, id: str(raw, 'id'), markdown: str(raw, 'markdown') }
78 case 'add_block': {
79 const bx = raw.box === undefined || raw.box === null ? undefined : boxSpec(raw.box)
80 return {
81 op, slide: str(raw, 'slide'), id: str(raw, 'id'), type: str(raw, 'type'), props: optProps(raw, 'props') ?? {},
82 region: optStr(raw, 'region'), box: bx, z: optNum(raw, 'z'), after: nullStr(raw, 'after'), markdown: optStr(raw, 'markdown')
83 }
84 }
85 case 'remove_block':
86 return { op, slide: str(raw, 'slide'), id: str(raw, 'id') }
87 case 'move_block':
88 return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), after: optStr(raw, 'after') }
89 case 'set_block': {
90 const bx = raw.box === undefined ? undefined : raw.box === null ? null : boxSpec(raw.box)
91 return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), props: optProps(raw, 'props'), region: nullStr(raw, 'region'), box: bx, z: optNum(raw, 'z') }
92 }
93 case 'set_text':
94 return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), markdown: str(raw, 'markdown') }
95 case 'set_phone': {
96 const p = field(raw, 'phone', isObject, 'a map')
97 for (const k of Object.keys(p)) if (!['hidden', 'order', 'size', 'box'].includes(k)) fail(`unknown field \`${k}\` in phone`)
98 const phone = {
99 hidden: optional(p, 'hidden', v => typeof v === 'boolean', 'a boolean'),
100 order: optional(p, 'order', isInt, 'an integer'),
101 size: optStr(p, 'size'),
102 box: p.box === undefined || p.box === null ? undefined : boxSpec(p.box)
103 }
104 return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), phone }
105 }
106 case 'register_asset':
107 return {
108 op, id: str(raw, 'id'), ref: str(raw, 'ref'), name: optStr(raw, 'name') ?? '',
109 width: optional(raw, 'width', isInt, 'an integer') ?? 0, height: optional(raw, 'height', isInt, 'an integer') ?? 0, alt: optStr(raw, 'alt') ?? ''
110 }
111 default:
112 fail(`unknown variant \`${op}\``)
113 }
114}
115