132 lines
3.4 KiB
TypeScript
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,
|
|
};
|
|
}
|