// The shape pass of applyOps — what serde does for core/src/ops.rs's Op // enum: required fields, the kinds of value each takes, and the difference // between an absent key and an explicit null where it matters (`after` // absent appends, `after: null` puts first; `accent: null` clears). The // result is one op in the enum's shape: absent fields undefined, nulls // kept only where they mean something. Every check here is one the core // makes too, so a batch this module accepts the core accepts. const fail = msg => { throw new Error(msg) } const isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v) const isInt = v => Number.isInteger(v) function field (o, key, check, what) { const v = o[key] if (v === undefined) fail(`missing field \`${key}\``) if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`) return v } function optional (o, key, check, what) { const v = o[key] if (v === undefined || v === null) return undefined if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`) return v } /** undefined when absent, null when explicitly null (so `after: null` and `accent: null` mean something). */ function nullable (o, key, check, what) { const v = o[key] if (v === undefined) return undefined if (v === null) return null if (!check(v)) fail(`invalid type for \`${key}\`, expected ${what}`) return v } const str = (o, k) => field(o, k, v => typeof v === 'string', 'a string') const optStr = (o, k) => optional(o, k, v => typeof v === 'string', 'a string') const nullStr = (o, k) => nullable(o, k, v => typeof v === 'string', 'a string') const optNum = (o, k) => optional(o, k, v => typeof v === 'number' && Number.isFinite(v), 'a number') const optProps = (o, k) => optional(o, k, isObject, 'a map') function boxSpec (v) { if (!isObject(v)) fail('invalid type for `box`, expected a map') for (const k of ['x', 'y', 'w', 'h']) field(v, k, n => typeof n === 'number' && Number.isFinite(n), 'a number') return { x: v.x, y: v.y, w: v.w, h: v.h } } /** One op as the core's enum: absent fields undefined, explicit nulls kept where they mean something. */ export function parseOp (raw) { if (!isObject(raw)) fail('an op must be an object') const op = raw.op if (typeof op !== 'string') fail('missing field `op`') switch (op) { case 'set_meta': { let fonts = nullable(raw, 'fonts', isObject, 'a map') if (fonts) fonts = { heading: str(fonts, 'heading'), body: str(fonts, 'body') } return { op, title: optStr(raw, 'title'), theme: optStr(raw, 'theme'), accent: nullStr(raw, 'accent'), fonts } } case 'add_slide': return { op, id: str(raw, 'id'), after: nullStr(raw, 'after'), layout: str(raw, 'layout'), template: optStr(raw, 'template') } case 'remove_slide': case 'remove_asset': return { op, id: str(raw, 'id') } case 'move_slide': return { op, id: str(raw, 'id'), after: optStr(raw, 'after') } case 'duplicate_slide': { const ids = field(raw, 'new_block_ids', v => Array.isArray(v) && v.every(x => typeof x === 'string'), 'a list of strings') return { op, id: str(raw, 'id'), new_id: str(raw, 'new_id'), new_block_ids: ids } } case 'set_slide': { let background = optional(raw, 'background', isObject, 'a map') if (background) background = { kind: str(background, 'kind'), value: optStr(background, 'value') ?? '' } return { op, id: str(raw, 'id'), layout: optStr(raw, 'layout'), template: optStr(raw, 'template'), background, phone: optStr(raw, 'phone'), transition: optStr(raw, 'transition') } } case 'set_notes': return { op, id: str(raw, 'id'), markdown: str(raw, 'markdown') } case 'add_block': { const bx = raw.box === undefined || raw.box === null ? undefined : boxSpec(raw.box) return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), type: str(raw, 'type'), props: optProps(raw, 'props') ?? {}, region: optStr(raw, 'region'), box: bx, z: optNum(raw, 'z'), after: nullStr(raw, 'after'), markdown: optStr(raw, 'markdown') } } case 'remove_block': return { op, slide: str(raw, 'slide'), id: str(raw, 'id') } case 'move_block': return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), after: optStr(raw, 'after') } case 'set_block': { const bx = raw.box === undefined ? undefined : raw.box === null ? null : boxSpec(raw.box) return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), props: optProps(raw, 'props'), region: nullStr(raw, 'region'), box: bx, z: optNum(raw, 'z') } } case 'set_text': return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), markdown: str(raw, 'markdown') } case 'set_phone': { const p = field(raw, 'phone', isObject, 'a map') for (const k of Object.keys(p)) if (!['hidden', 'order', 'size', 'box'].includes(k)) fail(`unknown field \`${k}\` in phone`) const phone = { hidden: optional(p, 'hidden', v => typeof v === 'boolean', 'a boolean'), order: optional(p, 'order', isInt, 'an integer'), size: optStr(p, 'size'), box: p.box === undefined || p.box === null ? undefined : boxSpec(p.box) } return { op, slide: str(raw, 'slide'), id: str(raw, 'id'), phone } } case 'register_asset': return { op, id: str(raw, 'id'), ref: str(raw, 'ref'), name: optStr(raw, 'name') ?? '', width: optional(raw, 'width', isInt, 'an integer') ?? 0, height: optional(raw, 'height', isInt, 'an integer') ?? 0, alt: optStr(raw, 'alt') ?? '' } default: fail(`unknown variant \`${op}\``) } }