import { resolveBlock } from "@/lib/blocks"; import type { AppliedTemplate, TemplateBlueprint, TemplateDataSourceBlueprint, } from "./types"; export type IdFactory = () => string; function cloneAndReplace( value: unknown, sourceIds: ReadonlyMap, nestedIds: Map, idFactory: IdFactory, ): unknown { if (typeof value === "string") { if (value.startsWith("@source:")) { const key = value.slice("@source:".length); const sourceId = sourceIds.get(key); if (!sourceId) throw new Error(`Template references missing data source: ${key}`); return sourceId; } if (value.startsWith("@id:")) { const key = value.slice("@id:".length); const existing = nestedIds.get(key); if (existing) return existing; const generated = idFactory(); nestedIds.set(key, generated); return generated; } return value; } if (Array.isArray(value)) { return value.map((item) => cloneAndReplace(item, sourceIds, nestedIds, idFactory), ); } if (value && typeof value === "object") { return Object.fromEntries( Object.entries(value).map(([key, item]) => [ key, cloneAndReplace(item, sourceIds, nestedIds, idFactory), ]), ); } return value; } function cloneDataSource( blueprint: TemplateDataSourceBlueprint, id: string, idFactory: IdFactory, ): AppliedTemplate["dataSources"][number] { return { id, name: blueprint.name, description: blueprint.description ?? null, version: 1, fields: structuredClone(blueprint.fields), rows: (blueprint.rows ?? []).map((row) => ({ id: row.id ?? idFactory(), values: structuredClone(row.values), hidden: row.hidden, })), }; } export function applyTemplate( template: TemplateBlueprint, characterId: string, idFactory: IdFactory = () => crypto.randomUUID(), ): AppliedTemplate { const sourceIds = new Map( template.dataSources.map((source) => [source.key, idFactory()]), ); const nestedIds = new Map(); const dataSources = template.dataSources.map((source) => cloneDataSource(source, sourceIds.get(source.key)!, idFactory), ); const pages = template.pages.map((page, pageIndex) => { const pageId = idFactory(); const blocks = page.blocks.map((block, blockIndex) => { const serialized = { id: idFactory(), type: block.type, schemaVersion: block.schemaVersion ?? 1, config: cloneAndReplace( block.config, sourceIds, nestedIds, idFactory, ) as Record, responsiveLayout: cloneAndReplace( block.responsiveLayout ?? {}, sourceIds, nestedIds, idFactory, ) as Record, sortOrder: blockIndex, }; const resolution = resolveBlock(serialized); if (resolution.kind !== "ready") { throw new Error( `Built-in template ${template.slug} contains an invalid ${block.type} block.`, ); } return resolution.block; }); return { id: pageId, characterId, slug: page.slug, title: page.title, publicNote: page.publicNote ?? null, navOrder: pageIndex, visible: false as const, version: 1 as const, blocks, }; }); return { templateSlug: template.slug, characterId, pages, dataSources, }; }