diff --git a/apps/app/src/components/generative-ui/open-generative-ui/__tests__/frame-content.test.ts b/apps/app/src/components/generative-ui/open-generative-ui/__tests__/frame-content.test.ts new file mode 100644 index 0000000..db441fa --- /dev/null +++ b/apps/app/src/components/generative-ui/open-generative-ui/__tests__/frame-content.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { buildFinalFrameContent } from "../frame-content"; + +describe("buildFinalFrameContent", () => { + // Websandbox rejects frame content without a literal "" and injects + // its bootstrap at that tag. + it.each([ + ['Tx'], + ["Tx"], + ])("normalizes a head tag websandbox would not find: %s", (html) => { + const frame = buildFinalFrameContent(html); + expect(frame).toContain(""); + expect(frame.indexOf("Content-Security-Policy")).toBeGreaterThan( + frame.indexOf("") + ); + expect(frame.indexOf("Content-Security-Policy")).toBeLessThan( + frame.search(//i) + ); + }); + + it("does not mistake <header> for the head", () => { + const frame = buildFinalFrameContent("<body><header>Top</header></body>"); + expect(frame.indexOf("<head>")).toBe(0); + expect(frame).toContain("<header>Top</header>"); + expect(frame.indexOf("Content-Security-Policy")).toBeLessThan( + frame.indexOf("<header>") + ); + }); +}); diff --git a/apps/app/src/components/generative-ui/open-generative-ui/frame-content.ts b/apps/app/src/components/generative-ui/open-generative-ui/frame-content.ts index 246e696..d2e7b02 100644 --- a/apps/app/src/components/generative-ui/open-generative-ui/frame-content.ts +++ b/apps/app/src/components/generative-ui/open-generative-ui/frame-content.ts @@ -38,8 +38,16 @@ export const CSP_META_TAG = `<meta http-equiv="Content-Security-Policy" content= https://unpkg.com; ">`; +const HEAD_OPEN_TAG = /<head(?:\s[^>]*)?>/i; + +/** + * Websandbox requires a literal lowercase "<head>" in its frame content and + * injects its bootstrap there, so "<HEAD>" or "<head lang=...>" is normalized + * (attributes on <head> have no effect) and a missing head is added. + */ export function ensureHead(html: string): string { - if (/<head[\s>]/i.test(html)) return html; + if (html.includes("<head>")) return html; + if (HEAD_OPEN_TAG.test(html)) return html.replace(HEAD_OPEN_TAG, "<head>"); return `<head></head>${html}`; } @@ -58,11 +66,7 @@ export function buildFinalFrameContent(html: string, css?: string): string { DESIGN_SYSTEM_STYLE_TAG + (css ? `<style>${css}</style>` : ""); const withHead = ensureHead(html); - const openTag = withHead.match(/<head[^>]*>/i); - if (!openTag || openTag.index === undefined) { - return `<head>${headContent}</head>${withHead}`; - } - const insertAt = openTag.index + openTag[0].length; + const insertAt = withHead.indexOf("<head>") + "<head>".length; return withHead.slice(0, insertAt) + headContent + withHead.slice(insertAt); }