From 6bc2704efe23f80ed45b78dda568e1252d60e120 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 25 May 2025 17:47:13 +0300 Subject: [PATCH 1/3] feat: download image when paste html with it Ref https://github.com/webstudio-is/webstudio/issues/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. --- .../shared/copy-paste/plugin-html.test.tsx | 8 ++++---- .../app/shared/copy-paste/plugin-html.ts | 6 ++++-- apps/builder/app/shared/html.test.tsx | 20 +++++++++++++++++-- apps/builder/app/shared/html.ts | 8 +++++++- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/builder/app/shared/copy-paste/plugin-html.test.tsx b/apps/builder/app/shared/copy-paste/plugin-html.test.tsx index 531fd92ce59f..f54d84b898b1 100644 --- a/apps/builder/app/shared/copy-paste/plugin-html.test.tsx +++ b/apps/builder/app/shared/copy-paste/plugin-html.test.tsx @@ -11,7 +11,7 @@ import { html } from "./plugin-html"; setEnv("*"); registerContainers(); -test("paste html fragment", () => { +test("paste html fragment", async () => { const data = renderData( @@ -24,7 +24,7 @@ test("paste html fragment", () => { ); $awareness.set({ pageId: "pageId", instanceSelector: ["divId", "bodyId"] }); expect( - html.onPaste?.(` + await html.onPaste?.(`

It works

@@ -48,7 +48,7 @@ test("paste html fragment", () => { ); }); -test("ignore html without any tags", () => { +test("ignore html without any tags", async () => { const data = renderData( @@ -60,6 +60,6 @@ test("ignore html without any tags", () => { createDefaultPages({ rootInstanceId: "bodyId", homePageId: "pageId" }) ); $awareness.set({ pageId: "pageId", instanceSelector: ["divId", "bodyId"] }); - expect(html.onPaste?.(`It works`)).toEqual(false); + expect(await html.onPaste?.(`It works`)).toEqual(false); expect($instances.get()).toEqual(data.instances); }); diff --git a/apps/builder/app/shared/copy-paste/plugin-html.ts b/apps/builder/app/shared/copy-paste/plugin-html.ts index 5fb87d20cf42..48b70745599f 100644 --- a/apps/builder/app/shared/copy-paste/plugin-html.ts +++ b/apps/builder/app/shared/copy-paste/plugin-html.ts @@ -1,11 +1,13 @@ import { generateFragmentFromHtml } from "../html"; import { insertWebstudioFragmentAt } from "../instance-utils"; +import { denormalizeSrcProps } from "./asset-upload"; import type { Plugin } from "./init-copy-paste"; export const html: Plugin = { mimeType: "text/plain", - onPaste: (html: string) => { - const fragment = generateFragmentFromHtml(html); + onPaste: async (html: string) => { + let fragment = generateFragmentFromHtml(html); + fragment = await denormalizeSrcProps(fragment); return insertWebstudioFragmentAt(fragment); }, }; diff --git a/apps/builder/app/shared/html.test.tsx b/apps/builder/app/shared/html.test.tsx index b36115dd4f97..3f8a5b928ccb 100644 --- a/apps/builder/app/shared/html.test.tsx +++ b/apps/builder/app/shared/html.test.tsx @@ -162,14 +162,14 @@ test("wrap text with span when spotted outside of rich text", () => { ); expect( generateFragmentFromHtml(` -
div
+
div
`) ).toEqual( renderTemplate( div - + ) @@ -301,3 +301,19 @@ test("generate select element", () => { ) ); }); + +test("generate Image component instead of img element", () => { + expect( + generateFragmentFromHtml(` +
+ +
+ `) + ).toEqual( + renderTemplate( + + <$.Image src="./my-url" /> + + ) + ); +}); diff --git a/apps/builder/app/shared/html.ts b/apps/builder/app/shared/html.ts index 0face7ba30d4..44f5cb12db8a 100644 --- a/apps/builder/app/shared/html.ts +++ b/apps/builder/app/shared/html.ts @@ -154,13 +154,19 @@ export const generateFragmentFromHtml = ( if (!tags.includes(node.tagName)) { return; } - const instance: Instance = { + let instance: Instance = { type: "instance", id: getNewId(), component: elementComponent, tag: node.tagName, children: [], }; + // users expect to get optimized images by default + // though still able to create raw img element + if (node.tagName === "img") { + instance.component = "Image"; + delete instance.tag; + } instances.set(instance.id, instance); for (const attr of node.attrs) { const id = `${instance.id}:${attr.name}`; From cbe87503ec8f75ac883166880b83ee64c0f9af27 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 25 May 2025 17:56:28 +0300 Subject: [PATCH 2/3] Fix lint --- apps/builder/app/shared/html.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/builder/app/shared/html.ts b/apps/builder/app/shared/html.ts index 44f5cb12db8a..71801ccc9911 100644 --- a/apps/builder/app/shared/html.ts +++ b/apps/builder/app/shared/html.ts @@ -154,7 +154,7 @@ export const generateFragmentFromHtml = ( if (!tags.includes(node.tagName)) { return; } - let instance: Instance = { + const instance: Instance = { type: "instance", id: getNewId(), component: elementComponent, From ec61fde0478258c261c8d2f0a314775789408d31 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Sun, 25 May 2025 22:59:17 +0300 Subject: [PATCH 3/3] Support upload in tailwind command as well --- apps/builder/app/builder/shared/commands.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/builder/app/builder/shared/commands.ts b/apps/builder/app/builder/shared/commands.ts index eba64e0c1396..1a23a906ec80 100644 --- a/apps/builder/app/builder/shared/commands.ts +++ b/apps/builder/app/builder/shared/commands.ts @@ -53,6 +53,7 @@ import { } from "~/shared/content-model"; import { generateFragmentFromHtml } from "~/shared/html"; import { generateFragmentFromTailwind } from "~/shared/tailwind/tailwind"; +import { denormalizeSrcProps } from "~/shared/copy-paste/asset-upload"; import { getInstanceLabel } from "./instance-label"; export const $styleSourceInputElement = atom(); @@ -536,6 +537,7 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ handler: async () => { const html = await navigator.clipboard.readText(); let fragment = generateFragmentFromHtml(html); + fragment = await denormalizeSrcProps(fragment); fragment = await generateFragmentFromTailwind(fragment); return insertWebstudioFragmentAt(fragment); },