Skip to content

Commit 57ea5da

Browse files
authored
feat: download image when paste html with it (#5232)
Ref #3632 Now images will be downloaded when paste it in html to make them accessible within project even when original one is no longer responds. https://github.com/user-attachments/assets/2591e981-3ec1-40f8-bf50-19332c322332
1 parent 3506fe8 commit 57ea5da

File tree

5 files changed

+34
-8
lines changed

5 files changed

+34
-8
lines changed

apps/builder/app/builder/shared/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
} from "~/shared/content-model";
5454
import { generateFragmentFromHtml } from "~/shared/html";
5555
import { generateFragmentFromTailwind } from "~/shared/tailwind/tailwind";
56+
import { denormalizeSrcProps } from "~/shared/copy-paste/asset-upload";
5657
import { getInstanceLabel } from "./instance-label";
5758

5859
export const $styleSourceInputElement = atom<HTMLInputElement | undefined>();
@@ -536,6 +537,7 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
536537
handler: async () => {
537538
const html = await navigator.clipboard.readText();
538539
let fragment = generateFragmentFromHtml(html);
540+
fragment = await denormalizeSrcProps(fragment);
539541
fragment = await generateFragmentFromTailwind(fragment);
540542
return insertWebstudioFragmentAt(fragment);
541543
},

apps/builder/app/shared/copy-paste/plugin-html.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { html } from "./plugin-html";
1111
setEnv("*");
1212
registerContainers();
1313

14-
test("paste html fragment", () => {
14+
test("paste html fragment", async () => {
1515
const data = renderData(
1616
<ws.element ws:tag="body" ws:id="bodyId">
1717
<ws.element ws:tag="div" ws:id="divId"></ws.element>
@@ -24,7 +24,7 @@ test("paste html fragment", () => {
2424
);
2525
$awareness.set({ pageId: "pageId", instanceSelector: ["divId", "bodyId"] });
2626
expect(
27-
html.onPaste?.(`
27+
await html.onPaste?.(`
2828
<section>
2929
<h1>It works</h1>
3030
</section>
@@ -48,7 +48,7 @@ test("paste html fragment", () => {
4848
);
4949
});
5050

51-
test("ignore html without any tags", () => {
51+
test("ignore html without any tags", async () => {
5252
const data = renderData(
5353
<ws.element ws:tag="body" ws:id="bodyId">
5454
<ws.element ws:tag="div" ws:id="divId"></ws.element>
@@ -60,6 +60,6 @@ test("ignore html without any tags", () => {
6060
createDefaultPages({ rootInstanceId: "bodyId", homePageId: "pageId" })
6161
);
6262
$awareness.set({ pageId: "pageId", instanceSelector: ["divId", "bodyId"] });
63-
expect(html.onPaste?.(`It works`)).toEqual(false);
63+
expect(await html.onPaste?.(`It works`)).toEqual(false);
6464
expect($instances.get()).toEqual(data.instances);
6565
});
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { generateFragmentFromHtml } from "../html";
22
import { insertWebstudioFragmentAt } from "../instance-utils";
3+
import { denormalizeSrcProps } from "./asset-upload";
34
import type { Plugin } from "./init-copy-paste";
45

56
export const html: Plugin = {
67
mimeType: "text/plain",
7-
onPaste: (html: string) => {
8-
const fragment = generateFragmentFromHtml(html);
8+
onPaste: async (html: string) => {
9+
let fragment = generateFragmentFromHtml(html);
10+
fragment = await denormalizeSrcProps(fragment);
911
return insertWebstudioFragmentAt(fragment);
1012
},
1113
};

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,14 @@ test("wrap text with span when spotted outside of rich text", () => {
162162
);
163163
expect(
164164
generateFragmentFromHtml(`
165-
<div>div<b><img></b></div>
165+
<div>div<b><br></b></div>
166166
`)
167167
).toEqual(
168168
renderTemplate(
169169
<ws.element ws:tag="div">
170170
<ws.element ws:tag="span">div</ws.element>
171171
<ws.element ws:tag="b">
172-
<ws.element ws:tag="img"></ws.element>
172+
<ws.element ws:tag="br"></ws.element>
173173
</ws.element>
174174
</ws.element>
175175
)
@@ -301,3 +301,19 @@ test("generate select element", () => {
301301
)
302302
);
303303
});
304+
305+
test("generate Image component instead of img element", () => {
306+
expect(
307+
generateFragmentFromHtml(`
308+
<div>
309+
<img src="./my-url">
310+
</div>
311+
`)
312+
).toEqual(
313+
renderTemplate(
314+
<ws.element ws:tag="div">
315+
<$.Image src="./my-url" />
316+
</ws.element>
317+
)
318+
);
319+
});

apps/builder/app/shared/html.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ export const generateFragmentFromHtml = (
161161
tag: node.tagName,
162162
children: [],
163163
};
164+
// users expect to get optimized images by default
165+
// though still able to create raw img element
166+
if (node.tagName === "img") {
167+
instance.component = "Image";
168+
delete instance.tag;
169+
}
164170
instances.set(instance.id, instance);
165171
for (const attr of node.attrs) {
166172
const id = `${instance.id}:${attr.name}`;

0 commit comments

Comments
 (0)