feat(templates): add six reusable guide structures
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user