From 5be9aa95c8942bda08c2229268292af56e32133d Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Wed, 7 May 2025 20:41:13 +0400 Subject: [PATCH] experimental: paste inline styles Ref https://github.com/webstudio-is/webstudio/issues/4404 Inline styles are easy to support and tailwind templates occasionally use them for unsupported stuff. --- apps/builder/app/shared/html.test.tsx | 34 ++++++++++++++++- apps/builder/app/shared/html.ts | 53 ++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/apps/builder/app/shared/html.test.tsx b/apps/builder/app/shared/html.test.tsx index 2831db3489ec..74cb68aa2178 100644 --- a/apps/builder/app/shared/html.test.tsx +++ b/apps/builder/app/shared/html.test.tsx @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { renderTemplate, ws } from "@webstudio-is/template"; +import { css, renderTemplate, ws } from "@webstudio-is/template"; import { generateFragmentFromHtml } from "./html"; test("generate instances from html", () => { @@ -190,3 +190,35 @@ test("do not wrap text with span when spotted near link", () => { ) ); }); + +test("collapse any spacing characters inside text", () => { + expect( + generateFragmentFromHtml(` +
+ line + another line +
+ `) + ).toEqual( + renderTemplate( + {" line another line "} + ) + ); +}); + +test("generate style attribute as local styles", () => { + expect( + generateFragmentFromHtml(` +
+ `) + ).toEqual( + renderTemplate( + + ) + ); +}); diff --git a/apps/builder/app/shared/html.ts b/apps/builder/app/shared/html.ts index bc8f290d8948..c205a787ab88 100644 --- a/apps/builder/app/shared/html.ts +++ b/apps/builder/app/shared/html.ts @@ -9,10 +9,15 @@ import { elementComponent, Prop, tags, + StyleDecl, + Breakpoint, + StyleSource, + StyleSourceSelection, } from "@webstudio-is/sdk"; import { ariaAttributes, attributesByTag } from "@webstudio-is/html-data"; import { richTextContentTags } from "./content-model"; import { setIsSubsetOf } from "./shim"; +import { camelCaseProperty, parseCss } from "@webstudio-is/css-data"; type ElementNode = DefaultTreeAdapterMap["element"]; @@ -65,12 +70,43 @@ const findContentTags = (element: ElementNode, tags = new Set()) => { export const generateFragmentFromHtml = (html: string): WebstudioFragment => { const attributeTypes = getAttributeTypes(); const instances = new Map(); + const styleSourceSelections: StyleSourceSelection[] = []; + const styleSources: StyleSource[] = []; + const styles: StyleDecl[] = []; + const breakpoints: Breakpoint[] = []; const props: Prop[] = []; let lastId = -1; const getNewId = () => { lastId += 1; return lastId.toString(); }; + + let baseBreakpoint: undefined | Breakpoint; + const getBaseBreakpointId = () => { + if (baseBreakpoint) { + return baseBreakpoint.id; + } + baseBreakpoint = { id: "base", label: "" }; + breakpoints.push(baseBreakpoint); + return baseBreakpoint.id; + }; + const createLocalStyles = (instanceId: string, css: string) => { + const localStyleSource: StyleSource = { + type: "local", + id: `${instanceId}:ws:style`, + }; + styleSources.push(localStyleSource); + styleSourceSelections.push({ instanceId, values: [localStyleSource.id] }); + for (const { property, value } of parseCss(`.styles{${css}}`)) { + styles.push({ + styleSourceId: localStyleSource.id, + breakpointId: getBaseBreakpointId(), + property: camelCaseProperty(property), + value, + }); + } + }; + const convertElementToInstance = (node: ElementNode) => { if (!tags.includes(node.tagName)) { return; @@ -92,6 +128,11 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => { attributeTypes.get(`${node.tagName}:${name}`) ?? attributeTypes.get(name) ?? "string"; + // ignore style attribute to not conflict with react + if (attr.name === "style") { + createLocalStyles(instanceId, attr.value); + continue; + } if (type === "string") { props.push({ id, instanceId, name, type, value: attr.value }); continue; @@ -124,7 +165,8 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => { } let child: Instance["children"][number] = { type: "text", - value: childNode.value, + // collapse spacing characters inside of text to avoid preserved newlines + value: childNode.value.replaceAll(/\s+/g, " "), }; // when element has content elements other than supported by rich text // wrap its text children with span, for example @@ -153,6 +195,7 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => { } return { type: "id" as const, value: instance.id }; }; + const documentFragment = parseFragment(html, { scriptingEnabled: false }); const children: Instance["children"] = []; for (const childNode of documentFragment.childNodes) { @@ -169,10 +212,10 @@ export const generateFragmentFromHtml = (html: string): WebstudioFragment => { props, dataSources: [], resources: [], - styleSourceSelections: [], - styleSources: [], - styles: [], - breakpoints: [], + styleSourceSelections, + styleSources, + styles, + breakpoints, assets: [], }; };