Files
buzz-sheet/lib/templates/apply.ts
T

132 lines
3.4 KiB
TypeScript

import { resolveBlock } from "@/lib/blocks";
import type {
AppliedTemplate,
TemplateBlueprint,
TemplateDataSourceBlueprint,
} from "./types";
export type IdFactory = () => string;
function cloneAndReplace(
value: unknown,
sourceIds: ReadonlyMap<string, string>,
nestedIds: Map<string, string>,
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<string, string>();
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<string, unknown>,
responsiveLayout: cloneAndReplace(
block.responsiveLayout ?? {},
sourceIds,
nestedIds,
idFactory,
) as Record<string, unknown>,
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,
};
}