67 lines
2.9 KiB
TypeScript
67 lines
2.9 KiB
TypeScript
import type { BlockDocument, Diagnostic, JsonValue, Result } from "./types.ts";
|
|
|
|
export interface VariableToken {
|
|
raw: string;
|
|
key: string;
|
|
fallback?: string;
|
|
}
|
|
|
|
const TOKEN = /\{\{\s*([A-Za-z][A-Za-z0-9_.-]*)(?:\|([^{}]*))?\s*\}\}/g;
|
|
|
|
export function variableTokens(value: string): VariableToken[] {
|
|
return [...value.matchAll(TOKEN)].map((match) => ({
|
|
raw: match[0],
|
|
key: match[1]!,
|
|
fallback: match[2]?.trim() || undefined,
|
|
}));
|
|
}
|
|
|
|
const validType = (value: unknown, type: string) => {
|
|
if (type === "string") return typeof value === "string";
|
|
if (type === "number") return typeof value === "number" && Number.isFinite(value);
|
|
if (type === "boolean") return typeof value === "boolean";
|
|
return type === "date" && (value instanceof Date || !Number.isNaN(Date.parse(String(value))));
|
|
};
|
|
|
|
export function resolveDocumentVariables(
|
|
document: BlockDocument,
|
|
catalog: Record<string, "string" | "number" | "date" | "boolean">,
|
|
data: Record<string, unknown> = {},
|
|
options: { requireValues?: boolean; preserveTokens?: boolean } = {},
|
|
): Result<BlockDocument> {
|
|
const diagnostics: Diagnostic[] = [];
|
|
if (options.preserveTokens && options.requireValues)
|
|
return { ok: false, errors: [{ code: "variable.mode", message: "Template tokens cannot be preserved when resolved values are required.", severity: "error" }] };
|
|
const visit = (value: JsonValue, path: string): JsonValue => {
|
|
if (typeof value === "string")
|
|
return value.replace(TOKEN, (raw, key: string, fallback: string | undefined) => {
|
|
const type = catalog[key];
|
|
if (!type) {
|
|
diagnostics.push({ code: "variable.unknown", message: `Unknown variable ${key}.`, severity: "error", path });
|
|
return raw;
|
|
}
|
|
if (options.preserveTokens) return raw;
|
|
const supplied = data[key];
|
|
if (supplied === undefined || supplied === null || supplied === "") {
|
|
if (fallback !== undefined) return fallback.trim();
|
|
if (options.requireValues)
|
|
diagnostics.push({ code: "variable.missing", message: `Variable ${key} is required.`, severity: "error", path });
|
|
return raw;
|
|
}
|
|
if (!validType(supplied, type)) {
|
|
diagnostics.push({ code: "variable.type", message: `Variable ${key} must be ${type}.`, severity: "error", path });
|
|
return raw;
|
|
}
|
|
return supplied instanceof Date ? supplied.toISOString() : String(supplied);
|
|
});
|
|
if (Array.isArray(value)) return value.map((item, index) => visit(item, `${path}.${index}`));
|
|
if (value && typeof value === "object")
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, item]) => [key, visit(item, `${path}.${key}`)]),
|
|
);
|
|
return value;
|
|
};
|
|
const resolved = visit(structuredClone(document) as unknown as JsonValue, "document") as unknown as BlockDocument;
|
|
return diagnostics.length ? { ok: false, errors: diagnostics } : { ok: true, value: resolved };
|
|
}
|