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, data: Record = {}, options: { requireValues?: boolean; preserveTokens?: boolean } = {}, ): Result { 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 }; }