From a369c5ca944e8b554e1e0884be1c06d81b7000a5 Mon Sep 17 00:00:00 2001 From: G Mo Date: Thu, 6 Nov 2025 17:48:27 -0500 Subject: [PATCH 1/4] deleted duplicate key check in zod --- server/src/preFlight/validateTreatmentFile.ts | 90 +++---------------- 1 file changed, 10 insertions(+), 80 deletions(-) diff --git a/server/src/preFlight/validateTreatmentFile.ts b/server/src/preFlight/validateTreatmentFile.ts index 730aa2867..d44e4bc7d 100644 --- a/server/src/preFlight/validateTreatmentFile.ts +++ b/server/src/preFlight/validateTreatmentFile.ts @@ -414,7 +414,6 @@ const elementBaseSchema = z .object({ name: nameSchema.optional(), desc: descriptionSchema.optional(), - file: fileSchema.or(fieldPlaceholderSchema).optional(), displayTime: displayTimeSchema.or(fieldPlaceholderSchema).optional(), hideTime: hideTimeSchema.or(fieldPlaceholderSchema).optional(), showToPositions: showToPositionsSchema @@ -555,6 +554,16 @@ export const elementSchema = altTemplateContext( const isObject = typeof data === "object" && data !== null; const hasTypeKey = isObject && "type" in data; + // if (hasTypeKey && (data as any).type === "prompt") { + // if (!("file" in (data as any)) || (data as any).file === undefined || (data as any).file === null) { + // ctx.addIssue({ + // code: z.ZodIssueCode.custom, + // path: ["file"], + // message: "Prompt elements must include a 'file' field.", + // }); + // } + // } + const schemaToUse = hasTypeKey ? z.discriminatedUnion("type", [ audioSchema, @@ -851,34 +860,6 @@ export const treatmentSchema = altTemplateContext( }); }); }); - - // Ensure unique element names within each treatment, grouped by type - const typeToNames = new Map>(); - gameStages?.forEach((stage: { elements: any[]; name: any }, stageIndex: string | number) => { - stage?.elements?.forEach((element: any, elementIndex: string | number) => { - if (!element) return; - - const isObject = typeof element === "object" && element !== null; - if (!isObject) return; - - const elType = typeof (element as any).type === "string" ? (element as any).type : undefined; - const elName = typeof (element as any).name === "string" ? (element as any).name : undefined; - - if (!elType || !elName) return; - - const seen = typeToNames.get(elType) ?? new Set(); - if (seen.has(elName)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["gameStages", stageIndex, "elements", elementIndex, "name"], - message: `Duplicate name "${elName}" for element type "${elType}" within treatment "${treatment.name}". Elements of the same type must have unique names within a single treatment.`, - }); - } else { - seen.add(elName); - typeToNames.set(elType, seen); - } - }); - }); }) ); @@ -999,57 +980,6 @@ export const templateContentSchema = z.any().superRefine((data, ctx) => { // This is done regardless of whether a treatmentSchema matched, // to catch treatments nested within other structures // (e.g., within an intro sequence or other custom structures) - const checkTreatmentForDuplicateNames = (treatment: any, basePath: any[] = []) => { - const typeToNamesLocal = new Map>(); - const gameStagesLocal = treatment?.gameStages; - gameStagesLocal?.forEach((stage: { elements: any[]; name: any }, stageIndex: string | number) => { - stage?.elements?.forEach((element: any, elementIndex: string | number) => { - if (!element) return; - - const isObject = typeof element === "object" && element !== null; - if (!isObject) return; - - const elType = typeof (element as any).type === "string" ? (element as any).type : undefined; - const elName = typeof (element as any).name === "string" ? (element as any).name : undefined; - - if (!elType || !elName) return; - - const seen = typeToNamesLocal.get(elType) ?? new Set(); - if (seen.has(elName)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [...basePath, "gameStages", stageIndex, "elements", elementIndex, "name"], - message: `Duplicate name "${elName}" for element type "${elType}" within treatment "${treatment?.name}". Elements of the same type must have unique names within a single treatment.`, - }); - } else { - seen.add(elName); - typeToNamesLocal.set(elType, seen); - } - }); - }); - }; - - const traverseAndCheck = (node: any, path: any[] = []) => { - if (Array.isArray(node)) { - node.forEach((item, idx) => traverseAndCheck(item, [...path, idx])); - } else if (node && typeof node === "object") { - if ("gameStages" in node && Array.isArray((node as any).gameStages)) { - checkTreatmentForDuplicateNames(node, path); - } - for (const [key, val] of Object.entries(node)) { - traverseAndCheck(val, [...path, key]); - } - } - }; - - try { - traverseAndCheck(data, []); - } catch (e) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Error while validating template content for duplicate element names: ${String(e)}`, - }); - } if (bestSchemaResult) { console.log( From d10f49cf64113fe4ee2576590883ed1c442e91d7 Mon Sep 17 00:00:00 2001 From: G Mo Date: Thu, 6 Nov 2025 18:08:59 -0500 Subject: [PATCH 2/4] adderd comment for fill templates --- server/src/preFlight/fillTemplates.js | 2 + server/src/preFlight/fillTemplates.ts | 348 ++++++++++++++++++++++++++ 2 files changed, 350 insertions(+) create mode 100644 server/src/preFlight/fillTemplates.ts diff --git a/server/src/preFlight/fillTemplates.js b/server/src/preFlight/fillTemplates.js index 74e53d1af..e621d6cd6 100644 --- a/server/src/preFlight/fillTemplates.js +++ b/server/src/preFlight/fillTemplates.js @@ -1,4 +1,6 @@ /* eslint-disable no-restricted-syntax */ +/* Another fillTemplates file in Typescript is in this same folder for deliberation-empirica +Please update all changes made here to the Typescript version, so expanding templates for treatments is consistent */ import { templateContextSchema } from "./validateTreatmentFile.ts"; diff --git a/server/src/preFlight/fillTemplates.ts b/server/src/preFlight/fillTemplates.ts new file mode 100644 index 000000000..097e93649 --- /dev/null +++ b/server/src/preFlight/fillTemplates.ts @@ -0,0 +1,348 @@ +/* eslint-disable no-restricted-syntax */ +import * as vscode from "vscode"; +import * as yaml from "js-yaml"; +import { templateContextSchema } from "./validateTreatmentFile"; +// if you use zod for the schema, you can uncomment these two lines to get a stricter type: +// import type { z } from "zod"; +// type TemplateContext = z.infer; + +type JsonLike = unknown; + +type TemplateDef = { + templateName: string; + templateContent: JsonLike; + // allow extra keys without constraining shape + [k: string]: unknown; +}; + +// If you want a strict type, replace `any` with the zod-inferred `TemplateContext` above. +type TemplateContext = any; + +export function substituteFields({ + templateContent, + fields, +}: { + templateContent: JsonLike; + // fields is usually a flat map, but we allow any values + fields: Record; +}): JsonLike { + // Deep clone the template to avoid mutating the original + let expandedTemplate: JsonLike = JSON.parse(JSON.stringify(templateContent)); + + // console.log("populating fields", fields); + for (const [key, value] of Object.entries(fields)) { + let stringifiedTemplate = JSON.stringify(expandedTemplate); + const stringifiedValue = JSON.stringify(value); + + // replace all instances of `"${key}"` with serialized value + // this handles objects and arrays, etc. + const objectReplacementRegex = new RegExp(`"\\$\\{${key}\\}"`, "g"); + stringifiedTemplate = stringifiedTemplate.replace( + objectReplacementRegex, + stringifiedValue + ); + + // if the value is just a string or number, we can also replace instances of ${key} within other strings + if (typeof value === "string") { + // replace all instances of `${key}` embedded in strings with other text with a serialized value + const stringReplacementRegex = new RegExp(`\\$\\{${key}\\}`, "g"); + stringifiedTemplate = stringifiedTemplate.replace( + stringReplacementRegex, + value + ); + } + // Todo: throw error message if we're trying to substitute an object within a string + + // Parse after each replacement to surface any errors + expandedTemplate = JSON.parse(stringifiedTemplate); + } + return expandedTemplate; +} + +export function expandTemplate({ + templates, + context, +}: { + templates: TemplateDef[]; + context: TemplateContext; +}): JsonLike { + // Step 1: Fill in any templates within the context itself + const newContext: any = JSON.parse(JSON.stringify(context)); + // you can use templates within fields and broadcast, but not within the template reference + if (newContext.fields) { + // eslint-disable-next-line no-use-before-define + newContext.fields = recursivelyFillTemplates({ + obj: newContext.fields, + templates, + }); + } + if (newContext.broadcast) { + // eslint-disable-next-line no-use-before-define + newContext.broadcast = recursivelyFillTemplates({ + obj: newContext.broadcast, + templates, + }); + } + // console.log("newContext", newContext); + + // Find the matching template + const template = templates.find( + (t) => t.templateName === newContext.template + ); + if (!template) { + // eslint-disable-next-line no-console + console.log( + "Found templates:", + templates.map((t) => t.templateName) + ); + throw new Error(`Template "${newContext.template}" not found`); + } + // console.log("template", template); + // Deep clone the template content to avoid mutating the original + let expandedTemplate: any = JSON.parse(JSON.stringify(template.templateContent)); + + // Step 3: Apply given fields if any + if (newContext.fields) { + expandedTemplate = substituteFields({ + templateContent: expandedTemplate, + fields: newContext.fields as Record, + }); + } + + // The template, even after the original given fields are filled, is still a template that we can fill with broadcast values + + // Step 4: Handle broadcast fields if any + + // Define recursive function to flatten and expand the broadcast axes + function flattenBroadcast( + dimensions: Record>> + ): Array> { + const dimensionIndices = Object.keys(dimensions); + const dimensionNumbers = dimensionIndices.map((i) => parseInt(i.slice(1), 10)); + const lowestDimension = Math.min(...dimensionNumbers); + + const currentDimension = dimensions[`d${lowestDimension}`]; + const remainingDimensions: typeof dimensions = JSON.parse( + JSON.stringify(dimensions) + ); + delete remainingDimensions[`d${lowestDimension}`]; + + let partialFields: Array> = [{}]; + if (Object.keys(remainingDimensions).length > 0) { + partialFields = flattenBroadcast(remainingDimensions); + } + + const flatFields: Array> = []; + for (const [index, entry] of currentDimension.entries()) { + for (const partialField of partialFields) { + const newField: Record = { ...entry, ...partialField }; + newField[`d${lowestDimension}`] = `${index}`; // convert to string + flatFields.push(newField); + } + } + return flatFields; + } + + if (newContext.broadcast) { + const broadcastFieldsArray = flattenBroadcast( + newContext.broadcast as Record>> + ); + const returnObjects: any[] = []; + for (const broadcastFields of broadcastFieldsArray) { + const newObj = substituteFields({ + templateContent: expandedTemplate, + fields: broadcastFields, + }); + if (Array.isArray(newObj)) { + returnObjects.push(...newObj); + } else if (typeof newObj === "object" && newObj !== null) { + returnObjects.push(newObj); + } else { + throw new Error("Unexpected type in broadcast fields"); + } + } + return returnObjects; + } + + return expandedTemplate; +} + +export function recursivelyFillTemplates({ + obj, + templates, +}: { + obj: JsonLike; + templates: TemplateDef[]; +}): JsonLike { + // obj is any object in the treatment file, whether it is a template context or not + let newObj: any; + try { + newObj = JSON.parse(JSON.stringify(obj)); // deep clone + } catch (e) { + // eslint-disable-next-line no-console + console.log("Error parsing", obj); + throw e; + } + // console.log("newObj", newObj); + + if (!Array.isArray(newObj) && typeof newObj === "object" && newObj !== null) { + // if we get to a node in the tree that is an object, it can either be a template + // context (ie, a reference to a template that needs to be filled), or a regular + // object that may contain other template contexts that need to be filled + // in this case, recursively navigate through the keys of the object + if (newObj && (newObj as any).template) { + // object is a template context + const context: TemplateContext = templateContextSchema.parse(newObj); + newObj = expandTemplate({ templates, context }); + newObj = recursivelyFillTemplates({ obj: newObj, templates }); + } else { + // eslint-disable-next-line guard-for-in + for (const key in newObj as Record) { + if (newObj[key] == null) { + // eslint-disable-next-line no-console + console.log(`key ${key} is undefined in`, newObj); + } + newObj[key] = recursivelyFillTemplates({ obj: newObj[key], templates }); + } + } + } else if (Array.isArray(newObj)) { + // if the node is itself an array, we need to iterate through each item in the array. + // if the item is a template context, we need to expand it and replace it with the expanded object + for (const [index, item] of (newObj as any[]).entries()) { + if (item && (item as any).template) { + const context: TemplateContext = templateContextSchema.parse(item); + const expandedItem = expandTemplate({ templates, context }); + if (Array.isArray(expandedItem)) { + newObj.splice(index, 1, ...expandedItem); + } else if (typeof expandedItem === "object" && expandedItem !== null) { + newObj[index] = expandedItem; + } else { + throw new Error("Unexpected type in expanded item"); + } + } else { + newObj[index] = recursivelyFillTemplates({ obj: item, templates }); + } + } + } + + return newObj; +} + +export function fillTemplates({ + obj, + templates, +}: { + obj: JsonLike; + templates: TemplateDef[]; +}): JsonLike { + let newObj = recursivelyFillTemplates({ obj, templates }); + + // Check that there are no remaining templates + const templatesRemainingRegex = /"template":/g; + let templatesRemaining = JSON.stringify(newObj).match(templatesRemainingRegex); + while (templatesRemaining) { + // eslint-disable-next-line no-console + console.log("Found unfilled template, trying again."); + newObj = recursivelyFillTemplates({ obj: newObj, templates }); + + templatesRemaining = JSON.stringify(newObj).match(templatesRemainingRegex); + } + + // Check that all fields are filled + const doubleCheckRegex = /\$\{[a-zA-Z0-9_]+\}/g; + const missingFields = JSON.stringify(newObj).match(doubleCheckRegex); + if (missingFields) { + // eslint-disable-next-line no-console + console.log("error in ", JSON.stringify(newObj, null, 4)); + // eslint-disable-next-line no-console + console.log("missing fields", missingFields); + throw new Error(`Missing fields: ${missingFields.join(", ")}`); + } + + // console.log("Filled templates: ", newObj); + return newObj; +} + +export const EXP_SCHEME = "deliberation-expanded"; +const MAX_LINES = 10_000; + +export class ExpandedTemplatesProvider implements vscode.TextDocumentContentProvider { + private _onDidChange = new vscode.EventEmitter(); + readonly onDidChange = this._onDidChange.event; + + private _srcByPreview = new Map(); + + async provideTextDocumentContent(uri: vscode.Uri): Promise { + const qp = new URLSearchParams(uri.query); + const srcStr = qp.get("src"); + if (!srcStr) return "# Error: missing source URI\n"; + + const src = vscode.Uri.parse(srcStr); + this._srcByPreview.set(uri.toString(), src); + + try { + const raw = await vscode.workspace.fs.readFile(src); + const text = new TextDecoder("utf-8").decode(raw); + const obj = yaml.load(text) as any; + + // Pull templates from the file; tweak if your templates array lives elsewhere. + const templates: any[] = + Array.isArray(obj?.templates) ? obj.templates : + Array.isArray(obj?.templateLibrary) ? obj.templateLibrary : []; + + // Lenient expansion + let expanded: any; + let warning: string | undefined; + + try { + expanded = fillTemplates({ obj, templates }); + } catch (e: any) { + warning = String(e?.message ?? e); + // Partial expansion + let tmp = recursivelyFillTemplates({ obj, templates }); + const tag = /"template":/g; + while (JSON.stringify(tmp).match(tag)) { + tmp = recursivelyFillTemplates({ obj: tmp, templates }); + } + expanded = tmp; + } + + // Always remove the templates section from the preview output whether + // expansion succeeded or we fell back to the partial expansion above. + if (expanded && typeof expanded === "object") { + delete (expanded as any).templates; + delete (expanded as any).templateLibrary; + } + + const dumped = yaml.dump(expanded, { noRefs: true, sortKeys: false, lineWidth: 100 }); + + const body = applyTruncation(dumped, MAX_LINES); + const header = + `# Preview (read-only): Expanded templates\n` + + `# Source: ${src.fsPath}\n` + + (warning ? `# Warning: ${warning}\n` : "") + + (body.truncated ? `# Note: output truncated to ${MAX_LINES} lines\n` : "") + + `\n`; + + return header + body.text; + } catch (err: any) { + return `# Error generating expanded YAML\n# ${err?.message ?? String(err)}\n`; + } + } + + refreshForSource(source: vscode.Uri) { + for (const [previewKey, src] of this._srcByPreview.entries()) { + if (src.toString() === source.toString()) { + this._onDidChange.fire(vscode.Uri.parse(previewKey)); + } + } + } +} + +function applyTruncation(s: string, maxLines: number): { text: string; truncated: boolean } { + const lines = s.split(/\r?\n/); + if (lines.length <= maxLines) return { text: s, truncated: false }; + const slice = lines.slice(0, maxLines); + slice.push("# … truncated …"); + return { text: slice.join("\n"), truncated: true }; +} \ No newline at end of file From e06dadc6d24e356155b84a11e72b82ab8c1ae24e Mon Sep 17 00:00:00 2001 From: G Mo Date: Wed, 26 Nov 2025 02:57:28 -0500 Subject: [PATCH 3/4] fixed bug with sed command in package.json --- server/src/preFlight/validateTreatmentFile.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/server/src/preFlight/validateTreatmentFile.ts b/server/src/preFlight/validateTreatmentFile.ts index 1e8d4560b..22895b9a5 100644 --- a/server/src/preFlight/validateTreatmentFile.ts +++ b/server/src/preFlight/validateTreatmentFile.ts @@ -890,12 +890,9 @@ export const treatmentSchema = altTemplateContext( }); }); }); -<<<<<<< HEAD -======= // Duplicate-name checks removed here. Unique-name validation may be // performed elsewhere if needed. ->>>>>>> main }) ); @@ -1011,16 +1008,8 @@ export const templateContentSchema = z.any().superRefine((data, ctx) => { } } -<<<<<<< HEAD - // After attempting all schemas, traverse the data to find any treatment objects - // and check for duplicate element names within each treatment - // This is done regardless of whether a treatmentSchema matched, - // to catch treatments nested within other structures - // (e.g., within an intro sequence or other custom structures) -======= // Duplicate-name traversal checks removed. Template content validation // will rely on schema-specific checks instead. ->>>>>>> main if (bestSchemaResult) { console.log( From 738a010a46b668994c0f655a064d7d64a88b5d62 Mon Sep 17 00:00:00 2001 From: G Mo Date: Fri, 5 Dec 2025 13:22:50 -0500 Subject: [PATCH 4/4] took vscode import out of fillTemplates --- server/src/preFlight/fillTemplates.ts | 220 ++++++++------------------ 1 file changed, 64 insertions(+), 156 deletions(-) diff --git a/server/src/preFlight/fillTemplates.ts b/server/src/preFlight/fillTemplates.ts index 097e93649..be281c6aa 100644 --- a/server/src/preFlight/fillTemplates.ts +++ b/server/src/preFlight/fillTemplates.ts @@ -1,64 +1,59 @@ /* eslint-disable no-restricted-syntax */ -import * as vscode from "vscode"; import * as yaml from "js-yaml"; import { templateContextSchema } from "./validateTreatmentFile"; -// if you use zod for the schema, you can uncomment these two lines to get a stricter type: -// import type { z } from "zod"; -// type TemplateContext = z.infer; -type JsonLike = unknown; +// ----- Types ----- -type TemplateDef = { +export type JsonLike = unknown; + +export type TemplateDef = { templateName: string; templateContent: JsonLike; - // allow extra keys without constraining shape - [k: string]: unknown; + [k: string]: unknown; // allow extra keys }; -// If you want a strict type, replace `any` with the zod-inferred `TemplateContext` above. -type TemplateContext = any; +// If you want a strict type, replace `any` with the zod-inferred type +export type TemplateContext = any; + +// ----- Pure field substitution ----- export function substituteFields({ templateContent, fields, }: { templateContent: JsonLike; - // fields is usually a flat map, but we allow any values fields: Record; }): JsonLike { - // Deep clone the template to avoid mutating the original let expandedTemplate: JsonLike = JSON.parse(JSON.stringify(templateContent)); - // console.log("populating fields", fields); for (const [key, value] of Object.entries(fields)) { let stringifiedTemplate = JSON.stringify(expandedTemplate); const stringifiedValue = JSON.stringify(value); - // replace all instances of `"${key}"` with serialized value - // this handles objects and arrays, etc. + // Replace values like "${key}" const objectReplacementRegex = new RegExp(`"\\$\\{${key}\\}"`, "g"); stringifiedTemplate = stringifiedTemplate.replace( objectReplacementRegex, stringifiedValue ); - // if the value is just a string or number, we can also replace instances of ${key} within other strings + // Replace inline string occurrences of ${key} if (typeof value === "string") { - // replace all instances of `${key}` embedded in strings with other text with a serialized value const stringReplacementRegex = new RegExp(`\\$\\{${key}\\}`, "g"); stringifiedTemplate = stringifiedTemplate.replace( stringReplacementRegex, value ); } - // Todo: throw error message if we're trying to substitute an object within a string - // Parse after each replacement to surface any errors expandedTemplate = JSON.parse(stringifiedTemplate); } + return expandedTemplate; } +// ----- Template expansion ----- + export function expandTemplate({ templates, context, @@ -66,42 +61,36 @@ export function expandTemplate({ templates: TemplateDef[]; context: TemplateContext; }): JsonLike { - // Step 1: Fill in any templates within the context itself const newContext: any = JSON.parse(JSON.stringify(context)); - // you can use templates within fields and broadcast, but not within the template reference + + // Step 1: fill templates inside context.fields or context.broadcast if (newContext.fields) { - // eslint-disable-next-line no-use-before-define newContext.fields = recursivelyFillTemplates({ obj: newContext.fields, templates, }); } + if (newContext.broadcast) { - // eslint-disable-next-line no-use-before-define newContext.broadcast = recursivelyFillTemplates({ obj: newContext.broadcast, templates, }); } - // console.log("newContext", newContext); - // Find the matching template + // Step 2: find template const template = templates.find( (t) => t.templateName === newContext.template ); if (!template) { - // eslint-disable-next-line no-console - console.log( - "Found templates:", - templates.map((t) => t.templateName) - ); throw new Error(`Template "${newContext.template}" not found`); } - // console.log("template", template); - // Deep clone the template content to avoid mutating the original - let expandedTemplate: any = JSON.parse(JSON.stringify(template.templateContent)); - // Step 3: Apply given fields if any + let expandedTemplate: any = JSON.parse( + JSON.stringify(template.templateContent) + ); + + // Step 3: apply fields if (newContext.fields) { expandedTemplate = substituteFields({ templateContent: expandedTemplate, @@ -109,16 +98,14 @@ export function expandTemplate({ }); } - // The template, even after the original given fields are filled, is still a template that we can fill with broadcast values - - // Step 4: Handle broadcast fields if any - - // Define recursive function to flatten and expand the broadcast axes + // Step 4: broadcast handling function flattenBroadcast( dimensions: Record>> ): Array> { const dimensionIndices = Object.keys(dimensions); - const dimensionNumbers = dimensionIndices.map((i) => parseInt(i.slice(1), 10)); + const dimensionNumbers = dimensionIndices.map((i) => + parseInt(i.slice(1), 10) + ); const lowestDimension = Math.min(...dimensionNumbers); const currentDimension = dimensions[`d${lowestDimension}`]; @@ -135,8 +122,8 @@ export function expandTemplate({ const flatFields: Array> = []; for (const [index, entry] of currentDimension.entries()) { for (const partialField of partialFields) { - const newField: Record = { ...entry, ...partialField }; - newField[`d${lowestDimension}`] = `${index}`; // convert to string + const newField = { ...entry, ...partialField }; + newField[`d${lowestDimension}`] = `${index}`; flatFields.push(newField); } } @@ -147,12 +134,14 @@ export function expandTemplate({ const broadcastFieldsArray = flattenBroadcast( newContext.broadcast as Record>> ); + const returnObjects: any[] = []; for (const broadcastFields of broadcastFieldsArray) { const newObj = substituteFields({ templateContent: expandedTemplate, fields: broadcastFields, }); + if (Array.isArray(newObj)) { returnObjects.push(...newObj); } else if (typeof newObj === "object" && newObj !== null) { @@ -167,6 +156,8 @@ export function expandTemplate({ return expandedTemplate; } +// ----- Recursive filler ----- + export function recursivelyFillTemplates({ obj, templates, @@ -174,44 +165,40 @@ export function recursivelyFillTemplates({ obj: JsonLike; templates: TemplateDef[]; }): JsonLike { - // obj is any object in the treatment file, whether it is a template context or not let newObj: any; + try { - newObj = JSON.parse(JSON.stringify(obj)); // deep clone + newObj = JSON.parse(JSON.stringify(obj)); } catch (e) { - // eslint-disable-next-line no-console console.log("Error parsing", obj); throw e; } - // console.log("newObj", newObj); if (!Array.isArray(newObj) && typeof newObj === "object" && newObj !== null) { - // if we get to a node in the tree that is an object, it can either be a template - // context (ie, a reference to a template that needs to be filled), or a regular - // object that may contain other template contexts that need to be filled - // in this case, recursively navigate through the keys of the object + // Template context? if (newObj && (newObj as any).template) { - // object is a template context const context: TemplateContext = templateContextSchema.parse(newObj); newObj = expandTemplate({ templates, context }); - newObj = recursivelyFillTemplates({ obj: newObj, templates }); - } else { - // eslint-disable-next-line guard-for-in - for (const key in newObj as Record) { - if (newObj[key] == null) { - // eslint-disable-next-line no-console - console.log(`key ${key} is undefined in`, newObj); - } - newObj[key] = recursivelyFillTemplates({ obj: newObj[key], templates }); + return recursivelyFillTemplates({ obj: newObj, templates }); + } + + // Recurse into object keys + for (const key in newObj as Record) { + if (newObj[key] == null) { + console.log(`key ${key} is undefined in`, newObj); } + newObj[key] = recursivelyFillTemplates({ + obj: newObj[key], + templates, + }); } } else if (Array.isArray(newObj)) { - // if the node is itself an array, we need to iterate through each item in the array. - // if the item is a template context, we need to expand it and replace it with the expanded object + // Recurse into arrays for (const [index, item] of (newObj as any[]).entries()) { if (item && (item as any).template) { const context: TemplateContext = templateContextSchema.parse(item); const expandedItem = expandTemplate({ templates, context }); + if (Array.isArray(expandedItem)) { newObj.splice(index, 1, ...expandedItem); } else if (typeof expandedItem === "object" && expandedItem !== null) { @@ -220,7 +207,10 @@ export function recursivelyFillTemplates({ throw new Error("Unexpected type in expanded item"); } } else { - newObj[index] = recursivelyFillTemplates({ obj: item, templates }); + newObj[index] = recursivelyFillTemplates({ + obj: item, + templates, + }); } } } @@ -228,6 +218,8 @@ export function recursivelyFillTemplates({ return newObj; } +// ----- Top-level fillTemplates ----- + export function fillTemplates({ obj, templates, @@ -237,112 +229,28 @@ export function fillTemplates({ }): JsonLike { let newObj = recursivelyFillTemplates({ obj, templates }); - // Check that there are no remaining templates + // Re-fix partially filled templates until none remain const templatesRemainingRegex = /"template":/g; - let templatesRemaining = JSON.stringify(newObj).match(templatesRemainingRegex); + let templatesRemaining = JSON.stringify(newObj).match( + templatesRemainingRegex + ); + while (templatesRemaining) { - // eslint-disable-next-line no-console console.log("Found unfilled template, trying again."); newObj = recursivelyFillTemplates({ obj: newObj, templates }); - - templatesRemaining = JSON.stringify(newObj).match(templatesRemainingRegex); + templatesRemaining = JSON.stringify(newObj).match( + templatesRemainingRegex + ); } - // Check that all fields are filled + // Check for missing fields const doubleCheckRegex = /\$\{[a-zA-Z0-9_]+\}/g; const missingFields = JSON.stringify(newObj).match(doubleCheckRegex); if (missingFields) { - // eslint-disable-next-line no-console console.log("error in ", JSON.stringify(newObj, null, 4)); - // eslint-disable-next-line no-console console.log("missing fields", missingFields); throw new Error(`Missing fields: ${missingFields.join(", ")}`); } - // console.log("Filled templates: ", newObj); return newObj; } - -export const EXP_SCHEME = "deliberation-expanded"; -const MAX_LINES = 10_000; - -export class ExpandedTemplatesProvider implements vscode.TextDocumentContentProvider { - private _onDidChange = new vscode.EventEmitter(); - readonly onDidChange = this._onDidChange.event; - - private _srcByPreview = new Map(); - - async provideTextDocumentContent(uri: vscode.Uri): Promise { - const qp = new URLSearchParams(uri.query); - const srcStr = qp.get("src"); - if (!srcStr) return "# Error: missing source URI\n"; - - const src = vscode.Uri.parse(srcStr); - this._srcByPreview.set(uri.toString(), src); - - try { - const raw = await vscode.workspace.fs.readFile(src); - const text = new TextDecoder("utf-8").decode(raw); - const obj = yaml.load(text) as any; - - // Pull templates from the file; tweak if your templates array lives elsewhere. - const templates: any[] = - Array.isArray(obj?.templates) ? obj.templates : - Array.isArray(obj?.templateLibrary) ? obj.templateLibrary : []; - - // Lenient expansion - let expanded: any; - let warning: string | undefined; - - try { - expanded = fillTemplates({ obj, templates }); - } catch (e: any) { - warning = String(e?.message ?? e); - // Partial expansion - let tmp = recursivelyFillTemplates({ obj, templates }); - const tag = /"template":/g; - while (JSON.stringify(tmp).match(tag)) { - tmp = recursivelyFillTemplates({ obj: tmp, templates }); - } - expanded = tmp; - } - - // Always remove the templates section from the preview output whether - // expansion succeeded or we fell back to the partial expansion above. - if (expanded && typeof expanded === "object") { - delete (expanded as any).templates; - delete (expanded as any).templateLibrary; - } - - const dumped = yaml.dump(expanded, { noRefs: true, sortKeys: false, lineWidth: 100 }); - - const body = applyTruncation(dumped, MAX_LINES); - const header = - `# Preview (read-only): Expanded templates\n` + - `# Source: ${src.fsPath}\n` + - (warning ? `# Warning: ${warning}\n` : "") + - (body.truncated ? `# Note: output truncated to ${MAX_LINES} lines\n` : "") + - `\n`; - - return header + body.text; - } catch (err: any) { - return `# Error generating expanded YAML\n# ${err?.message ?? String(err)}\n`; - } - } - - refreshForSource(source: vscode.Uri) { - for (const [previewKey, src] of this._srcByPreview.entries()) { - if (src.toString() === source.toString()) { - this._onDidChange.fire(vscode.Uri.parse(previewKey)); - } - } - } -} - -function applyTruncation(s: string, maxLines: number): { text: string; truncated: boolean } { - const lines = s.split(/\r?\n/); - if (lines.length <= maxLines) return { text: s, truncated: false }; - const slice = lines.slice(0, maxLines); - slice.push("# … truncated …"); - return { text: slice.join("\n"), truncated: true }; -} \ No newline at end of file