mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-31 19:06:14 +00:00
fix(studio): a first animation in a full-document sub-composition plays (#5008)
* fix(studio): a first animation in a full-document sub-composition plays * fix(studio): keep the comment share of the two touched files at their base * fix(studio): a first animation also plays when <html> carries the composition id * fix(studio): a root whose id sits on <html> keeps its first animation in <body> * fix(studio): a composition id on the template tag keeps the first animation inside it
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
injectScriptsIntoHtml,
|
||||
injectTagsAtHeadStart,
|
||||
insertBeforeCloseTag,
|
||||
hasCompositionOutsideTemplates,
|
||||
isFullHtmlDocument,
|
||||
parseHTMLContent,
|
||||
stripEmbeddedRuntimeScripts,
|
||||
@@ -272,3 +273,47 @@ describe("injectTagsAtHeadStart on long adversarial input", () => {
|
||||
expect(performance.now() - started).toBeLessThan(1500);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasCompositionOutsideTemplates", () => {
|
||||
it.each([
|
||||
["a root in <body>", '<body><div data-composition-id="main"></div></body>', true],
|
||||
[
|
||||
"a root in <body> beside an inline template",
|
||||
'<body><div data-composition-id="main"></div><template><div data-composition-id="x"></div></template></body>',
|
||||
true,
|
||||
],
|
||||
[
|
||||
"a full document whose composition is in its template",
|
||||
'<html><body><template id="s-template"><div data-composition-id="s"></div></template></body></html>',
|
||||
false,
|
||||
],
|
||||
["a bare template", '<template><div data-composition-id="s"></div></template>', false],
|
||||
[
|
||||
"an id on <html> and on the <template> tag itself",
|
||||
'<html data-composition-id="s"><body><template data-composition-id="s"><div></div></template></body></html>',
|
||||
false,
|
||||
],
|
||||
[
|
||||
"a root id on <html> beside a template with no composition",
|
||||
'<html data-composition-id="main"><body><template><p>clone me</p></template></body></html>',
|
||||
true,
|
||||
],
|
||||
[
|
||||
"a full document whose <html> carries the id",
|
||||
'<html data-composition-id="s"><body data-composition-id="s"><template><div data-composition-id="s"></div></template></body></html>',
|
||||
false,
|
||||
],
|
||||
[
|
||||
"the attribute only in a comment or text",
|
||||
'<body><!-- <div data-composition-id="a"> --><p>data-composition-id="b"</p></body>',
|
||||
false,
|
||||
],
|
||||
[
|
||||
"a nested template",
|
||||
'<template><template></template><div data-composition-id="s"></div></template>',
|
||||
false,
|
||||
],
|
||||
])("%s", (_name, html, expected) => {
|
||||
expect(hasCompositionOutsideTemplates(html)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,6 +300,35 @@ function findOuterTemplateClose(html: string): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const COMPOSITION_ID_ATTR = /\sdata-composition-id\s*=/;
|
||||
|
||||
const isDocumentWrapper = (lowered: string, open: number): boolean =>
|
||||
isTagAt(lowered, open, "<html") || isTagAt(lowered, open, "<body");
|
||||
|
||||
function carriesCompositionId(lowered: string, open: number): boolean {
|
||||
const end = findTagEnd(lowered, open + 1);
|
||||
return end !== -1 && COMPOSITION_ID_ATTR.test(lowered.slice(open, end));
|
||||
}
|
||||
|
||||
export function hasCompositionOutsideTemplates(html: string): boolean {
|
||||
const lowered = lowerAscii(html);
|
||||
let depth = 0;
|
||||
let idOnWrapper = false;
|
||||
let idInTemplate = false;
|
||||
for (const open of markupStarts(lowered)) {
|
||||
if (isTagAt(lowered, open, "<template")) {
|
||||
depth++;
|
||||
if (carriesCompositionId(lowered, open)) idInTemplate = true;
|
||||
} else if (isTagAt(lowered, open, "</template")) depth = Math.max(0, depth - 1);
|
||||
else if (/[a-z]/.test(lowered.charAt(open + 1)) && carriesCompositionId(lowered, open)) {
|
||||
if (depth > 0) idInTemplate = true;
|
||||
else if (isDocumentWrapper(lowered, open)) idOnWrapper = true;
|
||||
else return true;
|
||||
}
|
||||
}
|
||||
return idOnWrapper && !idInTemplate;
|
||||
}
|
||||
|
||||
function insertBeforeDocumentTag(html: string, tag: DocumentTag, markup: string): string | null {
|
||||
const at = tag === "</template" ? findOuterTemplateClose(html) : findDocumentTag(html, tag);
|
||||
return at === -1 ? null : html.slice(0, at) + markup + html.slice(at);
|
||||
|
||||
@@ -1949,11 +1949,40 @@ const tl = gsap.timeline({ paused: true });
|
||||
expect(html).not.toContain("<body");
|
||||
});
|
||||
|
||||
it("a first animation in a <body> file lands before </body>, outside its template", async () => {
|
||||
// The registry-block shape: the loader mounts the template's content, so a script outside it never runs.
|
||||
it("a first animation in a full-document sub-composition lands inside its template", async () => {
|
||||
const html = await addFirstAnimation(
|
||||
'<body><template><div data-composition-id="sub"><div id="card"></div></div></template></body>\n',
|
||||
'<!DOCTYPE html><html><head><meta charset="utf-8"></head><body><template id="sub-template"><div data-composition-id="sub"><div id="card"></div></div></template></body></html>\n',
|
||||
);
|
||||
expect(html.indexOf("<script")).toBeGreaterThan(html.indexOf("</template>"));
|
||||
const close = html.indexOf("</template>");
|
||||
expect(html.indexOf('window.__timelines["sub"]')).toBeGreaterThan(-1);
|
||||
expect(html.lastIndexOf("</script>")).toBeLessThan(close);
|
||||
expect(html.slice(close)).toBe("</template></body></html>\n");
|
||||
});
|
||||
|
||||
it("a first animation lands inside the template when <html> carries the composition id", async () => {
|
||||
const html = await addFirstAnimation(
|
||||
'<!DOCTYPE html><html data-composition-id="sub"><body><template><div data-composition-id="sub"><div id="card"></div></div></template></body></html>\n',
|
||||
);
|
||||
const close = html.indexOf("</template>");
|
||||
expect(html.lastIndexOf("</script>")).toBeLessThan(close);
|
||||
expect(html.slice(close)).toBe("</template></body></html>\n");
|
||||
});
|
||||
|
||||
it("a first animation lands inside the template when the <template> tag carries the composition id", async () => {
|
||||
const html = await addFirstAnimation(
|
||||
'<!DOCTYPE html><html data-composition-id="sub"><body><template data-composition-id="sub"><div id="card"></div></template></body></html>\n',
|
||||
);
|
||||
const close = html.indexOf("</template>");
|
||||
expect(html.lastIndexOf("</script>")).toBeLessThan(close);
|
||||
expect(html.slice(close)).toBe("</template></body></html>\n");
|
||||
});
|
||||
|
||||
it("a first animation in a file whose composition is in <body> lands before </body>", async () => {
|
||||
const html = await addFirstAnimation(
|
||||
'<body><div data-composition-id="main"><div id="card"></div></div><template id="other-template"><div data-composition-id="other"></div></template></body>\n',
|
||||
);
|
||||
expect(html.indexOf('window.__timelines["main"]')).toBeGreaterThan(html.indexOf("</template>"));
|
||||
expect(html.lastIndexOf("</script>")).toBeLessThan(html.indexOf("</body>"));
|
||||
});
|
||||
|
||||
|
||||
@@ -105,7 +105,10 @@ import {
|
||||
} from "../helpers/compositionInsertion.js";
|
||||
import { resolveGsapWriter } from "./gsapMutationCapabilities.js";
|
||||
import { requestSubPath } from "../helpers/requestSubPath.js";
|
||||
import { insertBeforeCloseTag } from "@hyperframes/core/compiler/html-document";
|
||||
import {
|
||||
hasCompositionOutsideTemplates,
|
||||
insertBeforeCloseTag,
|
||||
} from "@hyperframes/core/compiler/html-document";
|
||||
|
||||
// ── Server cutover flag ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -1457,9 +1460,12 @@ async function prepareGsapMutationScript(
|
||||
`window.__timelines["${compId}"] = tl;`,
|
||||
"</script>",
|
||||
].join("\n");
|
||||
const [first, second] = hasCompositionOutsideTemplates(html)
|
||||
? (["body", "template"] as const)
|
||||
: (["template", "body"] as const);
|
||||
html =
|
||||
insertBeforeCloseTag(html, "body", `${bootstrap}\n`) ??
|
||||
insertBeforeCloseTag(html, "template", `${bootstrap}\n`) ??
|
||||
insertBeforeCloseTag(html, first, `${bootstrap}\n`) ??
|
||||
insertBeforeCloseTag(html, second, `${bootstrap}\n`) ??
|
||||
`${html}\n${bootstrap}`;
|
||||
block = extractGsapScriptBlock(html);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user