From bae99d1ceb9d94384636e1a0f015eb328dab677b Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 3 Jan 2025 01:25:03 +0700 Subject: [PATCH 1/3] fix: prevent auto closing valid xml in svg Fixes https://github.com/webstudio-is/webstudio/issues/4589 Just added xml parser before html one. --- .../features/settings-panel/controls/code.tsx | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/builder/app/builder/features/settings-panel/controls/code.tsx b/apps/builder/app/builder/features/settings-panel/controls/code.tsx index 61bbc9e58ffb..7f8fc758af1f 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/code.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/code.tsx @@ -67,6 +67,24 @@ const ErrorInfo = ({ type Error = { message: string; value: string; expected: string }; +/** + * Use DOMParser in xml mode to parse potential svg + */ +const parseSvg = (value: string) => { + let doc = new DOMParser().parseFromString(value, "application/xml"); + let errorNode = doc.querySelector("parsererror"); + if (errorNode) { + return ""; + } + return doc.documentElement.outerHTML; +}; + +const parseHtml = (value: string) => { + const div = document.createElement("div"); + div.innerHTML = value; + return div.innerHTML; +}; + // The problem is to identify broken HTML and because browser is flexible and always tries to fix it we never // know if something is actually broken. // 1. Parse the HTML using DOM @@ -76,21 +94,29 @@ type Error = { message: string; value: string; expected: string }; // - different amount of whitespace // - unifying `boolean=""` is the same as `boolean` const validateHtml = (value: string): Error | undefined => { - const div = document.createElement("div"); - div.innerHTML = value; - const expected = div.innerHTML; const clean = (value: string) => { return ( value // Compare without whitespace to avoid false positives - .replace(/\s/g, "") + .replaceAll(/\s/g, "") // normalize boolean attributes by turning `boolean=""` into `boolean` - .replace('=""', "") + .replaceAll('=""', "") + // namespace attribute is always reordered first + .replaceAll('xmlns="http://www.w3.org/2000/svg"', "") ); }; - if (clean(value) !== clean(expected)) { - return { message: "Invalid HTML detected", value, expected }; + // in many cases svg is valid xml so serialize in xml mode first + // to avoid false positive of auto closing svg tags, for example + // -> + const xml = parseSvg(value); + if (clean(xml) === clean(value)) { + return; + } + const html = parseHtml(value); + if (clean(html) === clean(value)) { + return; } + return { message: "Invalid HTML detected", value, expected: html ?? "" }; }; export const CodeControl = ({ From 9189fbe4a983b9816dca99de789e9c38f0483d26 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 3 Jan 2025 01:28:09 +0700 Subject: [PATCH 2/3] Fix lint --- .../app/builder/features/settings-panel/controls/code.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/builder/app/builder/features/settings-panel/controls/code.tsx b/apps/builder/app/builder/features/settings-panel/controls/code.tsx index 7f8fc758af1f..0dfa756cc976 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/code.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/code.tsx @@ -71,8 +71,8 @@ type Error = { message: string; value: string; expected: string }; * Use DOMParser in xml mode to parse potential svg */ const parseSvg = (value: string) => { - let doc = new DOMParser().parseFromString(value, "application/xml"); - let errorNode = doc.querySelector("parsererror"); + const doc = new DOMParser().parseFromString(value, "application/xml"); + const errorNode = doc.querySelector("parsererror"); if (errorNode) { return ""; } From 4691d08b890c554429ec76ac943a2cb409406abd Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Fri, 3 Jan 2025 01:34:58 +0700 Subject: [PATCH 3/3] Update comment --- .../builder/features/settings-panel/controls/code.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/builder/app/builder/features/settings-panel/controls/code.tsx b/apps/builder/app/builder/features/settings-panel/controls/code.tsx index 0dfa756cc976..9c46f490783d 100644 --- a/apps/builder/app/builder/features/settings-panel/controls/code.tsx +++ b/apps/builder/app/builder/features/settings-panel/controls/code.tsx @@ -87,12 +87,14 @@ const parseHtml = (value: string) => { // The problem is to identify broken HTML and because browser is flexible and always tries to fix it we never // know if something is actually broken. -// 1. Parse the HTML using DOM -// 2. Get HTML via innerHTML -// 3. Compare the original HTML with innerHTML -// 4. We try to minimize the amount of false positives by removing +// 1. Parse potential SVG with XML parser and serialize +// 2. Compare the original SVG with resulting value +// 3. Parse the HTML using DOM parser and serialize +// 4. Compare the original HTML with resulting value +// 5. We try to minimize the amount of false positives by removing // - different amount of whitespace // - unifying `boolean=""` is the same as `boolean` +// - xmlns attirbute which is always reordered first const validateHtml = (value: string): Error | undefined => { const clean = (value: string) => { return (