| 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 | |
| 9 | const fail = msg => { throw new Error(msg) } |
| 10 | |
| 11 | const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v) |
| 12 | const isInt = v => Number.isInteger(v) |
| 13 | |
| 14 | function 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 | } |
| 20 | function 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). */ |
| 27 | function 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 | } |
| 34 | const str = (o, k) => field(o, k, v => typeof v === 'string', 'a string') |
| 35 | const optStr = (o, k) => optional(o, k, v => typeof v === 'string', 'a string') |
| 36 | const nullStr = (o, k) => nullable(o, k, v => typeof v === 'string', 'a string') |
| 37 | const optNum = (o, k) => optional(o, k, v => typeof v === 'number' && Number.isFinite(v), 'a number') |
| 38 | const optProps = (o, k) => optional(o, k, isObject, 'a map') |
| 39 | |
| 40 | function 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. */ |
| 47 | export 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 | |