feat(builder): add dynamic blocks email and dnd packages
This commit is contained in:
1 parent
06433e4ac4
commit
0bd2a6860d
37 files changed
+3060
-1
No files matched your search
@@ -0,0 +1,165 @@
|
||||
import type { BlockRegistry, ReadonlyBlockRegistry } from "./registry.ts";
|
||||
import type { DocumentKindDefinition } from "./kinds.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<BlockDocument> {
|
||||
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 ids = new Set<string>();
|
||||
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";
|
||||
Reference in new issue
Block a user