import * as errors from "@superbuilders/errors" import * as logger from "@superbuilders/slog" import { renderBlockContent, renderInlineContent } from "@/compiler/content-renderer" import { escapeXmlAttribute } from "@/compiler/utils/xml-utils" import type { AnyInteraction } from "@/core/interactions" export function compileInteraction< E extends readonly string[] = readonly string[] >( interaction: AnyInteraction, widgetSlots: Map, interactionSlots: Map ): string { let interactionXml: string switch (interaction.type) { case "choiceInteraction": { const promptXml = renderInlineContent( interaction.prompt, widgetSlots, interactionSlots ) const choicesXml = interaction.choices .map((c): string => { const contentXml = renderBlockContent( c.content, widgetSlots, interactionSlots ) return `${contentXml}` }) .join("\n ") interactionXml = ` ${promptXml} ${choicesXml} ` break } case "orderInteraction": { const promptXml = renderInlineContent( interaction.prompt, widgetSlots, interactionSlots ) const choicesXml = interaction.choices .map((c): string => { const contentXml = renderBlockContent( c.content, widgetSlots, interactionSlots ) return `${contentXml}` }) .join("\n ") interactionXml = ` ${promptXml} ${choicesXml} ` break } case "textEntryInteraction": { interactionXml = `` break } case "inlineChoiceInteraction": { const choicesXml = interaction.choices .map( (c): string => `${renderInlineContent(c.content, widgetSlots, interactionSlots)}` ) .join("\n ") interactionXml = ` ${choicesXml} ` break } case "gapMatchInteraction": { const gapTextsXml = interaction.gapTexts .map((gt): string => { const contentXml = renderInlineContent( gt.content, widgetSlots, interactionSlots ) return `${contentXml}` }) .join("\n ") const innerContent = interaction.content ? `\n ${renderBlockContent(interaction.content, widgetSlots, interactionSlots)}\n ` : "" // NEW: Compile-time guard - defense in depth if (innerContent.trim() === "") { logger.error("gap match rendering: no content resulted in empty XML", { responseIdentifier: interaction.responseIdentifier }) throw errors.new("gap match rendering: no content") } const renderedGapCount = (innerContent.match(/ ${gapTextsXml}${innerContent} ` break } default: { // This should never happen if all interaction types are handled // We can't access properties on 'never', so we throw a generic error logger.error("unhandled interaction type in switch statement", { interaction }) throw errors.new("Unhandled interaction type in switch statement") } } // Wrapping policy: // - Inline interactions (textEntryInteraction, inlineChoiceInteraction) remain unwrapped // - gapMatchInteraction must also remain unwrapped so that it can directly contain its own inner content // - Other block interactions are wrapped in a div for safe mixing with other block elements if ( interaction.type === "textEntryInteraction" || interaction.type === "inlineChoiceInteraction" || interaction.type === "gapMatchInteraction" ) { return interactionXml } return `
${interactionXml}
` }