From 2df897aa43bcafbdb2662b1f8d2ade6b379b44d0 Mon Sep 17 00:00:00 2001 From: "Abdullah M. Nazif" Date: Thu, 8 Oct 2026 21:30:16 +0000 Subject: [PATCH] fix(sdk): preserve unquoted escapes in inline styles (#5246) --- packages/sdk/src/engine/model.ts | 8 ++-- packages/sdk/src/engine/mutate.test.ts | 46 +++++++++++++++++++ .../sdk/src/session.render-faithful.test.ts | 24 ++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/engine/model.ts b/packages/sdk/src/engine/model.ts index 2910dd9fdb..98a025bf8f 100644 --- a/packages/sdk/src/engine/model.ts +++ b/packages/sdk/src/engine/model.ts @@ -236,11 +236,11 @@ interface StyleDeclarationScan { } function advanceStyleDeclarationScan(scan: StyleDeclarationScan, ch: string, next: string): void { + if (ch === "\\" && next) { + scan.skip = true; + return; + } if (scan.quote) { - if (ch === "\\" && next) { - scan.skip = true; - return; - } if (ch === scan.quote) scan.quote = null; return; } diff --git a/packages/sdk/src/engine/mutate.test.ts b/packages/sdk/src/engine/mutate.test.ts index 88c76e14a0..da91e9bdd7 100644 --- a/packages/sdk/src/engine/mutate.test.ts +++ b/packages/sdk/src/engine/mutate.test.ts @@ -914,6 +914,52 @@ describe("setElementStyles key normalization", () => { }); }); +describe("setElementStyles unquoted CSS escapes", () => { + it.each([ + [ + "semicolon", + String.raw`--label: alpha\;beta; width: 120px; color: red`, + String.raw`alpha\;beta`, + ], + [ + "opening parenthesis", + String.raw`--label: alpha\(beta; width: 120px; color: red`, + String.raw`alpha\(beta`, + ], + [ + "double quote", + String.raw`--label: alpha\"beta; width: 120px; color: red`, + String.raw`alpha\"beta`, + ], + [ + "single quote", + String.raw`--label: alpha\'beta; width: 120px; color: red`, + String.raw`alpha\'beta`, + ], + ])("preserves an escaped %s and the following declarations", (_name, style, label) => { + const parsed = parseMutable('
'); + const el = parsed.document.querySelector('[data-hf-id="hf-x"]'); + if (!el) throw new Error("missing fixture element"); + el.setAttribute("style", style); + expect(getElementStyles(el)).toMatchObject({ "--label": label, width: "120px", color: "red" }); + setElementStyles(el, { color: "blue" }); + expect(getElementStyles(el)).toMatchObject({ "--label": label, width: "120px", color: "blue" }); + }); + + it("treats the semicolon after an escaped backslash as a declaration boundary", () => { + const parsed = parseMutable('
'); + const el = parsed.document.querySelector('[data-hf-id="hf-x"]'); + if (!el) throw new Error("missing fixture element"); + el.setAttribute("style", String.raw`--label: alpha\\; width: 120px; color: red`); + setElementStyles(el, { color: "blue" }); + expect(getElementStyles(el)).toMatchObject({ + "--label": String.raw`alpha\\`, + width: "120px", + color: "blue", + }); + }); +}); + // ─── setVariableValue ───────────────────────────────────────────────────────── describe("setVariableValue", () => { diff --git a/packages/sdk/src/session.render-faithful.test.ts b/packages/sdk/src/session.render-faithful.test.ts index 5056242a12..53da99e002 100644 --- a/packages/sdk/src/session.render-faithful.test.ts +++ b/packages/sdk/src/session.render-faithful.test.ts @@ -185,3 +185,27 @@ describe("serialize() render-faithfulness (WS-F)", () => { expect(comp2.getElement("hf-body")?.text).toContain("Round-tripped body"); }); }); + +describe("serialize() preserves unquoted CSS escapes", () => { + it.each([ + String.raw`alpha\;beta`, + String.raw`alpha\(beta`, + String.raw`alpha\"beta`, + String.raw`alpha\'beta`, + ])("keeps %s through edit, undo, redo, and saved overrides", async (label) => { + const style = `--label: ${label}; width: 120px; color: red`.replace(/"/g, """); + const source = `
`; + const comp = await openComposition(source); + const expected = { "--label": label, width: "120px", color: "blue" }; + comp.setStyle("hf-box", { color: "blue" }); + expect(comp.getElement("hf-box")?.inlineStyles).toMatchObject(expected); + comp.undo(); + expect(comp.getElement("hf-box")?.inlineStyles).toMatchObject({ ...expected, color: "red" }); + comp.redo(); + expect(comp.getElement("hf-box")?.inlineStyles).toMatchObject(expected); + const reopened = await openComposition(comp.serialize()); + expect(reopened.getElement("hf-box")?.inlineStyles).toMatchObject(expected); + const overrides = await openComposition(source, { overrides: comp.getOverrides() }); + expect(overrides.getElement("hf-box")?.inlineStyles).toMatchObject(expected); + }); +});