diff --git a/packages/core/src/parsers/htmlParser.ts b/packages/core/src/parsers/htmlParser.ts
index 92864e04e..47859badf 100644
--- a/packages/core/src/parsers/htmlParser.ts
+++ b/packages/core/src/parsers/htmlParser.ts
@@ -11,6 +11,7 @@ import type {
CompositionVariable,
} from "../core.types";
import { validateCompositionGsap } from "./gsapSerialize";
+import { ensureHfIds } from "./hfIds.js";
import type { ValidationResult } from "../core.types";
const MEDIA_TYPES = new Set(["video", "image", "audio"]);
@@ -156,8 +157,9 @@ function resolveResolutionFromDimensions(width: number, height: number): CanvasR
}
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 = {};
@@ -190,7 +192,16 @@ export function parseHtml(html: string): ParsedHtml {
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);
diff --git a/packages/core/src/parsers/stableIds.test.ts b/packages/core/src/parsers/stableIds.test.ts
index a3887f04f..4656f98c8 100644
--- a/packages/core/src/parsers/stableIds.test.ts
+++ b/packages/core/src/parsers/stableIds.test.ts
@@ -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 = `
Text
@@ -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 = `
X
@@ -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 = `
A
B
@@ -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 = `
-
Hi
+
Hi
`;
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", () => {