Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/src/compiler/htmlDocument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,14 @@ describe("htmlDocument helpers", () => {
);
});

it("puts markup before the outer </template>, past a nested one and a commented one", () => {
const sub =
"<template id=t><div><template><i></i></template></div></template>\n<!-- </template> -->";
expect(insertBeforeCloseTag(sub, "template", "X")).toBe(
"<template id=t><div><template><i></i></template></div>X</template>\n<!-- </template> -->",
);
});

it("ends a comment at --!>, but not on its own opening dashes", () => {
expect(insertBeforeCloseTag("<head><!-- a --!></head>", "head", "X")).toBe(
"<head><!-- a --!>X</head>",
Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/compiler/htmlDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ function inlineScriptTags(scripts: readonly string[]): string {

const RAW_TEXT_TAGS = ["script", "style", "title", "textarea"] as const;

type DocumentTag = "<head" | "</head" | "<body" | "</body";
type DocumentTag = "<head" | "</head" | "<body" | "</body" | "</template";
const COMMENT_END = /--!?>/g;

function* markupStarts(lowered: string): Generator<number> {
Expand Down Expand Up @@ -290,15 +290,25 @@ function findRawTextClose(lowered: string, name: string, from: number): number {
return -1;
}

function findOuterTemplateClose(html: string): number {
const lowered = lowerAscii(html);
let depth = 0;
for (const open of markupStarts(lowered)) {
if (isTagAt(lowered, open, "<template")) depth++;
else if (isTagAt(lowered, open, "</template") && --depth === 0) return open;
}
return -1;
}

function insertBeforeDocumentTag(html: string, tag: DocumentTag, markup: string): string | null {
const at = findDocumentTag(html, tag);
const at = tag === "</template" ? findOuterTemplateClose(html) : findDocumentTag(html, tag);
return at === -1 ? null : html.slice(0, at) + markup + html.slice(at);
}

/** Insert markup just before the document's own `</head>` or `</body>`; null when it has none. */
/** Insert markup before the document's own `</head>`, `</body>` or `</template>`; null if it has none. */
export function insertBeforeCloseTag(
html: string,
name: "head" | "body",
name: "head" | "body" | "template",
markup: string,
): string | null {
return insertBeforeDocumentTag(html, `</${name}`, markup);
Expand Down
43 changes: 43 additions & 0 deletions packages/studio-server/src/routes/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1721,6 +1721,49 @@ const tl = gsap.timeline({ paused: true });
expect(html.indexOf('tl.set("#card"')).toBeGreaterThan(declaration);
});

async function addFirstAnimation(sub: string): Promise<string> {
const projectDir = createProjectDir();
const app = new Hono();
registerFileRoutes(app, createAdapter(projectDir));
mkdirSync(join(projectDir, "compositions"), { recursive: true });
writeHtml(projectDir, "compositions/sub.html", sub);
const res = await app.request(
"http://localhost/projects/demo/gsap-mutations/compositions/sub.html",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
type: "add",
targetSelector: "#card",
method: "to",
position: 0,
properties: { opacity: 0 },
}),
},
);
expect(res.status).toBe(200);
return readFileSync(join(projectDir, "compositions/sub.html"), "utf-8");
}

it("a first animation in a sub-composition file lands inside its template", async () => {
const html = await addFirstAnimation(
'<template id="sub-template"><div data-composition-id="sub"><div id="card"></div></div></template>\n',
);
const close = html.indexOf("</template>");
expect(html.indexOf('window.__timelines["sub"]')).toBeGreaterThan(-1);
expect(html.lastIndexOf("<script")).toBeLessThan(close);
expect(html.slice(close).trim()).toBe("</template>");
expect(html).not.toContain("<body");
});

it("a first animation in a <body> file lands before </body>, outside its template", async () => {
const html = await addFirstAnimation(
'<body><template><div data-composition-id="sub"><div id="card"></div></div></template></body>\n',
);
expect(html.indexOf("<script")).toBeGreaterThan(html.indexOf("</template>"));
expect(html.lastIndexOf("</script>")).toBeLessThan(html.indexOf("</body>"));
});

it("consolidate-position-writes leaves exactly one position write per selector", async () => {
const projectDir = createProjectDir();
const CORRUPTED = `<!DOCTYPE html><html><body><script data-hyperframes-gsap>
Expand Down
5 changes: 4 additions & 1 deletion packages/studio-server/src/routes/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,10 @@ async function prepareGsapMutationScript(
`window.__timelines["${compId}"] = tl;`,
"</script>",
].join("\n");
html = insertBeforeCloseTag(html, "body", `${bootstrap}\n`) ?? `${html}\n${bootstrap}`;
html =
insertBeforeCloseTag(html, "body", `${bootstrap}\n`) ??
insertBeforeCloseTag(html, "template", `${bootstrap}\n`) ??
`${html}\n${bootstrap}`;
block = extractGsapScriptBlock(html);
}
if (
Expand Down
Loading