Skip to content

Commit 535d87d

Browse files
authored
experimental: allow pasting svg as html embed (for now) (#5200)
Now icons are added as HtmlEmbed component when paste html. This is essential for tailwind paste which use a lot of icons. <img width="342" alt="image" src="https://github.com/user-attachments/assets/06bbe633-e2ae-4929-99c5-91040cda9472" /> <img width="431" alt="image" src="https://github.com/user-attachments/assets/8c7e1b5c-42b4-4d6f-8039-5918175bad7e" /> ```html <div style="display: flex; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;" > <button style="background-color: #f8f8f8; border: none; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-right: 1px solid #ccc; font-family: sans-serif; font-size: 12px;" > <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 5px;" > <line x1="3" y1="12" x2="21" y2="12"></line> <line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="18" x2="21" y2="18"></line> </svg> <span>Menu</span> </button> <button style="background-color: #f8f8f8; border: none; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-right: 1px solid #ccc; font-family: sans-serif; font-size: 12px;" > <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 5px;" > <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="16"></line> <line x1="8" y1="12" x2="16" y2="12"></line> </svg> <span>Add</span> </button> <button style="background-color: #f8f8f8; border: none; padding: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-family: sans-serif; font-size: 12px;" > <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 5px;" > <path d="M18 6L6 18"></path> <path d="M6 6L18 18"></path> </svg> <span>Close</span> </button> </div> ```
1 parent 7f77a04 commit 535d87d

File tree

2 files changed

+59
-2
lines changed

2 files changed

+59
-2
lines changed

apps/builder/app/shared/html.test.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from "vitest";
2-
import { css, renderTemplate, ws } from "@webstudio-is/template";
2+
import { $, css, renderTemplate, ws } from "@webstudio-is/template";
33
import { generateFragmentFromHtml } from "./html";
44

55
test("generate instances from html", () => {
@@ -222,3 +222,25 @@ test("generate style attribute as local styles", () => {
222222
)
223223
);
224224
});
225+
226+
test("paste svg as html embed", () => {
227+
expect(
228+
generateFragmentFromHtml(`
229+
<div>
230+
<svg viewBox="0 0 20 20">
231+
<rect x="5" y="5" width="10" height="10" />
232+
</svg>
233+
</div>
234+
`)
235+
).toEqual(
236+
renderTemplate(
237+
<ws.element ws:tag="div">
238+
<$.HtmlEmbed
239+
code={`<svg viewBox="0 0 20 20">
240+
<rect x="5" y="5" width="10" height="10" />
241+
</svg>`}
242+
/>
243+
</ws.element>
244+
)
245+
);
246+
});

apps/builder/app/shared/html.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,38 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => {
108108
};
109109

110110
const convertElementToInstance = (node: ElementNode) => {
111+
if (node.tagName === "svg" && node.sourceCodeLocation) {
112+
const { startCol, startOffset, endOffset } = node.sourceCodeLocation;
113+
const indent = startCol - 1;
114+
const htmlFragment = html
115+
.slice(startOffset, endOffset)
116+
// try to preserve indentation
117+
.split("\n")
118+
.map((line, index) => {
119+
if (index > 0 && /^\s+$/.test(line.slice(0, indent))) {
120+
return line.slice(indent);
121+
}
122+
return line;
123+
})
124+
.join("\n");
125+
const instance: Instance = {
126+
type: "instance",
127+
id: getNewId(),
128+
component: "HtmlEmbed",
129+
children: [],
130+
};
131+
instances.set(instance.id, instance);
132+
const name = "code";
133+
const codeProp: Prop = {
134+
id: `${instance.id}:${name}`,
135+
instanceId: instance.id,
136+
name,
137+
type: "string",
138+
value: htmlFragment,
139+
};
140+
props.push(codeProp);
141+
return { type: "id" as const, value: instance.id };
142+
}
111143
if (!tags.includes(node.tagName)) {
112144
return;
113145
}
@@ -196,7 +228,10 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => {
196228
return { type: "id" as const, value: instance.id };
197229
};
198230

199-
const documentFragment = parseFragment(html, { scriptingEnabled: false });
231+
const documentFragment = parseFragment(html, {
232+
scriptingEnabled: false,
233+
sourceCodeLocationInfo: true,
234+
});
200235
const children: Instance["children"] = [];
201236
for (const childNode of documentFragment.childNodes) {
202237
if (defaultTreeAdapter.isElementNode(childNode)) {

0 commit comments

Comments
 (0)