From 3366c0a241afe2b34285035582ae06ad392fc134 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 7 Jun 2026 20:51:42 -0700 Subject: [PATCH 1/2] feat(core): sourceMutation data-hf-id targeting (R1, T7) --- .../studio-api/helpers/sourceMutation.test.ts | 47 +++++++++++++++++-- .../src/studio-api/helpers/sourceMutation.ts | 8 +++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/core/src/studio-api/helpers/sourceMutation.test.ts b/packages/core/src/studio-api/helpers/sourceMutation.test.ts index c23b45911..d3b4552d0 100644 --- a/packages/core/src/studio-api/helpers/sourceMutation.test.ts +++ b/packages/core/src/studio-api/helpers/sourceMutation.test.ts @@ -368,13 +368,50 @@ describe("probeElementInSource", () => { // Covers the same surface as T3 (Studio sourcePatcher) — Core sourceMutation supports // all patch types (inline-style, attribute, text-content) via patchElementInHtml. describe("T7 — data-hf-id targeting (spec for R1)", () => { - it.todo("updates inline style by data-hf-id when no HTML id attribute is present"); + it("updates inline style by data-hf-id when no HTML id attribute is present", () => { + const source = `

Hello

`; + const { html, matched } = patchElementInHtml(source, { hfId: "hf-x7k2" }, [ + { type: "inline-style", property: "color", value: "blue" }, + ]); + expect(matched).toBe(true); + expect(html).toMatch(/color:\s*blue/); + expect(html).toContain('data-hf-id="hf-x7k2"'); + }); - it.todo("updates text content by data-hf-id"); + it("updates text content by data-hf-id", () => { + const source = `

Old text

`; + const { html, matched } = patchElementInHtml(source, { hfId: "hf-a1b2" }, [ + { type: "text-content", property: "", value: "New text" }, + ]); + expect(matched).toBe(true); + expect(html).toContain("New text"); + }); - it.todo("updates attribute by data-hf-id"); + it("updates attribute by data-hf-id", () => { + const source = `
`; + const { html, matched } = patchElementInHtml(source, { hfId: "hf-c3d4" }, [ + { type: "attribute", property: "start", value: "2.5" }, + ]); + expect(matched).toBe(true); + expect(html).toContain('data-start="2.5"'); + }); - it.todo("data-hf-id attribute survives the patch (can be targeted again)"); + it("data-hf-id attribute survives the patch (can be targeted again)", () => { + const source = `

Hello

`; + const { html } = patchElementInHtml(source, { hfId: "hf-x7k2" }, [ + { type: "inline-style", property: "color", value: "blue" }, + ]); + expect(html).toContain('data-hf-id="hf-x7k2"'); + }); - it.todo("hfId lookup falls through to selector when hfId is not found in the document"); + it("hfId lookup falls through to selector when hfId is not found in the document", () => { + const source = `

Hello

`; + const { html, matched } = patchElementInHtml( + source, + { hfId: "hf-missing", selector: ".headline" }, + [{ type: "inline-style", property: "color", value: "blue" }], + ); + expect(matched).toBe(true); + expect(html).toMatch(/color:\s*blue/); + }); }); diff --git a/packages/core/src/studio-api/helpers/sourceMutation.ts b/packages/core/src/studio-api/helpers/sourceMutation.ts index 8cdc5ae05..6f762377f 100644 --- a/packages/core/src/studio-api/helpers/sourceMutation.ts +++ b/packages/core/src/studio-api/helpers/sourceMutation.ts @@ -2,6 +2,7 @@ import { parseHTML } from "linkedom"; export interface SourceMutationTarget { id?: string | null; + hfId?: string; selector?: string; selectorIndex?: number; } @@ -32,6 +33,11 @@ function querySelectorAllWithTemplates(root: Document | Element, selector: strin } function findTargetElement(document: Document, target: SourceMutationTarget): Element | null { + if (target.hfId) { + const matches = querySelectorAllWithTemplates(document, `[data-hf-id="${target.hfId}"]`); + if (matches[0]) return matches[0]; + } + if (target.id) { const byId = document.getElementById(target.id); if (byId) return byId; @@ -207,7 +213,7 @@ export function patchElementInHtml( } export function probeElementInSource(source: string, target: SourceMutationTarget): boolean { - if (!target.id && !target.selector) return false; + if (!target.id && !target.hfId && !target.selector) return false; const { document } = parseSourceDocument(source); const el = findTargetElement(document, target); return el != null && isHTMLElement(el); From 5108cd34d92c301162957a02049e4168840da091 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 7 Jun 2026 21:25:38 -0700 Subject: [PATCH 2/2] test(core): update htmlParser baselines for R1 hf- id format Elements now get data-hf-id minted by ensureHfIds; parser reads data-hf-id as model id, so HTML id attrs are no longer the model id. Co-Authored-By: Claude Sonnet 4.6 --- packages/core/src/parsers/htmlParser.test.ts | 29 +++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/core/src/parsers/htmlParser.test.ts b/packages/core/src/parsers/htmlParser.test.ts index 949943bd0..2d786a7d3 100644 --- a/packages/core/src/parsers/htmlParser.test.ts +++ b/packages/core/src/parsers/htmlParser.test.ts @@ -26,13 +26,13 @@ describe("parseHtml", () => { const result = parseHtml(html); expect(result.elements).toHaveLength(2); - expect(result.elements[0].id).toBe("text1"); + expect(result.elements[0].id).toMatch(/^hf-[a-z0-9]{4}$/); expect(result.elements[0].startTime).toBe(0); expect(result.elements[0].duration).toBe(5); expect(result.elements[0].name).toBe("Title"); expect(result.elements[0].type).toBe("text"); - expect(result.elements[1].id).toBe("text2"); + expect(result.elements[1].id).toMatch(/^hf-[a-z0-9]{4}$/); expect(result.elements[1].startTime).toBe(2); expect(result.elements[1].duration).toBe(5); }); @@ -53,7 +53,7 @@ describe("parseHtml", () => { expect(result.elements).toHaveLength(1); expect(result.elements[0].type).toBe("composition"); - expect(result.elements[0].id).toBe("comp1"); + expect(result.elements[0].id).toMatch(/^hf-[a-z0-9]{4}$/); if (result.elements[0].type === "composition") { expect(result.elements[0].compositionId).toBe("abc123"); expect(result.elements[0].src).toBe("/compositions/abc123"); @@ -76,20 +76,20 @@ describe("parseHtml", () => { expect(result.elements).toHaveLength(3); - const video = result.elements.find((e) => e.id === "vid1"); + const video = result.elements.find((e) => e.type === "video"); expect(video).toBeDefined(); - expect(video?.type).toBe("video"); + expect(video?.id).toMatch(/^hf-[a-z0-9]{4}$/); if (video?.type === "video") { expect(video.src).toBe("video.mp4"); } - const audio = result.elements.find((e) => e.id === "aud1"); + const audio = result.elements.find((e) => e.type === "audio"); expect(audio).toBeDefined(); - expect(audio?.type).toBe("audio"); + expect(audio?.id).toMatch(/^hf-[a-z0-9]{4}$/); - const img = result.elements.find((e) => e.id === "img1"); + const img = result.elements.find((e) => e.type === "image"); expect(img).toBeDefined(); - expect(img?.type).toBe("image"); + expect(img?.id).toMatch(/^hf-[a-z0-9]{4}$/); }); it("handles missing attributes gracefully", () => { @@ -123,7 +123,7 @@ describe("parseHtml", () => { const result = parseHtml(html); expect(result.elements).toHaveLength(1); - expect(result.elements[0].id).toMatch(/^element-\d+$/); + expect(result.elements[0].id).toMatch(/^hf-[a-z0-9]{4}$/); }); it("extracts GSAP script from script tags", () => { @@ -398,9 +398,12 @@ describe("parseHtml", () => { `; const result = parseHtml(html); - expect(result.keyframes["text1"]).toBeDefined(); - expect(result.keyframes["text1"]).toHaveLength(2); - expect(result.keyframes["text1"][0].id).toBe("kf1"); + const elId = result.elements[0]?.id ?? ""; + expect(elId).toMatch(/^hf-[a-z0-9]{4}$/); + const kfs = result.keyframes[elId]; + expect(kfs).toBeDefined(); + expect(kfs).toHaveLength(2); + expect(kfs[0].id).toBe("kf1"); }); it("parses stage zoom keyframes", () => {