fix(studio-server): an edit aimed at a clip never lands on an audio group sharing its id (#5645)

* fix(studio-server): an edit aimed at a clip never lands on an audio group sharing its id

* refactor(studio-server): one comment line, the rest in names
This commit is contained in:
Miguel Ángel
2026-10-15 08:19:28 +00:00
committed by GitHub
parent 695110e46b
commit 49c49504a4
2 changed files with 27 additions and 9 deletions
@@ -603,6 +603,18 @@ describe("T7 — data-hf-id targeting (spec for R1)", () => {
expect(html).toMatch(/color:\s*blue/);
});
it("never falls back by id onto an element pinned to another hfId", () => {
// A clip in a sub-composition shares the audio group's id; its edit must not land on the group.
const source = `<div data-composition-id="main"><hf-audio-group id="voice" data-hf-id="hf-oova" data-fx-chain="[]"></hf-audio-group><div class="clip" data-start="0"></div></div>`;
const target = { id: "voice", hfId: "hf-6egx" };
expect(removeElementFromHtml(source, target)).toBe(source);
const { html, matched } = patchElementInHtml(source, target, [
{ type: "attribute", property: "data-start", value: "3" },
]);
expect(matched).toBe(false);
expect(html).toBe(source);
});
it("does not break out of the selector on a crafted hfId (CSS injection guard)", () => {
// A value with a quote/bracket must be escaped, not injected — it should
// simply match nothing and leave the source untouched, never throw.
@@ -23,19 +23,22 @@ export interface SourceMutationTarget {
selectorIndex?: number;
}
const pinnedInSource = new WeakMap<Document, ReadonlySet<string>>();
export function parseSourceDocument(raw: string): {
document: Document;
wrappedFragment: boolean;
} {
const source = ensureHfIds(raw);
const hasDocumentShell = /<!doctype|<html[\s>]/i.test(source);
if (hasDocumentShell) {
return { document: parseHTML(source).document, wrappedFragment: false };
}
return {
document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,
wrappedFragment: true,
};
const document = hasDocumentShell
? parseHTML(source).document
: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document;
pinnedInSource.set(
document,
new Set(Array.from(raw.matchAll(/\bdata-hf-id\s*=\s*["']?([^"'\s>]+)/gi), (m) => m[1]!)),
);
return { document, wrappedFragment: !hasDocumentShell };
}
function duplicateCssRulesForId(document: Document, originalId: string, newId: string): void {
@@ -123,16 +126,19 @@ export function findTargetElement(
const el = findByHfId(document, target.hfId);
if (el) return el;
}
const pinned = pinnedInSource.get(document);
const unlessPinnedElsewhere = (el: Element | null | undefined) =>
el && !(target.hfId && pinned?.has(el.getAttribute("data-hf-id") ?? "")) ? el : null;
if (target.id) {
const byId = document.getElementById(target.id);
const byId = unlessPinnedElsewhere(document.getElementById(target.id));
if (byId) return byId;
}
if (!target.selector) return null;
try {
const matches = querySelectorAllWithTemplates(document, target.selector);
return matches[target.selectorIndex ?? 0] ?? null;
return unlessPinnedElsewhere(matches[target.selectorIndex ?? 0]);
} catch {
return null;
}