mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(core): sanitize rich text on the way into a composition (#3141)
* feat(core): sanitize rich text on the way into a composition Studio's patch vocabulary was inline-style, attribute, html-attribute and text-content. text-content assigns textContent, and the text-field model escapes markup on the way out and refuses a change in child structure, so a styled span had no route into a composition file. Adds a rich-text operation with one, guarded by a single sanitizer called on both ends of the trip: in the browser so the preview shows what will be saved, and on the server because that is where the file is written. Tags and style properties are a small allowlist, and an unexpected tag loses its formatting rather than its words. Spans an edit adds get their ids in the same write, so a follow-up write cannot race it. No UI yet — this is the persistence contract the editor is built on. * fix(core): document and test the sanitizer boundary * fix(core): harden rich text sanitizer traversal
This commit is contained in:
@@ -542,3 +542,46 @@ describe("T7 — data-hf-id targeting (spec for R1)", () => {
|
||||
expect(html).toContain('data-hf-id="hf-a1b2"');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A rich-text operation adds elements, so it has to give them their stable ids
|
||||
* here, in the bytes it writes and returns.
|
||||
*
|
||||
* Otherwise the next preview request mints them and writes the file a second
|
||||
* time, after Studio has recorded the edit. The recorded "after" stops matching
|
||||
* disk, the content check refuses, and undo reports the file as changed outside
|
||||
* Studio — for every colour applied to a run of characters.
|
||||
*/
|
||||
describe("patchElementInHtml stamps the ids a rich-text patch introduces", () => {
|
||||
it("gives each new span its id in the same write", () => {
|
||||
const source = '<div data-hf-id="hf-a" id="t">plain</div>';
|
||||
const { html, matched } = patchElementInHtml(source, { id: "t" }, [
|
||||
{ type: "rich-text", property: "", value: 'a<span style="color: red">b</span>c' },
|
||||
]);
|
||||
|
||||
expect(matched).toBe(true);
|
||||
expect(html).toContain("color: red");
|
||||
expect((html.match(/data-hf-id=/g) ?? []).length).toBe(2);
|
||||
});
|
||||
|
||||
it("leaves an id a rich-text patch carried in alone", () => {
|
||||
const source = '<div data-hf-id="hf-a" id="t">plain</div>';
|
||||
const { html } = patchElementInHtml(source, { id: "t" }, [
|
||||
{ type: "rich-text", property: "", value: '<span data-hf-id="hf-keep">b</span>' },
|
||||
]);
|
||||
|
||||
expect(html).toContain('data-hf-id="hf-keep"');
|
||||
});
|
||||
|
||||
it("does not collide with an id inside a composition template", () => {
|
||||
const source = `<!doctype html><html><body><template data-composition-id="nested"><p data-hf-id="hf-3x72">nested</p></template><h1 id="title">plain</h1></body></html>`;
|
||||
const { html } = patchElementInHtml(source, { id: "title" }, [
|
||||
{ type: "rich-text", property: "", value: '<span style="color: red">b</span>' },
|
||||
]);
|
||||
|
||||
expect(html.match(/data-hf-id="hf-3x72"/g)).toHaveLength(1);
|
||||
const introducedId = /<span[^>]*data-hf-id="([^"]+)"/.exec(html)?.[1];
|
||||
expect(introducedId).toBeDefined();
|
||||
expect(introducedId).not.toBe("hf-3x72");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user