import { isJsonValue, type JsonValue } from './json-value.ts' export type AdfAttributes = { [key: string]: JsonValue } export type AdfMark = { attrs?: AdfAttributes type: string } export type AdfNode = { attrs?: AdfAttributes content?: AdfNode[] marks?: AdfMark[] text?: string type: string } export type AdfDocument = { content?: AdfNode[] type: 'doc' version: number } const documentKeys = ['content', 'type', 'version'] const markKeys = ['attrs', 'type'] const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type'] export function isAdfDocument(value: unknown): value is AdfDocument { if (!isRecord(value) || !holdsOnly(value, documentKeys)) return false if (value['type'] !== 'doc') return false if (typeof value['version'] !== 'number' || !Number.isFinite(value['version'])) return false return !('content' in value) || isNodeArray(value['content']) } function isAdfMark(value: unknown): value is AdfMark { if (!isRecord(value) || !holdsOnly(value, markKeys)) return false if (typeof value['type'] !== 'string') return false return !('attrs' in value) || isAttributes(value['attrs']) } function isNodeArray(value: unknown): value is AdfNode[] { if (!Array.isArray(value)) return false const pending: unknown[] = [...value] while (pending.length > 0) { const node = pending.pop() if (!isRecord(node) || !holdsOnly(node, nodeKeys)) return false if (typeof node['type'] !== 'string') return false if ('attrs' in node && !isAttributes(node['attrs'])) return false if ('marks' in node && !isArrayOf(node['marks'], isAdfMark)) return false if ('text' in node && typeof node['text'] !== 'string') return false if ('content' in node) { const content = node['content'] if (!Array.isArray(content)) return false pending.push(...content) } } return true } function isArrayOf(value: unknown, guard: (item: unknown) => item is T): value is T[] { return Array.isArray(value) && [...value].every(guard) } function isAttributes(value: unknown): value is AdfAttributes { return isRecord(value) && isJsonValue(value) } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function holdsOnly(value: Record, keys: readonly string[]): boolean { return Object.keys(value).every((key) => keys.includes(key)) }