Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions packages/core/src/generators/hyperframes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,21 +368,21 @@
const resolutionAttr = ` data-resolution="${resolution}"`;

return `<!DOCTYPE html>
<html data-composition-id="${compositionId}" data-composition-duration="${calculatedDuration}"${resolutionAttr}${customStylesAttr}>

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${includeStyles ? googleFontsLink : ""}
${gsapCdnTag}
${styleTags ? ` ${styleTags}` : ""}

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on library input might later allow
cross-site scripting
.
This HTML construction which depends on library input might later allow
cross-site scripting
.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
This HTML construction which depends on library input might later allow cross-site scripting.
</head>
<body>
<div id="stage">
<div id="stage-zoom-container"${zoomKeyframesAttr}>
${elementsHtml}

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
</div>
</div>
${gsapScriptTag}

Check warning

Code scanning / CodeQL

Unsafe HTML constructed from library input Medium

This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow
cross-site scripting
.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
This HTML construction which depends on
library input
might later allow cross-site scripting.
</body>
</html>`;
}
Expand Down Expand Up @@ -447,6 +447,7 @@
function generateElementHtml(element: TimelineElement, keyframes?: Keyframe[]): string {
const baseAttrs = [
`id="${element.id}"`,
`data-hf-id="${element.id}"`,
`data-start="${element.startTime}"`,
`data-end="${element.startTime + element.duration}"`,
`data-layer="${element.zIndex}"`,
Expand Down
15 changes: 13 additions & 2 deletions packages/core/src/parsers/htmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CompositionVariable,
} from "../core.types";
import { validateCompositionGsap } from "./gsapSerialize";
import { ensureHfIds } from "./hfIds.js";
import type { ValidationResult } from "../core.types";

const MEDIA_TYPES = new Set<string>(["video", "image", "audio"]);
Expand Down Expand Up @@ -156,8 +157,9 @@
}

export function parseHtml(html: string): ParsedHtml {
const withIds = ensureHfIds(html);
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const doc = parser.parseFromString(withIds, "text/html");

const elements: TimelineElement[] = [];
const keyframes: Record<string, Keyframe[]> = {};
Expand Down Expand Up @@ -190,7 +192,16 @@
duration = 5;
}

const id = el.id || `element-${++idCounter}`;
// R1: stable hf- id minted by ensureHfIds above; clips just read it.
// Legacy/migration note: ensureHfIds pins a pre-existing `data-hf-id`, and
// the generator emits `data-hf-id="${element.id}"`. So a clip authored
// before R1 with `id="my-title"` round-trips as `data-hf-id="my-title"` —
// a non-`hf-`-shaped but still stable, exact-match handle. This is safe
// indefinitely: targeting uses exact `[data-hf-id="…"]` match (it does not
// require the hf- prefix). ensureHfIds skips elements that already carry
// data-hf-id, so legacy values are NOT re-minted automatically — they
// persist until the user re-saves the composition through Studio. Not a bug.
const id = el.getAttribute("data-hf-id") || el.id || `element-${++idCounter}`;
const name = getElementName(el);
const zIndex = getZIndex(el);

Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/parsers/stableIds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { serialize } from "./test-utils.js";
describe("T2 — stable element ids (spec for R1)", () => {
// --- Spec (red until R1) ---

it.fails("[spec] elements without an id get a hf- prefixed id at parse", () => {
it("[spec] elements without an id get a hf- prefixed id at parse", () => {
const html = `<html><body><div id="stage">
<img src="logo.svg" data-start="0" data-end="5" data-name="Logo" />
<div data-start="0" data-end="5" data-name="Card"><div>Text</div></div>
Expand All @@ -29,7 +29,7 @@ describe("T2 — stable element ids (spec for R1)", () => {
}
});

it.fails("[spec] generated hf- ids match /^hf-[a-z0-9]{4}$/", () => {
it("[spec] generated hf- ids match /^hf-[a-z0-9]{4}$/", () => {
const html = `<html><body><div id="stage">
<div data-start="0" data-end="5" data-name="Unnamed"><div>X</div></div>
<video data-start="1" data-end="6" src="v.mp4" data-name="Clip"></video>
Expand All @@ -41,7 +41,7 @@ describe("T2 — stable element ids (spec for R1)", () => {
}
});

it.fails("[spec] adding an element before existing ones does not change existing ids", () => {
it("[spec] adding an element before existing ones does not change existing ids", () => {
const base = `<html><body><div id="stage">
<div data-start="0" data-end="5" data-name="AlphaEl"><div>A</div></div>
<div data-start="1" data-end="6" data-name="BetaEl"><div>B</div></div>
Expand All @@ -62,12 +62,12 @@ describe("T2 — stable element ids (spec for R1)", () => {

// --- Baseline (already pass, must not regress) ---

it("elements with an existing id keep it unchanged", () => {
it("existing data-hf-id is pinned and becomes the clip id (never re-minted)", () => {
const html = `<html><body><div id="stage">
<div id="my-title" data-start="0" data-end="5" data-name="Title"><div>Hi</div></div>
<div data-hf-id="hf-anch" data-start="0" data-end="5" data-name="Title"><div>Hi</div></div>
</div></body></html>`;
const { elements } = parseHtml(html);
expect(elements.some((e) => e.id === "my-title")).toBe(true);
expect(elements.some((e) => e.id === "hf-anch")).toBe(true);
});

it("ids are deterministic: same input produces same ids on re-parse", () => {
Expand Down
Loading