import type { BlockRegistry, ReadonlyBlockRegistry } from "./registry.ts"; import type { DocumentKindDefinition } from "./kinds.ts"; import { resolveDocumentVariables } from "./variables.ts"; import { DEFAULT_LIMITS, type BlockDocument, type BlockNode, type Diagnostic, type Result, } from "./types.ts"; const byteLength = (value: string): number => new TextEncoder().encode(value).byteLength; export function walkBlocks( root: BlockNode, visitor: (node: BlockNode, depth: number) => void, ): void { const visit = (node: BlockNode, depth: number) => { visitor(node, depth); for (const slot of Object.keys(node.slots).sort()) for (const child of node.slots[slot] ?? []) visit(child, depth + 1); }; visit(root, 0); } export function findBlock(root: BlockNode, id: string): BlockNode | undefined { let found: BlockNode | undefined; walkBlocks(root, (node) => { if (node.id === id) found = node; }); return found; } export function validateDocument( document: BlockDocument, kind: DocumentKindDefinition, registry: ReadonlyBlockRegistry, ): Result { const errors: Diagnostic[] = []; const limits = kind.limits ?? DEFAULT_LIMITS; let encoded = ""; try { encoded = JSON.stringify(document); } catch { errors.push({ code: "document.json", message: "Document must be JSON-serializable.", severity: "error", }); } if (byteLength(encoded) > limits.maxBytes) errors.push({ code: "document.size", message: `Document exceeds ${limits.maxBytes} bytes.`, severity: "error", }); if (document.kind !== kind.kind) errors.push({ code: "document.kind", message: `Expected document kind ${kind.kind}.`, severity: "error", }); if (!kind.rootTypes.includes(document.root.type)) errors.push({ code: "document.root", message: `Block ${document.root.type} cannot be the root.`, severity: "error", nodeId: document.root.id, }); const meta = kind.metadata.parse(document.meta, "meta"); if (!meta.ok) errors.push(...meta.errors); const variables = resolveDocumentVariables(document, kind.variables); if (!variables.ok) errors.push(...variables.errors); const ids = new Set(); let count = 0; walkBlocks(document.root, (node, depth) => { count++; if (!node.id || ids.has(node.id)) errors.push({ code: "document.id", message: `Duplicate or empty block id ${node.id}.`, severity: "error", nodeId: node.id, }); ids.add(node.id); if (depth > limits.maxDepth) errors.push({ code: "document.depth", message: `Document exceeds depth ${limits.maxDepth}.`, severity: "error", nodeId: node.id, }); const definition = registry.get(node.type); if (!definition) { errors.push({ code: "document.unknown-block", message: `Unknown block type ${node.type}.`, severity: "error", nodeId: node.id, }); return; } if (node.version !== definition.manifest.version) errors.push({ code: "document.block-version", message: `${node.type} requires migration to version ${definition.manifest.version}.`, severity: "error", nodeId: node.id, }); const props = definition.manifest.props.parse(node.props, `blocks.${node.id}.props`); if (!props.ok) errors.push(...props.errors.map((error) => ({ ...error, nodeId: node.id }))); const styles = definition.manifest.styles.parse(node.styles, `blocks.${node.id}.styles`); if (!styles.ok) errors.push(...styles.errors.map((error) => ({ ...error, nodeId: node.id }))); for (const slotName of Object.keys(node.slots)) if (!(slotName in definition.manifest.slots)) errors.push({ code: "document.slot", message: `Unknown slot ${slotName} on ${node.type}.`, severity: "error", nodeId: node.id, }); for (const [slotName, slot] of Object.entries(definition.manifest.slots)) { const children = node.slots[slotName] ?? []; const maximum = Math.min( slot.maxChildren ?? limits.maxChildrenPerSlot, limits.maxChildrenPerSlot, ); if (children.length < (slot.minChildren ?? 0) || children.length > maximum) errors.push({ code: "document.cardinality", message: `Slot ${slotName} has invalid child count.`, severity: "error", nodeId: node.id, }); for (const child of children) { const childDefinition = registry.get(child.type); const acceptedType = !slot.accepts?.types || slot.accepts.types.includes(child.type); const acceptedCapabilities = !slot.accepts?.capabilities || slot.accepts.capabilities.every((capability) => childDefinition?.manifest.capabilities?.includes(capability), ); if (!acceptedType || !acceptedCapabilities) errors.push({ code: "document.slot-accept", message: `${child.type} is not accepted by ${node.type}.${slotName}.`, severity: "error", nodeId: child.id, }); } } errors.push(...(definition.validate?.(node) ?? [])); }); if (count > limits.maxNodes) errors.push({ code: "document.nodes", message: `Document exceeds ${limits.maxNodes} nodes.`, severity: "error", }); return errors.length ? { ok: false, errors } : { ok: true, value: document }; } export function registryForKind(kind: DocumentKindDefinition): BlockRegistry { const registry = createRegistry(kind.blocks); return registry; } import { createRegistry } from "./registry.ts";