From 515fef72f11a73949a4cbd447822d93fca569674 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Tue, 2 May 2023 15:32:29 -0400 Subject: [PATCH 01/21] escape html strings (#66) --- core/src/code/generator/html/generator.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 89bb424..12f1d74 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -372,9 +372,30 @@ export const getTextProp = (node: Node): string => { return prop; } - return textNode.getText(); + return escapeHtml(textNode.getText()); }; +function escapeHtml(str: string) { + return str.replace(/[&<>"'{}]/g, function (match) { + switch (match) { + case "&": + return "&"; + case "<": + return "<"; + case ">": + return ">"; + case '"': + return """; + case "'": + return "'"; + case "{": + return "{"; + case "}": + return "}"; + } + }); +} + export const createMiniReactFile = ( componentCode: string, dataCode: string, From 24b8fa5f976c161be12171c80272dfc14b6de254 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Thu, 4 May 2023 00:11:05 -0700 Subject: [PATCH 02/21] [Major Update] New Figma plugin UI flow (#69) --- core/ee/cv/component-recognition.ts | 17 ++- core/ee/loop/loop.ts | 82 -------------- core/ee/ui/ai-application-registry.ts | 30 +++++ core/ee/web/request.ts | 23 ++-- core/src/analytic/amplitude.ts | 3 + core/src/index.ts | 68 +++++------ core/src/utils.ts | 9 ++ .../src/{analytics => analytic}/amplitude.ts | 1 + figma/src/assets/light-bulb-dark.svg | 2 + figma/src/assets/light-bulb.svg | 3 + figma/src/code.ts | 55 +++------ figma/src/constants.ts | 13 +++ figma/src/context/page-context.tsx | 1 + figma/src/pages/code-generation-status.tsx | 11 ++ figma/src/pages/code-output-setting.tsx | 47 +++++++- figma/src/pages/home.tsx | 106 +++++++----------- figma/src/pages/post-code-generation-ai.tsx | 83 ++++++++++++++ figma/src/pages/post-code-generation.tsx | 2 +- figma/src/ui.tsx | 80 +++++++++---- 19 files changed, 370 insertions(+), 266 deletions(-) create mode 100644 core/ee/ui/ai-application-registry.ts create mode 100644 core/src/analytic/amplitude.ts rename figma/src/{analytics => analytic}/amplitude.ts (87%) create mode 100644 figma/src/assets/light-bulb-dark.svg create mode 100644 figma/src/assets/light-bulb.svg create mode 100644 figma/src/pages/post-code-generation-ai.tsx diff --git a/core/ee/cv/component-recognition.ts b/core/ee/cv/component-recognition.ts index 1ca824d..732cc19 100644 --- a/core/ee/cv/component-recognition.ts +++ b/core/ee/cv/component-recognition.ts @@ -1,11 +1,10 @@ import { Node, NodeType } from "../../src/bricks/node"; import { ExportFormat } from "../../src/design/adapter/node"; import { traverseNodes } from "../../src/utils"; +import { AiApplication, aiApplicationRegistryGlobalInstance } from "../ui/ai-application-registry"; import { predictImage, predictText } from "../web/request"; -export const annotateNodeForHtmlTag = async (startingNode: Node): Promise => { - let isAiUsed: boolean = false; - +export const annotateNodeForHtmlTag = async (startingNode: Node) => { try { const idImageMap: Record = {}; const idTextMap: Record = {}; @@ -33,8 +32,8 @@ export const annotateNodeForHtmlTag = async (startingNode: Node): Promise { if (node.node) { @@ -67,7 +66,7 @@ export const annotateNodeForHtmlTag = async (startingNode: Node): Promise { } }; -export const detectWhetherSimilarNodesExist = (node: Node): boolean => { - if (optionRegistryGlobalInstance.getOption().uiFramework === UiFramework.react) { - annotateVectorGroupNodes(node); - return doSimilarNodesExist(node); - } - - return false; -}; - export const registerComponentFromNodes = (node: Node) => { if (node.getType() === NodeType.VECTOR_GROUP) { return; @@ -41,26 +32,6 @@ export const registerComponentFromNodes = (node: Node) => { } }; -export const doSimilarNodesExist = (node: Node): boolean => { - if (node.getType() === NodeType.VECTOR_GROUP) { - return false; - } - - const result: boolean = areChildrenSimilarNodes(node); - if (result) { - return result; - } - - const children: Node[] = node.getChildren(); - for (const child of children) { - if(doSimilarNodesExist(child)) { - return true; - }; - } - - return false; -}; - export const annotateVectorGroupNodes = (node: Node): boolean => { if (node.getType() === NodeType.TEXT || node.getType() === NodeType.IMAGE) { return false; @@ -167,59 +138,6 @@ export const registerComponentFromSimilarChildrenNodes = (node: Node) => { return; }; -export const areChildrenSimilarNodes = (node: Node): boolean => { - const children = node.getChildren(); - if (children.length === 0) { - return false; - } - - if (children.length === 1) { - return false; - } - - let modelNode: Node = children[0]; - const consecutiveNodeIds: Set = new Set(); - let consecutiveNodes: Node[] = []; - for (let i = 0; i < children.length; i++) { - const currentNode = children[i]; - if (currentNode.getId() === modelNode.getId()) { - continue; - } - - const [result, _]: [boolean, string] = areTwoNodesSimilar(currentNode, modelNode); - - if (!result) { - - modelNode = currentNode; - - if (consecutiveNodes.length > 2) { - if (registerComponentForConsecutiveNodes(consecutiveNodes)) { - return true; - } - } - - consecutiveNodes = []; - continue; - } - - if (!consecutiveNodeIds.has(modelNode.getId())) { - consecutiveNodeIds.add(modelNode.getId()); - consecutiveNodes.push(modelNode); - } - - consecutiveNodeIds.add(currentNode.getId()); - consecutiveNodes.push(currentNode); - } - - if (consecutiveNodes.length > 2) { - if (registerComponentForConsecutiveNodes(consecutiveNodes)) { - return true; - } - } - - return false; -}; - export const areAllNodesSimilar = (nodes: Node[]): boolean => { if (nodes.length < 2) { return false; diff --git a/core/ee/ui/ai-application-registry.ts b/core/ee/ui/ai-application-registry.ts new file mode 100644 index 0000000..7fea1ed --- /dev/null +++ b/core/ee/ui/ai-application-registry.ts @@ -0,0 +1,30 @@ +export let aiApplicationRegistryGlobalInstance: AiApplicationRegistry; +export const instantiateAiApplicationRegistryGlobalInstance = () => { + aiApplicationRegistryGlobalInstance = new AiApplicationRegistry(); +}; + +export enum AiApplication { + componentIdentification = "componentIdentification", + autoNaming = "autoNaming", +} + + +class AiApplicationRegistry { + applications: AiApplication[]; + + constructor() { + this.applications = []; + }; + + addApplication(application: AiApplication) { + if (this.applications.includes(application)) { + return; + } + + this.applications.push(application); + } + + getApplications(): AiApplication[] { + return this.applications; + } +} \ No newline at end of file diff --git a/core/ee/web/request.ts b/core/ee/web/request.ts index 8c7f8e2..cc09402 100644 --- a/core/ee/web/request.ts +++ b/core/ee/web/request.ts @@ -45,16 +45,19 @@ export const getNameMap = async (): Promise => { } try { - const response: any = await fetch(process.env.ML_BACKEND_API_ENDPOINT + "/generate/name", { - method: 'POST', - body: JSON.stringify({ - codeSamples: codeSampleRegistryGlobalInstance.getCodeSamples(), - uiFramework: codeSampleRegistryGlobalInstance.getUiFramework() as string, - cssFramework: codeSampleRegistryGlobalInstance.getCssFramework() as string, - userId: figma.currentUser.id, - username: figma.currentUser.name, - }), - }); + const response: any = await fetch( + process.env.ML_BACKEND_API_ENDPOINT + "/generate/name", + // "http://localhost:8080/generate/name", + { + method: 'POST', + body: JSON.stringify({ + codeSamples: codeSampleRegistryGlobalInstance.getCodeSamples(), + uiFramework: codeSampleRegistryGlobalInstance.getUiFramework() as string, + cssFramework: codeSampleRegistryGlobalInstance.getCssFramework() as string, + userId: figma.currentUser.id, + username: figma.currentUser.name, + }), + }); const text: string = await response.text(); const parsedArr: string[] = JSON.parse(text); diff --git a/core/src/analytic/amplitude.ts b/core/src/analytic/amplitude.ts new file mode 100644 index 0000000..68e1746 --- /dev/null +++ b/core/src/analytic/amplitude.ts @@ -0,0 +1,3 @@ +export const EVENT_AI_CODE_GEN_SUCCESS = "ai_code_gen_success"; +export const EVENT_AI_GET_NAME_SUCCESS = "ai_get_name_map_success"; +export const EVENT_AI_COMPONENT_IDENTIFICATION_SUCCESS = "ai_component_identification_success"; \ No newline at end of file diff --git a/core/src/index.ts b/core/src/index.ts index 6155977..33c4556 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1,21 +1,23 @@ import { convertFigmaNodesToBricksNodes } from "./design/adapter/figma/adapter"; import { generateCodingFiles } from "./code/generator/generator"; -import { Option, File, NameMap, UiFramework } from "./code/code"; +import { Option, File, NameMap } from "./code/code"; import { groupNodes } from "./bricks/grouping"; import { addAdditionalCssAttributesToNodes } from "./bricks/additional-css"; -import { registerRepeatedComponents, detectWhetherSimilarNodesExist } from "../ee/loop/loop"; +import { registerRepeatedComponents } from "../ee/loop/loop"; import { Node, GroupNode } from "./bricks/node"; import { getNameMap } from "../ee/web/request"; import { instantiateNameRegistryGlobalInstance } from "./code/name-registry/name-registry"; import { instantiateOptionRegistryGlobalInstance } from "./code/option-registry/option-registry"; import { instantiateFontsRegistryGlobalInstance } from "./code/generator/tailwindcss/fonts-registry"; import { removeCompletelyOverlappingNodes, removeNode } from "./bricks/remove-node"; -import { isEmpty, replaceVariableNameWithinFile } from "./utils"; +import { isEmpty, replaceVariableNameWithinFile, trackEvent } from "./utils"; import { instantiateCodeSampleRegistryGlobalInstance } from "../ee/loop/code-sample-registry"; import { instantiateDataArrRegistryGlobalInstance } from "../ee/loop/data-array-registry"; import { instantiatePropRegistryGlobalInstance } from "../ee/loop/prop-registry"; import { instantiateComponentRegistryGlobalInstance } from "../ee/loop/component-registry"; import { annotateNodeForHtmlTag } from "../ee/cv/component-recognition"; +import { instantiateAiApplicationRegistryGlobalInstance, AiApplication, aiApplicationRegistryGlobalInstance } from "../ee/ui/ai-application-registry"; +import { EVENT_AI_CODE_GEN_SUCCESS, EVENT_AI_COMPONENT_IDENTIFICATION_SUCCESS, EVENT_AI_GET_NAME_SUCCESS } from "./analytic/amplitude"; export const convertToCode = async ( figmaNodes: readonly SceneNode[], @@ -41,46 +43,15 @@ export const convertToCode = async ( return await generateCodingFiles(startingNode, option); }; -// ee feature -export const scanCodeForSimilarNodes = async ( - figmaNodes: readonly SceneNode[], - option: Option -): Promise => { - if (isEmpty(option.uiFramework) || option.uiFramework !== UiFramework.react) { - return false; - } - - const { nodes: converted } = convertFigmaNodesToBricksNodes(figmaNodes); - if (converted.length < 1) { - return false; - } - - - let startingNode: Node = - converted.length > 1 ? new GroupNode(converted) : converted[0]; - - groupNodes(startingNode); - - startingNode = removeNode(startingNode); - removeCompletelyOverlappingNodes(startingNode, null); - - addAdditionalCssAttributesToNodes(startingNode); - - instantiateRegistries(startingNode, option); - - let isAiUsed: boolean = await annotateNodeForHtmlTag(startingNode); - - return isAiUsed || detectWhetherSimilarNodesExist(startingNode); -}; - export const convertToCodeWithAi = async ( figmaNodes: readonly SceneNode[], option: Option -): Promise => { +): Promise<[File[], AiApplication[]]> => { + let start: number = Date.now(); const { nodes: converted } = convertFigmaNodesToBricksNodes(figmaNodes); if (converted.length < 1) { - return []; + return [[], []]; } let startingNode: Node = @@ -96,22 +67,43 @@ export const convertToCodeWithAi = async ( instantiateRegistries(startingNode, option); // ee features + let startAnnotateHtmlTag: number = Date.now(); await annotateNodeForHtmlTag(startingNode); registerRepeatedComponents(startingNode); + let endAnnotateHtmlTag: number = Date.now() - start; + trackEvent(EVENT_AI_COMPONENT_IDENTIFICATION_SUCCESS, { + duration: endAnnotateHtmlTag - startAnnotateHtmlTag, + }); const files: File[] = await generateCodingFiles(startingNode, option); // ee features + let startGetNameMap: number = Date.now(); const nameMap: NameMap = await getNameMap(); + let endGetNameMap: number = Date.now(); + trackEvent(EVENT_AI_GET_NAME_SUCCESS, { + duration: endGetNameMap - startGetNameMap, + }); + + if (!isEmpty(Object.values(nameMap))) { + aiApplicationRegistryGlobalInstance.addApplication(AiApplication.autoNaming); + } replaceVariableNameWithinFile(files, nameMap); - return files; + let end: number = Date.now() - start; + + trackEvent(EVENT_AI_CODE_GEN_SUCCESS, { + duration: end - start, + }); + + return [files, aiApplicationRegistryGlobalInstance.getApplications()]; }; const instantiateRegistries = (startingNode: Node, option: Option) => { instantiateOptionRegistryGlobalInstance(option); instantiateFontsRegistryGlobalInstance(startingNode); instantiateNameRegistryGlobalInstance(); + instantiateAiApplicationRegistryGlobalInstance(); instantiateCodeSampleRegistryGlobalInstance(); instantiateDataArrRegistryGlobalInstance(); diff --git a/core/src/utils.ts b/core/src/utils.ts index 00aacf5..b55422f 100644 --- a/core/src/utils.ts +++ b/core/src/utils.ts @@ -1,3 +1,4 @@ +import { Identify, identify, track } from "@amplitude/analytics-browser"; import { Node } from "./bricks/node"; import { NameMap, File } from "./code/code"; @@ -10,6 +11,14 @@ export const isEmpty = (value: any): boolean => { ); }; +export const trackEvent = (eventName: string, eventProperties: any) => { + const event = new Identify(); + event.setOnce("username", figma.currentUser.name); + identify(event); + track(eventName, isEmpty(eventProperties) ? {} : eventProperties); +}; + + export const traverseNodes = async ( node: Node, callback: (node: Node) => Promise diff --git a/figma/src/analytics/amplitude.ts b/figma/src/analytic/amplitude.ts similarity index 87% rename from figma/src/analytics/amplitude.ts rename to figma/src/analytic/amplitude.ts index f89542d..20189f8 100644 --- a/figma/src/analytics/amplitude.ts +++ b/figma/src/analytic/amplitude.ts @@ -2,6 +2,7 @@ export const EVENT_GENERATE_BUTTON_CLICK = "generate_button_click"; export const EVENT_GENERATE_WITH_AI_BUTTON_CLICK = "generate_with_ai_button_click"; export const EVENT_INSTALLATION_LINK_CLICK = "installation_link_click"; +export const EVENT_SAVE_SETTINGS = "save_settings"; export const EVENT_FAQ_LINK_CLICK = "faq_link_click"; export const EVENT_ERROR = "error"; export const EVENT_FEEDBACK = "feedback"; diff --git a/figma/src/assets/light-bulb-dark.svg b/figma/src/assets/light-bulb-dark.svg new file mode 100644 index 0000000..4130271 --- /dev/null +++ b/figma/src/assets/light-bulb-dark.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/figma/src/assets/light-bulb.svg b/figma/src/assets/light-bulb.svg new file mode 100644 index 0000000..ee1ea4f --- /dev/null +++ b/figma/src/assets/light-bulb.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/figma/src/code.ts b/figma/src/code.ts index 53d3c8c..b880664 100644 --- a/figma/src/code.ts +++ b/figma/src/code.ts @@ -1,11 +1,11 @@ import { convertToCode, convertToCodeWithAi, - scanCodeForSimilarNodes, } from "bricks-core/src"; import { isEmpty } from "bricks-core/src/utils"; import { init, Identify, identify, track } from "@amplitude/analytics-browser"; -import { EVENT_ERROR } from "./analytics/amplitude"; +import { EVENT_ERROR } from "./analytic/amplitude"; +import { GenerationMethod } from "./constants"; init(process.env.AMPLITUDE_API_KEY, figma.currentUser.id, { defaultTracking: { @@ -16,14 +16,14 @@ init(process.env.AMPLITUDE_API_KEY, figma.currentUser.id, { }, }); -function trackEvent(eventName: string, eventProperties: any) { +export const trackEvent = (eventName: string, eventProperties: any) => { const event = new Identify(); event.setOnce("username", figma.currentUser.name); identify(event); track(eventName, isEmpty(eventProperties) ? {} : eventProperties); -} +}; -figma.showUI(__html__, { height: 375, width: 350 }); +figma.showUI(__html__, { height: 300, width: 350 }); figma.ui.onmessage = async (msg) => { if (msg.type === "styled-bricks-nodes") { @@ -55,7 +55,7 @@ figma.ui.onmessage = async (msg) => { if (msg.type === "generate-code-with-ai") { try { - const files = await convertToCodeWithAi(figma.currentPage.selection, { + const [files, applications] = await convertToCodeWithAi(figma.currentPage.selection, { language: msg.options.language, cssFramework: msg.options.cssFramework, uiFramework: msg.options.uiFramework, @@ -64,6 +64,7 @@ figma.ui.onmessage = async (msg) => { figma.ui.postMessage({ type: "generated-files", files, + applications, }); } catch (e) { console.error("Error from Figma core:\n", e.stack); @@ -75,6 +76,7 @@ figma.ui.onmessage = async (msg) => { figma.ui.postMessage({ type: "generated-files", files: [], + applications: [], error: true, }); } @@ -133,50 +135,29 @@ figma.ui.onmessage = async (msg) => { } if (msg.type === "get-settings") { - const settings = await figma.clientStorage.getAsync("settings"); + let settings = await figma.clientStorage.getAsync("settings"); + + let generationMethod: GenerationMethod = GenerationMethod.withai; + if (settings.generationMethod) { + generationMethod = settings.generationMethod; + } figma.ui.postMessage({ type: "settings", userId: figma.currentUser.id, - settings, + settings: { + ...settings, + generationMethod, + }, }); } }; figma.on("selectionchange", async () => { - const limit: number = await figma.clientStorage.getAsync("limit"); - const connected: boolean = await figma.clientStorage.getAsync("connection-status"); - - if (limit > 0 && connected) { - figma.ui.postMessage({ - type: "scan-for-ai-start", - }); - } figma.ui.postMessage({ type: "selection-change", isComponentSelected: figma.currentPage.selection.length > 0, selectedComponents: figma.currentPage.selection.map((x) => x.name), }); - - if (limit > 0 && connected) { - const settings = await figma.clientStorage.getAsync("settings"); - const option = { - language: settings.language, - cssFramework: settings.cssFramework, - uiFramework: settings.uiFramework, - }; - - figma.ui.postMessage({ - type: "should-generate-with-ai", - shouldGenerateWithAi: await scanCodeForSimilarNodes( - figma.currentPage.selection, - option - ), - }); - - figma.ui.postMessage({ - type: "scan-for-ai-end", - }); - } }); diff --git a/figma/src/constants.ts b/figma/src/constants.ts index cbbb166..211ddf3 100644 --- a/figma/src/constants.ts +++ b/figma/src/constants.ts @@ -3,6 +3,7 @@ export interface Settings { language: Language; uiFramework: UiFramework; cssFramework: CssFramework; + generationMethod: GenerationMethod; } export enum Language { @@ -15,7 +16,19 @@ export enum UiFramework { html = "html", } + +export enum GenerationMethod { + withai = "withai", + withoutai = "without-ai", +} + export enum CssFramework { css = "css", tailwindcss = "tailwindcss", } + + +export enum AiApplication { + componentIdentification = "componentIdentification", + autoNaming = "autoNaming", +} \ No newline at end of file diff --git a/figma/src/context/page-context.tsx b/figma/src/context/page-context.tsx index 2aa85f6..db03fd2 100644 --- a/figma/src/context/page-context.tsx +++ b/figma/src/context/page-context.tsx @@ -5,6 +5,7 @@ export const PAGES = { SETTING: "SETTING", CODE_GENERATION: "CODE_GENERATION", POST_CODE_GENERATION: "POST_CODE_GENERATION", + POST_CODE_GENERATION_AI: "POST_CODE_GENERATION_AI", ERROR: "ERROR", }; diff --git a/figma/src/pages/code-generation-status.tsx b/figma/src/pages/code-generation-status.tsx index a6dfb38..dc07d09 100644 --- a/figma/src/pages/code-generation-status.tsx +++ b/figma/src/pages/code-generation-status.tsx @@ -1,19 +1,30 @@ import { useEffect, useContext, PropsWithChildren } from "react"; import PageContext, { PAGES } from "../context/page-context"; +import { GenerationMethod, UiFramework } from "../constants"; export interface Props { isGeneratingCode: boolean; isGeneratingCodeWithAi: boolean; + selectedUiFramework: UiFramework; + limit: number, + selectedGenerationMethod: GenerationMethod; } const CodeGenerationStatus = ({ isGeneratingCode, isGeneratingCodeWithAi, + selectedUiFramework, + limit, + selectedGenerationMethod, }: PropsWithChildren) => { const { setCurrentPage } = useContext(PageContext); useEffect(() => { if (!isGeneratingCode && !isGeneratingCodeWithAi) { + if (selectedGenerationMethod === GenerationMethod.withai && selectedUiFramework !== UiFramework.html && limit !== 0) { + setCurrentPage(PAGES.POST_CODE_GENERATION_AI); + return; + } setCurrentPage(PAGES.POST_CODE_GENERATION); } }, [isGeneratingCode, isGeneratingCodeWithAi]); diff --git a/figma/src/pages/code-output-setting.tsx b/figma/src/pages/code-output-setting.tsx index 2fde9cb..fb34c9f 100644 --- a/figma/src/pages/code-output-setting.tsx +++ b/figma/src/pages/code-output-setting.tsx @@ -1,15 +1,18 @@ import React, { useContext } from "react"; import { RadioGroup as BaseRadioGroup } from "@headlessui/react"; import PageContext, { PAGES } from "../context/page-context"; -import { CssFramework, Language, Settings, UiFramework } from "../constants"; +import { CssFramework, GenerationMethod, Language, Settings, UiFramework } from "../constants"; import * as logo from "../assets/arrow.png"; import htmlLogo from "../assets/html-logo.svg"; +import lightbulbLogo from "../assets/light-bulb.svg"; +import lightbulbDarkLogo from "../assets/light-bulb-dark.svg"; import reactLogo from "../assets/react-logo.svg"; import cssLogo from "../assets/css-logo.svg"; import tailwindcssLogo from "../assets/tailwindcss-logo.svg"; import javascriptLogo from "../assets/javascript-logo.svg"; import typescriptLogo from "../assets/typescript-logo.svg"; import Button from "../components/Button"; +import { EVENT_SAVE_SETTINGS } from "../analytic/amplitude"; type Option = { id: T; @@ -17,6 +20,11 @@ type Option = { logo: string; }; +const GenerationMethods: Option[] = [ + { id: GenerationMethod.withai, name: "With AI", logo: lightbulbLogo }, + { id: GenerationMethod.withoutai, name: "Without AI", logo: lightbulbDarkLogo }, +]; + const UiFrameworks: Option[] = [ { id: UiFramework.html, name: "HTML", logo: htmlLogo }, { id: UiFramework.react, name: "React", logo: reactLogo }, @@ -39,7 +47,8 @@ function classNames(...classes) { function updateSettings( uiFramework: string, cssFramework: string, - language: string + language: string, + generationMethod: string, ) { parent.postMessage( { @@ -49,6 +58,7 @@ function updateSettings( uiFramework, cssFramework, language, + generationMethod, } as Settings, }, }, @@ -88,7 +98,7 @@ const RadioGroup = ({ value: string; onChange: (value: string) => void; label: string; - options: Option[]; + options: Option[]; disabled?: boolean; }) => ( @@ -120,6 +130,9 @@ interface Props { setSelectedCssFramework: (value: CssFramework) => void; selectedLanguage: Language; setSelectedLanguage: (value: Language) => void; + selectedGenerationMethod: GenerationMethod; + limit: number; + setSelectedGenerationMethod: (value: GenerationMethod) => void; } const CodeOutputSetting: React.FC = ({ @@ -128,7 +141,10 @@ const CodeOutputSetting: React.FC = ({ selectedCssFramework, setSelectedCssFramework, selectedLanguage, + limit, setSelectedLanguage, + selectedGenerationMethod, + setSelectedGenerationMethod }) => { const { previousPage, setCurrentPage } = useContext(PageContext); @@ -137,8 +153,24 @@ const CodeOutputSetting: React.FC = ({ }; const handleSaveButtonClick = () => { - updateSettings(selectedUiFramework, selectedCssFramework, selectedLanguage); + updateSettings(selectedUiFramework, selectedCssFramework, selectedLanguage, selectedGenerationMethod); setCurrentPage(PAGES.HOME); + + parent.postMessage( + { + pluginMessage: { + type: "analytics", + eventName: EVENT_SAVE_SETTINGS, + eventProperties: { + uiFramework: selectedUiFramework, + cssFramework: selectedCssFramework, + language: selectedLanguage, + generationMethod: selectedGenerationMethod, + }, + }, + }, + "*" + ); }; return ( @@ -159,6 +191,13 @@ const CodeOutputSetting: React.FC = ({
+ void; setIsGeneratingCode: (value: boolean) => void; @@ -30,12 +28,11 @@ const Home = (props: PropsWithChildren) => { connectedToVSCode, isComponentSelected, setIsGeneratingCodeWithAi, - isScanningForAi, - canGenerateWithAi, limit, setIsGeneratingCode, selectedUiFramework, selectedCssFramework, + selectedGenerationMethod, selectedLanguage, } = props; const { setCurrentPage } = useContext(PageContext); @@ -147,55 +144,25 @@ const Home = (props: PropsWithChildren) => { }; const isGenerateCodeButtonEnabled = isComponentSelected && connectedToVSCode; - const isGenerateWithAiButtonEnabled = - isGenerateCodeButtonEnabled && - canGenerateWithAi && - selectedUiFramework === UiFramework.react && - !isScanningForAi && - limit > 0; - - - const getGenerateWithAiButton = () => { - if (!isGenerateWithAiButtonEnabled) { - let tooltipContent: string = `This beta feature only applies to potential buttons and repeated components that can render in a for loop. It has a daily limit of 6 times.`; - - if (limit === 0) { - tooltipContent = `This beta feature has a daily limits of 6 times. Reach out to - spike@bricks-tech.com if you want more.`; - } + const getGenerateCodeButton = () => { + if (selectedGenerationMethod === GenerationMethod.withai && limit > 0 && selectedUiFramework !== UiFramework.html) { return ( - - {tooltipContent} -

- } - trigger="hover" - arrow={false} + -
+ Generate Code With AI + ); } return ( ); }; @@ -248,30 +215,39 @@ const Home = (props: PropsWithChildren) => { ); }; - return ( -
-
-

Bricks

- -
+ const ranOutOfAiCredits = limit === 0 ? ( +
+ Ran out of daily AI credits?  + + spike@bricks-tech.com +

+ } + trigger="hover" + arrow={false} + > +
+ Contact us. +
-
{getCenterContent(connectedToVSCode)}
+
+
+ ) : null; -
-
- {getGenerateWithAiButton()} - + return ( +
+
{getCenterContent(connectedToVSCode)}
+
+
+ {getGenerateCodeButton()} {connectedToVSCode ? : null}
+ {ranOutOfAiCredits}
); }; diff --git a/figma/src/pages/post-code-generation-ai.tsx b/figma/src/pages/post-code-generation-ai.tsx new file mode 100644 index 0000000..d134596 --- /dev/null +++ b/figma/src/pages/post-code-generation-ai.tsx @@ -0,0 +1,83 @@ +import { useContext, PropsWithChildren, Component, ReactElement } from "react"; +import PageContext, { PAGES } from "../context/page-context"; +import Button from "../components/Button"; +import { AiApplication } from "../constants"; +import { isEmpty } from "bricks-core/src/utils"; + +export interface Props { + limit: number; + aiApplications: AiApplication[], +} + +const PostCodeGenerationAi = (props: PropsWithChildren) => { + const { + limit, + aiApplications, + } = props; + + const { setCurrentPage } = useContext(PageContext); + + const handleDismissButtonClick = () => { + setCurrentPage(PAGES.HOME); + }; + + const getAiTextContent = () => { + if (isEmpty(aiApplications)) { + return ( +

+ Ai is not applied in this code generation. No credits deducted. +

+ ); + } + + const applications: ReactElement[] = []; + + for (const aiApplication of aiApplications) { + if (aiApplication === AiApplication.componentIdentification) { + applications.push( +

+ * Auto identification of buttons and links. +

+ ); + } + + if (aiApplication === AiApplication.autoNaming) { + applications.push( +

+ * Auto naming of variables, data fields and props. +

+ ); + } + } + + return ( +
+

+ Here is how Ai is applied: +

+
+ {applications.map((application) => ( + application + ))} +
+
+ ); + }; + + return ( +
+
+ {getAiTextContent()} + +

+ Ai code gen daily credits left: {limit} +

+
+ +
+ ); +}; + +export default PostCodeGenerationAi; diff --git a/figma/src/pages/post-code-generation.tsx b/figma/src/pages/post-code-generation.tsx index 21002af..53ca7ef 100644 --- a/figma/src/pages/post-code-generation.tsx +++ b/figma/src/pages/post-code-generation.tsx @@ -3,7 +3,7 @@ import { useContext, useState } from "react"; import demo from "../assets/bricks-demo.gif"; import PageContext, { PAGES } from "../context/page-context"; import Button from "../components/Button"; -import { EVENT_FEEDBACK } from "../analytics/amplitude"; +import { EVENT_FEEDBACK } from "../analytic/amplitude"; const PostCodeGeneration = () => { const { setCurrentPage } = useContext(PageContext); diff --git a/figma/src/ui.tsx b/figma/src/ui.tsx index 16b20ec..ec7e2ef 100644 --- a/figma/src/ui.tsx +++ b/figma/src/ui.tsx @@ -2,15 +2,17 @@ import { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; import "./style.css"; import Home from "./pages/home"; +import PostCodeGenerationAi from "./pages/post-code-generation-ai"; import PostCodeGeneration from "./pages/post-code-generation"; import CodeGenerationStatus from "./pages/code-generation-status"; import CodeOutputSetting from "./pages/code-output-setting"; import Error from "./pages/error"; import PageContext, { PAGES } from "./context/page-context"; import { io } from "socket.io-client"; -import { CssFramework, Language, UiFramework } from "./constants"; +import { AiApplication, CssFramework, GenerationMethod, Language, UiFramework } from "./constants"; import { withTimeout } from "./utils"; -import { EVENT_ERROR } from "./analytics/amplitude"; +import { EVENT_ERROR } from "./analytic/amplitude"; +import { isEmpty } from "bricks-core/src/utils"; const socket = io("ws://localhost:32044"); @@ -21,8 +23,7 @@ const UI = () => { const [connectedToVSCode, setConnectedToVSCode] = useState(false); const [isGeneratingCode, setIsGeneratingCode] = useState(false); const [isGeneratingCodeWithAi, setIsGeneratingCodeWithAi] = useState(false); - const [canGenerateWithAi, setCanGenerateWithAi] = useState(false); - const [isScanningForAi, setisScanningForAi] = useState(false); + const [aiApplications, setAiApplications] = useState([AiApplication.componentIdentification, AiApplication.autoNaming]); const [limit, setLimit] = useState(0); // User settings @@ -30,12 +31,15 @@ const UI = () => { const [selectedUiFramework, setSelectedUiFramework] = useState( UiFramework.react ); + const [selectedGenerationMethod, setSelectedGenerationMethod] = useState( + GenerationMethod.withai + ); const [selectedCssFramework, setSelectedCssFramework] = useState( CssFramework.tailwindcss ); const setCurrentPageWithAdjustedScreenSize = (page: string) => { - if (page === PAGES.SETTING || page === PAGES.POST_CODE_GENERATION) { + if (page === PAGES.POST_CODE_GENERATION) { parent.postMessage( { pluginMessage: { @@ -46,6 +50,39 @@ const UI = () => { }, "*" ); + } else if (page === PAGES.POST_CODE_GENERATION_AI) { + parent.postMessage( + { + pluginMessage: { + type: "adjust-plugin-screen-size", + height: 300, + width: 350, + }, + }, + "*" + ); + } else if (page === PAGES.SETTING) { + parent.postMessage( + { + pluginMessage: { + type: "adjust-plugin-screen-size", + height: 680, + width: 350, + }, + }, + "*" + ); + } else if (page === PAGES.HOME) { + parent.postMessage( + { + pluginMessage: { + type: "adjust-plugin-screen-size", + height: 300, + width: 350, + }, + }, + "*" + ); } else { parent.postMessage( { @@ -141,20 +178,19 @@ const UI = () => { clearInterval(intervalId); }, 1000); + if (settings) { + + } + setSelectedLanguage(settings.language); setSelectedUiFramework(settings.uiFramework); setSelectedCssFramework(settings.cssFramework); - } - - if (pluginMessage.type === "scan-for-ai-start") { - setisScanningForAi(true); - } - - if (pluginMessage.type === "scan-for-ai-end") { - setisScanningForAi(false); + setSelectedGenerationMethod(settings.generationMethod); } if (pluginMessage.type === "get-limit") { + // resetLimit(); + if (Number.isInteger(pluginMessage.limit) && pluginMessage.limit >= 0) { setLimit(pluginMessage.limit); } else { @@ -178,10 +214,6 @@ const UI = () => { } } - if (pluginMessage.type === "should-generate-with-ai") { - setCanGenerateWithAi(pluginMessage.shouldGenerateWithAi); - } - if (pluginMessage.type === "selection-change") { setIsComponentSelected(pluginMessage.isComponentSelected); setPreviousPage(currentPage); @@ -193,6 +225,10 @@ const UI = () => { if (pluginMessage.type === "generated-files") { if (isGeneratingCodeWithAi) { + setAiApplications(pluginMessage.applications); + } + + if (!isEmpty(pluginMessage.applications)) { parent.postMessage( { pluginMessage: { @@ -280,12 +316,11 @@ const UI = () => { @@ -297,16 +332,23 @@ const UI = () => { selectedCssFramework={selectedCssFramework} setSelectedCssFramework={setSelectedCssFramework} selectedLanguage={selectedLanguage} + limit={limit} setSelectedLanguage={setSelectedLanguage} + selectedGenerationMethod={selectedGenerationMethod} + setSelectedGenerationMethod={setSelectedGenerationMethod} /> )} {currentPage === PAGES.CODE_GENERATION && ( )} {currentPage === PAGES.POST_CODE_GENERATION && } + {currentPage === PAGES.POST_CODE_GENERATION_AI && } {currentPage === PAGES.ERROR && }
From 88e35d046005815d1cae5e17817a5a4cf6eb7b8f Mon Sep 17 00:00:00 2001 From: Donovan So Date: Fri, 5 May 2023 16:27:53 -0400 Subject: [PATCH 03/21] Support mixed font sizes, families, and weights (#70) --- core/src/bricks/node.ts | 11 +- core/src/code/generator/css/generator.ts | 38 ++++--- core/src/code/generator/font.ts | 59 +++++----- core/src/code/generator/html/generator.ts | 104 +++++++++++++----- .../generator/tailwindcss/css-to-twcss.ts | 24 ++-- .../generator/tailwindcss/fonts-registry.ts | 6 +- .../code/generator/tailwindcss/generator.ts | 51 +++++---- core/src/design/adapter/figma/adapter.ts | 44 +++++++- core/src/design/adapter/figma/util.ts | 44 ++++++++ core/src/design/adapter/node.ts | 13 +++ core/src/google/google-fonts.ts | 56 +++++----- core/tsconfig.json | 1 + 12 files changed, 304 insertions(+), 147 deletions(-) diff --git a/core/src/bricks/node.ts b/core/src/bricks/node.ts index bb66e3b..bf65d4e 100644 --- a/core/src/bricks/node.ts +++ b/core/src/bricks/node.ts @@ -470,7 +470,6 @@ export class VisibleNode extends BaseNode { } export class TextNode extends VisibleNode { - fontSource: string; node: AdaptedTextNode; constructor(node: AdaptedTextNode) { super(node); @@ -489,14 +488,6 @@ export class TextNode extends VisibleNode { return this.node.getAbsoluteBoundingBoxCoordinates(); } - getFontSource() { - return this.fontSource; - } - - setFontSource(source: string) { - this.fontSource = source; - } - getText(): string { return this.node.getText(); } @@ -552,4 +543,4 @@ export class ImageNode extends VisibleNode { async export(exportFormat: ExportFormat): Promise { return await this.imageNode.export(exportFormat); } -} \ No newline at end of file +} diff --git a/core/src/code/generator/css/generator.ts b/core/src/code/generator/css/generator.ts index a8aa41c..ce124df 100644 --- a/core/src/code/generator/css/generator.ts +++ b/core/src/code/generator/css/generator.ts @@ -25,7 +25,10 @@ export class Generator { reactGenerator: ReactGenerator; constructor() { - this.htmlGenerator = new HtmlGenerator(getProps); + this.htmlGenerator = new HtmlGenerator( + getPropsFromNode, + convertCssClassesToInlineStyle + ); this.reactGenerator = new ReactGenerator(); } @@ -35,15 +38,17 @@ export class Generator { mainComponentName: string, isCssFileNeeded: boolean ): Promise<[string, ImportedComponentMeta[]]> { - const mainFileContent = - await this.htmlGenerator.generateHtml(node, option); + const mainFileContent = await this.htmlGenerator.generateHtml(node, option); - const [inFileComponents, inFileData]: [InFileComponentMeta[], InFileDataMeta[]] = this.htmlGenerator.getExtraComponentsMetaData(); + const [inFileComponents, inFileData]: [ + InFileComponentMeta[], + InFileDataMeta[] + ] = this.htmlGenerator.getExtraComponentsMetaData(); - const importComponents = extraFileRegistryGlobalInstance.getImportComponentMeta(); + const importComponents = + extraFileRegistryGlobalInstance.getImportComponentMeta(); if (option.uiFramework === UiFramework.react) { - return [ this.reactGenerator.generateReactFileContent( mainFileContent, @@ -51,7 +56,7 @@ export class Generator { isCssFileNeeded, [], inFileData, - inFileComponents, + inFileComponents ), importComponents, ]; @@ -114,7 +119,7 @@ export const buildCssFileContent = (fontUrl: string) => { }; // getProps retrieves formated css classes such as style="justify-content: center;" from a single node -const getProps = (node: Node, option: Option): string => { +const getPropsFromNode = (node: Node, option: Option): string => { switch (node.getType()) { case NodeType.TEXT: return convertCssClassesToInlineStyle( @@ -125,7 +130,7 @@ const getProps = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); case NodeType.GROUP: return convertCssClassesToInlineStyle( @@ -134,7 +139,7 @@ const getProps = (node: Node, option: Option): string => { ...node.getCssAttributes(), }, option, - node.getId(), + node.getId() ); case NodeType.VISIBLE: return convertCssClassesToInlineStyle( @@ -143,7 +148,7 @@ const getProps = (node: Node, option: Option): string => { ...node.getPositionalCssAttributes(), }, option, - node.getId(), + node.getId() ); case NodeType.IMAGE: return convertCssClassesToInlineStyle( @@ -156,7 +161,7 @@ const getProps = (node: Node, option: Option): string => { } ), option, - node.getId(), + node.getId() ); case NodeType.VECTOR: return convertCssClassesToInlineStyle( @@ -164,7 +169,7 @@ const getProps = (node: Node, option: Option): string => { absolutePositioningOnly: true, }), option, - node.getId(), + node.getId() ); case NodeType.VECTOR_GROUP: return convertCssClassesToInlineStyle( @@ -172,7 +177,7 @@ const getProps = (node: Node, option: Option): string => { absolutePositioningOnly: true, }), option, - node.getId(), + node.getId() ); } }; @@ -181,11 +186,12 @@ const getProps = (node: Node, option: Option): string => { const convertCssClassesToInlineStyle = ( attributes: Attributes, option: Option, - id: string, + id?: string ) => { let inlineStyle: string = ""; if (option.uiFramework === UiFramework.react) { - let [variableProps, cssKeyConnectedToProps]: [string, Set] = getVariablePropForCss(id); + let [variableProps, cssKeyConnectedToProps]: [string, Set] = + getVariablePropForCss(id); const lines: string[] = []; Object.entries(attributes).forEach(([key, value]) => { if (cssKeyConnectedToProps.has(key)) { diff --git a/core/src/code/generator/font.ts b/core/src/code/generator/font.ts index 641fc5b..d741bd8 100644 --- a/core/src/code/generator/font.ts +++ b/core/src/code/generator/font.ts @@ -8,10 +8,10 @@ export const getFontsMetadata = (node: Node): FontMetadataMap => { }; type Font = { - sizes: string[]; + sizes: number[]; isItalic: boolean; familyCss: string; - weights: string[]; + weights: number[]; }; export type FontMetadataMap = { @@ -22,33 +22,36 @@ export type FontMetadataMap = { const findAllFonts = (node: Node, fonts: FontMetadataMap) => { if (node.getType() === NodeType.TEXT) { const textNode = node as TextNode; - const attributes = textNode.getCssAttributes(); - const fontFamily = textNode.getFamilyName(); - const fontFamilyCss = attributes["font-family"]; - const fontSize = attributes["font-size"]; - const fontWeight = attributes["font-weight"]; - - if (fontFamily && fontSize) { - const font = fonts[fontFamily]; - if (!font) { - fonts[fontFamily] = { - isItalic: textNode.isItalic(), - familyCss: fontFamilyCss, - sizes: [fontSize], - weights: [fontWeight], - }; - - return; - } - - if (!font.weights.includes(fontWeight)) { - font.weights.push(fontWeight); - } - if (fontSize) { - fonts[fontFamily].sizes.push(fontSize); + const styledTextSegments = textNode.node.getStyledTextSegments(); + + styledTextSegments.forEach((styledTextSegment) => { + const { fontName, fontSize, fontWeight } = styledTextSegment; + const { family, style } = fontName; + const isItalic = style.toLowerCase().includes("italic"); + + if (family && fontSize) { + const font = fonts[family]; + if (!font) { + fonts[family] = { + isItalic, + familyCss: family, + sizes: [fontSize], + weights: [fontWeight], + }; + + return; + } + + if (!font.weights.includes(fontWeight)) { + font.weights.push(fontWeight); + } + + if (fontSize) { + fonts[family].sizes.push(fontSize); + } } - } + }); return; } @@ -57,4 +60,4 @@ const findAllFonts = (node: Node, fonts: FontMetadataMap) => { for (const child of children) { findAllFonts(child, fonts); } -}; \ No newline at end of file +}; diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 12f1d74..2789c74 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -20,7 +20,11 @@ import { import { componentRegistryGlobalInstance } from "../../../../ee/loop/component-registry"; import { codeSampleRegistryGlobalInstance } from "../../../../ee/loop/code-sample-registry"; -export type GetProps = (node: Node, option: Option) => string; +type GetPropsFromNode = (node: Node, option: Option) => string; +type GetPropsFromAttributes = ( + attributes: Attributes, + option: Option +) => string; export type ImportedComponentMeta = { node: VectorGroupNode | VectorNode | ImageNode; @@ -37,27 +41,32 @@ export type InFileDataMeta = { }; export class Generator { - getProps: GetProps; + getPropsFromNode: GetPropsFromNode; + getPropsFromAttributes: GetPropsFromAttributes; inFileComponents: InFileComponentMeta[]; inFileData: InFileDataMeta[]; - constructor(getProps: GetProps) { - this.getProps = getProps; + constructor( + getPropsFromNode: GetPropsFromNode, + getPropsFromAttributes: GetPropsFromAttributes + ) { + this.getPropsFromNode = getPropsFromNode; + this.getPropsFromAttributes = getPropsFromAttributes; this.inFileComponents = []; this.inFileData = []; } async generateHtml(node: Node, option: Option): Promise { - const importComponents: ImportedComponentMeta[] = []; const htmlTag = node.getAnnotation("htmlTag") || "div"; switch (node.getType()) { - case NodeType.TEXT: - const textNodeClassProps = this.getProps(node, option); + case NodeType.TEXT: { + const textNodeClassProps = this.getPropsFromNode(node, option); const attributes = htmlTag === "a" ? 'href="#" ' : ""; - const textProp = getTextProp(node); + const textProp = this.getText(node, option); return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}`; + } case NodeType.GROUP: // this edge case should never happen @@ -65,7 +74,7 @@ export class Generator { return `<${htmlTag}>`; } - const groupNodeClassProps = this.getProps(node, option); + const groupNodeClassProps = this.getPropsFromNode(node, option); return await this.generateHtmlFromNodes( node.getChildren(), [`<${htmlTag} ${groupNodeClassProps}>`, ``], @@ -73,7 +82,7 @@ export class Generator { ); case NodeType.VISIBLE: - const visibleNodeClassProps = this.getProps(node, option); + const visibleNodeClassProps = this.getPropsFromNode(node, option); if (isEmpty(node.getChildren())) { return `<${htmlTag} ${visibleNodeClassProps}> `; } @@ -241,7 +250,7 @@ export class Generator { "background-image": `url('./assets/${imageComponentName}.png')`, }); - return [`
`, `
`]; + return [`
`, `
`]; } renderNodeWithAbsolutePosition( @@ -252,7 +261,7 @@ export class Generator { const positionalCssAttribtues: Attributes = node.getPositionalCssAttributes(); if (positionalCssAttribtues["position"] === "absolute") { - return `
` + inner + `
`; + return `
` + inner + `
`; } return inner; } @@ -316,6 +325,56 @@ export class Generator { return codeStr; } + + getText(node: Node, option: Option): string { + const textNode: TextNode = node as TextNode; + + const prop: string = getTextVariableProp(node.getId()); + if (!isEmpty(prop)) { + return prop; + } + + const styledTextSegments = textNode.node.getStyledTextSegments(); + + if (styledTextSegments.length > 0) { + const defaultFontSize = textNode.getACssAttribute("font-size"); + const defaultFontFamily = textNode.getACssAttribute("font-family"); + const defaultFontWeight = textNode.getACssAttribute("font-weight"); + + return styledTextSegments + .map((styledTextSegment) => { + const overridingAttributes: Attributes = {}; + + const fontSize = `${styledTextSegment.fontSize}px`; + if (fontSize !== defaultFontSize) { + overridingAttributes["font-size"] = fontSize; + } + + const fontFamily = styledTextSegment.fontName.family; + if (fontFamily !== defaultFontFamily) { + overridingAttributes["font-family"] = fontFamily; + } + + const fontWeight = styledTextSegment.fontWeight.toString(); + if (fontWeight !== defaultFontWeight) { + overridingAttributes["font-weight"] = fontWeight; + } + + const text = escapeHtml(styledTextSegment.characters); + if (Object.keys(overridingAttributes).length === 0) { + return text; + } + const textNodeClassProps = this.getPropsFromAttributes( + overridingAttributes, + option + ); + return `${text}`; + }) + .join(""); + } else { + return escapeHtml(textNode.getText()); + } + } } const getWidthAndHeightProp = (node: Node): string => { @@ -333,7 +392,7 @@ const getWidthAndHeightProp = (node: Node): string => { return widthAndHeight; }; -export const getSrcProp = (node: Node): string => { +const getSrcProp = (node: Node): string => { const id: string = node.getId(); let fileExtension: string = "svg"; @@ -352,7 +411,7 @@ export const getSrcProp = (node: Node): string => { return `"./assets/${componentName}.${fileExtension}"`; }; -export const getAltProp = (node: Node): string => { +const getAltProp = (node: Node): string => { const id: string = node.getId(); const componentName: string = nameRegistryGlobalInstance.getAltName(id); @@ -364,18 +423,7 @@ export const getAltProp = (node: Node): string => { return `"${componentName}"`; }; -export const getTextProp = (node: Node): string => { - const textNode: TextNode = node as TextNode; - - const prop: string = getTextVariableProp(node.getId()); - if (!isEmpty(prop)) { - return prop; - } - - return escapeHtml(textNode.getText()); -}; - -function escapeHtml(str: string) { +const escapeHtml = (str: string) => { return str.replace(/[&<>"'{}]/g, function (match) { switch (match) { case "&": @@ -394,9 +442,9 @@ function escapeHtml(str: string) { return "}"; } }); -} +}; -export const createMiniReactFile = ( +const createMiniReactFile = ( componentCode: string, dataCode: string, arrCode: string diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index 11ef9e9..f69d42c 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -21,8 +21,8 @@ import { Option, UiFramework } from "../../code"; import { getVariablePropForTwcss } from "../../../../ee/code/prop"; export type TwcssPropRenderingMeta = { - numberOfTwcssClasses: number, - filledClassIndexes: Set, + numberOfTwcssClasses: number; + filledClassIndexes: Set; }; export type TwcssPropRenderingMap = { @@ -32,15 +32,19 @@ export type TwcssPropRenderingMap = { // convertCssClassesToTwcssClasses converts css classes to tailwindcss classes export const convertCssClassesToTwcssClasses = ( attributes: Attributes, - id: string, option: Option, + id?: string ): string => { let classPropName: string = "class"; let variableProps: string = ""; const twcssPropRenderingMap: TwcssPropRenderingMap = {}; Object.entries(attributes).forEach(([property, value]) => { - const twcssClasses: string[] = getTwcssClass(property, value, attributes).split(" "); + const twcssClasses: string[] = getTwcssClass( + property, + value, + attributes + ).split(" "); twcssPropRenderingMap[property] = { numberOfTwcssClasses: twcssClasses.length, filledClassIndexes: new Set(), @@ -54,13 +58,19 @@ export const convertCssClassesToTwcssClasses = ( let content: string = ""; Object.entries(attributes).forEach(([property, value]) => { - const twcssPropRenderingMeta: TwcssPropRenderingMeta = twcssPropRenderingMap[property]; - if (twcssPropRenderingMeta.numberOfTwcssClasses === twcssPropRenderingMeta.filledClassIndexes.size) { + const twcssPropRenderingMeta: TwcssPropRenderingMeta = + twcssPropRenderingMap[property]; + if ( + twcssPropRenderingMeta.numberOfTwcssClasses === + twcssPropRenderingMeta.filledClassIndexes.size + ) { return; } for (let i = 0; i < twcssPropRenderingMeta.numberOfTwcssClasses; i++) { - const parts: string[] = getTwcssClass(property, value, attributes).split(" "); + const parts: string[] = getTwcssClass(property, value, attributes).split( + " " + ); if (twcssPropRenderingMeta.filledClassIndexes.has(i)) { continue; } diff --git a/core/src/code/generator/tailwindcss/fonts-registry.ts b/core/src/code/generator/tailwindcss/fonts-registry.ts index 920447d..7269f1f 100644 --- a/core/src/code/generator/tailwindcss/fonts-registry.ts +++ b/core/src/code/generator/tailwindcss/fonts-registry.ts @@ -13,11 +13,10 @@ export const buildFontMetadataMapWithTwcssAliases = ( const alias = family.replaceAll(" ", "-"); twcssMetadataMap[family] = { familyCss, - weights, + weights: weights.map((weight) => weight.toString()), isItalic, alias: alias.toLowerCase(), }; - }); return twcssMetadataMap; @@ -42,7 +41,8 @@ export class FontsRegistry { constructor(node: Node) { const fontMetadataMap = getFontsMetadata(node); this.googleFontUrl = computeGoogleFontURL(fontMetadataMap); - this.fontMetadataMap = buildFontMetadataMapWithTwcssAliases(fontMetadataMap); + this.fontMetadataMap = + buildFontMetadataMapWithTwcssAliases(fontMetadataMap); } getFontMetadataInArray(): TwcssFontMetadataWithAlias[] { diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index 7b4a705..cb942c9 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -10,9 +10,7 @@ import { convertCssClassesToTwcssClasses, getImageFileNameFromUrl, } from "./css-to-twcss"; -import { - FontsRegistryGlobalInstance, -} from "./fonts-registry"; +import { FontsRegistryGlobalInstance } from "./fonts-registry"; import { Generator as HtmlGenerator, ImportedComponentMeta, @@ -28,7 +26,10 @@ export class Generator { reactGenerator: ReactGenerator; constructor() { - this.htmlGenerator = new HtmlGenerator(getProps); + this.htmlGenerator = new HtmlGenerator( + getPropsFromNode, + convertCssClassesToTwcssClasses + ); this.reactGenerator = new ReactGenerator(); } @@ -37,11 +38,14 @@ export class Generator { option: Option, mainComponentName: string ): Promise<[string, ImportedComponentMeta[]]> { - const mainFileContent = - await this.htmlGenerator.generateHtml(node, option); + const mainFileContent = await this.htmlGenerator.generateHtml(node, option); - const importComponents: ImportedComponentMeta[] = extraFileRegistryGlobalInstance.getImportComponentMeta(); - const [inFileComponents, inFileData]: [InFileComponentMeta[], InFileDataMeta[]] = this.htmlGenerator.getExtraComponentsMetaData(); + const importComponents: ImportedComponentMeta[] = + extraFileRegistryGlobalInstance.getImportComponentMeta(); + const [inFileComponents, inFileData]: [ + InFileComponentMeta[], + InFileDataMeta[] + ] = this.htmlGenerator.getExtraComponentsMetaData(); if (option.uiFramework === UiFramework.react) { return [ @@ -92,8 +96,7 @@ export class Generator { } // getProps converts a single node to formated tailwindcss classes -const getProps = (node: Node, option: Option): string => { - +const getPropsFromNode = (node: Node, option: Option): string => { switch (node.getType()) { case NodeType.TEXT: return convertCssClassesToTwcssClasses( @@ -101,10 +104,10 @@ const getProps = (node: Node, option: Option): string => { ...node.getCssAttributes(), ...filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningOnly: true, - }) + }), }, - node.getId(), option, + node.getId() ); case NodeType.GROUP: return convertCssClassesToTwcssClasses( @@ -112,8 +115,8 @@ const getProps = (node: Node, option: Option): string => { ...node.getPositionalCssAttributes(), ...node.getCssAttributes(), }, - node.getId(), option, + node.getId() ); case NodeType.VISIBLE: return convertCssClassesToTwcssClasses( @@ -121,8 +124,8 @@ const getProps = (node: Node, option: Option): string => { ...node.getPositionalCssAttributes(), ...node.getCssAttributes(), }, - node.getId(), - option + option, + node.getId() ); case NodeType.IMAGE: @@ -130,8 +133,8 @@ const getProps = (node: Node, option: Option): string => { filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningOnly: true, }), - node.getId(), - option + option, + node.getId() ); case NodeType.VECTOR: @@ -139,16 +142,16 @@ const getProps = (node: Node, option: Option): string => { filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningOnly: true, }), - node.getId(), - option + option, + node.getId() ); case NodeType.VECTOR_GROUP: return convertCssClassesToTwcssClasses( filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningOnly: true, }), - node.getId(), - option + option, + node.getId() ); default: @@ -216,11 +219,11 @@ export const buildTwcssCssFileContent = () => { fontImportStatements = `@import url("${googleFontUrl}");`; } - const file = `@tailwind base; + const file = `${fontImportStatements} + @tailwind base; @tailwind components; @tailwind utilities; - ${fontImportStatements} - `; +`; return file; }; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 658db96..9a451a2 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -16,8 +16,10 @@ import { rgbaToString, isFrameNodeTransparent, doesNodeContainsAnImage, + getMostCommonFieldInString, } from "./util"; import { GoogleFontsInstance } from "../../../google/google-fonts"; +import { StyledTextSegment } from "../node"; enum NodeType { GROUP = "GROUP", @@ -48,8 +50,9 @@ const addDropShadowCssProperty = ( .map((effect: DropShadowEffect | InnerShadowEffect) => { const { offset, radius, spread, color } = effect; - const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0 - }px ${rgbaToString(color)}`; + const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${ + spread ?? 0 + }px ${rgbaToString(color)}`; if (effect.type === "INNER_SHADOW") { return "inset " + dropShadowString; @@ -283,11 +286,29 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { ] = `'${fontFamily}', ${GoogleFontsInstance.getGenericFontFamily( fontFamily )}`; + } else { + const mostCommonFontName = getMostCommonFieldInString( + figmaNode, + "fontName" + ); + + if (mostCommonFontName) { + attributes["font-family"] = mostCommonFontName.family; + } } // font size if (figmaNode.fontSize !== figma.mixed) { attributes["font-size"] = `${figmaNode.fontSize}px`; + } else { + const fontSizeWithLongestLength = getMostCommonFieldInString( + figmaNode, + "fontSize" + ); + + if (fontSizeWithLongestLength) { + attributes["font-size"] = `${fontSizeWithLongestLength}px`; + } } // width and height @@ -312,7 +333,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { Math.abs( figmaNode.absoluteBoundingBox.width - absoluteRenderBounds.width ) / - figmaNode.absoluteBoundingBox.width > + figmaNode.absoluteBoundingBox.width > 0.2 ) { width = absoluteRenderBounds.width + 4; @@ -455,6 +476,15 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // font weight if (figmaNode.fontWeight !== figma.mixed) { attributes["font-weight"] = figmaNode.fontWeight.toString(); + } else { + const mostCommonFontWeight = getMostCommonFieldInString( + figmaNode, + "fontWeight" + ); + + if (mostCommonFontWeight) { + attributes["font-weight"] = mostCommonFontWeight.toString(); + } } // font style @@ -623,6 +653,14 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { // @ts-ignore return this.node.characters; } + + getStyledTextSegments(): StyledTextSegment[] { + return this.node.getStyledTextSegments([ + "fontSize", + "fontName", + "fontWeight", + ]); + } } const EXPORTABLE_NODE_TYPES: string[] = [ diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 0abeb39..71d25a1 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -59,3 +59,47 @@ export const doesNodeContainsAnImage = ( return false; }; + +export function getMostCommonFieldInString< + T extends keyof Omit +>(figmaTextNode: TextNode, field: T) { + const styledTextSegments = figmaTextNode.getStyledTextSegments([field]); + + type Variation = Pick< + StyledTextSegment, + T | "characters" | "start" | "end" + >[T]; + + // Count the number of characters that has each variation of "field". + // For example, if field is "fontSize", variations are the different font sizes (12, 14, etc.) + // Pick[T] + const fieldNumOfChars = new Map(); + styledTextSegments.forEach((segment) => { + const variation = segment[field]; + if (!fieldNumOfChars.has(variation)) { + fieldNumOfChars.set(variation, 0); + } + + fieldNumOfChars.set( + variation, + fieldNumOfChars.get(variation) + segment.characters.length + ); + }); + + let variationWithLongestLength: Variation; + let currentLongestLength = -Infinity; + for (const [key, value] of fieldNumOfChars) { + if (value > currentLongestLength) { + currentLongestLength = value; + variationWithLongestLength = key; + } + } + + console.log( + "variation with longest length for field:", + field, + " = ", + variationWithLongestLength + ); + return variationWithLongestLength; +} diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index b64c10e..2795940 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -29,10 +29,23 @@ export interface Node { export(exportFormat: ExportFormat): Promise; } +export interface StyledTextSegment { + characters: string; + start: number; + end: number; + fontSize: number; + fontName: { + family: string; + style: string; + }; + fontWeight: number; +} + export interface TextNode extends Node { getText(): string; isItalic(): boolean; getFamilyName(): string; + getStyledTextSegments(): StyledTextSegment[]; } export interface VectorNode extends Node {} diff --git a/core/src/google/google-fonts.ts b/core/src/google/google-fonts.ts index 3af8bbc..f2d9f9e 100644 --- a/core/src/google/google-fonts.ts +++ b/core/src/google/google-fonts.ts @@ -2,8 +2,8 @@ import { isEmpty } from "../utils"; import * as rawData from "./google-fonts-metadata.json"; import { FontMetadataMap } from "../code/generator/font"; -const baseURL = "https://fonts.googleapis.com/css?family="; -const regularFontSize = "400"; +const BASE_URL = "https://fonts.googleapis.com/css?family="; +const REGULAR_FONT_SIZE = 400; interface GoogleFontMetadata { family: string; @@ -56,7 +56,9 @@ export class GoogleFonts { export const GoogleFontsInstance = new GoogleFonts(); -export const computeGoogleFontURL = (fontsMetadata: FontMetadataMap): string => { +export const computeGoogleFontURL = ( + fontsMetadata: FontMetadataMap +): string => { if (isEmpty(fontsMetadata)) { return ""; } @@ -64,39 +66,37 @@ export const computeGoogleFontURL = (fontsMetadata: FontMetadataMap): string => let googleFontFamily = ""; const fontMetadataEntries = Object.entries(fontsMetadata); - fontMetadataEntries.forEach(([family, { isItalic, weights }], index: number) => { - let fontDetails = ""; + fontMetadataEntries.forEach( + ([family, { isItalic, weights }], index: number) => { + let fontDetails = ""; - const familyName = family.replaceAll(" ", "+"); + const familyName = family.replaceAll(" ", "+"); - const fontVariants: string[] = []; - for (const fontWeight of weights) { - const isRegular = fontWeight === regularFontSize; - if (isRegular) { - fontVariants.push("regular"); - continue; + const fontVariants: string[] = []; + for (const fontWeight of weights) { + fontVariants.push( + fontWeight === REGULAR_FONT_SIZE ? "regular" : fontWeight.toString() + ); } - fontVariants.push(fontWeight); - } + if (isItalic) { + fontVariants.push("italic"); + } - if (isItalic) { - fontVariants.push("italic"); - } + const googleFontsVariants = GoogleFontsInstance.getAvailableVariants( + fontVariants, + family + ); - const googleFontsVariants = GoogleFontsInstance.getAvailableVariants( - fontVariants, - family - ); + fontDetails += familyName + ":" + googleFontsVariants.join(","); - fontDetails += familyName + ":" + googleFontsVariants.join(","); + if (index !== fontMetadataEntries.length - 1) { + fontDetails += "|"; + } - if (index !== fontMetadataEntries.length - 1) { - fontDetails += "|"; + googleFontFamily += fontDetails; } + ); - googleFontFamily += fontDetails; - }); - - return baseURL + googleFontFamily; + return BASE_URL + googleFontFamily; }; diff --git a/core/tsconfig.json b/core/tsconfig.json index f77643c..6f8241c 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "es2015", "outDir": "dist", "allowSyntheticDefaultImports": true, "isolatedModules": true, From 084244515d1d5349256969fa1ea95395482f2154 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Fri, 5 May 2023 16:47:09 -0400 Subject: [PATCH 04/21] add support for mixed text decoration --- core/src/code/generator/html/generator.ts | 5 +++++ core/src/design/adapter/figma/adapter.ts | 15 ++++++++++++++- core/src/design/adapter/node.ts | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 2789c74..6c4aa1a 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -360,6 +360,11 @@ export class Generator { overridingAttributes["font-weight"] = fontWeight; } + const textDecoration = styledTextSegment.textDecoration; + if (textDecoration !== "normal") { + overridingAttributes["text-decoration"] = textDecoration; + } + const text = escapeHtml(styledTextSegment.characters); if (Object.keys(overridingAttributes).length === 0) { return text; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 9a451a2..49a707e 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -655,11 +655,24 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { } getStyledTextSegments(): StyledTextSegment[] { - return this.node.getStyledTextSegments([ + const styledTextSegments = this.node.getStyledTextSegments([ "fontSize", "fontName", "fontWeight", + "textDecoration", ]); + + // for converting figma textDecoration to css textDecoration + const figmaToCssTextDecorationMap = { + STRIKETHROUGH: "line-through", + UNDERLINE: "underline", + NONE: "normal", + } as const; + + return styledTextSegments.map((segment) => ({ + ...segment, + textDecoration: figmaToCssTextDecorationMap[segment.textDecoration], + })); } } diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index 2795940..d059570 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -39,6 +39,7 @@ export interface StyledTextSegment { style: string; }; fontWeight: number; + textDecoration: "normal" | "line-through" | "underline"; } export interface TextNode extends Node { From 1a81eb63cd85b69ffa0895f2c91c662474613157 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Fri, 5 May 2023 17:04:06 -0400 Subject: [PATCH 05/21] support mixed text transform --- core/src/code/generator/html/generator.ts | 5 +++++ core/src/design/adapter/figma/adapter.ts | 15 +++++++++++++-- core/src/design/adapter/node.ts | 1 + 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 6c4aa1a..30b4022 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -365,6 +365,11 @@ export class Generator { overridingAttributes["text-decoration"] = textDecoration; } + const textTransform = styledTextSegment.textTransform; + if (textTransform !== "none") { + overridingAttributes["text-transform"] = textTransform; + } + const text = escapeHtml(styledTextSegment.characters); if (Object.keys(overridingAttributes).length === 0) { return text; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 49a707e..2ea0be5 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -660,18 +660,29 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "fontName", "fontWeight", "textDecoration", + "textCase", ]); // for converting figma textDecoration to css textDecoration - const figmaToCssTextDecorationMap = { + const figmaTextDecorationToCssMap = { STRIKETHROUGH: "line-through", UNDERLINE: "underline", NONE: "normal", } as const; + const figmaTextCaseToCssTextTransformMap = { + ORIGINAL: "none", + SMALL_CAPS: "none", // TODO: support CSS font-variant-caps property + SMALL_CAPS_FORCED: "none", // TODO: support CSS font-variant-caps property + UPPER: "uppercase", + LOWER: "lowercase", + TITLE: "capitalize", + } as const; + return styledTextSegments.map((segment) => ({ ...segment, - textDecoration: figmaToCssTextDecorationMap[segment.textDecoration], + textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], + textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], })); } } diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index d059570..ef74b97 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -40,6 +40,7 @@ export interface StyledTextSegment { }; fontWeight: number; textDecoration: "normal" | "line-through" | "underline"; + textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; } export interface TextNode extends Node { From 3013c0618f15afcaddf2409533a4342fe81c8d2f Mon Sep 17 00:00:00 2001 From: Donovan So Date: Sat, 6 May 2023 10:34:14 -0400 Subject: [PATCH 06/21] add support for blending multiple text fills together --- core/src/design/adapter/figma/adapter.ts | 28 +++++++++++++++--------- core/src/design/adapter/figma/util.ts | 22 +++++++++++++++++++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 2ea0be5..aad3a02 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -17,6 +17,7 @@ import { isFrameNodeTransparent, doesNodeContainsAnImage, getMostCommonFieldInString, + getFinalRgbaColor, } from "./util"; import { GoogleFontsInstance } from "../../../google/google-fonts"; import { StyledTextSegment } from "../node"; @@ -379,16 +380,23 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } // font color - const colors = figmaNode.fills; - if ( - colors !== figma.mixed && - colors.length > 0 && - colors[0].type === "SOLID" - ) { - attributes["color"] = colorToStringWithOpacity( - colors[0].color, - colors[0].opacity - ); + const paints = figmaNode.fills; + if (paints !== figma.mixed && paints.length > 0) { + const solidPaints = paints.filter( + (paint) => paint.type === "SOLID" + ) as SolidPaint[]; + + if (solidPaints.length > 0) { + const colors = solidPaints.map(({ color, opacity }) => ({ + r: color.r, + g: color.g, + b: color.b, + a: opacity, + })); + + const finalColor = getFinalRgbaColor(colors); + attributes["color"] = rgbaToString(finalColor); + } } const textContainingOnlyOneWord = diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 71d25a1..1e62545 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -103,3 +103,25 @@ export function getMostCommonFieldInString< ); return variationWithLongestLength; } + +// calculating fills +function blendColors(color1: RGBA, color2: RGBA) { + const a = 1 - (1 - color2.a) * (1 - color1.a); + const r = + (color2.r * color2.a) / a + (color1.r * color1.a * (1 - color2.a)) / a; + const g = + (color2.g * color2.a) / a + (color1.g * color1.a * (1 - color2.a)) / a; + const b = + (color2.b * color2.a) / a + (color1.b * color1.a * (1 - color2.a)) / a; + return { r, g, b, a } as RGBA; +} + +export function getFinalRgbaColor(colors: RGBA[]) { + if (colors.length === 0) { + throw new Error("At least one color is required"); + } + + return colors.reduce((finalColor, currentColor) => { + return blendColors(finalColor, currentColor); + }); +} From c1ea8462e9ed0eb1dc2b77a5a9d7169d58f829a4 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Sat, 6 May 2023 13:21:12 -0400 Subject: [PATCH 07/21] add support for mixed font fills --- core/src/code/generator/html/generator.ts | 6 ++ core/src/design/adapter/figma/adapter.ts | 26 ++++-- core/src/design/adapter/figma/util.ts | 107 +++++++++++++++++----- core/src/design/adapter/node.ts | 1 + 4 files changed, 107 insertions(+), 33 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 30b4022..dd7c2a5 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -340,6 +340,7 @@ export class Generator { const defaultFontSize = textNode.getACssAttribute("font-size"); const defaultFontFamily = textNode.getACssAttribute("font-family"); const defaultFontWeight = textNode.getACssAttribute("font-weight"); + const defaultColor = textNode.getACssAttribute("color"); return styledTextSegments .map((styledTextSegment) => { @@ -370,6 +371,11 @@ export class Generator { overridingAttributes["text-transform"] = textTransform; } + const color = styledTextSegment.color; + if (color !== defaultColor) { + overridingAttributes["color"] = color; + } + const text = escapeHtml(styledTextSegment.characters); if (Object.keys(overridingAttributes).length === 0) { return text; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index aad3a02..b317885 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -17,7 +17,7 @@ import { isFrameNodeTransparent, doesNodeContainsAnImage, getMostCommonFieldInString, - getFinalRgbaColor, + getRgbaFromPaints, } from "./util"; import { GoogleFontsInstance } from "../../../google/google-fonts"; import { StyledTextSegment } from "../node"; @@ -387,16 +387,22 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { ) as SolidPaint[]; if (solidPaints.length > 0) { - const colors = solidPaints.map(({ color, opacity }) => ({ - r: color.r, - g: color.g, - b: color.b, - a: opacity, - })); - - const finalColor = getFinalRgbaColor(colors); + const finalColor = getRgbaFromPaints(solidPaints); attributes["color"] = rgbaToString(finalColor); } + } else if (paints === figma.mixed) { + const mostCommonPaints = getMostCommonFieldInString(figmaNode, "fills", { + areVariationsEqual: (paint1, paint2) => + JSON.stringify(paint1) === JSON.stringify(paint2), + variationModifier: (paint) => { + // don't consider non-solid paints for now + const solidPaints = paint.filter((p) => p.type === "SOLID"); + return solidPaints.length > 0 ? solidPaints : null; + }, + }) as SolidPaint[]; + + const finalColor = getRgbaFromPaints(mostCommonPaints); + attributes["color"] = rgbaToString(finalColor); } const textContainingOnlyOneWord = @@ -669,6 +675,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "fontWeight", "textDecoration", "textCase", + "fills", ]); // for converting figma textDecoration to css textDecoration @@ -691,6 +698,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { ...segment, textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], + color: rgbaToString(getRgbaFromPaints(segment.fills)), })); } } diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 1e62545..63d6d78 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -60,38 +60,85 @@ export const doesNodeContainsAnImage = ( return false; }; +type Variation< + T extends keyof Omit +> = Pick[T]; + +/** + * + * @param figmaTextNode + * @param field + * @param options + * @returns + */ export function getMostCommonFieldInString< T extends keyof Omit ->(figmaTextNode: TextNode, field: T) { +>( + figmaTextNode: TextNode, + field: T, + options: { + /** + * areVariationsEqual is an optional function that returns if two variations are equal are not. + * @returns true if variations are equal, false otherwise + */ + areVariationsEqual?: ( + variation1: Variation, + variation2: Variation + ) => boolean; + /** + * variationModifier is an optional function used to modify a variation before it's used to count the number of characters. + * Return null if you don't want the variation to be considered. + * @returns modified variation or null + */ + variationModifier?: (variation: Variation) => Variation | null; + } = {} +): Variation { + const { areVariationsEqual, variationModifier } = options; const styledTextSegments = figmaTextNode.getStyledTextSegments([field]); - type Variation = Pick< - StyledTextSegment, - T | "characters" | "start" | "end" - >[T]; - // Count the number of characters that has each variation of "field". // For example, if field is "fontSize", variations are the different font sizes (12, 14, etc.) - // Pick[T] - const fieldNumOfChars = new Map(); + const fieldNumOfChars = new Map, number>(); styledTextSegments.forEach((segment) => { - const variation = segment[field]; - if (!fieldNumOfChars.has(variation)) { - fieldNumOfChars.set(variation, 0); + const variation = variationModifier + ? variationModifier(segment[field]) + : segment[field]; + + if (variation === null) { + return; } - fieldNumOfChars.set( - variation, - fieldNumOfChars.get(variation) + segment.characters.length - ); + const segmentLength = segment.characters.length; + if (areVariationsEqual) { + for (const [existingVariation, sum] of fieldNumOfChars) { + // if variation already exists, add to current sum + if (areVariationsEqual(variation, existingVariation)) { + fieldNumOfChars.set(existingVariation, sum + segmentLength); + return; + } + } + // if variation does not exist, intialize it + fieldNumOfChars.set(variation, segmentLength); + } else { + // if variation already exists, add to current sum + if (fieldNumOfChars.has(variation)) { + fieldNumOfChars.set( + variation, + fieldNumOfChars.get(variation) + segmentLength + ); + } else { + // if variation does not exist, intialize it + fieldNumOfChars.set(variation, segmentLength); + } + } }); - let variationWithLongestLength: Variation; + let variationWithLongestLength: Variation; let currentLongestLength = -Infinity; - for (const [key, value] of fieldNumOfChars) { - if (value > currentLongestLength) { - currentLongestLength = value; - variationWithLongestLength = key; + for (const [variation, sum] of fieldNumOfChars) { + if (sum > currentLongestLength) { + currentLongestLength = sum; + variationWithLongestLength = variation; } } @@ -116,12 +163,24 @@ function blendColors(color1: RGBA, color2: RGBA) { return { r, g, b, a } as RGBA; } -export function getFinalRgbaColor(colors: RGBA[]) { - if (colors.length === 0) { - throw new Error("At least one color is required"); +export function getRgbaFromPaints(paints: Paint[]) { + // TODO: support GradientPaint + const solidPaints = paints.filter( + (paint) => paint.type === "SOLID" + ) as SolidPaint[]; + + if (solidPaints.length === 0) { + throw new Error("No solid paints found"); } + const colors = solidPaints.map(({ color, opacity }) => ({ + r: color.r, + g: color.g, + b: color.b, + a: opacity, + })); + return colors.reduce((finalColor, currentColor) => { return blendColors(finalColor, currentColor); - }); + }) as RGBA; } diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index ef74b97..9d40221 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -41,6 +41,7 @@ export interface StyledTextSegment { fontWeight: number; textDecoration: "normal" | "line-through" | "underline"; textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; + color: string; } export interface TextNode extends Node { From ca1744ced82fbebdab3afa7a9db24c1f6903b10e Mon Sep 17 00:00:00 2001 From: Donovan So Date: Mon, 8 May 2023 10:49:19 -0400 Subject: [PATCH 08/21] wip: support lists --- core/src/code/generator/html/generator.ts | 27 ++++++++++++++++++----- core/src/design/adapter/figma/adapter.ts | 8 +++++++ core/src/design/adapter/node.ts | 1 + 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index dd7c2a5..e03ca8c 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -336,6 +336,7 @@ export class Generator { const styledTextSegments = textNode.node.getStyledTextSegments(); + // list if (styledTextSegments.length > 0) { const defaultFontSize = textNode.getACssAttribute("font-size"); const defaultFontFamily = textNode.getACssAttribute("font-family"); @@ -376,15 +377,29 @@ export class Generator { overridingAttributes["color"] = color; } + const hasOverridingAttributes = + Object.keys(overridingAttributes).length > 0; + const textNodeClassProps = hasOverridingAttributes + ? ` ${this.getPropsFromAttributes(overridingAttributes, option)}` + : ""; + const text = escapeHtml(styledTextSegment.characters); - if (Object.keys(overridingAttributes).length === 0) { + + if (styledTextSegment.listType !== "none") { + const listTag = styledTextSegment.listType; + const listContent = text + .split("\n") + .filter((line) => line !== "") + .map((line) => `
  • ${line}
  • `) + .join(""); + return `<${listTag}${textNodeClassProps}>${listContent}`; + } + + if (!hasOverridingAttributes) { return text; } - const textNodeClassProps = this.getPropsFromAttributes( - overridingAttributes, - option - ); - return `${text}`; + + return `${text}`; }) .join(""); } else { diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index b317885..bb3c3e0 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -676,6 +676,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "textDecoration", "textCase", "fills", + "listOptions", ]); // for converting figma textDecoration to css textDecoration @@ -694,11 +695,18 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { TITLE: "capitalize", } as const; + const figmaListOptionsToHtmlTagMap = { + NONE: "none", + UNORDERED: "ul", + ORDERED: "ol", + } as const; + return styledTextSegments.map((segment) => ({ ...segment, textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], color: rgbaToString(getRgbaFromPaints(segment.fills)), + listType: figmaListOptionsToHtmlTagMap[segment.listOptions.type], })); } } diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index 9d40221..c93291f 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -42,6 +42,7 @@ export interface StyledTextSegment { textDecoration: "normal" | "line-through" | "underline"; textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; color: string; + listType: "none" | "ul" | "ol"; } export interface TextNode extends Node { From c44b9df501a2c238f8154403be8635eb3b6b5031 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Mon, 8 May 2023 15:45:08 -0400 Subject: [PATCH 09/21] support mixed text styles in lists --- core/src/code/generator/html/generator.ts | 199 ++++++++++++++-------- core/src/design/adapter/figma/adapter.ts | 19 ++- core/src/design/adapter/node.ts | 7 + 3 files changed, 151 insertions(+), 74 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index e03ca8c..61faeb4 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -336,78 +336,141 @@ export class Generator { const styledTextSegments = textNode.node.getStyledTextSegments(); - // list - if (styledTextSegments.length > 0) { - const defaultFontSize = textNode.getACssAttribute("font-size"); - const defaultFontFamily = textNode.getACssAttribute("font-family"); - const defaultFontWeight = textNode.getACssAttribute("font-weight"); - const defaultColor = textNode.getACssAttribute("color"); - - return styledTextSegments - .map((styledTextSegment) => { - const overridingAttributes: Attributes = {}; - - const fontSize = `${styledTextSegment.fontSize}px`; - if (fontSize !== defaultFontSize) { - overridingAttributes["font-size"] = fontSize; - } - - const fontFamily = styledTextSegment.fontName.family; - if (fontFamily !== defaultFontFamily) { - overridingAttributes["font-family"] = fontFamily; - } - - const fontWeight = styledTextSegment.fontWeight.toString(); - if (fontWeight !== defaultFontWeight) { - overridingAttributes["font-weight"] = fontWeight; - } - - const textDecoration = styledTextSegment.textDecoration; - if (textDecoration !== "normal") { - overridingAttributes["text-decoration"] = textDecoration; - } - - const textTransform = styledTextSegment.textTransform; - if (textTransform !== "none") { - overridingAttributes["text-transform"] = textTransform; - } - - const color = styledTextSegment.color; - if (color !== defaultColor) { - overridingAttributes["color"] = color; - } - - const hasOverridingAttributes = - Object.keys(overridingAttributes).length > 0; - const textNodeClassProps = hasOverridingAttributes - ? ` ${this.getPropsFromAttributes(overridingAttributes, option)}` - : ""; - - const text = escapeHtml(styledTextSegment.characters); - - if (styledTextSegment.listType !== "none") { - const listTag = styledTextSegment.listType; - const listContent = text - .split("\n") - .filter((line) => line !== "") - .map((line) => `
  • ${line}
  • `) + const defaultFontSize = textNode.getACssAttribute("font-size"); + const defaultFontFamily = textNode.getACssAttribute("font-family"); + const defaultFontWeight = textNode.getACssAttribute("font-weight"); + const defaultColor = textNode.getACssAttribute("color"); + + const cssAttributesSegments = styledTextSegments.map( + (styledTextSegment) => { + // here we only keep attributes if they are different from the default attribute + const cssAttributes: Attributes = {}; + + const fontSize = `${styledTextSegment.fontSize}px`; + if (fontSize !== defaultFontSize) { + cssAttributes["font-size"] = fontSize; + } + + const fontFamily = styledTextSegment.fontName.family; + if (fontFamily !== defaultFontFamily) { + cssAttributes["font-family"] = fontFamily; + } + + const fontWeight = styledTextSegment.fontWeight.toString(); + if (fontWeight !== defaultFontWeight) { + cssAttributes["font-weight"] = fontWeight; + } + + const textDecoration = styledTextSegment.textDecoration; + if (textDecoration !== "normal") { + cssAttributes["text-decoration"] = textDecoration; + } + + const textTransform = styledTextSegment.textTransform; + if (textTransform !== "none") { + cssAttributes["text-transform"] = textTransform; + } + + const color = styledTextSegment.color; + if (color !== defaultColor) { + cssAttributes["color"] = color; + } + + return { + start: styledTextSegment.start, + end: styledTextSegment.end, + cssAttributes, + }; + } + ); + + // Here are handle lists in text, where: + // - A "list segment" is a segment of text that is an ordered list, an unordered list, or not a list at all + // - A "list item" is text inside a list segment, separated by a new line character. + // + // For example: + //
      <- this is a "list segment" + //
    • item 1
    • <- this is a "list item" + //
    • item 2
    • <- this is another "list item" + //
    + return textNode.node + .getListSegments() + .map((listSegment) => { + const listTag = listSegment.listType; + + const listItems = splitByNewLine(listSegment.characters); + + // for keeping track of where we are in listSegment.characters + let currentIndex = listSegment.start; + + const listContent = listItems + .map((listItemText) => { + const itemStartIndex = currentIndex; + const itemEndIndex = currentIndex + listItemText.length - 1; + + const cssAttributesSegmentsInListItem = + cssAttributesSegments.filter( + (segment) => + !( + segment.end < itemStartIndex || segment.start > itemEndIndex + ) + ); + + let result = cssAttributesSegmentsInListItem + .map((segment) => { + let text = escapeHtml( + listItemText.substring( + segment.start - itemStartIndex, + segment.end - itemStartIndex + ) + ); + + if (!isEmpty(segment.cssAttributes)) { + const textProps = this.getPropsFromAttributes( + segment.cssAttributes, + option + ); + const htmlTag = + listItemText.length === segment.end - segment.start + ? "li" + : "span"; + + text = `<${htmlTag} ${textProps}>${text}`; + } + + return text; + }) .join(""); - return `<${listTag}${textNodeClassProps}>${listContent}`; - } - - if (!hasOverridingAttributes) { - return text; - } - - return `${text}`; - }) - .join(""); - } else { - return escapeHtml(textNode.getText()); - } + + if (listTag !== "none" && !result.startsWith("${result}`; + } + + currentIndex = itemEndIndex + 1; + return result; + }) + .join(""); + + if (listTag === "none") { + return listContent; + } else { + return `<${listTag}>${listContent}`; + } + }) + .join(""); } } +const splitByNewLine = (text: string) => { + const listItems = text.split("\n").map((line) => line + "\n"); + + if (listItems[listItems.length - 1] === "\n") { + listItems.pop(); + } + + return listItems; +}; + const getWidthAndHeightProp = (node: Node): string => { const cssAttribtues: Attributes = node.getCssAttributes(); let widthAndHeight: string = getWidthAndHeightVariableProp(node.getId()); diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index bb3c3e0..3f8624d 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -9,7 +9,7 @@ import { VisibleNode, } from "../../../bricks/node"; import { isEmpty } from "../../../utils"; -import { BoxCoordinates, Attributes, ExportFormat } from "../node"; +import { BoxCoordinates, Attributes, ExportFormat, ListSegment } from "../node"; import { colorToString, colorToStringWithOpacity, @@ -676,7 +676,6 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "textDecoration", "textCase", "fills", - "listOptions", ]); // for converting figma textDecoration to css textDecoration @@ -695,17 +694,25 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { TITLE: "capitalize", } as const; + return styledTextSegments.map((segment) => ({ + ...segment, + textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], + textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], + color: rgbaToString(getRgbaFromPaints(segment.fills)), + })); + } + + getListSegments(): ListSegment[] { const figmaListOptionsToHtmlTagMap = { NONE: "none", UNORDERED: "ul", ORDERED: "ol", } as const; - return styledTextSegments.map((segment) => ({ + const listSegments = this.node.getStyledTextSegments(["listOptions"]); + + return listSegments.map((segment) => ({ ...segment, - textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], - textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], - color: rgbaToString(getRgbaFromPaints(segment.fills)), listType: figmaListOptionsToHtmlTagMap[segment.listOptions.type], })); } diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index c93291f..a1adfad 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -42,6 +42,12 @@ export interface StyledTextSegment { textDecoration: "normal" | "line-through" | "underline"; textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; color: string; +} + +export interface ListSegment { + characters: string; + start: number; + end: number; listType: "none" | "ul" | "ol"; } @@ -50,6 +56,7 @@ export interface TextNode extends Node { isItalic(): boolean; getFamilyName(): string; getStyledTextSegments(): StyledTextSegment[]; + getListSegments(): ListSegment[]; } export interface VectorNode extends Node {} From 7dca4006003bc1d1606a1952ea56457caf372862 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Mon, 8 May 2023 18:41:43 -0400 Subject: [PATCH 10/21] add list style for twcss, remove redundant div when there is only one list --- core/src/code/generator/html/generator.ts | 16 +++++++-- .../generator/tailwindcss/css-to-twcss.ts | 9 +++++ .../code/generator/tailwindcss/generator.ts | 34 +++++++++++++------ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 61faeb4..bd961a7 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -65,6 +65,14 @@ export class Generator { const textNodeClassProps = this.getPropsFromNode(node, option); const attributes = htmlTag === "a" ? 'href="#" ' : ""; const textProp = this.getText(node, option); + + //@ts-ignore + const listSegments = node.node.getListSegments(); + const listType = listSegments[0].listType; + if (listSegments.length === 1 && listType !== "none") { + return `<${listType} ${attributes}${textNodeClassProps}>${textProp}`; + } + return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}`; } @@ -395,7 +403,7 @@ export class Generator { // return textNode.node .getListSegments() - .map((listSegment) => { + .map((listSegment, _, listSegments) => { const listTag = listSegment.listType; const listItems = splitByNewLine(listSegment.characters); @@ -451,7 +459,11 @@ export class Generator { }) .join(""); - if (listTag === "none") { + if ( + listTag === "none" || + // if there is only one list, we don't wrap the list items in a
      or
        tag because we're doing it outside + listSegments.length === 1 + ) { return listContent; } else { return `<${listTag}>${listContent}`; diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index f69d42c..ad4a7e9 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -913,6 +913,15 @@ export const getTwcssClass = ( return findClosestTwcssFontWeight(cssValue); } + case "list-style-type": { + if (cssValue === "disc") { + return "list-disc"; + } + if (cssValue === "decimal") { + return "list-decimal"; + } + } + default: return ""; } diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index cb942c9..69366ae 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -20,6 +20,7 @@ import { import { Generator as ReactGenerator } from "../react/generator"; import { filterAttributes } from "../../../bricks/util"; import { extraFileRegistryGlobalInstance } from "../../extra-file-registry/extra-file-registry"; +import { Attributes } from "../../../design/adapter/node"; export class Generator { htmlGenerator: HtmlGenerator; @@ -98,17 +99,28 @@ export class Generator { // getProps converts a single node to formated tailwindcss classes const getPropsFromNode = (node: Node, option: Option): string => { switch (node.getType()) { - case NodeType.TEXT: - return convertCssClassesToTwcssClasses( - { - ...node.getCssAttributes(), - ...filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), - }, - option, - node.getId() - ); + case NodeType.TEXT: { + const attributes: Attributes = { + ...node.getCssAttributes(), + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningOnly: true, + }), + }; + + //@ts-ignore + const listSegments = node.node.getListSegments(); + // Extra classes needed for lists due to Tailwind's CSS reset + const listType = listSegments[0].listType; + if (listSegments.length === 1 && listType === "ul") { + attributes["list-style-type"] = "disc"; + } + + if (listSegments.length === 1 && listType === "ol") { + attributes["list-style-type"] = "decimal"; + } + + return convertCssClassesToTwcssClasses(attributes, option, node.getId()); + } case NodeType.GROUP: return convertCssClassesToTwcssClasses( { From f97e10278420c3ff2be3a0bacef7b8dd5402e089 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Mon, 8 May 2023 23:43:30 -0700 Subject: [PATCH 11/21] Various bug fixes (#75) --- core/ee/cv/component-recognition.ts | 6 - core/ee/web/request.ts | 50 ++- core/src/bricks/additional-css.ts | 123 +++++- core/src/bricks/inclusion.ts | 7 - core/src/bricks/line.ts | 4 +- core/src/bricks/node.ts | 16 +- core/src/bricks/remove-node.ts | 219 ++++++++--- core/src/bricks/util.ts | 79 ++-- .../extra-file-registry.ts | 1 - core/src/code/generator/css/generator.ts | 96 ++++- core/src/code/generator/html/generator.ts | 34 +- .../generator/tailwindcss/css-to-twcss.ts | 14 +- .../code/generator/tailwindcss/generator.ts | 98 ++++- core/src/code/generator/util.ts | 13 + core/src/design/adapter/figma/adapter.ts | 358 +++++++++++++----- core/src/design/adapter/figma/util.ts | 6 +- core/src/index.ts | 25 +- figma/src/code.ts | 19 +- figma/src/constants.ts | 4 +- figma/src/pages/code-generation-status.tsx | 10 +- figma/src/pages/code-output-setting.tsx | 33 +- figma/src/pages/home.tsx | 57 +-- figma/src/pages/post-code-generation-ai.tsx | 11 +- figma/src/ui.html | 5 +- figma/src/ui.tsx | 18 +- 25 files changed, 955 insertions(+), 351 deletions(-) diff --git a/core/ee/cv/component-recognition.ts b/core/ee/cv/component-recognition.ts index 732cc19..4619638 100644 --- a/core/ee/cv/component-recognition.ts +++ b/core/ee/cv/component-recognition.ts @@ -32,9 +32,6 @@ export const annotateNodeForHtmlTag = async (startingNode: Node) => { return node?.getType() !== NodeType.VECTOR_GROUP; }); - // console.log("idImageMap", idImageMap); - // console.log("idTextMap", idTextMap); - const [predictImagesResult, predictTextsResult] = await Promise.allSettled([ predictImage(idImageMap), predictText(idTextMap), @@ -56,9 +53,6 @@ export const annotateNodeForHtmlTag = async (startingNode: Node) => { console.error("Error with image prediction", predictTextsResult.reason); } - // console.log("imagePredictions", imagePredictions); - // console.log("textPredictions", textPredictions); - await traverseNodes(startingNode, async (node) => { if (node.node) { const originalId = node.node.getOriginalId(); diff --git a/core/ee/web/request.ts b/core/ee/web/request.ts index cc09402..4351562 100644 --- a/core/ee/web/request.ts +++ b/core/ee/web/request.ts @@ -46,7 +46,7 @@ export const getNameMap = async (): Promise => { try { const response: any = await fetch( - process.env.ML_BACKEND_API_ENDPOINT + "/generate/name", + process.env.ML_BACKEND_API_ENDPOINT + "/generate/name", // "http://localhost:8080/generate/name", { method: 'POST', @@ -71,13 +71,7 @@ export const getNameMap = async (): Promise => { parsedNameMapArr.push(JSON.parse(nameMapStr)); } - const consolidatedNameMap: NameMap = {}; - parsedNameMapArr.forEach((nameMap: NameMap) => { - Object.entries((nameMap)).forEach(([oldName, newName]) => { - consolidatedNameMap[oldName] = newName; - }); - }); - + const consolidatedNameMap: NameMap = getConsolidateNameMap(parsedNameMapArr); dedupNames(consolidatedNameMap); return consolidatedNameMap; @@ -109,4 +103,44 @@ const dedupNames = (nameMap: NameMap) => { return; }); +}; + +const getConsolidateNameMap = (parsedNameMapArr: NameMap[]): NameMap => { + const consolidatedNameMap: NameMap = {}; + parsedNameMapArr.forEach((nameMap: NameMap) => { + const nonDuplicateDataFields: Set = new Set(); + const nonDuplicateProps: Set = new Set(); + let dataFieldCounter: number = 1; + let propCounter: number = 1; + + Object.entries((nameMap)).forEach(([oldName, newName]) => { + if (nonDuplicateDataFields.has(newName)) { + let dedupedName: string = newName + dataFieldCounter; + consolidatedNameMap[oldName] = dedupedName; + nonDuplicateDataFields.add(dedupedName); + dataFieldCounter++; + return; + } + + if (nonDuplicateProps.has(newName)) { + let dedupedName: string = newName + propCounter; + consolidatedNameMap[oldName] = dedupedName; + nonDuplicateProps.add(dedupedName); + propCounter++; + return; + } + + consolidatedNameMap[oldName] = newName; + + if (oldName.startsWith("dataField")) { + nonDuplicateDataFields.add(newName); + } + + if (oldName.startsWith("prop")) { + nonDuplicateProps.add(newName); + } + }); + }); + + return consolidatedNameMap; }; \ No newline at end of file diff --git a/core/src/bricks/additional-css.ts b/core/src/bricks/additional-css.ts index 450e3ab..eb84d55 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -10,6 +10,7 @@ import { ImageNode, Node, NodeType, VisibleNode } from "./node"; import { getContainerLineFromNodes, getLinesFromNodes, + Line, getLineBasedOnDirection, } from "./line"; import { filterCssValue } from "./util"; @@ -19,6 +20,11 @@ export const selectBox = ( node: Node, useBoundingBox: boolean = false ): BoxCoordinates => { + const attributes: Attributes = node.getCssAttributes(); + if (!isEmpty(attributes["box-shadow"])) { + return node.getAbsBoundingBox(); + } + if (node.getType() === NodeType.VISIBLE) { const visibleNode = node as VisibleNode; return visibleNode.getAbsBoundingBox(); @@ -68,6 +74,7 @@ export const addAdditionalCssAttributesToNodes = (node: Node) => { node.addCssAttributes(getAdditionalCssAttributes(node)); node.addPositionalCssAttributes(getPositionalCssAttributes(node, direction)); adjustChildrenHeightAndWidthCssValue(node); + adjustNodeHeightAndWidthCssValue(node); for (const child of children) { addAdditionalCssAttributesToNodes(child); @@ -86,18 +93,20 @@ export const getPaddingInPixels = ( let paddingLeft: number = 0; let paddingRight: number = 0; - const targetLine = getContainerLineFromNodes(node.getChildren(), direction); - const parentLine = getContainerLineFromNodes([node], direction); + const targetLine = getContainerLineFromNodes(node.getChildren(), direction, true); + const parentLine = getContainerLineFromNodes([node], direction, true); const perpendicularTargetLine = getContainerLineFromNodes( node.getChildren(), - getOppositeDirection(direction) + getOppositeDirection(direction), + true, ); // const boundingBoxPerpendicularTargetLine = getContainerLineFromNodes(node.getChildren(), direction, true); const perpendicularParentLine = getContainerLineFromNodes( [node], - getOppositeDirection(direction) + getOppositeDirection(direction), + true, ); if (direction === Direction.VERTICAL) { @@ -192,28 +201,30 @@ const setMarginsForChildren = ( let leftGap: number = 0; let rightGap: number = 0; - const targetLine = getLineBasedOnDirection(targetNode, direction); - const parentLine = getLineBasedOnDirection(parentNode, direction); + const targetLine = getLineBasedOnDirection(targetNode, direction, true); + const parentLine = getLineBasedOnDirection(parentNode, direction, true); const perpendicularTargetLine = getLineBasedOnDirection( targetNode, - getOppositeDirection(direction) + getOppositeDirection(direction), + true ); const perpendicularParentLine = getLineBasedOnDirection( parentNode, - getOppositeDirection(direction) + getOppositeDirection(direction), + true, ); let prevTarget = children[i]; if (i > 0) { prevTarget = children[i - 1]; } - const prevTargetLine = getLineBasedOnDirection(prevTarget, direction); + const prevTargetLine = getLineBasedOnDirection(prevTarget, direction, true); let nextTarget = children[i]; if (i < children.length - 1) { nextTarget = children[i + 1]; } - const nextTargetLine = getLineBasedOnDirection(nextTarget, direction); + const nextTargetLine = getLineBasedOnDirection(nextTarget, direction, true); if (direction === Direction.HORIZONTAL) { botGap = @@ -223,7 +234,7 @@ const setMarginsForChildren = ( topGap = i === 0 ? targetLine.lower - parentLine.lower - paddingTop - : prevTargetLine.upper - targetLine.lower; + : targetLine.lower - prevTargetLine.upper; switch (justifyContentValue) { case JustifyContent.SPACE_BETWEEN: @@ -362,12 +373,28 @@ export const getAdditionalCssAttributes = (node: Node): Attributes => { return attributes; }; +const adjustNodeHeightAndWidthCssValue = (node: Node) => { + const attributes: Attributes = node.getCssAttributes(); + if (!isEmpty(attributes["box-shadow"])) { + const width: number = Math.abs(node.getAbsBoundingBox().leftTop.x - node.getAbsBoundingBox().rightBot.x); + const height: number = Math.abs(node.getAbsBoundingBox().leftTop.y - node.getAbsBoundingBox().rightBot.y); + attributes["width"] = `${width}px`; + attributes["height"] = `${height}px`; + } + + node.setCssAttributes(attributes); +}; + + const adjustChildrenHeightAndWidthCssValue = (node: Node) => { if (!isEmpty(node.getPositionalCssAttributes())) { const [maxWidth, maxHeight] = getAllowedMaxWidthAndHeight(node); const flexDir = node.getAPositionalAttribute("flex-direction"); + const justifyContent = node.getAPositionalAttribute("justify-content"); + const alignItems = node.getAPositionalAttribute("align-items"); + let gap: number = 0; let gapCssVal: string = node.getACssAttribute("gap"); if (!isCssValueEmpty(gapCssVal)) { @@ -441,6 +468,12 @@ const adjustChildrenHeightAndWidthCssValue = (node: Node) => { } child.addCssAttributes(attributes); + + if (alignItems === "center" && child.getType() === NodeType.TEXT) { + const childAttributes: Attributes = child.getCssAttributes(); + delete (childAttributes["width"]); + child.setCssAttributes(childAttributes); + } } } @@ -650,7 +683,47 @@ const getJustifyContentValue = ( } } - return JustifyContent.SPACE_BETWEEN; + if (targetLines.length === 2) { + return JustifyContent.SPACE_BETWEEN; + } + + if (targetLines.length > 2) { + const gaps: number[] = []; + let prevLine: Line = null; + for (let i = 0; i < targetLines.length; i++) { + const targetLine: Line = targetLines[i]; + if (i === 0) { + prevLine = targetLine; + continue; + } + + gaps.push(targetLine.lower - prevLine.upper); + prevLine = targetLine; + } + + const averageGap: number = gaps.reduce((a, b) => a + b) / gaps.length; + let isJustifyCenter: boolean = true; + for (let i = 0; i < targetLines.length; i++) { + const targetLine: Line = targetLines[i]; + if (i === 0) { + prevLine = targetLine; + continue; + } + + const gap: number = targetLine.lower - prevLine.upper; + + if (Math.abs(gap - averageGap) / averageGap > 0.1) { + isJustifyCenter = false; + } + prevLine = targetLine; + } + + if (isJustifyCenter) { + return JustifyContent.SPACE_BETWEEN; + } + } + + return JustifyContent.FLEX_START; }; // getAlignItemsValue determines the value of align-items css property given a node and flex-direction. @@ -685,7 +758,9 @@ const getAlignItemsValue = ( } let numberOfItemsTippingLeft: number = 0; + let numberOfItemsTippingLeftStrict: number = 0; let numberOfItemsTippingRight: number = 0; + let numberOfItemsTippingRightStrict: number = 0; let numberOfItemsInTheMiddle: number = 0; let noGapItems: number = 0; @@ -744,6 +819,30 @@ const getAlignItemsValue = ( } } + if (noGapItems === targetLines.length) { + for (const targetLine of targetLines) { + const leftGap = Math.abs(parentLine.lower - targetLine.lower); + const rightGap = Math.abs(parentLine.upper - targetLine.upper); + if (leftGap > rightGap) { + numberOfItemsTippingRightStrict++; + } + + if (rightGap > leftGap) { + numberOfItemsTippingLeftStrict++; + } + } + } + + if (noGapItems !== 0 && numberOfItemsInTheMiddle === 0) { + if (numberOfItemsTippingLeftStrict !== 0 && numberOfItemsTippingRightStrict === 0) { + return AlignItems.FLEX_START; + } + + if (numberOfItemsTippingRightStrict !== 0 && numberOfItemsTippingLeftStrict === 0) { + return AlignItems.FLEX_END; + } + } + if (noGapItems !== 0 && numberOfItemsInTheMiddle !== 0) { return AlignItems.CENTER; } diff --git a/core/src/bricks/inclusion.ts b/core/src/bricks/inclusion.ts index 1e4dbf9..631180f 100644 --- a/core/src/bricks/inclusion.ts +++ b/core/src/bricks/inclusion.ts @@ -1,7 +1,5 @@ import { PostionalRelationship, Node } from "./node"; -const inclusionAnnotation: string = "checkedForInclusion"; - // groupNodesByInclusion groups nodes if they have an inclusion relationship // input nodes are ordered by z-index export const groupNodesByInclusion = (nodes: Node[]): Node[] => { @@ -13,10 +11,6 @@ export const groupNodesByInclusion = (nodes: Node[]): Node[] => { for (let i = nodes.length - 1; i >= 0; i--) { let currentNode = nodes[i]; - if (currentNode.getAnnotation(inclusionAnnotation)) { - return nodes; - } - if (removedNodes.has(currentNode.getId())) { continue; } @@ -55,7 +49,6 @@ export const groupNodesByInclusion = (nodes: Node[]): Node[] => { for (let i = 0; i < nodes.length; i++) { let currentNode = nodes[i]; - currentNode.addAnnotations(inclusionAnnotation, true); if (removedNodes.has(currentNode.getId())) { continue; } diff --git a/core/src/bricks/line.ts b/core/src/bricks/line.ts index 164beda..98e9850 100644 --- a/core/src/bricks/line.ts +++ b/core/src/bricks/line.ts @@ -10,8 +10,8 @@ enum RelativePoisition { } // getLineBasedOnDirection gets the boundary of a node depending on the input direction. -export const getLineBasedOnDirection = (node: Node, direction: Direction) => { - const coordinates = selectBox(node); +export const getLineBasedOnDirection = (node: Node, direction: Direction, useBoundingBox: boolean = false) => { + const coordinates = selectBox(node, useBoundingBox); if (direction === Direction.HORIZONTAL) { return new Line(coordinates.leftTop.y, coordinates.rightBot.y); diff --git a/core/src/bricks/node.ts b/core/src/bricks/node.ts index bf65d4e..9cec7b9 100644 --- a/core/src/bricks/node.ts +++ b/core/src/bricks/node.ts @@ -23,7 +23,9 @@ export enum PostionalRelationship { export type Option = { truncateNumbers?: boolean; zeroValueAllowed?: boolean; - absolutePositioningOnly?: boolean; + absolutePositioningFilter?: boolean; + marginFilter?: boolean; + excludeBackgroundColor?: boolean; }; export type Node = GroupNode | VisibleNode | TextNode | VectorNode | ImageNode; @@ -65,7 +67,8 @@ export class BaseNode { option: Option = { zeroValueAllowed: false, truncateNumbers: true, - absolutePositioningOnly: false, + absolutePositioningFilter: false, + marginFilter: false, } ): Attributes { return filterAttributes(this.positionalCssAttributes, option); @@ -90,7 +93,8 @@ export class BaseNode { option: Option = { zeroValueAllowed: false, truncateNumbers: true, - absolutePositioningOnly: false, + absolutePositioningFilter: false, + marginFilter: false, } ): Attributes { return filterAttributes(this.cssAttributes, option); @@ -373,17 +377,11 @@ export class GroupNode extends BaseNode { if (coordinates.rightBot.y > yb) { yb = coordinates.rightBot.y; } - - // console.log("child: ", child); } this.cssAttributes["width"] = `${Math.abs(xr - xl)}px`; this.cssAttributes["height"] = `${Math.abs(yb - yt)}px`; - // console.log("node: ", this.node); - // console.log(`this.cssAttributes["width"]: `, this.cssAttributes["width"]); - // console.log(`this.cssAttributes["height"]: `, this.cssAttributes["height"]); - return { leftTop: { x: xl, diff --git a/core/src/bricks/remove-node.ts b/core/src/bricks/remove-node.ts index 583096a..91322a2 100644 --- a/core/src/bricks/remove-node.ts +++ b/core/src/bricks/remove-node.ts @@ -1,71 +1,172 @@ import { Node, computePositionalRelationship, PostionalRelationship, NodeType } from "./node"; import { Attributes } from "../design/adapter/node"; import { isEmpty } from "../utils"; +import { cssStrToNum } from "../code/generator/util"; export const removeNode = (node: Node): Node => { - const children: Node[] = node.getChildren(); - if (children.length === 1) { - const child = children[0]; - if (child.getType() !== NodeType.VISIBLE && child.getType() !== NodeType.GROUP) { - return node; - } - - if (computePositionalRelationship(node.getAbsBoundingBox(), child.getAbsBoundingBox()) === PostionalRelationship.COMPLETE_OVERLAP) { - const cssAttributes: Attributes = { - ...node.getCssAttributes(), - ...child.getCssAttributes(), - }; - - child.setCssAttributes(cssAttributes); - - return removeNode(child); - } - } - - return node; - }; - - - export const removeCompletelyOverlappingNodes = (node: Node, parentNode: Node) => { - if (isEmpty(node)) { - return; + const children: Node[] = node.getChildren(); + if (children.length === 1) { + const child = children[0]; + if (haveSimlarWidthAndHeight(node, child)) { + const cssAttributes: Attributes = { + ...node.getCssAttributes(), + ...child.getCssAttributes(), + }; + + const positionalCssAttributes: Attributes = mergeAttributes(node.getPositionalCssAttributes(), child.getPositionalCssAttributes()); + + child.setCssAttributes(cssAttributes); + child.setPositionalCssAttributes(positionalCssAttributes); + + return removeNode(child); } - - - let children: Node[] = node.getChildren(); - if (children.length === 0) { - return; + } + + return node; +}; + +export const removeChildrenNode = (node: Node): Node => { + const children: Node[] = node.getChildren(); + let newChildren: Node[] = []; + for (let i = 0; i < children.length; i++) { + const child: Node = children[i]; + if (child.getType() === NodeType.IMAGE || child.getType() === NodeType.VECTOR) { + newChildren.push(child); + continue; } - - if (children.length > 1) { - for (const child of children) { - removeCompletelyOverlappingNodes(child, node); - } - - return; + + if (haveSimlarWidthAndHeight(node, child)) { + const cssAttributes: Attributes = { + ...node.getCssAttributes(), + ...child.getCssAttributes(), + }; + + const positionalCssAttributes: Attributes = { + ...node.getPositionalCssAttributes(), + ...child.getPositionalCssAttributes(), + }; + + node.setCssAttributes(cssAttributes); + node.setPositionalCssAttributes(positionalCssAttributes); + continue; } - - const child: Node = children[0]; - const pruned: Node = removeNode(node); - if (pruned.getId() === node.getId()) { - removeCompletelyOverlappingNodes(child, pruned); + + const newChildNode: Node = removeChildrenNode(child); + newChildren.push(newChildNode); + } + + node.setChildren(newChildren); + return node; +}; + +const haveSimlarWidthAndHeight = (currentNode: Node, targetNode: Node): boolean => { + if (computePositionalRelationship(currentNode.getAbsBoundingBox(), targetNode.getAbsBoundingBox()) === PostionalRelationship.COMPLETE_OVERLAP) { + return true; + } + + const currentWidth: string = currentNode.getACssAttribute("width"); + const targetWidth: string = targetNode.getACssAttribute("width"); + let similarWidth: boolean = false; + + if (isEmpty(currentWidth) || isEmpty(targetWidth)) { + return false; + } + + let diffInWidth: number = Math.abs(cssStrToNum(currentWidth) - cssStrToNum(targetWidth)); + if (diffInWidth <= 1) { + similarWidth = true; + } + + const currentHeight: string = currentNode.getACssAttribute("height"); + const targetHeight: string = targetNode.getACssAttribute("height"); + + if (isEmpty(currentHeight) || isEmpty(targetHeight)) { + return false; + } + + let similarHeight: boolean = false; + let diffInHeight: number = Math.abs(cssStrToNum(currentHeight) - cssStrToNum(targetHeight)); + if (diffInHeight <= 1) { + similarHeight = true; + } + + return similarHeight && similarWidth; +}; + +const filterAttributes = (attribtues: Attributes): Attributes => { + const result = {}; + + Object.entries(attribtues).forEach(([key, value]) => { + if (key === "flex-direction" || key === "display" || key === "justify-content" || key === "align-items") { return; } - - if (isEmpty(parentNode)) { - return; + + result[key] = value; + }); + + return result; +}; + +const mergeAttributes = (parentPosAttributes: Attributes, childPosAttributes: Attributes): Attributes => { + if (!isEmpty(parentPosAttributes["display"]) && isEmpty(childPosAttributes["display"])) { + return { + ...parentPosAttributes, + ...childPosAttributes, + }; + } + + if (parentPosAttributes["display"] !== childPosAttributes["display"] || parentPosAttributes["flex-direction"] !== childPosAttributes["flex-direction"] || parentPosAttributes["align-items"] !== childPosAttributes["align-items"] || parentPosAttributes["justify-content"] !== childPosAttributes["justify-content"]) { + return { + ...filterAttributes(parentPosAttributes), + ...childPosAttributes, + }; + } + + return { + ...parentPosAttributes, + ...childPosAttributes, + }; +}; + + +export const removeCompletelyOverlappingNodes = (node: Node, parentNode: Node) => { + if (isEmpty(node)) { + return; + } + + + let children: Node[] = node.getChildren(); + if (children.length === 0) { + return; + } + + if (children.length > 1) { + for (const child of children) { + removeCompletelyOverlappingNodes(child, node); } - - const parentChildren = parentNode.getChildren(); - let nodeToReplace: number = 0; - for (let i = 0; i < parentChildren.length; i++) { - if (parentChildren[i].getId() === node.getId()) { - nodeToReplace = i; - } + + return; + } + + const child: Node = children[0]; + const pruned: Node = removeNode(node); + if (pruned.getId() === node.getId()) { + removeCompletelyOverlappingNodes(child, pruned); + return; + } + + if (isEmpty(parentNode)) { + return; + } + + const parentChildren = parentNode.getChildren(); + let nodeToReplace: number = 0; + for (let i = 0; i < parentChildren.length; i++) { + if (parentChildren[i].getId() === node.getId()) { + nodeToReplace = i; } - - parentChildren[nodeToReplace] = pruned; - parentNode.setChildren(parentChildren); - removeCompletelyOverlappingNodes(pruned, parentNode); - }; - \ No newline at end of file + } + + parentChildren[nodeToReplace] = pruned; + removeCompletelyOverlappingNodes(pruned, parentNode); +}; diff --git a/core/src/bricks/util.ts b/core/src/bricks/util.ts index 9f192ef..2fa0177 100644 --- a/core/src/bricks/util.ts +++ b/core/src/bricks/util.ts @@ -4,6 +4,15 @@ import { Option } from "./node"; const toOneDecimal = (num: number): number => Math.round(num * 10) / 10; +// backgroundColorFilter filters background color +export const backgroundColorFilter = (key: string, _: string): boolean => { + if (key === "background-color") { + return false; + } + + return true; +}; + // absolutePositioningFilter filters non absolute positioning related attributes export const absolutePositioningFilter = (key: string, _: string): boolean => { const absolutePositioningFilters: string[] = [ @@ -21,6 +30,23 @@ export const absolutePositioningFilter = (key: string, _: string): boolean => { return false; }; +// marignFilter filters non marign related attributes +export const marignFilter = (key: string, _: string): boolean => { + const marginFilter: string[] = [ + "margin-left", + "margin-right", + "margin-top", + "margin-bottom", + ]; + + if (marginFilter.includes(key)) { + return true; + } + + return false; +}; + + // values taken from different sources could have a lot of fractional digits. // for readability purposes, these numbers should be truncated export const truncateNumbers = (value: string): string => { @@ -81,10 +107,19 @@ export const filterAttributes = ( modifiers.push(truncateNumbers); } - if (option.absolutePositioningOnly) { + if (option.excludeBackgroundColor) { + filters.push(backgroundColorFilter); + } + + + if (option.absolutePositioningFilter) { filters.push(absolutePositioningFilter); } + if (option.marginFilter) { + filters.push(marignFilter); + } + if (isEmpty(modifiers) && isEmpty(filters)) { return attributes; } @@ -119,7 +154,7 @@ export const filterCssValue = (cssValue: string, option: Option): string => { filters.push(zeroValueFilter); } - if (option.absolutePositioningOnly) { + if (option.absolutePositioningFilter) { filters.push(absolutePositioningFilter); } @@ -137,7 +172,7 @@ export const filterCssValue = (cssValue: string, option: Option): string => { let pass: boolean = true; for (const filterFunction of filters) { - pass = pass && filterFunction("", cssValue); + pass = pass || filterFunction("", cssValue); } if (!pass) { @@ -150,40 +185,4 @@ export const filterCssValue = (cssValue: string, option: Option): string => { } return updated; -}; - -// // calculateOutsidePercentage calculates the area of the target rectangle that is outside of the current rectangle -// // as percentage of the current rectangle's area -// export const calculateOutsidePercentage = (target: BoxCoordinates, current: BoxCoordinates): number => { -// // Calculate the area of rectangle a -// const areaOfA = calculateArea(target); - -// // Calculate the area of the intersection between a and b -// const intersectionArea = calculateIntersectionArea(target, current); - -// // Calculate the area of rectangle b -// const areaOfB = calculateArea(current); - -// // Calculate the area of rectangle a that is outside of rectangle b -// const areaOfAOutsideB = areaOfA - intersectionArea; - -// // Calculate the percentage of area of a that is outside of b as a percentage of b's overall area -// return (areaOfAOutsideB / areaOfB) * 100; -// }; - -// const calculateArea = (box: BoxCoordinates): number => { -// const { leftTop, leftBot, rightTop } = box; -// const width = Math.abs(rightTop.x - leftTop.x); -// const height = Math.abs(leftTop.y - leftBot.y); -// return width * height; -// }; - -// const calculateIntersectionArea = (a: BoxCoordinates, b: BoxCoordinates): number => { -// const left = Math.max(a.leftTop.x, b.leftTop.x); -// const right = Math.min(a.rightTop.x, b.rightTop.x); -// const top = Math.max(a.leftTop.y, b.leftTop.y); -// const bottom = Math.min(a.leftBot.y, b.leftBot.y); -// const width = Math.max(right - left, 0); -// const height = Math.max(bottom - top, 0); -// return width * height; -// }; +}; \ No newline at end of file diff --git a/core/src/code/extra-file-registry/extra-file-registry.ts b/core/src/code/extra-file-registry/extra-file-registry.ts index 8065588..4b124a8 100644 --- a/core/src/code/extra-file-registry/extra-file-registry.ts +++ b/core/src/code/extra-file-registry/extra-file-registry.ts @@ -77,7 +77,6 @@ export const gatherExtraFilesAndImportedComponentsMeta = (node: Node) => { const vectorNode: VectorNode = node as VectorNode; extraFileRegistryGlobalInstance.addExportableFile(vectorNode); extraFileRegistryGlobalInstance.addImportStatement(vectorNode); - return; } if (node.getType() === NodeType.VECTOR_GROUP) { diff --git a/core/src/code/generator/css/generator.ts b/core/src/code/generator/css/generator.ts index ce124df..2e98733 100644 --- a/core/src/code/generator/css/generator.ts +++ b/core/src/code/generator/css/generator.ts @@ -6,6 +6,7 @@ import { getFileExtensionFromLanguage, constructExtraFiles, snakeCaseToCamelCase, + shouldUseAsBackgroundImage, } from "../util"; import { Generator as HtmlGenerator, @@ -19,6 +20,7 @@ import { computeGoogleFontURL } from "../../../google/google-fonts"; import { filterAttributes } from "../../../bricks/util"; import { getVariablePropForCss } from "../../../../ee/code/prop"; import { extraFileRegistryGlobalInstance } from "../../extra-file-registry/extra-file-registry"; +import { nameRegistryGlobalInstance } from "../../name-registry/name-registry"; export class Generator { htmlGenerator: HtmlGenerator; @@ -125,9 +127,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { return convertCssClassesToInlineStyle( { ...node.getCssAttributes(), - ...filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + ...node.getPositionalCssAttributes(), }, option, node.getId() @@ -142,6 +142,14 @@ const getPropsFromNode = (node: Node, option: Option): string => { node.getId() ); case NodeType.VISIBLE: + const attribtues: Attributes = node.getCssAttributes(); + if (isEmpty(node.getChildren()) && !isEmpty(attribtues)) { + const height: string = attribtues["height"]; + attribtues["min-height"] = height; + delete attribtues["height"]; + node.setCssAttributes(attribtues); + } + return convertCssClassesToInlineStyle( { ...node.getCssAttributes(), @@ -151,31 +159,87 @@ const getPropsFromNode = (node: Node, option: Option): string => { node.getId() ); case NodeType.IMAGE: - return convertCssClassesToInlineStyle( - filterAttributes( + if (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.png')`, + }); + } + + if (isEmpty(node.getChildren())) { + return convertCssClassesToInlineStyle( { - ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), }, - { - absolutePositioningOnly: true, - } - ), + option, + node.getId(), + ); + } + + return convertCssClassesToInlineStyle( + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, option, node.getId() ); case NodeType.VECTOR: + if (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.svg')`, + }); + } + + if (isEmpty(node.getChildren())) { + return convertCssClassesToInlineStyle( + { + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, + option, + node.getId(), + ); + } + return convertCssClassesToInlineStyle( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, option, node.getId() ); + // TODO: VECTOR_GROUP node type is deprecated case NodeType.VECTOR_GROUP: - return convertCssClassesToInlineStyle( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, + return convertCssClassesToInlineStyle({ + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, }), + }, option, node.getId() ); diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index bd961a7..7458b2d 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -101,12 +101,13 @@ export class Generator { option ); + // TODO: VECTOR_GROUP node type is deprecated case NodeType.VECTOR_GROUP: const vectorGroupNode = node as VectorGroupNode; const vectorGroupCodeString = await this.generateHtmlElementForVectorNode(vectorGroupNode, option); - return this.renderNodeWithAbsolutePosition( + return this.renderNodeWithPositionalAttributes( vectorGroupNode, vectorGroupCodeString, option @@ -119,9 +120,18 @@ export class Generator { option ); - return this.renderNodeWithAbsolutePosition( - vectorNode, - vectorCodeString, + if (isEmpty(node.getChildren())) { + return this.renderNodeWithPositionalAttributes( + vectorNode, + vectorCodeString, + option + ); + } + + const vectorNodeClassProps = this.getPropsFromNode(node, option); + return await this.generateHtmlFromNodes( + node.getChildren(), + [`
        `, `
        `], option ); case NodeType.IMAGE: @@ -237,16 +247,16 @@ export class Generator { } return [ - this.renderNodeWithAbsolutePosition( + this.renderNodeWithPositionalAttributes( node, - `${alt}`, + `${alt}`, option ), ]; } return [ - this.renderNodeWithAbsolutePosition( + this.renderNodeWithPositionalAttributes( node, `${alt}`, option @@ -261,16 +271,22 @@ export class Generator { return [`
        `, `
        `]; } - renderNodeWithAbsolutePosition( + renderNodeWithPositionalAttributes( node: ImageNode | VectorNode | VectorGroupNode, inner: string, option: Option ): string { const positionalCssAttribtues: Attributes = node.getPositionalCssAttributes(); - if (positionalCssAttribtues["position"] === "absolute") { + + if (positionalCssAttribtues["position"] === "absolute" || + positionalCssAttribtues["margin-left"] || + positionalCssAttribtues["margin-right"] || + positionalCssAttribtues["margin-top"] || + positionalCssAttribtues["margin-bottom"]) { return `
        ` + inner + `
        `; } + return inner; } diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index ad4a7e9..5b9fa43 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -123,8 +123,9 @@ export const buildTwcssConfigFileContent = ( return file; }; -const largestTWCHeightInPixels = 384; -const largestTWCWidthInPixels = 384; +const largestTwcssHeightInPixels = 384; +const largestTwcssWidthInPixels = 384; +const largestTwcssLineheightInPixels = 45; // url("./assets/image-1.png") -> "image-1" export const getImageFileNameFromUrl = (path: string) => { @@ -436,7 +437,7 @@ export const getTwcssClass = ( switch (cssProperty) { case "height": const heightNum = extractPixelNumberFromString(cssValue); - if (heightNum > largestTWCHeightInPixels) { + if (cssValue.endsWith("px") && heightNum > largestTwcssHeightInPixels) { return `h-[${heightNum}px]`; } @@ -457,7 +458,7 @@ export const getTwcssClass = ( case "width": const widthNum = extractPixelNumberFromString(cssValue); - if (widthNum > largestTWCWidthInPixels) { + if (cssValue.endsWith("px") && widthNum > largestTwcssWidthInPixels) { return `w-[${widthNum}px]`; } @@ -829,6 +830,11 @@ export const getTwcssClass = ( } case "line-height": { + const lineHeightNum = extractPixelNumberFromString(cssValue); + if (cssValue.endsWith("px") && lineHeightNum > largestTwcssLineheightInPixels) { + return `leading-[${lineHeightNum}px]`; + } + return findClosestTwcssLineHeight(cssValue); } diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index 69366ae..5088e75 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -20,6 +20,8 @@ import { import { Generator as ReactGenerator } from "../react/generator"; import { filterAttributes } from "../../../bricks/util"; import { extraFileRegistryGlobalInstance } from "../../extra-file-registry/extra-file-registry"; +import { nameRegistryGlobalInstance } from "../../name-registry/name-registry"; +import { shouldUseAsBackgroundImage } from "../util"; import { Attributes } from "../../../design/adapter/node"; export class Generator { @@ -102,9 +104,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { case NodeType.TEXT: { const attributes: Attributes = { ...node.getCssAttributes(), - ...filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + ...node.getPositionalCssAttributes(), }; //@ts-ignore @@ -141,29 +141,91 @@ const getPropsFromNode = (node: Node, option: Option): string => { ); case NodeType.IMAGE: + if (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.png')`, + }); + } + + if (isEmpty(node.getChildren())) { + return convertCssClassesToTwcssClasses( + { + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, + option, + node.getId(), + ); + } + return convertCssClassesToTwcssClasses( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, option, - node.getId() + node.getId(), ); case NodeType.VECTOR: + if (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.svg')`, + }); + } + + if (isEmpty(node.getChildren())) { + return convertCssClassesToTwcssClasses( + { + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, + option, + node.getId(), + ); + } + return convertCssClassesToTwcssClasses( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, option, - node.getId() + node.getId(), ); + // TODO: VECTOR_GROUP node type is deprecated case NodeType.VECTOR_GROUP: return convertCssClassesToTwcssClasses( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), + { + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, option, - node.getId() + node.getId(), ); default: @@ -194,6 +256,12 @@ export const buildTwcssConfigFileContent = ( importComponent.importPath )}": "url(.${importComponent.importPath})",`; } + + if (extension === "svg" && shouldUseAsBackgroundImage(importComponent.node)) { + backgroundImages += `"${getImageFileNameFromUrl( + importComponent.importPath + )}": "url(.${importComponent.importPath})",`; + } }); } diff --git a/core/src/code/generator/util.ts b/core/src/code/generator/util.ts index f3f84a4..82b1ed0 100644 --- a/core/src/code/generator/util.ts +++ b/core/src/code/generator/util.ts @@ -2,6 +2,7 @@ import { isEmpty } from "../../utils"; import { ImportedComponentMeta } from "./html/generator"; import { ExportFormat } from "../../design/adapter/node"; import { Option, File, UiFramework, Language } from "../code"; +import { Node, NodeType } from "../../bricks/node"; // getFileExtensionFromLanguage determines file extension for the main file depending on the input option export const getFileExtensionFromLanguage = (option: Option): string => { @@ -96,3 +97,15 @@ export const snakeCaseToCamelCase = (prop: string) => { return camel.join(""); }; + +export const shouldUseAsBackgroundImage = (node: Node): boolean => { + if (node.getType() === NodeType.VECTOR && !isEmpty(node.getChildren())) { + return true; + } + + if (node.getType() === NodeType.IMAGE && !isEmpty(node.getChildren())) { + return true; + } + + return false; +}; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 3f8624d..c04a304 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -4,9 +4,9 @@ import { ImageNode, Node, TextNode as BricksTextNode, - VectorGroupNode, VectorNode as BricksVector, VisibleNode, + computePositionalRelationship, } from "../../../bricks/node"; import { isEmpty } from "../../../utils"; import { BoxCoordinates, Attributes, ExportFormat, ListSegment } from "../node"; @@ -20,19 +20,56 @@ import { getRgbaFromPaints, } from "./util"; import { GoogleFontsInstance } from "../../../google/google-fonts"; +import { PostionalRelationship } from "../../../bricks/node"; +import { getLineBasedOnDirection } from "../../../bricks/line"; +import { Direction } from "../../../bricks/direction"; import { StyledTextSegment } from "../node"; enum NodeType { GROUP = "GROUP", TEXT = "TEXT", + IMAGE = "IMAGE", VECTOR = "VECTOR", ELLIPSE = "ELLIPSE", FRAME = "FRAME", RECTANGLE = "RECTANGLE", INSTANCE = "INSTANCE", + STAR = "STAR", + SLICE = "SLICE", COMPONENT = "COMPONENT", + BOOLEAN_OPERATION = "BOOLEAN_OPERATION" } + +const safelySetWidthAndHeight = (nodeType: string, figmaNode: SceneNode, attributes: Attributes) => { + if (nodeType === NodeType.FRAME || nodeType === NodeType.IMAGE || nodeType === NodeType.GROUP || nodeType === NodeType.INSTANCE) { + if (!isEmpty(figmaNode.absoluteBoundingBox)) { + attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; + attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; + // @ts-ignore + } else if (!isEmpty(figmaNode.absoluteRenderBounds)) { + // @ts-ignore + attributes["width"] = `${figmaNode.absoluteRenderBounds.width}px`; + // @ts-ignore + attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; + } + + return; + } + + // @ts-ignore + if (!isEmpty(figmaNode.absoluteRenderBounds)) { + // @ts-ignore + attributes["width"] = `${figmaNode.absoluteRenderBounds.width}px`; + // @ts-ignore + attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; + } else if (!isEmpty(figmaNode.absoluteBoundingBox)) { + attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; + attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; + } + +}; + const addDropShadowCssProperty = ( figmaNode: | GroupNode @@ -51,9 +88,8 @@ const addDropShadowCssProperty = ( .map((effect: DropShadowEffect | InnerShadowEffect) => { const { offset, radius, spread, color } = effect; - const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${ - spread ?? 0 - }px ${rgbaToString(color)}`; + const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0 + }px ${rgbaToString(color)}`; if (effect.type === "INNER_SHADOW") { return "inset " + dropShadowString; @@ -107,7 +143,7 @@ const getPositionalCssAttributes = (figmaNode: SceneNode): Attributes => { switch (figmaNode.primaryAxisAlignItems) { case "MIN": - attributes["justify-content"] = "start"; + attributes["justify-content"] = "flex-start"; break; case "CENTER": attributes["justify-content"] = "center"; @@ -116,24 +152,23 @@ const getPositionalCssAttributes = (figmaNode: SceneNode): Attributes => { attributes["justify-content"] = "space-between"; break; case "MAX": - attributes["justify-content"] = "end"; + attributes["justify-content"] = "flex-end"; break; } switch (figmaNode.counterAxisAlignItems) { case "MIN": - attributes["align-items"] = "start"; + attributes["align-items"] = "flex-start"; break; case "CENTER": attributes["align-items"] = "center"; break; case "MAX": - attributes["align-items"] = "end"; + attributes["align-items"] = "flex-end"; break; } - if (figmaNode.children.length > 1) { - // gap has no effects when only there is only one child + if (figmaNode.itemSpacing) { attributes["gap"] = `${figmaNode.itemSpacing}px`; } @@ -162,11 +197,8 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { const attributes: Attributes = {}; if (figmaNode.type === NodeType.GROUP) { - // width - attributes["width"] = `${figmaNode.absoluteRenderBounds.width}px`; + safelySetWidthAndHeight(figmaNode.type, figmaNode, attributes); - // height - attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; addDropShadowCssProperty(figmaNode, attributes); } @@ -174,12 +206,20 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { figmaNode.type === NodeType.VECTOR || figmaNode.type === NodeType.ELLIPSE ) { - if (!isEmpty(figmaNode.absoluteRenderBounds)) { - // width - attributes["width"] = `${figmaNode.absoluteRenderBounds.width}px`; + safelySetWidthAndHeight(figmaNode.type, figmaNode, attributes); - // height - attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; + const fills = figmaNode.fills; + if (fills !== figma.mixed && fills.length > 0 && fills[0].visible) { + // background color + const solidPaint = fills.find( + (fill) => fill.type === "SOLID" + ) as SolidPaint; + if (solidPaint) { + attributes["background-color"] = colorToStringWithOpacity( + solidPaint.color, + solidPaint.opacity + ); + } } } @@ -315,29 +355,55 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // width and height const { absoluteRenderBounds, absoluteBoundingBox } = figmaNode; - const boundingBoxWidth = figmaNode.absoluteBoundingBox.width; - const renderBoundsWidth = figmaNode.absoluteRenderBounds.width; - const renderBoundsHeight = figmaNode.absoluteRenderBounds.height; + let width: number = absoluteRenderBounds ? absoluteRenderBounds.width + 2 : absoluteBoundingBox.width; + let height: number = absoluteRenderBounds ? absoluteRenderBounds.height : absoluteBoundingBox.height; let moreThanOneRow: boolean = false; - if (figmaNode.fontSize !== figma.mixed) { - moreThanOneRow = renderBoundsHeight > figmaNode.fontSize; - } + if (absoluteRenderBounds) { + const renderBoundsHeight = absoluteRenderBounds.height; + + if (figmaNode.fontSize !== figma.mixed) { + moreThanOneRow = renderBoundsHeight > figmaNode.fontSize; + } - if (!moreThanOneRow) { - attributes["white-space"] = "nowrap"; + if (!moreThanOneRow) { + attributes["white-space"] = "nowrap"; + } } - let width = absoluteRenderBounds.width + 2; + if (absoluteBoundingBox && absoluteRenderBounds) { + const renderBoundsWidth = absoluteRenderBounds.width; + const boundingBoxWidth = absoluteBoundingBox.width; + + // If bounding box and rendering box are similar in size, horizontal text alignment doesn't have any + // actual effects therefore should be always considered as "text-align": "left" when there is only one row + if ( + Math.abs(boundingBoxWidth - renderBoundsWidth) / boundingBoxWidth > 0.1 || + moreThanOneRow + ) { + // text alignment + switch (figmaNode.textAlignHorizontal) { + case "CENTER": + attributes["text-align"] = "center"; + break; + case "RIGHT": + attributes["text-align"] = "right"; + break; + case "JUSTIFIED": + attributes["text-align"] = "justify"; + break; + } + } - if ( - Math.abs( - figmaNode.absoluteBoundingBox.width - absoluteRenderBounds.width - ) / - figmaNode.absoluteBoundingBox.width > - 0.2 - ) { - width = absoluteRenderBounds.width + 4; + if ( + Math.abs( + absoluteBoundingBox.width - absoluteRenderBounds.width + ) / + absoluteBoundingBox.width > + 0.2 + ) { + width = absoluteRenderBounds.width + 4; + } } // @ts-ignore @@ -345,30 +411,51 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { attributes["width"] = `${absoluteBoundingBox.width}px`; } - if (moreThanOneRow) { - switch (figmaNode.textAutoResize) { - case "NONE": { - attributes["width"] = `${width}px`; - attributes["height"] = `${absoluteRenderBounds.height}px`; - break; - } - case "HEIGHT": { - attributes["width"] = `${width}px`; - break; - } - case "WIDTH_AND_HEIGHT": { - // do nothing - break; - } - case "TRUNCATE": { - attributes["width"] = `${width}px`; - attributes["height"] = `${absoluteRenderBounds.height}px`; - attributes["text-overflow"] = "ellipsis"; - break; - } + switch (figmaNode.textAutoResize) { + case "NONE": { + attributes["width"] = `${width}px`; + // attributes["height"] = `${height}px`; + break; + } + case "HEIGHT": { + attributes["width"] = `${width}px`; + break; + } + case "WIDTH_AND_HEIGHT": { + // do nothing + attributes["width"] = `${width}px`; + break; + } + case "TRUNCATE": { + attributes["width"] = `${width}px`; + // attributes["height"] = `${height}px`; + attributes["text-overflow"] = "ellipsis"; + break; } } + // switch (figmaNode.textAutoResize) { + // // case "NONE": { + // // attributes["width"] = `${width}px`; + // // attributes["height"] = `${absoluteRenderBounds.height}px`; + // // break; + // // } + // // case "HEIGHT": { + // // attributes["width"] = `${width}px`; + // // break; + // // } + // case "WIDTH_AND_HEIGHT": { + // attributes["width"] = `${width}px`; + // break; + // } + // // case "TRUNCATE": { + // // attributes["width"] = `${width}px`; + // // attributes["height"] = `${absoluteRenderBounds.height}px`; + // // attributes["text-overflow"] = "ellipsis"; + // // break; + // // } + // } + // text decoration switch (figmaNode.textDecoration) { case "STRIKETHROUGH": @@ -412,26 +499,6 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { attributes["overflow-wrap"] = "break-word"; } - // If bounding box and rendering box are similar in size, horizontal text alignment doesn't have any - // actual effects therefore should be always considered as "text-align": "left" when there is only one row - if ( - Math.abs(boundingBoxWidth - renderBoundsWidth) / boundingBoxWidth > 0.1 || - moreThanOneRow - ) { - // text alignment - switch (figmaNode.textAlignHorizontal) { - case "CENTER": - attributes["text-align"] = "center"; - break; - case "RIGHT": - attributes["text-align"] = "right"; - break; - case "JUSTIFIED": - attributes["text-align"] = "justify"; - break; - } - } - /* TODO: This field is causing styling differences between Figma design and rendered Bricks components. @@ -521,6 +588,7 @@ export class FigmaNodeAdapter { constructor(node: SceneNode) { this.node = node; this.cssAttributes = getCssAttributes(node); + this.positionalCssAttribtues = getPositionalCssAttributes(node); } @@ -617,8 +685,8 @@ export class FigmaNodeAdapter { } export class FigmaVectorNodeAdapter extends FigmaNodeAdapter { - node: VectorNode; - constructor(node: VectorNode) { + node: SceneNode; + constructor(node: SceneNode) { super(node); this.node = node; } @@ -721,22 +789,50 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { const EXPORTABLE_NODE_TYPES: string[] = [ NodeType.ELLIPSE, NodeType.VECTOR, + NodeType.IMAGE, + NodeType.INSTANCE, + NodeType.GROUP, + NodeType.STAR, NodeType.FRAME, NodeType.RECTANGLE, + NodeType.BOOLEAN_OPERATION, ]; -type Feedbacks = { +const VECTOR_NODE_TYPES: string[] = [ + NodeType.ELLIPSE, + NodeType.VECTOR, + NodeType.STAR, + NodeType.BOOLEAN_OPERATION, + NodeType.RECTANGLE, +]; + +type Feedback = { nodes: Node[]; areAllNodesExportable: boolean; + doNodesContainImage: boolean; + doNodesHaveNonOverlappingChildren: boolean; + isSingleRectangle: boolean; }; // convertFigmaNodesToBricksNodes converts Figma nodes to Bricks export const convertFigmaNodesToBricksNodes = ( figmaNodes: readonly SceneNode[] -): Feedbacks => { +): Feedback => { let reordered = [...figmaNodes]; + if (reordered.length > 1) { reordered.sort((a, b) => { + let wrappedNodeA: Node = new VisibleNode(new FigmaNodeAdapter(a)); + let wrappedNodeB: Node = new VisibleNode(new FigmaNodeAdapter(b)); + + if (computePositionalRelationship(wrappedNodeA.getAbsRenderingBox(), wrappedNodeB.getAbsRenderingBox()) === PostionalRelationship.INCLUDE) { + return 1; + } + + if (computePositionalRelationship(wrappedNodeB.getAbsRenderingBox(), wrappedNodeA.getAbsRenderingBox()) === PostionalRelationship.INCLUDE) { + return -1; + } + if (a.parent.children.indexOf(a) < b.parent.children.indexOf(b)) { return -1; } @@ -745,11 +841,24 @@ export const convertFigmaNodesToBricksNodes = ( }); } - let result: Feedbacks = { + let result: Feedback = { nodes: [], areAllNodesExportable: true, + doNodesContainImage: false, + doNodesHaveNonOverlappingChildren: false, + isSingleRectangle: false, }; + let sliceNode: SceneNode = null; + let allNodesAreOfVectorNodeTypes: boolean = true; + + if (reordered.length === 1) { + const figmaNode = reordered[0]; + if (figmaNode.type === NodeType.RECTANGLE) { + result.isSingleRectangle = true; + } + } + for (let i = 0; i < reordered.length; i++) { const figmaNode = reordered[i]; @@ -758,13 +867,20 @@ export const convertFigmaNodesToBricksNodes = ( result.areAllNodesExportable = false; } - let newNode: Node = new VisibleNode(new FigmaNodeAdapter(figmaNode)); + if (!VECTOR_NODE_TYPES.includes(figmaNode.type)) { + allNodesAreOfVectorNodeTypes = false; + } + if (figmaNode.type === NodeType.SLICE) { + sliceNode = figmaNode; + } + + let newNode: Node = new VisibleNode(new FigmaNodeAdapter(figmaNode)); switch (figmaNode.type) { case NodeType.RECTANGLE: if (doesNodeContainsAnImage(figmaNode)) { newNode = new ImageNode(new FigmaImageNodeAdapter(figmaNode)); - result.areAllNodesExportable = false; + result.doNodesContainImage = true; } break; case NodeType.GROUP: @@ -781,36 +897,92 @@ export const convertFigmaNodesToBricksNodes = ( newNode = new BricksTextNode(new FigmaTextNodeAdapter(figmaNode)); break; case NodeType.VECTOR: + case NodeType.STAR: newNode = new BricksVector(new FigmaVectorNodeAdapter(figmaNode)); break; case NodeType.ELLIPSE: if (doesNodeContainsAnImage(figmaNode)) { newNode = new ImageNode(new FigmaImageNodeAdapter(figmaNode)); - result.areAllNodesExportable = false; + result.doNodesContainImage = true; break; } } //@ts-ignore if (!isEmpty(figmaNode?.children)) { + let isExportableNode: boolean = false; //@ts-ignore - const feedbacks = convertFigmaNodesToBricksNodes(figmaNode.children); - if (feedbacks.areAllNodesExportable) { - newNode = new VectorGroupNode( - new FigmaVectorGroupNodeAdapter(figmaNode), - feedbacks.nodes - ); + const feedback = convertFigmaNodesToBricksNodes(figmaNode.children); + if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { + if (!feedback.doNodesHaveNonOverlappingChildren) { + isExportableNode = true; + if (feedback.doNodesContainImage) { + newNode = new ImageNode( + new FigmaVectorGroupNodeAdapter(figmaNode), + ); + } else { + newNode = new BricksVector( + new FigmaVectorGroupNodeAdapter(figmaNode), + ); + } + } } result.areAllNodesExportable = - feedbacks.areAllNodesExportable && result.areAllNodesExportable; + feedback.areAllNodesExportable && result.areAllNodesExportable; + + result.doNodesContainImage = + feedback.doNodesContainImage || result.doNodesContainImage; - newNode.setChildren(feedbacks.nodes); + if (!isExportableNode) { + newNode.setChildren(feedback.nodes); + } } result.nodes.push(newNode); } } + let horizontalOverlap: boolean = areNodesOverlappingByDirection(result.nodes, Direction.HORIZONTAL); + let verticalOverlap: boolean = areNodesOverlappingByDirection(result.nodes, Direction.VERTICAL); + result.doNodesHaveNonOverlappingChildren = !horizontalOverlap || !verticalOverlap; + + if (allNodesAreOfVectorNodeTypes) { + result.doNodesHaveNonOverlappingChildren = false; + } + + if (!isEmpty(sliceNode)) { + result.nodes = [new BricksVector(new FigmaVectorNodeAdapter(sliceNode))]; + result.doNodesHaveNonOverlappingChildren = false; + result.areAllNodesExportable = false; + } + return result; }; + +const areNodesOverlappingByDirection = (nodes: Node[], direction: Direction): boolean => { + let overlap: boolean = false; + for (let i = 0; i < nodes.length; i++) { + const currentNode: Node = nodes[i]; + let currentLine = getLineBasedOnDirection(currentNode, direction); + + for (let j = 0; j < nodes.length; j++) { + const targetNode: Node = nodes[j]; + if (targetNode.getId() === currentNode.getId()) { + continue; + } + + const targetLine = getLineBasedOnDirection(targetNode, direction); + if (currentLine.overlap(targetLine)) { + overlap = true; + break; + } + } + + if (overlap) { + break; + } + } + + return overlap; +}; diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 63d6d78..65656e2 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -52,8 +52,10 @@ export const doesNodeContainsAnImage = ( node: RectangleNode | EllipseNode ): boolean => { if (node.fills != figma.mixed) { - if (!isEmpty(node.fills) && node.fills[0].type === "IMAGE") { - return true; + for (const fill of node.fills) { + if (fill.type === "IMAGE") { + return true; + } } } diff --git a/core/src/index.ts b/core/src/index.ts index cfd5181..23c6745 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -9,7 +9,7 @@ import { getNameMap } from "../ee/web/request"; import { instantiateNameRegistryGlobalInstance } from "./code/name-registry/name-registry"; import { instantiateOptionRegistryGlobalInstance } from "./code/option-registry/option-registry"; import { instantiateFontsRegistryGlobalInstance } from "./code/generator/tailwindcss/fonts-registry"; -import { removeCompletelyOverlappingNodes, removeNode } from "./bricks/remove-node"; +import { removeChildrenNode, removeCompletelyOverlappingNodes, removeNode } from "./bricks/remove-node"; import { isEmpty, replaceVariableNameWithinFile, trackEvent } from "./utils"; import { instantiateCodeSampleRegistryGlobalInstance } from "../ee/loop/code-sample-registry"; import { instantiateDataArrRegistryGlobalInstance } from "../ee/loop/data-array-registry"; @@ -28,18 +28,26 @@ export const convertToCode = async ( return []; } + const dedupedNodes: Node[] = []; + for (const node of converted) { + let newNode: Node = removeNode(node); + removeCompletelyOverlappingNodes(newNode, null); + removeChildrenNode(newNode); + dedupedNodes.push(newNode); + } + let startingNode: Node = - converted.length > 1 ? new GroupNode(converted) : converted[0]; + dedupedNodes.length > 1 ? new GroupNode(dedupedNodes) : dedupedNodes[0]; groupNodes(startingNode); startingNode = removeNode(startingNode); removeCompletelyOverlappingNodes(startingNode, null); + removeChildrenNode(startingNode); addAdditionalCssAttributesToNodes(startingNode); instantiateRegistries(startingNode, option); - return await generateCodingFiles(startingNode, option); }; @@ -54,13 +62,22 @@ export const convertToCodeWithAi = async ( return [[], []]; } + const dedupedNodes: Node[] = []; + for (const node of converted) { + let newNode: Node = removeNode(node); + removeCompletelyOverlappingNodes(newNode, null); + removeChildrenNode(newNode); + dedupedNodes.push(newNode); + } + let startingNode: Node = - converted.length > 1 ? new GroupNode(converted) : converted[0]; + dedupedNodes.length > 1 ? new GroupNode(dedupedNodes) : dedupedNodes[0]; groupNodes(startingNode); startingNode = removeNode(startingNode); removeCompletelyOverlappingNodes(startingNode, null); + removeChildrenNode(startingNode); addAdditionalCssAttributesToNodes(startingNode); diff --git a/figma/src/code.ts b/figma/src/code.ts index b880664..721c241 100644 --- a/figma/src/code.ts +++ b/figma/src/code.ts @@ -1,7 +1,4 @@ -import { - convertToCode, - convertToCodeWithAi, -} from "bricks-core/src"; +import { convertToCode, convertToCodeWithAi } from "bricks-core/src"; import { isEmpty } from "bricks-core/src/utils"; import { init, Identify, identify, track } from "@amplitude/analytics-browser"; import { EVENT_ERROR } from "./analytic/amplitude"; @@ -55,11 +52,14 @@ figma.ui.onmessage = async (msg) => { if (msg.type === "generate-code-with-ai") { try { - const [files, applications] = await convertToCodeWithAi(figma.currentPage.selection, { - language: msg.options.language, - cssFramework: msg.options.cssFramework, - uiFramework: msg.options.uiFramework, - }); + const [files, applications] = await convertToCodeWithAi( + figma.currentPage.selection, + { + language: msg.options.language, + cssFramework: msg.options.cssFramework, + uiFramework: msg.options.uiFramework, + } + ); figma.ui.postMessage({ type: "generated-files", @@ -154,7 +154,6 @@ figma.ui.onmessage = async (msg) => { }; figma.on("selectionchange", async () => { - figma.ui.postMessage({ type: "selection-change", isComponentSelected: figma.currentPage.selection.length > 0, diff --git a/figma/src/constants.ts b/figma/src/constants.ts index 211ddf3..1139135 100644 --- a/figma/src/constants.ts +++ b/figma/src/constants.ts @@ -16,7 +16,6 @@ export enum UiFramework { html = "html", } - export enum GenerationMethod { withai = "withai", withoutai = "without-ai", @@ -27,8 +26,7 @@ export enum CssFramework { tailwindcss = "tailwindcss", } - export enum AiApplication { componentIdentification = "componentIdentification", autoNaming = "autoNaming", -} \ No newline at end of file +} diff --git a/figma/src/pages/code-generation-status.tsx b/figma/src/pages/code-generation-status.tsx index dc07d09..baa0145 100644 --- a/figma/src/pages/code-generation-status.tsx +++ b/figma/src/pages/code-generation-status.tsx @@ -6,7 +6,7 @@ export interface Props { isGeneratingCode: boolean; isGeneratingCodeWithAi: boolean; selectedUiFramework: UiFramework; - limit: number, + limit: number; selectedGenerationMethod: GenerationMethod; } @@ -21,7 +21,11 @@ const CodeGenerationStatus = ({ useEffect(() => { if (!isGeneratingCode && !isGeneratingCodeWithAi) { - if (selectedGenerationMethod === GenerationMethod.withai && selectedUiFramework !== UiFramework.html && limit !== 0) { + if ( + selectedGenerationMethod === GenerationMethod.withai && + selectedUiFramework !== UiFramework.html && + limit !== 0 + ) { setCurrentPage(PAGES.POST_CODE_GENERATION_AI); return; } @@ -32,7 +36,7 @@ const CodeGenerationStatus = ({ const generatingCodeText = isGeneratingCodeWithAi ? (

        Generating Code With AI.
        - This could take up to 1 minute.
        + This could take up to 3 minute.
        Please wait patiently.

        ) : ( diff --git a/figma/src/pages/code-output-setting.tsx b/figma/src/pages/code-output-setting.tsx index fb34c9f..4a079d0 100644 --- a/figma/src/pages/code-output-setting.tsx +++ b/figma/src/pages/code-output-setting.tsx @@ -1,7 +1,13 @@ import React, { useContext } from "react"; import { RadioGroup as BaseRadioGroup } from "@headlessui/react"; import PageContext, { PAGES } from "../context/page-context"; -import { CssFramework, GenerationMethod, Language, Settings, UiFramework } from "../constants"; +import { + CssFramework, + GenerationMethod, + Language, + Settings, + UiFramework, +} from "../constants"; import * as logo from "../assets/arrow.png"; import htmlLogo from "../assets/html-logo.svg"; import lightbulbLogo from "../assets/light-bulb.svg"; @@ -22,7 +28,11 @@ type Option = { const GenerationMethods: Option[] = [ { id: GenerationMethod.withai, name: "With AI", logo: lightbulbLogo }, - { id: GenerationMethod.withoutai, name: "Without AI", logo: lightbulbDarkLogo }, + { + id: GenerationMethod.withoutai, + name: "Without AI", + logo: lightbulbDarkLogo, + }, ]; const UiFrameworks: Option[] = [ @@ -48,7 +58,7 @@ function updateSettings( uiFramework: string, cssFramework: string, language: string, - generationMethod: string, + generationMethod: string ) { parent.postMessage( { @@ -144,7 +154,7 @@ const CodeOutputSetting: React.FC = ({ limit, setSelectedLanguage, selectedGenerationMethod, - setSelectedGenerationMethod + setSelectedGenerationMethod, }) => { const { previousPage, setCurrentPage } = useContext(PageContext); @@ -153,7 +163,12 @@ const CodeOutputSetting: React.FC = ({ }; const handleSaveButtonClick = () => { - updateSettings(selectedUiFramework, selectedCssFramework, selectedLanguage, selectedGenerationMethod); + updateSettings( + selectedUiFramework, + selectedCssFramework, + selectedLanguage, + selectedGenerationMethod + ); setCurrentPage(PAGES.HOME); parent.postMessage( @@ -162,10 +177,10 @@ const CodeOutputSetting: React.FC = ({ type: "analytics", eventName: EVENT_SAVE_SETTINGS, eventProperties: { - uiFramework: selectedUiFramework, - cssFramework: selectedCssFramework, - language: selectedLanguage, - generationMethod: selectedGenerationMethod, + uiFramework: selectedUiFramework, + cssFramework: selectedCssFramework, + language: selectedLanguage, + generationMethod: selectedGenerationMethod, }, }, }, diff --git a/figma/src/pages/home.tsx b/figma/src/pages/home.tsx index 11fd0f1..1584cc2 100644 --- a/figma/src/pages/home.tsx +++ b/figma/src/pages/home.tsx @@ -1,7 +1,12 @@ import { useContext, PropsWithChildren } from "react"; import * as settingsLogo from "../assets/setting-logo.png"; import PageContext, { PAGES } from "../context/page-context"; -import { CssFramework, UiFramework, Language, GenerationMethod } from "../constants"; +import { + CssFramework, + UiFramework, + Language, + GenerationMethod, +} from "../constants"; import { EVENT_GENERATE_BUTTON_CLICK, EVENT_INSTALLATION_LINK_CLICK, @@ -146,7 +151,11 @@ const Home = (props: PropsWithChildren) => { const isGenerateCodeButtonEnabled = isComponentSelected && connectedToVSCode; const getGenerateCodeButton = () => { - if (selectedGenerationMethod === GenerationMethod.withai && limit > 0 && selectedUiFramework !== UiFramework.html) { + if ( + selectedGenerationMethod === GenerationMethod.withai && + limit > 0 && + selectedUiFramework !== UiFramework.html + ) { return ( : null} + {connectedToVSCode ? ( + + ) : null}
    {ranOutOfAiCredits} -
    + ); }; diff --git a/figma/src/pages/post-code-generation-ai.tsx b/figma/src/pages/post-code-generation-ai.tsx index d134596..dc41c36 100644 --- a/figma/src/pages/post-code-generation-ai.tsx +++ b/figma/src/pages/post-code-generation-ai.tsx @@ -6,14 +6,11 @@ import { isEmpty } from "bricks-core/src/utils"; export interface Props { limit: number; - aiApplications: AiApplication[], + aiApplications: AiApplication[]; } const PostCodeGenerationAi = (props: PropsWithChildren) => { - const { - limit, - aiApplications, - } = props; + const { limit, aiApplications } = props; const { setCurrentPage } = useContext(PageContext); @@ -56,9 +53,7 @@ const PostCodeGenerationAi = (props: PropsWithChildren) => { Here is how Ai is applied:

    - {applications.map((application) => ( - application - ))} + {applications.map((application) => application)}
    ); diff --git a/figma/src/ui.html b/figma/src/ui.html index 182020b..3895b82 100644 --- a/figma/src/ui.html +++ b/figma/src/ui.html @@ -5,6 +5,9 @@ --body-font: "'Roboto', sans-serif"; } - +
    diff --git a/figma/src/ui.tsx b/figma/src/ui.tsx index ec7e2ef..b6a82aa 100644 --- a/figma/src/ui.tsx +++ b/figma/src/ui.tsx @@ -9,7 +9,13 @@ import CodeOutputSetting from "./pages/code-output-setting"; import Error from "./pages/error"; import PageContext, { PAGES } from "./context/page-context"; import { io } from "socket.io-client"; -import { AiApplication, CssFramework, GenerationMethod, Language, UiFramework } from "./constants"; +import { + AiApplication, + CssFramework, + GenerationMethod, + Language, + UiFramework, +} from "./constants"; import { withTimeout } from "./utils"; import { EVENT_ERROR } from "./analytic/amplitude"; import { isEmpty } from "bricks-core/src/utils"; @@ -23,7 +29,10 @@ const UI = () => { const [connectedToVSCode, setConnectedToVSCode] = useState(false); const [isGeneratingCode, setIsGeneratingCode] = useState(false); const [isGeneratingCodeWithAi, setIsGeneratingCodeWithAi] = useState(false); - const [aiApplications, setAiApplications] = useState([AiApplication.componentIdentification, AiApplication.autoNaming]); + const [aiApplications, setAiApplications] = useState([ + AiApplication.componentIdentification, + AiApplication.autoNaming, + ]); const [limit, setLimit] = useState(0); // User settings @@ -179,7 +188,6 @@ const UI = () => { }, 1000); if (settings) { - } setSelectedLanguage(settings.language); @@ -348,7 +356,9 @@ const UI = () => { /> )} {currentPage === PAGES.POST_CODE_GENERATION && } - {currentPage === PAGES.POST_CODE_GENERATION_AI && } + {currentPage === PAGES.POST_CODE_GENERATION_AI && ( + + )} {currentPage === PAGES.ERROR && } From b74523aefd8d8b13148adbcd91e8fcdb0771917c Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Mon, 8 May 2023 23:50:55 -0700 Subject: [PATCH 12/21] replace slack with discord --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1fd7f50..ef5a42e 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,9 @@ Bricks is composed of a number of components. Below is a description of each com - `core`: engine that converts Figma nodes into coding files. ## License -Distributed under the Apache 2.0 License. See `LICENSE` for more information. +Distributed under the Apache 2.0 License and Bricks Enterprise License. See `LICENSE` for more information. ## Get in Touch Email: spike@bricks-tech.com -Join Bricks on Slack \ No newline at end of file +Join Bricks on Discord From fa8a42899fc2619fa34bcabc2e5d37b981fa7253 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Tue, 9 May 2023 11:51:24 -0400 Subject: [PATCH 13/21] add support for mixed letter spacing, fix bug with wrong
  • --- core/src/code/generator/html/generator.ts | 30 +++++++++++++++++++++-- core/src/design/adapter/figma/adapter.ts | 30 +++++++++++++++++------ core/src/design/adapter/figma/util.ts | 20 +++++++++++++++ core/src/design/adapter/node.ts | 1 + 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index bd961a7..ec2561f 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -348,6 +348,7 @@ export class Generator { const defaultFontFamily = textNode.getACssAttribute("font-family"); const defaultFontWeight = textNode.getACssAttribute("font-weight"); const defaultColor = textNode.getACssAttribute("color"); + const defaultLetterSpacing = textNode.getACssAttribute("letter-spacing"); const cssAttributesSegments = styledTextSegments.map( (styledTextSegment) => { @@ -384,6 +385,11 @@ export class Generator { cssAttributes["color"] = color; } + const letterSpacing = styledTextSegment.letterSpacing; + if (letterSpacing !== defaultLetterSpacing) { + cssAttributes["letter-spacing"] = letterSpacing; + } + return { start: styledTextSegment.start, end: styledTextSegment.end, @@ -438,11 +444,21 @@ export class Generator { segment.cssAttributes, option ); + + // if css attribute applies to the whole list item, wrap it in a
  • tag const htmlTag = + listTag !== "none" && listItemText.length === segment.end - segment.start ? "li" : "span"; + if (listTag === "none") { + // replace all \n in text with
    tag + text = text + .split("\n") + .join(option.uiFramework === "html" ? "
    " : "
    "); + } + text = `<${htmlTag} ${textProps}>${text}`; } @@ -474,10 +490,20 @@ export class Generator { } const splitByNewLine = (text: string) => { - const listItems = text.split("\n").map((line) => line + "\n"); + let listItems = text.split("\n"); - if (listItems[listItems.length - 1] === "\n") { + // if last item is "", it means there is a new line at the end of the last item + if (listItems[listItems.length - 1] === "") { listItems.pop(); + listItems = listItems.map((item) => item + "\n"); + } else { + // otherwise, it means there is not a new line at the end of the last item + listItems = listItems.map((item, index, array) => { + if (index === array.length - 1) { + return item; + } + return item + "\n"; + }); } return listItems; diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 3f8624d..8e617ff 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -18,6 +18,8 @@ import { doesNodeContainsAnImage, getMostCommonFieldInString, getRgbaFromPaints, + figmaLineHeightToCssString, + figmaLetterSpacingToCssString, } from "./util"; import { GoogleFontsInstance } from "../../../google/google-fonts"; import { StyledTextSegment } from "../node"; @@ -452,8 +454,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // line height const lineHeight = figmaNode.lineHeight; if (lineHeight !== figma.mixed && lineHeight.unit !== "AUTO") { - const unit = lineHeight.unit === "PIXELS" ? "px" : "%"; - attributes["line-height"] = `${lineHeight.value}${unit}`; + attributes["line-height"] = figmaLineHeightToCssString(lineHeight); } // text transform @@ -478,13 +479,24 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // letter spacing const letterSpacing = figmaNode.letterSpacing; if (letterSpacing !== figma.mixed && letterSpacing.value !== 0) { - const unit = letterSpacing.unit === "PIXELS" ? "px" : "em"; - const value = - letterSpacing.unit === "PIXELS" - ? letterSpacing.value - : letterSpacing.value / 100; + attributes["letter-spacing"] = + figmaLetterSpacingToCssString(letterSpacing); + } else if (letterSpacing === figma.mixed) { + const mostCommonLetterSpacing = getMostCommonFieldInString( + figmaNode, + "letterSpacing", + { + areVariationsEqual: (letterSpacing1, letterSpacing2) => + JSON.stringify(letterSpacing1.value) === + JSON.stringify(letterSpacing2.value), + } + ); - attributes["letter-spacing"] = `${value}${unit}`; + if (mostCommonLetterSpacing) { + attributes["letter-spacing"] = figmaLetterSpacingToCssString( + mostCommonLetterSpacing + ); + } } // font weight @@ -676,6 +688,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "textDecoration", "textCase", "fills", + "letterSpacing", ]); // for converting figma textDecoration to css textDecoration @@ -699,6 +712,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], color: rgbaToString(getRgbaFromPaints(segment.fills)), + letterSpacing: figmaLetterSpacingToCssString(segment.letterSpacing), })); } diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 63d6d78..7aeb7ac 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -184,3 +184,23 @@ export function getRgbaFromPaints(paints: Paint[]) { return blendColors(finalColor, currentColor); }) as RGBA; } + +export const figmaLineHeightToCssString = (lineHeight: LineHeight) => { + switch (lineHeight.unit) { + case "AUTO": + return "normal"; + case "PERCENT": + return `${lineHeight.value}%`; + case "PIXELS": + return `${lineHeight.value}px`; + } +}; + +export const figmaLetterSpacingToCssString = (letterSpacing: LetterSpacing) => { + switch (letterSpacing.unit) { + case "PERCENT": + return `${roundToTwoDps(letterSpacing.value / 100)}em`; + case "PIXELS": + return `${letterSpacing.value}px`; + } +}; diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index a1adfad..936e434 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -42,6 +42,7 @@ export interface StyledTextSegment { textDecoration: "normal" | "line-through" | "underline"; textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; color: string; + letterSpacing: string; } export interface ListSegment { From ac5cba06aa4c04a92573fa086d39a1ba63881677 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Tue, 9 May 2023 12:00:54 -0400 Subject: [PATCH 14/21] get rid of redundant letter spacing styles --- core/src/code/generator/html/generator.ts | 12 +++++++++--- core/src/design/adapter/figma/util.ts | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 65feee5..358240b 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -279,11 +279,13 @@ export class Generator { const positionalCssAttribtues: Attributes = node.getPositionalCssAttributes(); - if (positionalCssAttribtues["position"] === "absolute" || + if ( + positionalCssAttribtues["position"] === "absolute" || positionalCssAttribtues["margin-left"] || positionalCssAttribtues["margin-right"] || positionalCssAttribtues["margin-top"] || - positionalCssAttribtues["margin-bottom"]) { + positionalCssAttribtues["margin-bottom"] + ) { return `
    ` + inner + `
    `; } @@ -402,7 +404,11 @@ export class Generator { } const letterSpacing = styledTextSegment.letterSpacing; - if (letterSpacing !== defaultLetterSpacing) { + if ( + letterSpacing !== defaultLetterSpacing && + defaultLetterSpacing && + letterSpacing !== "normal" + ) { cssAttributes["letter-spacing"] = letterSpacing; } diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 4f580fe..078ddf5 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -199,6 +199,10 @@ export const figmaLineHeightToCssString = (lineHeight: LineHeight) => { }; export const figmaLetterSpacingToCssString = (letterSpacing: LetterSpacing) => { + if (letterSpacing.value === 0) { + return "normal"; + } + switch (letterSpacing.unit) { case "PERCENT": return `${roundToTwoDps(letterSpacing.value / 100)}em`; From 8126e11fdae7ed4f6c17b4bf593a626d5a35ea74 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Tue, 9 May 2023 12:11:08 -0400 Subject: [PATCH 15/21] get rid of redundant font family styles --- core/src/code/generator/html/generator.ts | 2 +- core/src/design/adapter/figma/adapter.ts | 99 +++++++++++++++-------- core/src/design/adapter/figma/util.ts | 8 ++ core/src/design/adapter/node.ts | 4 +- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 358240b..2b3afc7 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -378,7 +378,7 @@ export class Generator { cssAttributes["font-size"] = fontSize; } - const fontFamily = styledTextSegment.fontName.family; + const fontFamily = styledTextSegment.fontFamily; if (fontFamily !== defaultFontFamily) { cssAttributes["font-family"] = fontFamily; } diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 0587ebd..cdd0b64 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -20,8 +20,8 @@ import { getRgbaFromPaints, figmaLineHeightToCssString, figmaLetterSpacingToCssString, + figmaFontNameToCssString, } from "./util"; -import { GoogleFontsInstance } from "../../../google/google-fonts"; import { PostionalRelationship } from "../../../bricks/node"; import { getLineBasedOnDirection } from "../../../bricks/line"; import { Direction } from "../../../bricks/direction"; @@ -39,12 +39,20 @@ enum NodeType { STAR = "STAR", SLICE = "SLICE", COMPONENT = "COMPONENT", - BOOLEAN_OPERATION = "BOOLEAN_OPERATION" + BOOLEAN_OPERATION = "BOOLEAN_OPERATION", } - -const safelySetWidthAndHeight = (nodeType: string, figmaNode: SceneNode, attributes: Attributes) => { - if (nodeType === NodeType.FRAME || nodeType === NodeType.IMAGE || nodeType === NodeType.GROUP || nodeType === NodeType.INSTANCE) { +const safelySetWidthAndHeight = ( + nodeType: string, + figmaNode: SceneNode, + attributes: Attributes +) => { + if ( + nodeType === NodeType.FRAME || + nodeType === NodeType.IMAGE || + nodeType === NodeType.GROUP || + nodeType === NodeType.INSTANCE + ) { if (!isEmpty(figmaNode.absoluteBoundingBox)) { attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; @@ -69,7 +77,6 @@ const safelySetWidthAndHeight = (nodeType: string, figmaNode: SceneNode, attribu attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; } - }; const addDropShadowCssProperty = ( @@ -90,8 +97,9 @@ const addDropShadowCssProperty = ( .map((effect: DropShadowEffect | InnerShadowEffect) => { const { offset, radius, spread, color } = effect; - const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0 - }px ${rgbaToString(color)}`; + const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${ + spread ?? 0 + }px ${rgbaToString(color)}`; if (effect.type === "INNER_SHADOW") { return "inset " + dropShadowString; @@ -320,23 +328,26 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } if (figmaNode.type === NodeType.TEXT) { - const fontFamily = (figmaNode.fontName as FontName).family; - // font family if (figmaNode.fontName !== figma.mixed) { - attributes[ - "font-family" - ] = `'${fontFamily}', ${GoogleFontsInstance.getGenericFontFamily( - fontFamily - )}`; + attributes["font-family"] = figmaFontNameToCssString(figmaNode.fontName); } else { const mostCommonFontName = getMostCommonFieldInString( figmaNode, - "fontName" + "fontName", + { + areVariationsEqual: (fontName1, fontName2) => { + return ( + fontName1.family === fontName2.family && + fontName1.style === fontName2.style + ); + }, + } ); if (mostCommonFontName) { - attributes["font-family"] = mostCommonFontName.family; + attributes["font-family"] = + figmaFontNameToCssString(mostCommonFontName); } } @@ -357,8 +368,12 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // width and height const { absoluteRenderBounds, absoluteBoundingBox } = figmaNode; - let width: number = absoluteRenderBounds ? absoluteRenderBounds.width + 2 : absoluteBoundingBox.width; - let height: number = absoluteRenderBounds ? absoluteRenderBounds.height : absoluteBoundingBox.height; + let width: number = absoluteRenderBounds + ? absoluteRenderBounds.width + 2 + : absoluteBoundingBox.width; + let height: number = absoluteRenderBounds + ? absoluteRenderBounds.height + : absoluteBoundingBox.height; let moreThanOneRow: boolean = false; if (absoluteRenderBounds) { @@ -380,7 +395,8 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // If bounding box and rendering box are similar in size, horizontal text alignment doesn't have any // actual effects therefore should be always considered as "text-align": "left" when there is only one row if ( - Math.abs(boundingBoxWidth - renderBoundsWidth) / boundingBoxWidth > 0.1 || + Math.abs(boundingBoxWidth - renderBoundsWidth) / boundingBoxWidth > + 0.1 || moreThanOneRow ) { // text alignment @@ -398,10 +414,8 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } if ( - Math.abs( - absoluteBoundingBox.width - absoluteRenderBounds.width - ) / - absoluteBoundingBox.width > + Math.abs(absoluteBoundingBox.width - absoluteRenderBounds.width) / + absoluteBoundingBox.width > 0.2 ) { width = absoluteRenderBounds.width + 4; @@ -777,6 +791,7 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { return styledTextSegments.map((segment) => ({ ...segment, + fontFamily: figmaFontNameToCssString(segment.fontName), textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], color: rgbaToString(getRgbaFromPaints(segment.fills)), @@ -839,11 +854,21 @@ export const convertFigmaNodesToBricksNodes = ( let wrappedNodeA: Node = new VisibleNode(new FigmaNodeAdapter(a)); let wrappedNodeB: Node = new VisibleNode(new FigmaNodeAdapter(b)); - if (computePositionalRelationship(wrappedNodeA.getAbsRenderingBox(), wrappedNodeB.getAbsRenderingBox()) === PostionalRelationship.INCLUDE) { + if ( + computePositionalRelationship( + wrappedNodeA.getAbsRenderingBox(), + wrappedNodeB.getAbsRenderingBox() + ) === PostionalRelationship.INCLUDE + ) { return 1; } - if (computePositionalRelationship(wrappedNodeB.getAbsRenderingBox(), wrappedNodeA.getAbsRenderingBox()) === PostionalRelationship.INCLUDE) { + if ( + computePositionalRelationship( + wrappedNodeB.getAbsRenderingBox(), + wrappedNodeA.getAbsRenderingBox() + ) === PostionalRelationship.INCLUDE + ) { return -1; } @@ -932,11 +957,11 @@ export const convertFigmaNodesToBricksNodes = ( isExportableNode = true; if (feedback.doNodesContainImage) { newNode = new ImageNode( - new FigmaVectorGroupNodeAdapter(figmaNode), + new FigmaVectorGroupNodeAdapter(figmaNode) ); } else { newNode = new BricksVector( - new FigmaVectorGroupNodeAdapter(figmaNode), + new FigmaVectorGroupNodeAdapter(figmaNode) ); } } @@ -957,9 +982,16 @@ export const convertFigmaNodesToBricksNodes = ( } } - let horizontalOverlap: boolean = areNodesOverlappingByDirection(result.nodes, Direction.HORIZONTAL); - let verticalOverlap: boolean = areNodesOverlappingByDirection(result.nodes, Direction.VERTICAL); - result.doNodesHaveNonOverlappingChildren = !horizontalOverlap || !verticalOverlap; + let horizontalOverlap: boolean = areNodesOverlappingByDirection( + result.nodes, + Direction.HORIZONTAL + ); + let verticalOverlap: boolean = areNodesOverlappingByDirection( + result.nodes, + Direction.VERTICAL + ); + result.doNodesHaveNonOverlappingChildren = + !horizontalOverlap || !verticalOverlap; if (allNodesAreOfVectorNodeTypes) { result.doNodesHaveNonOverlappingChildren = false; @@ -974,7 +1006,10 @@ export const convertFigmaNodesToBricksNodes = ( return result; }; -const areNodesOverlappingByDirection = (nodes: Node[], direction: Direction): boolean => { +const areNodesOverlappingByDirection = ( + nodes: Node[], + direction: Direction +): boolean => { let overlap: boolean = false; for (let i = 0; i < nodes.length; i++) { const currentNode: Node = nodes[i]; diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index 078ddf5..bd06880 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -1,3 +1,4 @@ +import { GoogleFontsInstance } from "../../../google/google-fonts"; import { isEmpty } from "../../../utils"; const round = Math.round; @@ -210,3 +211,10 @@ export const figmaLetterSpacingToCssString = (letterSpacing: LetterSpacing) => { return `${letterSpacing.value}px`; } }; + +export const figmaFontNameToCssString = (fontName: FontName) => { + const fontFamily = fontName.family; + return `'${fontFamily}', ${GoogleFontsInstance.getGenericFontFamily( + fontFamily + )}`; +}; diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index 936e434..68ba071 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -33,11 +33,13 @@ export interface StyledTextSegment { characters: string; start: number; end: number; - fontSize: number; fontName: { family: string; style: string; }; + // CSS strings + fontSize: number; + fontFamily: string; fontWeight: number; textDecoration: "normal" | "line-through" | "underline"; textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; From b6ea4b547cb1c78f8d410563ebdbba37370342e1 Mon Sep 17 00:00:00 2001 From: Donovan So Date: Tue, 9 May 2023 13:01:41 -0400 Subject: [PATCH 16/21] support letter spacing in twcss --- core/src/code/generator/html/generator.ts | 14 ++++++-- .../generator/tailwindcss/css-to-twcss.ts | 33 +++++++++++++------ .../code/generator/tailwindcss/generator.ts | 15 +++++---- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 2b3afc7..d853c29 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -21,9 +21,12 @@ import { componentRegistryGlobalInstance } from "../../../../ee/loop/component-r import { codeSampleRegistryGlobalInstance } from "../../../../ee/loop/code-sample-registry"; type GetPropsFromNode = (node: Node, option: Option) => string; -type GetPropsFromAttributes = ( +export type GetPropsFromAttributes = ( attributes: Attributes, - option: Option + option: Option, + id?: string, + // sometimes needed because the value of an attribute can depend on the parent's attributes + parentCssAttributes?: Attributes ) => string; export type ImportedComponentMeta = { @@ -372,6 +375,7 @@ export class Generator { (styledTextSegment) => { // here we only keep attributes if they are different from the default attribute const cssAttributes: Attributes = {}; + const parentCssAttributes: Attributes = {}; const fontSize = `${styledTextSegment.fontSize}px`; if (fontSize !== defaultFontSize) { @@ -410,12 +414,14 @@ export class Generator { letterSpacing !== "normal" ) { cssAttributes["letter-spacing"] = letterSpacing; + parentCssAttributes["font-size"] = defaultFontSize; } return { start: styledTextSegment.start, end: styledTextSegment.end, cssAttributes, + parentCssAttributes, }; } ); @@ -464,7 +470,9 @@ export class Generator { if (!isEmpty(segment.cssAttributes)) { const textProps = this.getPropsFromAttributes( segment.cssAttributes, - option + option, + undefined, + segment.parentCssAttributes ); // if css attribute applies to the whole list item, wrap it in a
  • tag diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index 5b9fa43..3fe03dc 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -19,6 +19,7 @@ import { Attributes } from "../../../design/adapter/node"; import { FontsRegistryGlobalInstance } from "./fonts-registry"; import { Option, UiFramework } from "../../code"; import { getVariablePropForTwcss } from "../../../../ee/code/prop"; +import { GetPropsFromAttributes } from "../html/generator"; export type TwcssPropRenderingMeta = { numberOfTwcssClasses: number; @@ -30,11 +31,12 @@ export type TwcssPropRenderingMap = { }; // convertCssClassesToTwcssClasses converts css classes to tailwindcss classes -export const convertCssClassesToTwcssClasses = ( +export const convertCssClassesToTwcssClasses: GetPropsFromAttributes = ( attributes: Attributes, option: Option, - id?: string -): string => { + id?: string, + parentAttributes?: Attributes +) => { let classPropName: string = "class"; let variableProps: string = ""; const twcssPropRenderingMap: TwcssPropRenderingMap = {}; @@ -43,7 +45,8 @@ export const convertCssClassesToTwcssClasses = ( const twcssClasses: string[] = getTwcssClass( property, value, - attributes + attributes, + parentAttributes ).split(" "); twcssPropRenderingMap[property] = { numberOfTwcssClasses: twcssClasses.length, @@ -68,9 +71,12 @@ export const convertCssClassesToTwcssClasses = ( } for (let i = 0; i < twcssPropRenderingMeta.numberOfTwcssClasses; i++) { - const parts: string[] = getTwcssClass(property, value, attributes).split( - " " - ); + const parts: string[] = getTwcssClass( + property, + value, + attributes, + parentAttributes + ).split(" "); if (twcssPropRenderingMeta.filledClassIndexes.has(i)) { continue; } @@ -428,7 +434,8 @@ const renderTwcssProperty = (prefix: string, value: string) => { export const getTwcssClass = ( cssProperty: string, cssValue: string, - cssAttributes: Attributes + cssAttributes: Attributes, + parentAttributes?: Attributes ): string => { if (isEmpty(cssValue)) { return ""; @@ -831,7 +838,10 @@ export const getTwcssClass = ( case "line-height": { const lineHeightNum = extractPixelNumberFromString(cssValue); - if (cssValue.endsWith("px") && lineHeightNum > largestTwcssLineheightInPixels) { + if ( + cssValue.endsWith("px") && + lineHeightNum > largestTwcssLineheightInPixels + ) { return `leading-[${lineHeightNum}px]`; } @@ -866,8 +876,11 @@ export const getTwcssClass = ( } case "letter-spacing": { + const fontSizeString = + cssAttributes?.["font-size"] || parentAttributes?.["font-size"]; + // assume font size is always in px - const fontSize = parseInt(cssAttributes["font-size"].slice(0, -2), 10); + const fontSize = parseInt(fontSizeString.slice(0, -2), 10); const twClass = findClosestTwcssLetterSpacing(cssValue, fontSize); return twClass; } diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index 5088e75..565f5a9 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -162,7 +162,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); } @@ -174,7 +174,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); case NodeType.VECTOR: @@ -199,7 +199,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); } @@ -211,7 +211,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); // TODO: VECTOR_GROUP node type is deprecated case NodeType.VECTOR_GROUP: @@ -225,7 +225,7 @@ const getPropsFromNode = (node: Node, option: Option): string => { }), }, option, - node.getId(), + node.getId() ); default: @@ -257,7 +257,10 @@ export const buildTwcssConfigFileContent = ( )}": "url(.${importComponent.importPath})",`; } - if (extension === "svg" && shouldUseAsBackgroundImage(importComponent.node)) { + if ( + extension === "svg" && + shouldUseAsBackgroundImage(importComponent.node) + ) { backgroundImages += `"${getImageFileNameFromUrl( importComponent.importPath )}": "url(.${importComponent.importPath})",`; From ee68a29503e9a6279932864e61ff84c046dabc36 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Sat, 13 May 2023 10:38:49 -0700 Subject: [PATCH 17/21] save progress --- core/src/bricks/additional-css.ts | 5 ++ core/src/bricks/remove-node.ts | 18 ++-- .../generator/tailwindcss/css-to-twcss.ts | 83 ++++++++++++++++++- .../tailwindcss/twcss-conversion-map.ts | 12 ++- core/src/design/adapter/figma/adapter.ts | 4 + core/src/index.ts | 4 + figma/src/code.ts | 4 + figma/src/ui.tsx | 3 - 8 files changed, 117 insertions(+), 16 deletions(-) diff --git a/core/src/bricks/additional-css.ts b/core/src/bricks/additional-css.ts index eb84d55..eb06b19 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -819,6 +819,11 @@ const getAlignItemsValue = ( } } + // console.log("numberOfItemsInTheMiddle: ", numberOfItemsInTheMiddle); + // console.log("numberOfItemsTippingLeft: ", numberOfItemsTippingLeft); + // console.log("numberOfItemsTippingRight: ", numberOfItemsTippingRight); + + if (noGapItems === targetLines.length) { for (const targetLine of targetLines) { const leftGap = Math.abs(parentLine.lower - targetLine.lower); diff --git a/core/src/bricks/remove-node.ts b/core/src/bricks/remove-node.ts index 91322a2..b1ba4d0 100644 --- a/core/src/bricks/remove-node.ts +++ b/core/src/bricks/remove-node.ts @@ -7,7 +7,13 @@ export const removeNode = (node: Node): Node => { const children: Node[] = node.getChildren(); if (children.length === 1) { const child = children[0]; + + // console.log("haveSimlarWidthAndHeight(node, child): ", haveSimlarWidthAndHeight(node, child)); + + if (haveSimlarWidthAndHeight(node, child)) { + + const cssAttributes: Attributes = { ...node.getCssAttributes(), ...child.getCssAttributes(), @@ -41,10 +47,7 @@ export const removeChildrenNode = (node: Node): Node => { ...child.getCssAttributes(), }; - const positionalCssAttributes: Attributes = { - ...node.getPositionalCssAttributes(), - ...child.getPositionalCssAttributes(), - }; + const positionalCssAttributes: Attributes = mergeAttributes(node.getPositionalCssAttributes(), node.getPositionalCssAttributes()); node.setCssAttributes(cssAttributes); node.setPositionalCssAttributes(positionalCssAttributes); @@ -108,13 +111,6 @@ const filterAttributes = (attribtues: Attributes): Attributes => { }; const mergeAttributes = (parentPosAttributes: Attributes, childPosAttributes: Attributes): Attributes => { - if (!isEmpty(parentPosAttributes["display"]) && isEmpty(childPosAttributes["display"])) { - return { - ...parentPosAttributes, - ...childPosAttributes, - }; - } - if (parentPosAttributes["display"] !== childPosAttributes["display"] || parentPosAttributes["flex-direction"] !== childPosAttributes["flex-direction"] || parentPosAttributes["align-items"] !== childPosAttributes["align-items"] || parentPosAttributes["justify-content"] !== childPosAttributes["justify-content"]) { return { ...filterAttributes(parentPosAttributes), diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index 3fe03dc..dc21cfb 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -14,6 +14,7 @@ import { twWidthMap, tailwindTextDecorationMap, MAX_BORDER_RADIUS_IN_PIXELS, + twcssDropShadowToYOffSetMap, } from "./twcss-conversion-map"; import { Attributes } from "../../../design/adapter/node"; import { FontsRegistryGlobalInstance } from "./fonts-registry"; @@ -337,6 +338,85 @@ const findClosestTwcssLetterSpacing = ( return twClassToUse; }; + +const getYOffsetFromBoxShadow = (boxShadowValue: string): number => { + const spaceParts: string[] = boxShadowValue.split(" "); + if (spaceParts.length <= 4) { + return 1; + } + + if (spaceParts[1].endsWith("px")) { + return parseInt(boxShadowValue.slice(0, -2)); + } + + return 1; +}; + + +const getRadiusFromBoxShadow = (boxShadowValue: string): number => { + const spaceParts: string[] = boxShadowValue.split(" "); + if (spaceParts.length <= 4) { + return 1; + } + + if (spaceParts[2].endsWith("px")) { + return parseInt(boxShadowValue.slice(0, -2)); + } + + return 1; +}; + +const findClosestTwcssDropShadowClassUsingPixel = ( + cssValue: string, +) => { + let closestTwClass = ""; + const radius: number = getYOffsetFromBoxShadow(cssValue); + + let largestBoxShadowValue: string = ""; + let targetPixelNum: number = 1; + + const dropShadowParts: string[] = cssValue.split("),"); + + if (isEmpty(dropShadowParts)) { + return ""; + } + + const newShadowParts: string[] = []; + + for (let i = 0; i < dropShadowParts.length; i++) { + if (i !== dropShadowParts.length - 1) { + newShadowParts.push(dropShadowParts[i] + ")"); + continue; + } + + newShadowParts.push(dropShadowParts[i]); + } + + let largestYOffset: number = -Infinity; + for (let i = 0; i < newShadowParts.length; i++) { + const yOffset: number = getYOffsetFromBoxShadow(newShadowParts[i]); + if (yOffset > largestYOffset) { + largestBoxShadowValue = cssValue; + targetPixelNum = yOffset; + } + } + + let smallestDiff: number = -Infinity; + Object.entries(twcssDropShadowToYOffSetMap).forEach(([key, val]) => { + const pixelNum = extractPixelNumberFromString(val); + if (val.endsWith("px")) { + const diff = Math.abs(targetPixelNum - pixelNum); + if (diff < smallestDiff) { + smallestDiff = diff; + closestTwClass = key; + } + } + }); + + return closestTwClass; +}; + + // findClosestTwcssFontWeight finds the closest tailwincss font weight given the css font weight const findClosestTwcssFontWeight = (fontWeight: string): string => { const givenFontWeight = parseInt(fontWeight); @@ -584,13 +664,14 @@ export const getTwcssClass = ( return `bg-${getImageFileNameFromUrl(cssValue)}`; case "box-shadow": { + console.log(cssValue); // A very naive conversion for now, because parsing box-shadow string is too complicated if (cssValue.includes("inset")) { // inner shadow return "shadow-inner"; } else { // drop shadow - return "shadow-xl"; + return findClosestTwcssDropShadowClassUsingPixel(cssValue); } } diff --git a/core/src/code/generator/tailwindcss/twcss-conversion-map.ts b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts index 94ab973..40e317b 100644 --- a/core/src/code/generator/tailwindcss/twcss-conversion-map.ts +++ b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts @@ -4,6 +4,16 @@ export const tailwindTextDecorationMap = { none: "no-underline", }; + +export const twcssDropShadowToYOffSetMap = { + "sm": "drop-shadow(0 1px 1px rgba(0,0,0,0.05))", + "": "drop-shadow(0 1px 2px rgba(0,0,0,0.1))", + "md": "drop-shadow(0 4px 3px rgba(0,0,0,0.07))", + "lg": "drop-shadow(0 10px 8px rgba(0,0,0,0.04))", + "xl": "drop-shadow(0 20px 13px rgba(0,0,0,0.03))", + "2xl": "drop-shadow(0 25px 25px rgba(0,0,0,0.15))", +}; + export const twHeightMap = { "h-0": "0px", "h-px": "1px", @@ -418,7 +428,7 @@ export const twLetterSpacingMap = { "tracking-widest": "0.1em", }; -export const twFontWeightMap: { [key: string]: number } = { +export const twFontWeightMap: { [key: string]: number; } = { "font-thin": 100, "font-extralight": 200, "font-light": 300, diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index cdd0b64..c100f52 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -151,6 +151,10 @@ const getPositionalCssAttributes = (figmaNode: SceneNode): Attributes => { } } + // console.log("figmaNode: ", figmaNode); + // console.log("primaryAxisAlignItems: ", figmaNode.primaryAxisAlignItems); + // console.log("counterAxisAlignItems: ", figmaNode.counterAxisAlignItems); + switch (figmaNode.primaryAxisAlignItems) { case "MIN": attributes["justify-content"] = "flex-start"; diff --git a/core/src/index.ts b/core/src/index.ts index 23c6745..23c486b 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -28,6 +28,8 @@ export const convertToCode = async ( return []; } + // console.log("converted: ", converted); + const dedupedNodes: Node[] = []; for (const node of converted) { let newNode: Node = removeNode(node); @@ -39,6 +41,8 @@ export const convertToCode = async ( let startingNode: Node = dedupedNodes.length > 1 ? new GroupNode(dedupedNodes) : dedupedNodes[0]; + // console.log("startingNode: ", startingNode); + groupNodes(startingNode); startingNode = removeNode(startingNode); diff --git a/figma/src/code.ts b/figma/src/code.ts index 721c241..8e017e4 100644 --- a/figma/src/code.ts +++ b/figma/src/code.ts @@ -118,6 +118,10 @@ figma.ui.onmessage = async (msg) => { } if (msg.type === "get-last-reset") { + if (figma.currentUser.id === "624412189236026359") { + await figma.clientStorage.setAsync("limit", 6); + } + const reset: number = await figma.clientStorage.getAsync("last-reset"); figma.ui.postMessage({ diff --git a/figma/src/ui.tsx b/figma/src/ui.tsx index b6a82aa..1659428 100644 --- a/figma/src/ui.tsx +++ b/figma/src/ui.tsx @@ -152,7 +152,6 @@ const UI = () => { }, []); const resetLimit = () => { - console.log("called!!!"); parent.postMessage( { pluginMessage: { @@ -197,8 +196,6 @@ const UI = () => { } if (pluginMessage.type === "get-limit") { - // resetLimit(); - if (Number.isInteger(pluginMessage.limit) && pluginMessage.limit >= 0) { setLimit(pluginMessage.limit); } else { From d59c6d32b8fb1c8cd70a32e46768490a8d94c0e4 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Mon, 15 May 2023 17:40:28 -0700 Subject: [PATCH 18/21] save progress --- core/ee/loop/loop.ts | 4 +- core/src/bricks/additional-css.ts | 126 ++++- core/src/bricks/grouping.ts | 15 + core/src/bricks/line.ts | 53 +- core/src/bricks/node.ts | 102 +++- core/src/bricks/overlap.ts | 6 + core/src/bricks/remove-css.ts | 85 ++++ core/src/bricks/remove-node.ts | 35 +- core/src/code/generator/html/generator.ts | 443 +++++++++++------ .../generator/tailwindcss/css-to-twcss.ts | 183 +++++-- .../code/generator/tailwindcss/generator.ts | 24 +- .../tailwindcss/twcss-conversion-map.ts | 36 +- core/src/code/generator/util.ts | 4 + core/src/design/adapter/figma/adapter.ts | 461 +++++++++++++----- core/src/design/adapter/figma/util.ts | 6 +- core/src/design/adapter/node.ts | 10 +- core/src/index.ts | 13 +- core/src/utils.ts | 1 + 18 files changed, 1209 insertions(+), 398 deletions(-) create mode 100644 core/src/bricks/remove-css.ts diff --git a/core/ee/loop/loop.ts b/core/ee/loop/loop.ts index 36fa3d7..03b1647 100644 --- a/core/ee/loop/loop.ts +++ b/core/ee/loop/loop.ts @@ -203,8 +203,8 @@ const areTwoNodesSimilar = (currentNode: Node, targetNode: Node): [boolean, stri } } - const [currentWidth, currentHeight] = currentNode.getAbsBoundingBoxWidthAndHeights(); - const [targetWidth, taregtHeight] = currentNode.getAbsBoundingBoxWidthAndHeights(); + const [currentWidth, currentHeight] = currentNode.getAbsBoundingBoxWidthAndHeight(); + const [targetWidth, taregtHeight] = currentNode.getAbsBoundingBoxWidthAndHeight(); if (currentWidth === targetWidth) { return [true, "similar widths"]; diff --git a/core/src/bricks/additional-css.ts b/core/src/bricks/additional-css.ts index eb06b19..0fa5f96 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -6,15 +6,18 @@ import { reorderNodesBasedOnDirection, getDirection, } from "./direction"; -import { ImageNode, Node, NodeType, VisibleNode } from "./node"; +import { Node, NodeType } from "./node"; import { getContainerLineFromNodes, getLinesFromNodes, Line, getLineBasedOnDirection, + getContainerRenderingLineFromNodes, + getLineUsingRenderingBoxBasedOnDirection, } from "./line"; import { filterCssValue } from "./util"; import { absolutePositioningAnnotation } from "./overlap"; +import { getFigmaLineBasedOnDirection } from "../design/adapter/figma/adapter"; export const selectBox = ( node: Node, @@ -26,15 +29,18 @@ export const selectBox = ( } if (node.getType() === NodeType.VISIBLE) { - const visibleNode = node as VisibleNode; - return visibleNode.getAbsBoundingBox(); + return node.getAbsBoundingBox(); } if (node.getType() === NodeType.IMAGE) { - const imageNode = node as ImageNode; - return imageNode.getAbsBoundingBox(); + return node.getAbsBoundingBox(); + } + + if (node.getType() === NodeType.TEXT) { + return node.getAbsBoundingBox(); } + if (useBoundingBox) { return node.getAbsBoundingBox(); } @@ -64,6 +70,13 @@ enum RelativePoisition { // addAdditionalCssAttributesToNodes adds additional css information to a node and its children. export const addAdditionalCssAttributesToNodes = (node: Node) => { + if (isEmpty(node)) { + return; + } + + adjustNodeHeightAndWidthCssValue(node); + addAdditionalCssAttributes(node); + const children = node.getChildren(); if (isEmpty(children)) { return; @@ -71,16 +84,57 @@ export const addAdditionalCssAttributesToNodes = (node: Node) => { const direction = getDirection(node.children); reorderNodesBasedOnDirection(node.children, direction); - node.addCssAttributes(getAdditionalCssAttributes(node)); node.addPositionalCssAttributes(getPositionalCssAttributes(node, direction)); adjustChildrenHeightAndWidthCssValue(node); - adjustNodeHeightAndWidthCssValue(node); + adjustChildrenPositionalCssValue(node, direction); for (const child of children) { addAdditionalCssAttributesToNodes(child); } }; +const adjustChildrenPositionalCssValue = (node: Node, direction: Direction) => { + const children = node.getChildren(); + if (isEmpty(children)) { + return; + } + + children.sort((a, b): number => { + const currentRenderingBox: BoxCoordinates = a.getAbsRenderingBox(); + const targetRenderingBox: BoxCoordinates = b.getAbsRenderingBox(); + + if (direction === Direction.HORIZONTAL) { + return currentRenderingBox.leftTop.y - targetRenderingBox.leftTop.y; + } + + return currentRenderingBox.leftTop.x - targetRenderingBox.leftTop.x; + }); + + const zIndexArr: string[] = ["50", "40", "30", "20", "10"]; + + if (node.hasAnnotation(absolutePositioningAnnotation)) { + if (children.length <= 5) { + for (let i = 0; i < children.length; i++) { + const current: Node = children[i]; + const zIndex: string = zIndexArr[zIndexArr.length - children.length + i]; + current.addPositionalCssAttributes({ + "z-index": zIndex, + }); + + } + } + + if (children.length > 5) { + for (let i = 0; i < children.length; i++) { + const current: Node = children[i]; + current.addPositionalCssAttributes({ + "z-index": `${children.length - i}`, + }); + } + } + } +}; + // getPaddingInPixels calculates paddings given a node. export const getPaddingInPixels = ( node: Node, @@ -358,19 +412,45 @@ const isCssValueEmpty = (value: string): boolean => { ); }; -// getAdditionalCssAttributes gets additioanl css information of a node in relation to its children. -export const getAdditionalCssAttributes = (node: Node): Attributes => { - const attributes: Attributes = {}; +export const addAdditionalCssAttributes = (node: Node) => { + if (node.getType() === NodeType.IMAGE) { + node.addCssAttributes({ + "overflow": "hidden", + }); + return; + } - if ( - (!isCssValueEmpty(node.getACssAttribute("border-radius")) || - !isCssValueEmpty(node.getACssAttribute("border-width"))) && - node.areThereOverflowingChildren() - ) { - attributes["overflow"] = "hidden"; + if (isEmpty(node.getChildren())) { + return; } - return attributes; + if (isEmpty(node.getACssAttribute("border-radius"))) { + return; + } + + const childrenContainerLineY = getContainerRenderingLineFromNodes(node.getChildren(), Direction.HORIZONTAL); + const childrenHeight = Math.abs(childrenContainerLineY.upper - childrenContainerLineY.lower); + const childrenContainerLineX = getContainerRenderingLineFromNodes(node.getChildren(), Direction.VERTICAL); + const childrenWidth = Math.abs(childrenContainerLineX.upper - childrenContainerLineX.lower); + + const containerLineY = getContainerRenderingLineFromNodes([node], Direction.HORIZONTAL); + const height = Math.abs(containerLineY.upper - containerLineY.lower); + const containerLineX = getContainerRenderingLineFromNodes([node], Direction.VERTICAL); + const width = Math.abs(containerLineX.upper - containerLineX.lower); + + const borderRadius: string = node.getACssAttribute("border-radius"); + if (!borderRadius.endsWith("px")) { + return; + } + + const borderRadiusNum: number = parseInt(borderRadius.slice(0, -2)); + if (childrenHeight < height - borderRadiusNum || childrenWidth < width - borderRadiusNum) { + return; + } + + node.addCssAttributes({ + "overflow": "hidden", + }); }; const adjustNodeHeightAndWidthCssValue = (node: Node) => { @@ -382,6 +462,13 @@ const adjustNodeHeightAndWidthCssValue = (node: Node) => { attributes["height"] = `${height}px`; } + if (node.getType() === NodeType.VECTOR) { + const width: number = Math.abs(node.getAbsRenderingBox().leftTop.x - node.getAbsRenderingBox().rightBot.x); + const height: number = Math.abs(node.getAbsRenderingBox().leftTop.y - node.getAbsRenderingBox().rightBot.y); + attributes["width"] = `${width}px`; + attributes["height"] = `${height}px`; + } + node.setCssAttributes(attributes); }; @@ -593,10 +680,10 @@ export const getPositionalCssAttributes = ( if (node.hasAnnotation(absolutePositioningAnnotation)) { attributes["position"] = "relative"; - const currentBox = node.getAbsRenderingBox(); + const currentBox = selectBox(node); for (const child of node.getChildren()) { const childAttributes: Attributes = {}; - const targetBox = child.getAbsRenderingBox(); + const targetBox = selectBox(child); const top = Math.abs(currentBox.leftTop.y - targetBox.leftTop.y); const bottom = Math.abs(currentBox.rightBot.y - targetBox.rightBot.y); const left = Math.abs(currentBox.leftTop.x - targetBox.leftTop.x); @@ -608,6 +695,7 @@ export const getPositionalCssAttributes = ( childAttributes["right"] = `${right}px`; childAttributes["left"] = `${left}px`; + child.addPositionalCssAttributes(childAttributes); } diff --git a/core/src/bricks/grouping.ts b/core/src/bricks/grouping.ts index 1a3ea46..3044b80 100644 --- a/core/src/bricks/grouping.ts +++ b/core/src/bricks/grouping.ts @@ -24,23 +24,38 @@ export const groupNodes = (parentNode: Node) => { } let groupedNodes = groupNodesByInclusion(children); + + // console.log("groupNodesByInclusion: ", groupedNodes); + groupedNodes = groupNodesByOverlap(groupedNodes); + // console.log("groupNodesByOverlap: ", groupedNodes); + const horizontalSegmentedNodes = groupNodesByDirectionalOverlap( groupedNodes, Direction.HORIZONTAL ); + // console.log("horizontalSegmentedNodes: ", horizontalSegmentedNodes); + + const verticalSegmentedNodes = groupNodesByDirectionalOverlap( groupedNodes, Direction.VERTICAL ); + // console.log("verticalSegmentedNodes: ", verticalSegmentedNodes); + + const decided = decideBetweenDirectionalOverlappingNodes( horizontalSegmentedNodes, verticalSegmentedNodes ); + + // console.log("decided: ", decided); + + if (!isEmpty(decided)) { groupedNodes = decided; } diff --git a/core/src/bricks/line.ts b/core/src/bricks/line.ts index 98e9850..b5a857c 100644 --- a/core/src/bricks/line.ts +++ b/core/src/bricks/line.ts @@ -20,6 +20,18 @@ export const getLineBasedOnDirection = (node: Node, direction: Direction, useBou return new Line(coordinates.leftTop.x, coordinates.rightBot.x); }; + +// getLineUsingRenderingBoxBasedOnDirection gets the rendering boundary of a node depending on the input direction. +export const getLineUsingRenderingBoxBasedOnDirection = (node: Node, direction: Direction, useBoundingBox: boolean = false) => { + const coordinates = node.getAbsRenderingBox(); + + if (direction === Direction.HORIZONTAL) { + return new Line(coordinates.leftTop.y, coordinates.rightBot.y); + } + + return new Line(coordinates.leftTop.x, coordinates.rightBot.x); +}; + // Bricks nodes are bounded by a rectangular box. // Line could be seen as a boundary of this rectangular box. export class Line { @@ -67,7 +79,7 @@ export class Line { return distanceFromUpper - distanceFromLower; }; - overlap(l: Line): boolean { + overlapStrict(l: Line): boolean { if (this.lower > l.upper) { return false; } @@ -78,6 +90,18 @@ export class Line { return true; } + + overlap(l: Line): boolean { + if (this.lower + 2 > l.upper) { + return false; + } + + if (this.upper - 2 < l.lower) { + return false; + } + + return true; + } } // getLinesFromNodes gets boundaries from nodes based on direction. @@ -128,3 +152,30 @@ export const getContainerLineFromNodes = ( return new Line(lower, upper); }; + +export const getContainerRenderingLineFromNodes = ( + nodes: Node[], + direction: Direction, + useBoundingBox: boolean = false +): Line => { + let lower: number = Infinity; + let upper: number = -Infinity; + if (direction === Direction.HORIZONTAL) { + for (let i = 0; i < nodes.length; i++) { + const renderingBox = nodes[i].getAbsRenderingBox(); + lower = renderingBox.leftTop.y < lower ? renderingBox.leftTop.y : lower; + upper = renderingBox.rightBot.y > upper ? renderingBox.rightBot.y : upper; + } + + return new Line(lower, upper); + } + + for (let i = 0; i < nodes.length; i++) { + const renderingBox = nodes[i].getAbsRenderingBox(); + lower = renderingBox.leftTop.x < lower ? renderingBox.leftTop.x : lower; + upper = renderingBox.rightBot.x > upper ? renderingBox.rightBot.x : upper; + } + + return new Line(lower, upper); +}; + diff --git a/core/src/bricks/node.ts b/core/src/bricks/node.ts index 9cec7b9..8b2f373 100644 --- a/core/src/bricks/node.ts +++ b/core/src/bricks/node.ts @@ -140,11 +140,56 @@ export class BaseNode { } } +function findIntersection(rectangle1: BoxCoordinates, rectangle2: BoxCoordinates): BoxCoordinates { + const xOverlap = Math.max(0, Math.min(rectangle1.rightBot.x, rectangle2.rightBot.x) - Math.max(rectangle1.leftTop.x, rectangle2.leftTop.x)); + const yOverlap = Math.max(0, Math.min(rectangle1.rightBot.y, rectangle2.rightBot.y) - Math.max(rectangle1.leftTop.y, rectangle2.leftTop.y)); + + if (xOverlap === 0 || yOverlap === 0) { + return null; // No intersection + } + + const intersection: BoxCoordinates = { + rightBot: { + x: Math.max(rectangle1.rightBot.x, rectangle2.rightBot.x), + y: Math.min(rectangle1.rightBot.y, rectangle2.rightBot.y) + }, + rightTop: { + x: Math.min(rectangle1.rightTop.x, rectangle2.rightTop.x), + y: Math.max(rectangle1.rightTop.y, rectangle2.rightTop.y) + }, + leftBot: { + x: Math.max(rectangle1.leftBot.x, rectangle2.leftBot.x), + y: Math.min(rectangle1.leftBot.y, rectangle2.leftBot.y) + }, + leftTop: { + x: Math.max(rectangle1.leftTop.x, rectangle2.leftTop.x), + y: Math.max(rectangle1.leftTop.y, rectangle2.leftTop.y) + }, + }; + + return intersection; +} + + // doOverlap determines whether two boxes overlap with one another. export const doOverlap = ( currentCoordinate: BoxCoordinates, targetCoordinates: BoxCoordinates ): boolean => { + const intersection: BoxCoordinates = findIntersection(currentCoordinate, targetCoordinates); + + if (!isEmpty(intersection)) { + const intersectionWidth: number = Math.abs(intersection.leftTop.x - intersection.rightBot.x); + const intersectionHeight: number = Math.abs(intersection.leftTop.y - intersection.rightBot.y); + + // console.log("intersectionWidth: ", intersectionWidth); + // console.log("intersectionHeight: ", intersectionHeight); + + if (intersectionWidth < 2 || intersectionHeight < 2) { + return false; + } + } + if ( currentCoordinate.leftTop.x === currentCoordinate.rightBot.x || currentCoordinate.leftTop.y === currentCoordinate.rightBot.y @@ -300,6 +345,16 @@ export class GroupNode extends BaseNode { setChildren(children: Node[]) { this.children = children; + if (!isEmpty(this.node)) { + const absBoundingBox: BoxCoordinates = this.node.getAbsoluteBoundingBoxCoordinates(); + this.cssAttributes["width"] = `${Math.abs( + absBoundingBox.rightBot.x - absBoundingBox.leftTop.x + )}px`; + this.cssAttributes["height"] = `${Math.abs( + absBoundingBox.rightBot.y - absBoundingBox.rightTop.y + )}px`; + } + this.absRenderingBox = this.computeAbsRenderingBox(); } @@ -315,7 +370,7 @@ export class GroupNode extends BaseNode { return this.getAbsRenderingBox(); } - getAbsBoundingBoxWidthAndHeights(): number[] { + getAbsBoundingBoxWidthAndHeight(): number[] { const coordinates = this.getAbsBoundingBox(); const width = Math.abs(coordinates.rightTop.x - coordinates.leftBot.x); const height = Math.abs(coordinates.rightBot.y - coordinates.leftTop.y); @@ -323,9 +378,20 @@ export class GroupNode extends BaseNode { } getPositionalRelationship(targetNode: Node): PostionalRelationship { + let currentBox: BoxCoordinates = this.getAbsRenderingBox(); + let targetBox: BoxCoordinates = targetNode.getAbsRenderingBox(); + + if (!isEmpty(this.getACssAttribute("box-shadow"))) { + currentBox = this.getAbsBoundingBox(); + } + + if (!isEmpty(targetNode.getACssAttribute("box-shadow"))) { + targetBox = targetNode.getAbsBoundingBox(); + } + return computePositionalRelationship( - this.absRenderingBox, - targetNode.getAbsRenderingBox() + currentBox, + targetBox ); } @@ -345,12 +411,6 @@ export class GroupNode extends BaseNode { private computeAbsRenderingBox(): BoxCoordinates { if (!isEmpty(this.node)) { this.absRenderingBox = this.node.getRenderingBoundsCoordinates(); - this.cssAttributes["width"] = `${Math.abs( - this.absRenderingBox.rightBot.x - this.absRenderingBox.leftTop.x - )}px`; - this.cssAttributes["height"] = `${Math.abs( - this.absRenderingBox.rightBot.y - this.absRenderingBox.rightTop.y - )}px`; return this.absRenderingBox; } @@ -418,7 +478,7 @@ export class VisibleNode extends BaseNode { return this.node.getAbsoluteBoundingBoxCoordinates(); } - getAbsBoundingBoxWidthAndHeights(): number[] { + getAbsBoundingBoxWidthAndHeight(): number[] { const coordinates = this.node.getAbsoluteBoundingBoxCoordinates(); const width = Math.abs(coordinates.rightTop.x - coordinates.leftBot.x); const height = Math.abs(coordinates.rightBot.y - coordinates.leftTop.y); @@ -433,10 +493,28 @@ export class VisibleNode extends BaseNode { return this.node.getRenderingBoundsCoordinates(); } + getRenderingBoxWidthAndHeight(): number[] { + const coordinates = this.getAbsRenderingBox(); + const width = Math.abs(coordinates.rightTop.x - coordinates.leftBot.x); + const height = Math.abs(coordinates.rightBot.y - coordinates.leftTop.y); + return [width, height]; + } + getPositionalRelationship(targetNode: Node): PostionalRelationship { + let currentBox: BoxCoordinates = this.getAbsRenderingBox(); + let targetBox: BoxCoordinates = targetNode.getAbsRenderingBox(); + + if (!isEmpty(this.getACssAttribute("box-shadow"))) { + currentBox = this.getAbsBoundingBox(); + } + + if (!isEmpty(targetNode.getACssAttribute("box-shadow"))) { + targetBox = targetNode.getAbsBoundingBox(); + } + return computePositionalRelationship( - this.getAbsRenderingBox(), - targetNode.getAbsRenderingBox() + currentBox, + targetBox ); } diff --git a/core/src/bricks/overlap.ts b/core/src/bricks/overlap.ts index 878fd49..6088a01 100644 --- a/core/src/bricks/overlap.ts +++ b/core/src/bricks/overlap.ts @@ -68,6 +68,12 @@ export const findOverlappingNodes = ( startingNode.getPositionalRelationship(targetNode) === PostionalRelationship.OVERLAP ) { + + console.log("targetNode: ", targetNode); + console.log("startingNode: ", startingNode); + console.log("startingNode: ", ); + console.log("startingNode.getPositionalRelationship(targetNode): ", startingNode.getPositionalRelationship(targetNode)); + overlappingNodes.push(targetNode); if (!currentPath.has(startingNode.getId())) { diff --git a/core/src/bricks/remove-css.ts b/core/src/bricks/remove-css.ts new file mode 100644 index 0000000..bcfb806 --- /dev/null +++ b/core/src/bricks/remove-css.ts @@ -0,0 +1,85 @@ +import { cssStrToNum } from "../code/generator/util"; +import { Attributes } from "../design/adapter/node"; +import { isEmpty } from "../utils"; +import { Node } from "./node"; + +// addAdditionalCssAttributesToNodes adds additional css information to a node and its children. +export const removeCssFromNode = (node: Node) => { + if (isEmpty(node)) { + return; + } + + const children = node.getChildren(); + if (isEmpty(children)) { + return; + } + + const positionalAttributes: Attributes = node.getPositionalCssAttributes(); + let cssAttributes: Attributes = node.getCssAttributes(); + + if (isEmpty(positionalAttributes["position"]) && positionalAttributes["justify-content"] !== "space-between") { + const actualChildrenWidth: number = calculateActualChildrenWidth(node); + const width: number = cssStrToNum(cssAttributes["width"]); + + if (Math.abs(actualChildrenWidth - width) <= 5 && isEmpty(positionalAttributes["position"])) { + delete (cssAttributes["width"]); + } + } + + + for (const child of children) { + const childAttributes: Attributes = child.getCssAttributes(); + if (isEmpty(positionalAttributes["position"]) && positionalAttributes["justify-content"] !== "space-between") { + if (!isEmpty(childAttributes["width"]) && !isEmpty(cssAttributes["width"]) && childAttributes["width"] === cssAttributes["width"]) { + delete (cssAttributes["width"]); + } + + if (!isEmpty(childAttributes["height"]) && !isEmpty(cssAttributes["height"]) && childAttributes["height"] === cssAttributes["height"]) { + delete (cssAttributes["height"]); + } + + node.setCssAttributes(cssAttributes); + } + + removeCssFromNode(child); + } +}; + +const calculateActualChildrenWidth = (node: Node): number => { + const positionalAttributes: Attributes = node.getPositionalCssAttributes(); + + const children = node.getChildren(); + let paddingCum: number = 0; + let gapCum: number = 0; + let widthCum: number = 0; + + + paddingCum += cssStrToNum(positionalAttributes["padding-left"]); + paddingCum += cssStrToNum(positionalAttributes["padding-right"]); + + if (positionalAttributes["gap"]) { + gapCum += cssStrToNum(positionalAttributes["gap"]) * children.length - 1; + } + + for (let i = 0; i < children.length; i++) { + const child: Node = children[i]; + const childAttributes: Attributes = child.getCssAttributes(); + const childPositionalAttributes: Attributes = child.getPositionalCssAttributes(); + + if (childAttributes["width"]) { + widthCum += cssStrToNum(childAttributes["width"]); + } + + if (isEmpty(positionalAttributes["gap"])) { + if (childPositionalAttributes["margin-left"]) { + gapCum += cssStrToNum(childPositionalAttributes["margin-left"]); + } + + if (childPositionalAttributes["margin-right"]) { + gapCum += cssStrToNum(childPositionalAttributes["margin-right"]); + } + } + } + + return paddingCum + gapCum + widthCum; +}; diff --git a/core/src/bricks/remove-node.ts b/core/src/bricks/remove-node.ts index b1ba4d0..eac42eb 100644 --- a/core/src/bricks/remove-node.ts +++ b/core/src/bricks/remove-node.ts @@ -1,4 +1,4 @@ -import { Node, computePositionalRelationship, PostionalRelationship, NodeType } from "./node"; +import { Node, NodeType } from "./node"; import { Attributes } from "../design/adapter/node"; import { isEmpty } from "../utils"; import { cssStrToNum } from "../code/generator/util"; @@ -7,13 +7,7 @@ export const removeNode = (node: Node): Node => { const children: Node[] = node.getChildren(); if (children.length === 1) { const child = children[0]; - - // console.log("haveSimlarWidthAndHeight(node, child): ", haveSimlarWidthAndHeight(node, child)); - - if (haveSimlarWidthAndHeight(node, child)) { - - const cssAttributes: Attributes = { ...node.getCssAttributes(), ...child.getCssAttributes(), @@ -41,7 +35,7 @@ export const removeChildrenNode = (node: Node): Node => { continue; } - if (haveSimlarWidthAndHeight(node, child)) { + if (haveSimlarWidthAndHeight(node, child) && isEmpty(child.getChildren())) { const cssAttributes: Attributes = { ...node.getCssAttributes(), ...child.getCssAttributes(), @@ -63,10 +57,6 @@ export const removeChildrenNode = (node: Node): Node => { }; const haveSimlarWidthAndHeight = (currentNode: Node, targetNode: Node): boolean => { - if (computePositionalRelationship(currentNode.getAbsBoundingBox(), targetNode.getAbsBoundingBox()) === PostionalRelationship.COMPLETE_OVERLAP) { - return true; - } - const currentWidth: string = currentNode.getACssAttribute("width"); const targetWidth: string = targetNode.getACssAttribute("width"); let similarWidth: boolean = false; @@ -93,7 +83,28 @@ const haveSimlarWidthAndHeight = (currentNode: Node, targetNode: Node): boolean similarHeight = true; } + return similarHeight && similarWidth; + + + // const currentBorderRadius: string = currentNode.getACssAttribute("border-radius"); + // const targetBorderRadius: string = targetNode.getACssAttribute("border-radius"); + + // // console.log("currentNode: ", currentNode); + // // console.log("targetNode: ", targetNode); + // // console.log("currentBorderRadius: ", currentBorderRadius); + // // console.log("targetBorderRadius: ", targetBorderRadius); + // if (isEmpty(currentBorderRadius) || isEmpty(targetBorderRadius)) { + // return similarHeight && similarWidth; + // } + + // let similarCornerRadius: boolean = false; + // let diffInCornerRadius: number = Math.abs(cssStrToNum(currentBorderRadius) - cssStrToNum(targetHeight)); + // if (diffInCornerRadius <= 1) { + // similarCornerRadius = true; + // } + + // return similarHeight && similarWidth && similarCornerRadius; }; const filterAttributes = (attribtues: Attributes): Attributes => { diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index d853c29..53d7296 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -8,7 +8,11 @@ import { VectorNode, TextNode, } from "../../../bricks/node"; -import { Attributes, ExportFormat } from "../../../design/adapter/node"; +import { + Attributes, + ExportFormat, + StyledTextSegment, +} from "../../../design/adapter/node"; import { generateProps } from "../../../../ee/loop/data-array-registry"; import { nameRegistryGlobalInstance } from "../../name-registry/name-registry"; import { Component, Data, DataArr } from "../../../../ee/loop/component"; @@ -69,14 +73,34 @@ export class Generator { const attributes = htmlTag === "a" ? 'href="#" ' : ""; const textProp = this.getText(node, option); - //@ts-ignore - const listSegments = node.node.getListSegments(); - const listType = listSegments[0].listType; - if (listSegments.length === 1 && listType !== "none") { - return `<${listType} ${attributes}${textNodeClassProps}>${textProp}`; + // if textProp is enclosed in a
      or
        tag, we don't want to wrap another
        around it + const result = /^<(ul|ol)>.*<\/(ul|ol)>$/s.exec(textProp); + if (result && result[1] === result[2]) { + const listTag = result[1]; + const textPropWithoutListTag = textProp.substring( + 4, // length of
          or
            + textProp.length - 5 // length of
        or
      + ); + return `<${listTag} ${textNodeClassProps}>${textPropWithoutListTag}`; + } + + const children: Node[] = node.getChildren(); + + if (isEmpty(children)) { + return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}`; } - return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}`; + for (const child of children) { + if (child.getType() === NodeType.TEXT) { + child.addAnnotations("htmlTag", "span"); + } + } + + return await this.generateHtmlFromNodes( + children, + [`<${htmlTag} ${attributes} ${textNodeClassProps}>${textProp} {" "}`, ``], + option + ); } case NodeType.GROUP: @@ -282,12 +306,16 @@ export class Generator { const positionalCssAttribtues: Attributes = node.getPositionalCssAttributes(); + const cssAttribtues: Attributes = + node.getCssAttributes(); + if ( positionalCssAttribtues["position"] === "absolute" || positionalCssAttribtues["margin-left"] || positionalCssAttribtues["margin-right"] || positionalCssAttribtues["margin-top"] || - positionalCssAttribtues["margin-bottom"] + positionalCssAttribtues["margin-bottom"] || + cssAttribtues["border-radius"] ) { return `
      ` + inner + `
      `; } @@ -365,165 +393,286 @@ export class Generator { const styledTextSegments = textNode.node.getStyledTextSegments(); - const defaultFontSize = textNode.getACssAttribute("font-size"); - const defaultFontFamily = textNode.getACssAttribute("font-family"); - const defaultFontWeight = textNode.getACssAttribute("font-weight"); - const defaultColor = textNode.getACssAttribute("color"); - const defaultLetterSpacing = textNode.getACssAttribute("letter-spacing"); - - const cssAttributesSegments = styledTextSegments.map( - (styledTextSegment) => { - // here we only keep attributes if they are different from the default attribute - const cssAttributes: Attributes = {}; - const parentCssAttributes: Attributes = {}; - - const fontSize = `${styledTextSegment.fontSize}px`; - if (fontSize !== defaultFontSize) { - cssAttributes["font-size"] = fontSize; - } + // for keeping track of nested tags + const htmlTagStack: ("ol" | "ul" | "li")[] = []; + let prevIndentation = 0; + + let resultText = styledTextSegments + .map( + (styledTextSegment, styledTextSegmentIndex, styledTextSegmentArr) => { + // get attributes that are different from parent attributes + const { cssAttributes, parentCssAttributes } = getAttributeOverrides( + node as TextNode, + styledTextSegment + ); - const fontFamily = styledTextSegment.fontFamily; - if (fontFamily !== defaultFontFamily) { - cssAttributes["font-family"] = fontFamily; - } + const { characters, listType, indentation, href } = styledTextSegment; + + if (listType === "none") { + const result = this.wrapTextIfHasAttributes( + characters, + href, + cssAttributes, + parentCssAttributes, + option + ); + + + let newStr: string = replaceNewLine(result, option); + newStr = replaceLeadingWhiteSpace(newStr, option); + + return newStr; + } + + // create enough lists to match identation + let resultText = ""; + const indentationToAdd = indentation - prevIndentation; + + if (indentationToAdd > 0) { + // Open new sublists + for (let i = 0; i < indentationToAdd; i++) { + const lastOpenTag = htmlTagStack[htmlTagStack.length - 1]; + + // According to the HTML5 W3C spec,
        or
          can only contain
        1. . + // Hence, we are appending a
        2. tag if the last open tag is
            or
              . + if (lastOpenTag === "ul" || lastOpenTag === "ol") { + resultText += `
            1. `; + htmlTagStack.push("li"); + } + + if (option.cssFramework === "tailwindcss") { + // Extra attributes needed due to Tailwind's CSS reset + const listProps = this.getPropsFromAttributes( + { + "margin-left": "40px", + "list-style-type": listType === "ul" ? "disc" : "decimal", + }, + option + ); - const fontWeight = styledTextSegment.fontWeight.toString(); - if (fontWeight !== defaultFontWeight) { - cssAttributes["font-weight"] = fontWeight; - } + resultText += `<${listType} ${listProps}>`; + } else { + resultText += `<${listType}>`; + } - const textDecoration = styledTextSegment.textDecoration; - if (textDecoration !== "normal") { - cssAttributes["text-decoration"] = textDecoration; - } + htmlTagStack.push(listType); + } + } else if (indentationToAdd < 0) { + // Close sublists + for ( + let numOfListClosed = 0; + numOfListClosed < Math.abs(indentationToAdd); + + ) { + const htmlTag = htmlTagStack.pop(); + if (htmlTag === "li") { + resultText += `
            2. `; + } else { + resultText += ``; + numOfListClosed++; + } + } + } + // update indentation for the next loop + prevIndentation = indentation; + + // create list items + const listItems = splitByNewLine(characters); + resultText += listItems + .map((listItem, listItemIndex, listItemArr) => { + const hasOpenListItem = + htmlTagStack[htmlTagStack.length - 1] === "li"; + + let result = ""; + + if (!hasOpenListItem) { + htmlTagStack.push("li"); + result += "
            3. "; + } + + const lastListItem = + listItemArr[listItemIndex - 1] || + styledTextSegmentArr[styledTextSegmentIndex - 1].characters || + ""; + if (hasOpenListItem && lastListItem.endsWith("\n")) { + result += "
            4. "; + } + + result += this.wrapTextIfHasAttributes( + listItem, + href, + cssAttributes, + parentCssAttributes, + option + ); + + const isLastListItem = listItemIndex === listItemArr.length - 1; + if (listItem.endsWith("\n") && !isLastListItem) { + // close list item + htmlTagStack.pop(); + result += "
            5. "; + } - const textTransform = styledTextSegment.textTransform; - if (textTransform !== "none") { - cssAttributes["text-transform"] = textTransform; + return result; + }) + .join(""); + + return resultText; } + ) + .join(""); + + // close all open tags + while (htmlTagStack.length) { + resultText += ``; + } + + return resultText; + } - const color = styledTextSegment.color; - if (color !== defaultColor) { - cssAttributes["color"] = color; + wrapTextIfHasAttributes( + text: string, + href: string, + cssAttributes: Attributes, + parentCssAttributes: Attributes, + option: Option + ) { + const resultText = escapeHtml(text); + + if (isEmpty(cssAttributes) && !href) { + return resultText; + } + + const htmlTag = href ? "a" : "span"; + const hrefAttribute = href ? ` href="${href}"` : ""; + const styleAttribute = !isEmpty(cssAttributes) + ? ` ${this.getPropsFromAttributes( + cssAttributes, + option, + undefined, + parentCssAttributes + )}` + : ""; + + return `<${htmlTag}${hrefAttribute}${styleAttribute}>${resultText}`; + } +} + +const getAttributeOverrides = ( + textNode: TextNode, + styledTextSegment: StyledTextSegment +) => { + const defaultFontSize = textNode.getACssAttribute("font-size"); + const defaultFontFamily = textNode.getACssAttribute("font-family"); + const defaultFontWeight = textNode.getACssAttribute("font-weight"); + const defaultColor = textNode.getACssAttribute("color"); + const defaultLetterSpacing = textNode.getACssAttribute("letter-spacing"); + + const cssAttributes: Attributes = {}; + const parentCssAttributes: Attributes = {}; + + const fontSize = `${styledTextSegment.fontSize}px`; + if (fontSize !== defaultFontSize) { + cssAttributes["font-size"] = fontSize; + } + + const fontFamily = styledTextSegment.fontFamily; + if (fontFamily !== defaultFontFamily) { + cssAttributes["font-family"] = fontFamily; + } + + const fontWeight = styledTextSegment.fontWeight.toString(); + if (fontWeight !== defaultFontWeight) { + cssAttributes["font-weight"] = fontWeight; + } + + const textDecoration = styledTextSegment.textDecoration; + if (textDecoration !== "normal") { + cssAttributes["text-decoration"] = textDecoration; + } + + const textTransform = styledTextSegment.textTransform; + if (textTransform !== "none") { + cssAttributes["text-transform"] = textTransform; + } + + const color = styledTextSegment.color; + if (color !== defaultColor) { + cssAttributes["color"] = color; + } + + const letterSpacing = styledTextSegment.letterSpacing; + if ( + letterSpacing !== defaultLetterSpacing && + defaultLetterSpacing && + letterSpacing !== "normal" + ) { + cssAttributes["letter-spacing"] = letterSpacing; + parentCssAttributes["font-size"] = defaultFontSize; + } + + return { + cssAttributes, + parentCssAttributes, + }; +}; + +const replaceNewLine = (str: string, option: Option) => { + let newStrParts: string[] = []; + let start: number = 0; + for (let i = 0; i < str.length; i++) { + if (i === str.length - 1) { + newStrParts.push(str.substring(start)); + } + + if (str.charAt(i) === "\n") { + let numberOfNewLines: number = 1; + let end: number = i + 1; + for (let j = i + 1; j < str.length; j++) { + if (str.charAt(j) !== "\n") { + break; } - const letterSpacing = styledTextSegment.letterSpacing; - if ( - letterSpacing !== defaultLetterSpacing && - defaultLetterSpacing && - letterSpacing !== "normal" - ) { - cssAttributes["letter-spacing"] = letterSpacing; - parentCssAttributes["font-size"] = defaultFontSize; + end = j + 1; + numberOfNewLines++; + } + + if (numberOfNewLines > 1) { + newStrParts.push(str.substring(start, i - 1)); + + for (let j = 0; j < numberOfNewLines; j++) { + newStrParts.push(option.uiFramework === "html" ? "
              " : "
              "); } - return { - start: styledTextSegment.start, - end: styledTextSegment.end, - cssAttributes, - parentCssAttributes, - }; + start = end; + i = start; } - ); - - // Here are handle lists in text, where: - // - A "list segment" is a segment of text that is an ordered list, an unordered list, or not a list at all - // - A "list item" is text inside a list segment, separated by a new line character. - // - // For example: - //
                <- this is a "list segment" - //
              • item 1
              • <- this is a "list item" - //
              • item 2
              • <- this is another "list item" - //
              - return textNode.node - .getListSegments() - .map((listSegment, _, listSegments) => { - const listTag = listSegment.listType; - - const listItems = splitByNewLine(listSegment.characters); - - // for keeping track of where we are in listSegment.characters - let currentIndex = listSegment.start; - - const listContent = listItems - .map((listItemText) => { - const itemStartIndex = currentIndex; - const itemEndIndex = currentIndex + listItemText.length - 1; - - const cssAttributesSegmentsInListItem = - cssAttributesSegments.filter( - (segment) => - !( - segment.end < itemStartIndex || segment.start > itemEndIndex - ) - ); + } + } - let result = cssAttributesSegmentsInListItem - .map((segment) => { - let text = escapeHtml( - listItemText.substring( - segment.start - itemStartIndex, - segment.end - itemStartIndex - ) - ); + return newStrParts.join(""); +}; - if (!isEmpty(segment.cssAttributes)) { - const textProps = this.getPropsFromAttributes( - segment.cssAttributes, - option, - undefined, - segment.parentCssAttributes - ); - - // if css attribute applies to the whole list item, wrap it in a
            6. tag - const htmlTag = - listTag !== "none" && - listItemText.length === segment.end - segment.start - ? "li" - : "span"; - - if (listTag === "none") { - // replace all \n in text with
              tag - text = text - .split("\n") - .join(option.uiFramework === "html" ? "
              " : "
              "); - } - - text = `<${htmlTag} ${textProps}>${text}`; - } - - return text; - }) - .join(""); - - if (listTag !== "none" && !result.startsWith("${result}
            7. `; - } +const replaceLeadingWhiteSpace = (str: string, option: Option) => { + let newStrParts: string[] = []; + for (let i = 0; i < str.length; i++) { + if (str.charCodeAt(i) === 160) { + newStrParts.push(" "); + continue; + } - currentIndex = itemEndIndex + 1; - return result; - }) - .join(""); - - if ( - listTag === "none" || - // if there is only one list, we don't wrap the list items in a
                or
                  tag because we're doing it outside - listSegments.length === 1 - ) { - return listContent; - } else { - return `<${listTag}>${listContent}`; - } - }) - .join(""); + newStrParts.push(str.substring(i)); + break; } -} + + return newStrParts.join(""); +}; const splitByNewLine = (text: string) => { let listItems = text.split("\n"); // if last item is "", it means there is a new line at the end of the last item - if (listItems[listItems.length - 1] === "") { + if (text !== "" && listItems[listItems.length - 1] === "") { listItems.pop(); listItems = listItems.map((item) => item + "\n"); } else { diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index dc21cfb..e87b82b 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -13,8 +13,9 @@ import { twUnitMap, twWidthMap, tailwindTextDecorationMap, - MAX_BORDER_RADIUS_IN_PIXELS, - twcssDropShadowToYOffSetMap, + twcssDropShadowToSumMap, + twcssRotateToDegMap, + twcssZIndexMap, } from "./twcss-conversion-map"; import { Attributes } from "../../../design/adapter/node"; import { FontsRegistryGlobalInstance } from "./fonts-registry"; @@ -232,6 +233,10 @@ const findClosestTwcssFontSize = (cssFontSize: string) => { } }); + if (!(smallestDiff < 2)) { + return `text-[${cssFontSize}]`; + } + return closestTailwindFontSize; }; @@ -246,7 +251,7 @@ const findClosestTwcssClassUsingPixel = ( targetPixelStr: string, twClassToPixelMap: Record, defaultClass: string -) => { +): [string, number] => { let closestTwClass = defaultClass; const targetPixelNum = extractPixelNumberFromString(targetPixelStr); @@ -262,7 +267,7 @@ const findClosestTwcssClassUsingPixel = ( } }); - return closestTwClass; + return [closestTwClass, smallestDiff]; }; // findTwcssTextDecoration translates text-decoration from css to tailwindcss @@ -338,29 +343,29 @@ const findClosestTwcssLetterSpacing = ( return twClassToUse; }; - -const getYOffsetFromBoxShadow = (boxShadowValue: string): number => { +const getRadiusFromBoxShadow = (boxShadowValue: string): number => { const spaceParts: string[] = boxShadowValue.split(" "); - if (spaceParts.length <= 4) { - return 1; + + if (spaceParts.length < 4) { + return 2; } - if (spaceParts[1].endsWith("px")) { - return parseInt(boxShadowValue.slice(0, -2)); + if (spaceParts[2].endsWith("px")) { + return parseInt(spaceParts[2].slice(0, -2)); } - return 1; + return 2; }; - -const getRadiusFromBoxShadow = (boxShadowValue: string): number => { +const getYOffSetFromBoxShadow = (boxShadowValue: string): number => { const spaceParts: string[] = boxShadowValue.split(" "); - if (spaceParts.length <= 4) { + + if (spaceParts.length < 4) { return 1; } - if (spaceParts[2].endsWith("px")) { - return parseInt(boxShadowValue.slice(0, -2)); + if (spaceParts[1].endsWith("px")) { + return parseInt(spaceParts[1].slice(0, -2)); } return 1; @@ -370,15 +375,10 @@ const findClosestTwcssDropShadowClassUsingPixel = ( cssValue: string, ) => { let closestTwClass = ""; - const radius: number = getYOffsetFromBoxShadow(cssValue); - - let largestBoxShadowValue: string = ""; - let targetPixelNum: number = 1; - const dropShadowParts: string[] = cssValue.split("),"); if (isEmpty(dropShadowParts)) { - return ""; + return "shadow"; } const newShadowParts: string[] = []; @@ -392,20 +392,28 @@ const findClosestTwcssDropShadowClassUsingPixel = ( newShadowParts.push(dropShadowParts[i]); } + + let largestRadius: number = -Infinity; let largestYOffset: number = -Infinity; + for (let i = 0; i < newShadowParts.length; i++) { - const yOffset: number = getYOffsetFromBoxShadow(newShadowParts[i]); + const radius: number = getRadiusFromBoxShadow(newShadowParts[i]); + const yOffset: number = getYOffSetFromBoxShadow(newShadowParts[i]); + + if (radius > largestRadius) { + largestRadius = radius; + } + if (yOffset > largestYOffset) { - largestBoxShadowValue = cssValue; - targetPixelNum = yOffset; + largestYOffset = yOffset; } } - let smallestDiff: number = -Infinity; - Object.entries(twcssDropShadowToYOffSetMap).forEach(([key, val]) => { + let smallestDiff: number = Infinity; + Object.entries(twcssDropShadowToSumMap).forEach(([key, val]) => { const pixelNum = extractPixelNumberFromString(val); if (val.endsWith("px")) { - const diff = Math.abs(targetPixelNum - pixelNum); + const diff: number = Math.abs(largestRadius + largestYOffset - pixelNum); if (diff < smallestDiff) { smallestDiff = diff; closestTwClass = key; @@ -413,7 +421,11 @@ const findClosestTwcssDropShadowClassUsingPixel = ( } }); - return closestTwClass; + if (isEmpty(closestTwClass)) { + return "shadow"; + } + + return "shadow" + "-" + closestTwClass; }; @@ -528,7 +540,7 @@ export const getTwcssClass = ( return `h-[${heightNum}px]`; } - const approximatedTwcssHeightClass = findClosestTwcssClassUsingPixel( + const [approximatedTwcssHeightClass] = findClosestTwcssClassUsingPixel( cssValue, twHeightMap, "h-0" @@ -543,13 +555,17 @@ export const getTwcssClass = ( return approximatedTwcssHeightClass; + case "min-width": + const minWidthNum = extractPixelNumberFromString(cssValue); + return `min-w-[${minWidthNum}px]`; + case "width": const widthNum = extractPixelNumberFromString(cssValue); if (cssValue.endsWith("px") && widthNum > largestTwcssWidthInPixels) { return `w-[${widthNum}px]`; } - const approximatedTwcssWidthClass = findClosestTwcssClassUsingPixel( + const [approximatedTwcssWidthClass] = findClosestTwcssClassUsingPixel( cssValue, twWidthMap, "w-0" @@ -568,7 +584,7 @@ export const getTwcssClass = ( return "border-" + findClosestTwcssColor(cssValue); case "border-width": { - const borderWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -582,7 +598,7 @@ export const getTwcssClass = ( } case "border-top-width": { - const borderTopWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderTopWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -596,7 +612,7 @@ export const getTwcssClass = ( } case "border-bottom-width": { - const borderBottomWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderBottomWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -610,7 +626,7 @@ export const getTwcssClass = ( } case "border-left-width": { - const borderLeftWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderLeftWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -624,7 +640,7 @@ export const getTwcssClass = ( } case "border-right-width": { - const borderRightWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderRightWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -638,19 +654,24 @@ export const getTwcssClass = ( } case "border-radius": { - const borderRadiusTwSize = findClosestTwcssClassUsingPixel( + const [borderRadiusTwSize, smallestDiff] = findClosestTwcssClassUsingPixel( cssValue, twBorderRadiusMap, "none" ); + + if (smallestDiff > 2) { + return `rounded-[${cssValue}]`; + } + if (borderRadiusTwSize === "0") { return ""; } - const borderRadiusSize = extractPixelNumberFromString(cssValue); - if (borderRadiusSize > MAX_BORDER_RADIUS_IN_PIXELS) { - return `rounded-[${borderRadiusSize}px]`; - } + // const borderRadiusSize = extractPixelNumberFromString(cssValue); + // if (borderRadiusSize > MAX_BORDER_RADIUS_IN_PIXELS) { + // return `rounded-[${borderRadiusSize}px]`; + // } return borderRadiusTwSize === "" ? "rounded" @@ -1022,7 +1043,87 @@ export const getTwcssClass = ( } } + case "transform": { + if (cssValue.startsWith("rotate")) { + return findClosestTwcssRotate(cssValue); + } + } + + case "z-index": { + return findClosestZIndex(cssValue); + } + default: return ""; } }; + +const findClosestZIndex = (cssValue: string) => { + let num: number = parseInt(cssValue); + + if (isEmpty(num) || num === 0) { + return ""; + } + + let minDiff = Infinity; + let twcssClass: string = ""; + Object.entries(twcssZIndexMap).forEach(([twcssValue, index]) => { + let diff: number = Math.abs(num - index); + + if (diff < minDiff) { + minDiff = diff; + twcssClass = twcssValue; + } + }); + + if (isEmpty(twcssClass)) { + return ""; + } + + if (Math.abs(minDiff) !== 0) { + return "z-" + `[${num}]`; + } + + return twcssClass; +}; + + +const findClosestTwcssRotate = (cssValue: string) => { + const start: number = cssValue.indexOf("(") + 1; + const end: number = cssValue.indexOf("d"); + const numStr: string = cssValue.substring(start, end); + + console.log(cssValue.substring(start, end)); + let numRaw: number = parseInt(numStr); + if (isEmpty(numRaw)) { + return ""; + } + + let num: number = numRaw; + let rotatePrefix: string = ""; + if (numRaw < 0) { + num = numRaw * -1; + rotatePrefix = "-"; + } + + let minDiff = Infinity; + let twcssClass: string = ""; + Object.entries(twcssRotateToDegMap).forEach(([twValue, deg]) => { + let diff: number = Math.abs(num - deg); + + if (diff < minDiff) { + minDiff = diff; + twcssClass = twValue; + } + }); + + if (isEmpty(twcssClass)) { + return ""; + } + + if (Math.abs(minDiff) > 3) { + return rotatePrefix + "rotate" + `[${num}deg]`; + } + + return rotatePrefix + twcssClass; +}; diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index 565f5a9..88c5f93 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -103,22 +103,17 @@ const getPropsFromNode = (node: Node, option: Option): string => { switch (node.getType()) { case NodeType.TEXT: { const attributes: Attributes = { + ...{ + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, ...node.getCssAttributes(), - ...node.getPositionalCssAttributes(), }; - //@ts-ignore - const listSegments = node.node.getListSegments(); - // Extra classes needed for lists due to Tailwind's CSS reset - const listType = listSegments[0].listType; - if (listSegments.length === 1 && listType === "ul") { - attributes["list-style-type"] = "disc"; - } - - if (listSegments.length === 1 && listType === "ol") { - attributes["list-style-type"] = "decimal"; - } - return convertCssClassesToTwcssClasses(attributes, option, node.getId()); } case NodeType.GROUP: @@ -154,6 +149,9 @@ const getPropsFromNode = (node: Node, option: Option): string => { if (isEmpty(node.getChildren())) { return convertCssClassesToTwcssClasses( { + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), ...filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningFilter: true, }), diff --git a/core/src/code/generator/tailwindcss/twcss-conversion-map.ts b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts index 40e317b..c4e42c5 100644 --- a/core/src/code/generator/tailwindcss/twcss-conversion-map.ts +++ b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts @@ -5,15 +5,37 @@ export const tailwindTextDecorationMap = { }; -export const twcssDropShadowToYOffSetMap = { - "sm": "drop-shadow(0 1px 1px rgba(0,0,0,0.05))", - "": "drop-shadow(0 1px 2px rgba(0,0,0,0.1))", - "md": "drop-shadow(0 4px 3px rgba(0,0,0,0.07))", - "lg": "drop-shadow(0 10px 8px rgba(0,0,0,0.04))", - "xl": "drop-shadow(0 20px 13px rgba(0,0,0,0.03))", - "2xl": "drop-shadow(0 25px 25px rgba(0,0,0,0.15))", +export const twcssDropShadowToSumMap = { + "sm": "3px", + "": "4px", + "md": "10px", + "lg": "25px", + "xl": "45px", + "2xl": "75px", +}; + +export const twcssRotateToDegMap = { + "rotate-0": 0, + "rotate-1": 1, + "rotate-2": 2, + "rotate-3": 3, + "rotate-6": 6, + "rotate-12": 12, + "rotate-45": 45, + "rotate-90": 90, + "rotate-180": 180, }; +export const twcssZIndexMap = { + "z-0": 0, + "z-10": 10, + "z-20": 20, + "z-30": 30, + "z-40": 40, + "z-50": 50, +}; + + export const twHeightMap = { "h-0": "0px", "h-px": "1px", diff --git a/core/src/code/generator/util.ts b/core/src/code/generator/util.ts index 82b1ed0..d5eb30a 100644 --- a/core/src/code/generator/util.ts +++ b/core/src/code/generator/util.ts @@ -35,6 +35,10 @@ const isNumeric = (str: string): boolean => { export const cssStrToNum = (value: string): number => { + if (isEmpty(value)) { + return 0; + } + if (value.endsWith("px")) { return parseInt(value.slice(0, -2)); } diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index c100f52..4a61715 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -7,9 +7,10 @@ import { VectorNode as BricksVector, VisibleNode, computePositionalRelationship, + VectorNode, } from "../../../bricks/node"; import { isEmpty } from "../../../utils"; -import { BoxCoordinates, Attributes, ExportFormat, ListSegment } from "../node"; +import { BoxCoordinates, Attributes, ExportFormat } from "../node"; import { colorToString, colorToStringWithOpacity, @@ -23,9 +24,9 @@ import { figmaFontNameToCssString, } from "./util"; import { PostionalRelationship } from "../../../bricks/node"; -import { getLineBasedOnDirection } from "../../../bricks/line"; import { Direction } from "../../../bricks/direction"; import { StyledTextSegment } from "../node"; +import { Line } from "../../../bricks/line"; enum NodeType { GROUP = "GROUP", @@ -47,6 +48,15 @@ const safelySetWidthAndHeight = ( figmaNode: SceneNode, attributes: Attributes ) => { + // @ts-ignore + if (!isEmpty(figmaNode.rotation) && figmaNode.rotation !== 0) { + // @ts-ignore + attributes["width"] = `${figmaNode.width}px`; + // @ts-ignore + attributes["height"] = `${figmaNode.height}px`; + return; + } + if ( nodeType === NodeType.FRAME || nodeType === NodeType.IMAGE || @@ -64,6 +74,24 @@ const safelySetWidthAndHeight = ( attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; } + // @ts-ignore + if (!isEmpty(figmaNode.absoluteRenderBounds)) { + const boundingWidth: number = figmaNode.absoluteBoundingBox.width; + const boundingHeight: number = figmaNode.absoluteBoundingBox.height; + + // @ts-ignore + const renderingWidth: number = figmaNode.absoluteRenderBounds.width; + // @ts-ignore + const renderingHeight: number = figmaNode.absoluteRenderBounds.height; + + if (renderingWidth * 0.5 > boundingWidth || renderingHeight * 0.5 > boundingHeight) { + // @ts-ignore + attributes["width"] = `${figmaNode.absoluteRenderBounds.width}px`; + // @ts-ignore + attributes["height"] = `${figmaNode.absoluteRenderBounds.height}px`; + } + } + return; } @@ -97,9 +125,8 @@ const addDropShadowCssProperty = ( .map((effect: DropShadowEffect | InnerShadowEffect) => { const { offset, radius, spread, color } = effect; - const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${ - spread ?? 0 - }px ${rgbaToString(color)}`; + const dropShadowString = `${offset.x}px ${offset.y}px ${radius}px ${spread ?? 0 + }px ${rgbaToString(color)}`; if (effect.type === "INNER_SHADOW") { return "inset " + dropShadowString; @@ -151,10 +178,6 @@ const getPositionalCssAttributes = (figmaNode: SceneNode): Attributes => { } } - // console.log("figmaNode: ", figmaNode); - // console.log("primaryAxisAlignItems: ", figmaNode.primaryAxisAlignItems); - // console.log("counterAxisAlignItems: ", figmaNode.counterAxisAlignItems); - switch (figmaNode.primaryAxisAlignItems) { case "MIN": attributes["justify-content"] = "flex-start"; @@ -216,6 +239,12 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { addDropShadowCssProperty(figmaNode, attributes); } + // @ts-ignore + if (!isEmpty(figmaNode.rotation) && figmaNode.rotation !== 0) { + // @ts-ignore + attributes["transform"] = `rotate(${figmaNode.rotation}deg)`; + } + if ( figmaNode.type === NodeType.VECTOR || figmaNode.type === NodeType.ELLIPSE @@ -289,11 +318,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; } - // width - attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; - - // height - attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; + safelySetWidthAndHeight(figmaNode.type, figmaNode, attributes); // box shadow addDropShadowCssProperty(figmaNode, attributes); @@ -375,24 +400,28 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { let width: number = absoluteRenderBounds ? absoluteRenderBounds.width + 2 : absoluteBoundingBox.width; - let height: number = absoluteRenderBounds - ? absoluteRenderBounds.height - : absoluteBoundingBox.height; let moreThanOneRow: boolean = false; - if (absoluteRenderBounds) { + let fontSize: number = 0; + + if (!isEmpty(absoluteRenderBounds)) { const renderBoundsHeight = absoluteRenderBounds.height; if (figmaNode.fontSize !== figma.mixed) { - moreThanOneRow = renderBoundsHeight > figmaNode.fontSize; + fontSize = figmaNode.fontSize; + } else { + for (const segment of figmaNode.getStyledTextSegments(["fontSize"])) { + if (segment.fontSize > fontSize) { + fontSize = segment.fontSize; + } + } } - if (!moreThanOneRow) { - attributes["white-space"] = "nowrap"; - } + + moreThanOneRow = renderBoundsHeight > fontSize * 1.5; } - if (absoluteBoundingBox && absoluteRenderBounds) { + if (!isEmpty(absoluteBoundingBox) && !isEmpty(absoluteRenderBounds)) { const renderBoundsWidth = absoluteRenderBounds.width; const boundingBoxWidth = absoluteBoundingBox.width; @@ -400,7 +429,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // actual effects therefore should be always considered as "text-align": "left" when there is only one row if ( Math.abs(boundingBoxWidth - renderBoundsWidth) / boundingBoxWidth > - 0.1 || + 0.1 || moreThanOneRow ) { // text alignment @@ -419,39 +448,19 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { if ( Math.abs(absoluteBoundingBox.width - absoluteRenderBounds.width) / - absoluteBoundingBox.width > + absoluteBoundingBox.width > 0.2 ) { - width = absoluteRenderBounds.width + 4; + width = absoluteRenderBounds.width + 6; } } - // @ts-ignore - if (isAutoLayout(figmaNode.parent)) { - attributes["width"] = `${absoluteBoundingBox.width}px`; - } - switch (figmaNode.textAutoResize) { - case "NONE": { - attributes["width"] = `${width}px`; - // attributes["height"] = `${height}px`; - break; - } - case "HEIGHT": { - attributes["width"] = `${width}px`; - break; - } - case "WIDTH_AND_HEIGHT": { - // do nothing - attributes["width"] = `${width}px`; - break; - } - case "TRUNCATE": { - attributes["width"] = `${width}px`; - // attributes["height"] = `${height}px`; - attributes["text-overflow"] = "ellipsis"; - break; - } + if (!moreThanOneRow && !isEmpty(absoluteRenderBounds) && (Math.abs(absoluteBoundingBox.width - absoluteRenderBounds.width) < fontSize)) { + attributes["min-width"] = `${absoluteBoundingBox.width}px`; + attributes["white-space"] = "nowrap"; + } else { + attributes["width"] = `${absoluteBoundingBox.width}px`; } // switch (figmaNode.textAutoResize) { @@ -489,12 +498,8 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // font color const paints = figmaNode.fills; if (paints !== figma.mixed && paints.length > 0) { - const solidPaints = paints.filter( - (paint) => paint.type === "SOLID" - ) as SolidPaint[]; - - if (solidPaints.length > 0) { - const finalColor = getRgbaFromPaints(solidPaints); + const finalColor = getRgbaFromPaints(paints); + if (finalColor) { attributes["color"] = rgbaToString(finalColor); } } else if (paints === figma.mixed) { @@ -509,7 +514,9 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { }) as SolidPaint[]; const finalColor = getRgbaFromPaints(mostCommonPaints); - attributes["color"] = rgbaToString(finalColor); + if (finalColor) { + attributes["color"] = rgbaToString(finalColor); + } } const textContainingOnlyOneWord = @@ -610,6 +617,35 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { return attributes; }; +const getBoxCoordinatesFromFigmaNode = (figmaNode: SceneNode): BoxCoordinates => { + let boundingBox = figmaNode.absoluteBoundingBox; + // // @ts-ignore + + // if (figmaNode.absoluteRenderBounds) { + // // @ts-ignore + // boundingBox = figmaNode.absoluteRenderBounds; + // } + + return { + leftTop: { + x: boundingBox.x, + y: boundingBox.y, + }, + leftBot: { + x: boundingBox.x, + y: boundingBox.y + boundingBox.height, + }, + rightTop: { + x: boundingBox.x + boundingBox.width, + y: boundingBox.y, + }, + rightBot: { + x: boundingBox.x + boundingBox.width, + y: boundingBox.y + boundingBox.height, + }, + }; +}; + export class FigmaNodeAdapter { node: SceneNode; private cssAttributes: Attributes; @@ -641,6 +677,48 @@ export class FigmaNodeAdapter { getAbsoluteBoundingBoxCoordinates(): BoxCoordinates { let boundingBox = this.node.absoluteBoundingBox; + // // @ts-ignore + // if (!isEmpty(this.node.rotation)) { + // const leftTop: Coordinate = { + // x: this.node.absoluteTransform[0][2], + // y: this.node.absoluteTransform[1][2] + // }; + + // return { + // leftTop: { + // x: leftTop.x, + // y: leftTop.y, + // }, + // leftBot: { + // x: leftTop.x, + // y: leftTop.y + this.node.height, + // }, + // rightTop: { + // x: leftTop.x + this.node.width, + // y: leftTop.y, + // }, + // rightBot: { + // x: leftTop.x + this.node.width, + // y: leftTop.y + this.node.height, + // }, + // }; + // } + + // @ts-ignore + if (!isEmpty(this.node.absoluteRenderBounds)) { + const boundingWidth: number = this.node.absoluteBoundingBox.width; + const boundingHeight: number = this.node.absoluteBoundingBox.height; + + // @ts-ignore + const renderingWidth: number = this.node.absoluteRenderBounds.width; + // @ts-ignore + const renderingHeight: number = this.node.absoluteRenderBounds.height; + + if (renderingWidth * 0.5 > boundingWidth || renderingHeight * 0.5 > boundingHeight) { + return this.getRenderingBoundsCoordinates(); + } + } + return { leftTop: { x: boundingBox.x, @@ -663,9 +741,15 @@ export class FigmaNodeAdapter { getRenderingBoundsCoordinates(): BoxCoordinates { let boundingBox = this.node.absoluteBoundingBox; - // @ts-ignore - if (this.node.absoluteRenderBounds) { + // // @ts-ignore + // if (!isEmpty(this.node.rotation)) { + // return this.getAbsoluteBoundingBoxCoordinates(); + // } + + + // @ts-ignore + if (!isEmpty(this.node.absoluteRenderBounds)) { // @ts-ignore boundingBox = this.node.absoluteRenderBounds; } @@ -775,6 +859,9 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { "textCase", "fills", "letterSpacing", + "listOptions", + "indentation", + "hyperlink", ]); // for converting figma textDecoration to css textDecoration @@ -793,29 +880,25 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { TITLE: "capitalize", } as const; - return styledTextSegments.map((segment) => ({ - ...segment, - fontFamily: figmaFontNameToCssString(segment.fontName), - textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], - textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], - color: rgbaToString(getRgbaFromPaints(segment.fills)), - letterSpacing: figmaLetterSpacingToCssString(segment.letterSpacing), - })); - } - - getListSegments(): ListSegment[] { const figmaListOptionsToHtmlTagMap = { NONE: "none", UNORDERED: "ul", ORDERED: "ol", } as const; - const listSegments = this.node.getStyledTextSegments(["listOptions"]); - - return listSegments.map((segment) => ({ - ...segment, - listType: figmaListOptionsToHtmlTagMap[segment.listOptions.type], - })); + return styledTextSegments.map((segment) => { + const rgba = getRgbaFromPaints(segment.fills); + return { + ...segment, + fontFamily: figmaFontNameToCssString(segment.fontName), + textDecoration: figmaTextDecorationToCssMap[segment.textDecoration], + textTransform: figmaTextCaseToCssTextTransformMap[segment.textCase], + color: rgba ? rgbaToString(rgba) : "", + letterSpacing: figmaLetterSpacingToCssString(segment.letterSpacing), + listType: figmaListOptionsToHtmlTagMap[segment.listOptions.type], + href: segment?.hyperlink?.type === "URL" ? segment.hyperlink.value : "", + }; + }); } } @@ -851,7 +934,14 @@ type Feedback = { export const convertFigmaNodesToBricksNodes = ( figmaNodes: readonly SceneNode[] ): Feedback => { - let reordered = [...figmaNodes]; + let reordered = []; + + for (let i = 0; i < figmaNodes.length; i++) { + const figmaNode: SceneNode = figmaNodes[i]; + if (figmaNode.visible) { + reordered.push(figmaNode); + } + } if (reordered.length > 1) { reordered.sort((a, b) => { @@ -897,11 +987,13 @@ export const convertFigmaNodesToBricksNodes = ( if (reordered.length === 1) { const figmaNode = reordered[0]; - if (figmaNode.type === NodeType.RECTANGLE) { + if (figmaNode.type === NodeType.RECTANGLE && !doesNodeContainsAnImage(figmaNode)) { result.isSingleRectangle = true; } } + const newNodes: Node[] = []; + for (let i = 0; i < reordered.length; i++) { const figmaNode = reordered[i]; @@ -929,11 +1021,23 @@ export const convertFigmaNodesToBricksNodes = ( case NodeType.GROUP: newNode = new BricksGroupNode([], new FigmaNodeAdapter(figmaNode)); break; - case NodeType.INSTANCE: case NodeType.FRAME: + if (doesNodeContainsAnImage(figmaNode)) { + newNode = new ImageNode(new FigmaImageNodeAdapter(figmaNode)); + result.doNodesContainImage = true; + break; + } + + if (isFrameNodeTransparent(figmaNode)) { + newNode = new BricksGroupNode([], new FigmaNodeAdapter(figmaNode)); + } + + break; + case NodeType.INSTANCE: case NodeType.COMPONENT: if (isFrameNodeTransparent(figmaNode)) { newNode = new BricksGroupNode([], new FigmaNodeAdapter(figmaNode)); + break; } break; case NodeType.TEXT: @@ -949,57 +1053,139 @@ export const convertFigmaNodesToBricksNodes = ( result.doNodesContainImage = true; break; } + + if (!isEmpty(figmaNode.rotation)) { + newNode = new VectorNode(new FigmaImageNodeAdapter(figmaNode)); + break; + } } + + newNodes.push(newNode); + + // //@ts-ignore + // if (!isEmpty(figmaNode?.children)) { + // let isExportableNode: boolean = false; + // //@ts-ignore + // const feedback = convertFigmaNodesToBricksNodes(figmaNode.children); + // if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { + // if (!feedback.doNodesHaveNonOverlappingChildren) { + // isExportableNode = true; + // if (feedback.doNodesContainImage) { + // newNode = new ImageNode( + // new FigmaVectorGroupNodeAdapter(figmaNode) + // ); + // } else { + // newNode = new BricksVector( + // new FigmaVectorGroupNodeAdapter(figmaNode) + // ); + // } + // } + // } + + // result.areAllNodesExportable = + // feedback.areAllNodesExportable && result.areAllNodesExportable; + + // result.doNodesContainImage = + // feedback.doNodesContainImage || result.doNodesContainImage; + + // if (!isExportableNode) { + // newNode.setChildren(feedback.nodes); + // } + // } + + // result.nodes.push(newNode); + } + } + + // let horizontalOverlap: boolean = areNodesOverlappingByDirection( + // reordered, + // Direction.HORIZONTAL + // ); + + // let verticalOverlap: boolean = areNodesOverlappingByDirection( + // reordered, + // Direction.VERTICAL + // ); + + // result.doNodesHaveNonOverlappingChildren = + // !horizontalOverlap || !verticalOverlap; + + // console.log("result.doNodesHaveNonOverlappingChildren: ", result.doNodesHaveNonOverlappingChildren); + + + // console.log(figmaNodes); + // console.log(result); + + for (let i = 0; i < reordered.length; i++) { + const figmaNode = reordered[i]; + + let newNode: Node = newNodes[i]; + + //@ts-ignore + if (!isEmpty(figmaNode?.children)) { + let isExportableNode: boolean = false; //@ts-ignore - if (!isEmpty(figmaNode?.children)) { - let isExportableNode: boolean = false; + const feedback: Feedback = convertFigmaNodesToBricksNodes(figmaNode.children); + + let doNodesHaveNonOverlappingChildren: boolean = true; + let horizontalNonOverlap: boolean = areThereNonOverlappingByDirection( //@ts-ignore - const feedback = convertFigmaNodesToBricksNodes(figmaNode.children); - if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { - if (!feedback.doNodesHaveNonOverlappingChildren) { - isExportableNode = true; - if (feedback.doNodesContainImage) { - newNode = new ImageNode( - new FigmaVectorGroupNodeAdapter(figmaNode) - ); - } else { - newNode = new BricksVector( - new FigmaVectorGroupNodeAdapter(figmaNode) - ); - } + figmaNode?.children, + Direction.HORIZONTAL + ); + + //@ts-ignore + let verticalNonOverlap: boolean = areThereNonOverlappingByDirection( + //@ts-ignore + figmaNode?.children, + Direction.VERTICAL + ); + + doNodesHaveNonOverlappingChildren = + horizontalNonOverlap || verticalNonOverlap || feedback.doNodesHaveNonOverlappingChildren; + + if (allNodesAreOfVectorNodeTypes) { + doNodesHaveNonOverlappingChildren = false; + } + + + // console.log("parent:<<<<<<<<<<<<<<<<<<< ", figmaNodes); + // console.log("feedback: ", feedback); + // console.log("figmaNode: ", figmaNode); + // console.log("!doNodesHaveNonOverlappingChildren: ", !doNodesHaveNonOverlappingChildren); + if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { + if (!doNodesHaveNonOverlappingChildren) { + isExportableNode = true; + if (feedback.doNodesContainImage) { + newNode = new ImageNode( + new FigmaVectorGroupNodeAdapter(figmaNode) + ); + } else { + newNode = new BricksVector( + new FigmaVectorGroupNodeAdapter(figmaNode) + ); } } + } - result.areAllNodesExportable = - feedback.areAllNodesExportable && result.areAllNodesExportable; + result.areAllNodesExportable = + feedback.areAllNodesExportable && result.areAllNodesExportable; - result.doNodesContainImage = - feedback.doNodesContainImage || result.doNodesContainImage; + result.doNodesContainImage = + feedback.doNodesContainImage || result.doNodesContainImage; - if (!isExportableNode) { - newNode.setChildren(feedback.nodes); - } + result.doNodesHaveNonOverlappingChildren = doNodesHaveNonOverlappingChildren; + + if (!isExportableNode) { + newNode.setChildren(feedback.nodes); } - result.nodes.push(newNode); + newNodes[i] = newNode; } } - let horizontalOverlap: boolean = areNodesOverlappingByDirection( - result.nodes, - Direction.HORIZONTAL - ); - let verticalOverlap: boolean = areNodesOverlappingByDirection( - result.nodes, - Direction.VERTICAL - ); - result.doNodesHaveNonOverlappingChildren = - !horizontalOverlap || !verticalOverlap; - - if (allNodesAreOfVectorNodeTypes) { - result.doNodesHaveNonOverlappingChildren = false; - } + result.nodes = newNodes; if (!isEmpty(sliceNode)) { result.nodes = [new BricksVector(new FigmaVectorNodeAdapter(sliceNode))]; @@ -1010,32 +1196,47 @@ export const convertFigmaNodesToBricksNodes = ( return result; }; -const areNodesOverlappingByDirection = ( - nodes: Node[], +const areThereNonOverlappingByDirection = ( + nodes: readonly SceneNode[], direction: Direction ): boolean => { - let overlap: boolean = false; + for (let i = 0; i < nodes.length; i++) { - const currentNode: Node = nodes[i]; - let currentLine = getLineBasedOnDirection(currentNode, direction); + const currentNode: SceneNode = nodes[i]; + let currentLine = getFigmaLineBasedOnDirection(currentNode, direction); + let nonOverlap: boolean = true; for (let j = 0; j < nodes.length; j++) { - const targetNode: Node = nodes[j]; - if (targetNode.getId() === currentNode.getId()) { + const targetNode: SceneNode = nodes[j]; + if (i === j) { continue; } - const targetLine = getLineBasedOnDirection(targetNode, direction); - if (currentLine.overlap(targetLine)) { - overlap = true; + + const targetLine = getFigmaLineBasedOnDirection(targetNode, direction); + + if (currentLine.overlapStrict(targetLine)) { + nonOverlap = false; break; } } - if (overlap) { - break; + if (nonOverlap) { + return true; } } - return overlap; + return false; +}; + + +// getFigmaLineBasedOnDirection gets the boundary of a node depending on the input direction. +export const getFigmaLineBasedOnDirection = (figmaNode: SceneNode, direction: Direction) => { + const coordinates = getBoxCoordinatesFromFigmaNode(figmaNode); + + if (direction === Direction.HORIZONTAL) { + return new Line(coordinates.leftTop.y, coordinates.rightBot.y); + } + + return new Line(coordinates.leftTop.x, coordinates.rightBot.x); }; diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index bd06880..ed48358 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -50,7 +50,7 @@ export const isFrameNodeTransparent = ( // doesNodeContainsAnImage tests whether rectangle node contain an image export const doesNodeContainsAnImage = ( - node: RectangleNode | EllipseNode + node: RectangleNode | EllipseNode | FrameNode ): boolean => { if (node.fills != figma.mixed) { for (const fill of node.fills) { @@ -166,14 +166,14 @@ function blendColors(color1: RGBA, color2: RGBA) { return { r, g, b, a } as RGBA; } -export function getRgbaFromPaints(paints: Paint[]) { +export function getRgbaFromPaints(paints: readonly Paint[]) { // TODO: support GradientPaint const solidPaints = paints.filter( (paint) => paint.type === "SOLID" ) as SolidPaint[]; if (solidPaints.length === 0) { - throw new Error("No solid paints found"); + return null; } const colors = solidPaints.map(({ color, opacity }) => ({ diff --git a/core/src/design/adapter/node.ts b/core/src/design/adapter/node.ts index 68ba071..5a0bcf1 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -37,7 +37,6 @@ export interface StyledTextSegment { family: string; style: string; }; - // CSS strings fontSize: number; fontFamily: string; fontWeight: number; @@ -45,13 +44,9 @@ export interface StyledTextSegment { textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; color: string; letterSpacing: string; -} - -export interface ListSegment { - characters: string; - start: number; - end: number; listType: "none" | "ul" | "ol"; + indentation: number; + href: string; } export interface TextNode extends Node { @@ -59,7 +54,6 @@ export interface TextNode extends Node { isItalic(): boolean; getFamilyName(): string; getStyledTextSegments(): StyledTextSegment[]; - getListSegments(): ListSegment[]; } export interface VectorNode extends Node {} diff --git a/core/src/index.ts b/core/src/index.ts index 23c486b..e4996bc 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -18,6 +18,7 @@ import { instantiateComponentRegistryGlobalInstance } from "../ee/loop/component import { annotateNodeForHtmlTag } from "../ee/cv/component-recognition"; import { instantiateAiApplicationRegistryGlobalInstance, AiApplication, aiApplicationRegistryGlobalInstance } from "../ee/ui/ai-application-registry"; import { EVENT_AI_CODE_GEN_SUCCESS, EVENT_AI_COMPONENT_IDENTIFICATION_SUCCESS, EVENT_AI_GET_NAME_SUCCESS } from "./analytic/amplitude"; +import { removeCssFromNode } from "./bricks/remove-css"; export const convertToCode = async ( figmaNodes: readonly SceneNode[], @@ -28,28 +29,34 @@ export const convertToCode = async ( return []; } - // console.log("converted: ", converted); - const dedupedNodes: Node[] = []; for (const node of converted) { let newNode: Node = removeNode(node); + // console.log("after ssssss: ", newNode); removeCompletelyOverlappingNodes(newNode, null); removeChildrenNode(newNode); dedupedNodes.push(newNode); } + let startingNode: Node = dedupedNodes.length > 1 ? new GroupNode(dedupedNodes) : dedupedNodes[0]; - // console.log("startingNode: ", startingNode); + console.log("after trim: ", startingNode); + groupNodes(startingNode); + console.log("after group: ", startingNode); + startingNode = removeNode(startingNode); removeCompletelyOverlappingNodes(startingNode, null); removeChildrenNode(startingNode); + // console.log("startingNode: ", startingNode); + addAdditionalCssAttributesToNodes(startingNode); + removeCssFromNode(startingNode); instantiateRegistries(startingNode, option); return await generateCodingFiles(startingNode, option); diff --git a/core/src/utils.ts b/core/src/utils.ts index b55422f..f083e6e 100644 --- a/core/src/utils.ts +++ b/core/src/utils.ts @@ -6,6 +6,7 @@ export const isEmpty = (value: any): boolean => { return ( value === undefined || value === null || + Number.isNaN(value) || (typeof value === "object" && Object.keys(value).length === 0) || (typeof value === "string" && value.trim().length === 0) ); From becfe3908e0ee41ec2f9c6f8301ed08761bbd1c5 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Wed, 17 May 2023 18:58:41 -0700 Subject: [PATCH 19/21] fix various bugs for launch --- core/ee/code/prop.ts | 8 + core/ee/cv/component-recognition.ts | 14 +- core/ee/loop/component.ts | 54 +++--- core/ee/loop/loop.ts | 140 +++++++++++----- core/ee/web/request.ts | 6 +- core/src/bricks/additional-css.ts | 88 ++++++---- core/src/bricks/annotation.ts | 1 + core/src/bricks/direction.ts | 29 +++- core/src/bricks/grouping.ts | 15 -- core/src/bricks/line.ts | 2 +- core/src/bricks/node.ts | 16 +- core/src/bricks/overlap.ts | 5 - core/src/bricks/remove-css.ts | 8 +- core/src/bricks/remove-node.ts | 22 +-- core/src/bricks/util.ts | 14 +- core/src/code/generator/css/generator.ts | 33 ++-- core/src/code/generator/font.ts | 4 +- core/src/code/generator/html/generator.ts | 5 +- .../generator/tailwindcss/css-to-twcss.ts | 6 +- .../code/generator/tailwindcss/generator.ts | 21 --- core/src/code/name-registry/name-registry.ts | 11 +- core/src/design/adapter/figma/adapter.ts | 154 +++++++++--------- core/src/design/adapter/figma/util.ts | 8 +- core/src/index.ts | 15 +- figma/src/code.ts | 20 ++- 25 files changed, 374 insertions(+), 325 deletions(-) create mode 100644 core/src/bricks/annotation.ts diff --git a/core/ee/code/prop.ts b/core/ee/code/prop.ts index 33d00b1..30dc6ec 100644 --- a/core/ee/code/prop.ts +++ b/core/ee/code/prop.ts @@ -142,6 +142,14 @@ export const getTextVariableProp = (nodeId: string): string => { for (const propBinding of propBindings) { for (const location of propBinding.locations) { if (location.type === "text") { + if (propBinding.dataType === DataType.boolean) { + if (isEmpty(propBinding.conditionalValue)) { + continue; + } + + return `{${propBinding.prop} ? "${propBinding.defaultValue}" : "${propBinding.conditionalValue}"}`; + } + return `{${propBinding.prop}}`; } } diff --git a/core/ee/cv/component-recognition.ts b/core/ee/cv/component-recognition.ts index 4619638..9b13687 100644 --- a/core/ee/cv/component-recognition.ts +++ b/core/ee/cv/component-recognition.ts @@ -2,7 +2,7 @@ import { Node, NodeType } from "../../src/bricks/node"; import { ExportFormat } from "../../src/design/adapter/node"; import { traverseNodes } from "../../src/utils"; import { AiApplication, aiApplicationRegistryGlobalInstance } from "../ui/ai-application-registry"; -import { predictImage, predictText } from "../web/request"; +import { predictImage } from "../web/request"; export const annotateNodeForHtmlTag = async (startingNode: Node) => { try { @@ -32,14 +32,10 @@ export const annotateNodeForHtmlTag = async (startingNode: Node) => { return node?.getType() !== NodeType.VECTOR_GROUP; }); - const [predictImagesResult, predictTextsResult] = await Promise.allSettled([ + const [predictImagesResult] = await Promise.allSettled([ predictImage(idImageMap), - predictText(idTextMap), ]); - const textPredictions = - predictTextsResult.status === "fulfilled" ? predictTextsResult.value : {}; - const imagePredictions = predictImagesResult.status === "fulfilled" ? predictImagesResult.value @@ -49,15 +45,11 @@ export const annotateNodeForHtmlTag = async (startingNode: Node) => { console.error("Error with image prediction", predictImagesResult.reason); } - if (predictTextsResult.status === "rejected") { - console.error("Error with image prediction", predictTextsResult.reason); - } - await traverseNodes(startingNode, async (node) => { if (node.node) { const originalId = node.node.getOriginalId(); const predictedHtmlTag = - imagePredictions[originalId] || textPredictions[originalId]; + imagePredictions[originalId]; if (predictedHtmlTag) { aiApplicationRegistryGlobalInstance.addApplication(AiApplication.componentIdentification); diff --git a/core/ee/loop/component.ts b/core/ee/loop/component.ts index 8ed14e2..44930bb 100644 --- a/core/ee/loop/component.ts +++ b/core/ee/loop/component.ts @@ -3,10 +3,11 @@ import { isEmpty } from "../../src/utils"; import { CssFramework } from "../../src/code/code"; import { getTwcssClass } from "../../src/code/generator/tailwindcss/css-to-twcss"; import { optionRegistryGlobalInstance } from "../../src/code/option-registry/option-registry"; -import { areAllNodesSimilar, vectorGroupAnnotation } from "./loop"; +import { areAllNodesSimilar } from "./loop"; import { nameRegistryGlobalInstance } from "../../src/code/name-registry/name-registry"; import { IdToPropBindingMap, propRegistryGlobalInstance } from "./prop-registry"; import uuid from "react-native-uuid"; +import { shouldUseAsBackgroundImage } from "../../src/bricks/util"; type PropLocation = { type: string; @@ -393,39 +394,32 @@ export const gatherPropsFromSimilarNodes = (nodes: Node[], instanceIds: string[] return [false, {}]; } - const cssProps: ComponentProperties = optionRegistryGlobalInstance.getOption().cssFramework === CssFramework.tailwindcss ? gatherTwcssPropsFromNodes(nodes, instanceIds) : gatherCssPropsFromNodes(nodes, instanceIds); + const [result, similarNodes] = areAllNodesSimilar(nodes); + if (!result) { + return [false, {}]; + } + + const cssProps: ComponentProperties = optionRegistryGlobalInstance.getOption().cssFramework === CssFramework.tailwindcss ? gatherTwcssPropsFromNodes(similarNodes, instanceIds) : gatherCssPropsFromNodes(similarNodes, instanceIds); let componentProps: ComponentProperties = { - ...gatherPropsFromImageNodes(nodes, instanceIds), - ...gatherPropsFromVectorNodes(nodes, instanceIds), - ...gatherTextPropsFromNodes(nodes, instanceIds), + ...gatherPropsFromImageNodes(similarNodes, instanceIds), + ...gatherPropsFromVectorNodes(similarNodes, instanceIds), + ...gatherTextPropsFromNodes(similarNodes, instanceIds), ...cssProps, }; - - let allVectorGroups: boolean = true; - for (const node of nodes) { - if (!node.hasAnnotation(vectorGroupAnnotation)) { - allVectorGroups = false; - } - } - - if (allVectorGroups) { + let children: Node[] = similarNodes[0].getChildren(); + if (isEmpty(children)) { return [true, componentProps]; } - if (!areAllNodesSimilar(nodes)) { - return [false, {}]; - } - - let children: Node[] = nodes[0].getChildren(); for (let i = 0; i < children.length; i++) { - let similarNodes: Node[] = []; - for (const targetNode of nodes) { + let similarChildrenNodes: Node[] = []; + for (const targetNode of similarNodes) { const targetChildren: Node[] = targetNode.getChildren(); - similarNodes.push(targetChildren[i]); + similarChildrenNodes.push(targetChildren[i]); } - const [result, targetProps] = gatherPropsFromSimilarNodes(similarNodes, instanceIds); + const [result, targetProps] = gatherPropsFromSimilarNodes(similarChildrenNodes, instanceIds); if (!result) { return [result, {}]; } @@ -447,7 +441,7 @@ export const gatherPropsFromVectorNodes = (nodes: Node[], instanceIds: string[]) } for (const node of nodes) { - if (!node.hasAnnotation(vectorGroupAnnotation)) { + if (node.getType() !== NodeType.VECTOR || shouldUseAsBackgroundImage(node)) { return properties; } } @@ -503,7 +497,7 @@ export const gatherPropsFromImageNodes = (nodes: Node[], instanceIds: string[]): } for (const node of nodes) { - if (node.getType() !== NodeType.IMAGE) { + if (node.getType() !== NodeType.IMAGE || shouldUseAsBackgroundImage(node)) { return properties; } } @@ -606,7 +600,8 @@ export const gatherCssPropsFromNodes = (potentiallyRepeatedNode: Node[], instanc return properties; } - const sampleNodeType: NodeType = potentiallyRepeatedNode[0].getType(); + const sampleNode: Node = potentiallyRepeatedNode[0]; + const sampleNodeType: NodeType = sampleNode.getType(); let existingCssKeys: Set = new Set(); for (const node of potentiallyRepeatedNode) { @@ -676,7 +671,7 @@ export const gatherCssPropsFromNodes = (potentiallyRepeatedNode: Node[], instanc Object.entries(properties).forEach(([id, { cssKey, bindings }]) => { let firstBinding: PropValueBinding = bindings[0]; - if (sampleNodeType === NodeType.IMAGE || sampleNodeType === NodeType.VECTOR_GROUP || sampleNodeType === NodeType.VECTOR) { + if (!shouldUseAsBackgroundImage(sampleNode) && (sampleNodeType === NodeType.IMAGE || sampleNodeType === NodeType.VECTOR_GROUP || sampleNodeType === NodeType.VECTOR)) { if (cssKey !== "width" && cssKey !== "height") { delete properties[id]; return; @@ -702,7 +697,8 @@ export const gatherTwcssPropsFromNodes = (potentiallyRepeatedNode: Node[], insta return properties; } - const sampleNodeType: NodeType = potentiallyRepeatedNode[0].getType(); + const sampleNode: Node = potentiallyRepeatedNode[0]; + const sampleNodeType: NodeType = sampleNode.getType(); let existingCssKeys: Set = new Set(); for (const node of potentiallyRepeatedNode) { @@ -778,7 +774,7 @@ export const gatherTwcssPropsFromNodes = (potentiallyRepeatedNode: Node[], insta } Object.entries(properties).forEach(([id, { cssKey, bindings }]) => { - if (sampleNodeType === NodeType.IMAGE || sampleNodeType === NodeType.VECTOR_GROUP || sampleNodeType === NodeType.VECTOR) { + if (!shouldUseAsBackgroundImage(sampleNode) && (sampleNodeType === NodeType.IMAGE || sampleNodeType === NodeType.VECTOR_GROUP || sampleNodeType === NodeType.VECTOR)) { if (cssKey !== "width" && cssKey !== "height") { delete properties[id]; return; diff --git a/core/ee/loop/loop.ts b/core/ee/loop/loop.ts index 03b1647..e758f99 100644 --- a/core/ee/loop/loop.ts +++ b/core/ee/loop/loop.ts @@ -6,11 +6,11 @@ import { Component, gatherPropsFromSimilarNodes } from "./component"; import { componentRegistryGlobalInstance } from "./component-registry"; import { optionRegistryGlobalInstance } from "../../src/code/option-registry/option-registry"; import { UiFramework } from "../../src/code/code"; +import { replacedParentAnnotation } from "../../src/bricks/annotation"; +import { StyledTextSegment } from "../../src/design/adapter/node"; -export const vectorGroupAnnotation: string = "vectorGroup"; export const registerRepeatedComponents = (node: Node) => { if (optionRegistryGlobalInstance.getOption().uiFramework === UiFramework.react) { - annotateVectorGroupNodes(node); registerComponentFromNodes(node); } }; @@ -32,39 +32,12 @@ export const registerComponentFromNodes = (node: Node) => { } }; -export const annotateVectorGroupNodes = (node: Node): boolean => { - if (node.getType() === NodeType.TEXT || node.getType() === NodeType.IMAGE) { - return false; - } - - if (node.getType() === NodeType.VECTOR_GROUP) { - return true; - } - - let result: boolean = true; - const children: Node[] = node.getChildren(); - for (const child of children) { - const childResult: boolean = annotateVectorGroupNodes(child); - if (childResult) { - child.addAnnotations(vectorGroupAnnotation, true); - } - - result = result && childResult; - } - - if (result) { - node.addAnnotations(vectorGroupAnnotation, true); - } - - return result; -}; const registerComponentForConsecutiveNodes = (nodes: Node[]): boolean => { const component: Component = new Component(); const instanceIds: string[] = nodes.map((node) => { const instanceId: string = uuid.v1() as string; - component.addIdtoInstanceIdMapping(node.getId(), instanceId); return instanceId; @@ -85,6 +58,37 @@ const registerComponentForConsecutiveNodes = (nodes: Node[]): boolean => { return true; }; +const checkWhetherTwoNodesAreSimilarAccountingForRemovedNodes = (currentNode: Node, modelNode: Node): [boolean, string, Node[]] => { + let [result, reason]: [boolean, string] = areTwoNodesSimilar(currentNode, modelNode); + + const modelNodeChildren: Node[] = modelNode.getChildren(); + const currentNodeChildren: Node[] = currentNode.getChildren(); + + if (currentNode.hasAnnotation(replacedParentAnnotation) && modelNode.hasAnnotation(replacedParentAnnotation) && modelNodeChildren.length === 1 && currentNodeChildren.length === 1) { + const [altResult, altReason] = areTwoNodesSimilar(currentNodeChildren[0], modelNodeChildren[0]); + if (altResult) { + return [altResult, altReason, [modelNodeChildren[0], currentNodeChildren[0]]]; + } + } + + + if (!result && currentNode.hasAnnotation(replacedParentAnnotation) && modelNodeChildren.length === 1) { + const [altResult, altReason] = areTwoNodesSimilar(currentNode, modelNodeChildren[0]); + if (altResult) { + return [altResult, altReason, [modelNodeChildren[0], currentNode]]; + } + } + + if (!result && modelNode.hasAnnotation(replacedParentAnnotation) && currentNodeChildren.length === 1) { + const [altResult, altReason] = areTwoNodesSimilar(currentNodeChildren[0], modelNode); + if (altResult) { + return [altResult, altReason, [modelNode, currentNodeChildren[0]]]; + } + } + + return [result, reason, [modelNode, currentNode]]; +}; + export const registerComponentFromSimilarChildrenNodes = (node: Node) => { const children = node.getChildren(); if (children.length === 0) { @@ -104,7 +108,7 @@ export const registerComponentFromSimilarChildrenNodes = (node: Node) => { continue; } - const [result, _]: [boolean, string] = areTwoNodesSimilar(currentNode, modelNode); + const [result, _]: [boolean, string, Node[]] = checkWhetherTwoNodesAreSimilarAccountingForRemovedNodes(currentNode, modelNode); if (!result) { @@ -138,12 +142,14 @@ export const registerComponentFromSimilarChildrenNodes = (node: Node) => { return; }; -export const areAllNodesSimilar = (nodes: Node[]): boolean => { +export const areAllNodesSimilar = (nodes: Node[]): [boolean, Node[]] => { if (nodes.length < 2) { - return false; + return [false, []]; } const prevNode: Node = nodes[0]; + let firstPassFailed: boolean = false; + for (let i = 0; i < nodes.length; i++) { if (i === 0) { continue; @@ -152,11 +158,41 @@ export const areAllNodesSimilar = (nodes: Node[]): boolean => { const [result, _]: [boolean, string] = areTwoNodesSimilar(prevNode, nodes[i]); if (!result) { - return false; + firstPassFailed = true; + break; } } - return true; + if (!firstPassFailed) { + return [true, nodes]; + } + + let similarNodes: Node[] = []; + const id: Set = new Set(); + + for (let i = 0; i < nodes.length; i++) { + if (i === 0) { + continue; + } + + const [result, _, [modelNode, currentNode]]: [boolean, string, Node[]] = checkWhetherTwoNodesAreSimilarAccountingForRemovedNodes(prevNode, nodes[i]); + + if (!result) { + return [false, []]; + } + + if (!id.has(modelNode.getId())) { + id.add(modelNode.getId()); + similarNodes.push(modelNode); + } + + if (!id.has(currentNode.getId())) { + id.add(currentNode.getId()); + similarNodes.push(currentNode); + } + } + + return [true, similarNodes]; }; const areTwoNodesSimilar = (currentNode: Node, targetNode: Node): [boolean, string] => { @@ -179,11 +215,10 @@ const areTwoNodesSimilar = (currentNode: Node, targetNode: Node): [boolean, stri const currentChildren = currentNode.getChildren(); const targetChildren = targetNode.getChildren(); - if (currentNode.hasAnnotation(vectorGroupAnnotation)) { - return [targetNode.hasAnnotation(vectorGroupAnnotation), "both are vector groups"]; + if (currentNode.getType() === NodeType.VECTOR) { + return [targetNode.getType() === NodeType.VECTOR, "both nodes are vectors"]; } - const [result, reason]: [boolean, string] = doTwoNodesHaveTheSameType(currentNode, targetNode); if (!result) { return [false, reason]; @@ -258,12 +293,35 @@ const isVectorGroup = (node: Node): boolean => { }; const detectTextNodeSimilarities = (currentNode: TextNode, targetNode: TextNode): [boolean, string] => { - if (currentNode.getACssAttribute("font-family") !== targetNode.getACssAttribute("font-family")) { - return [false, "font family not the same"]; + let largestTargetFontSize: number = -Infinity; + let largestCurrentFontSize: number = -Infinity; + + const targetSegments: StyledTextSegment[] = targetNode.getStyledTextSegments(); + const currentSegments: StyledTextSegment[] = currentNode.getStyledTextSegments(); + const targetFontFamilies: Set = new Set(); + + if (targetSegments.length !== currentSegments.length) { + return [false, "styled segments not the same"]; + } + + for (const targetSegment of targetSegments) { + if (targetSegment.fontSize > largestTargetFontSize) { + largestTargetFontSize = targetSegment.fontSize; + }; + targetFontFamilies.add(targetSegment.fontFamily); + } + + for (const currentSegment of currentSegments) { + if (currentSegment.fontSize > largestCurrentFontSize) { + largestCurrentFontSize = currentSegment.fontSize; + if (!targetFontFamilies.has(currentSegment.fontFamily)) { + return [false, "font families not the same"]; + } + }; } - if (currentNode.getACssAttribute("font-size") !== targetNode.getACssAttribute("font-size")) { - return [false, "font size not the same"]; + if (largestCurrentFontSize > largestTargetFontSize + 4 || largestCurrentFontSize < largestTargetFontSize - 4) { + return [false, "font sizes are not similar"];; } return [true, "similar text nodes"];; diff --git a/core/ee/web/request.ts b/core/ee/web/request.ts index 4351562..394b65f 100644 --- a/core/ee/web/request.ts +++ b/core/ee/web/request.ts @@ -60,7 +60,7 @@ export const getNameMap = async (): Promise => { }); const text: string = await response.text(); - const parsedArr: string[] = JSON.parse(text); + const parsedArr: NameMap[] = JSON.parse(text); if (isEmpty(parsedArr)) { return {}; @@ -68,7 +68,7 @@ export const getNameMap = async (): Promise => { const parsedNameMapArr: NameMap[] = []; for (const nameMapStr of parsedArr) { - parsedNameMapArr.push(JSON.parse(nameMapStr)); + parsedNameMapArr.push(nameMapStr); } const consolidatedNameMap: NameMap = getConsolidateNameMap(parsedNameMapArr); @@ -97,7 +97,7 @@ const dedupNames = (nameMap: NameMap) => { return; } - if (oldName.startsWith("dataArr") || oldName.startsWith("Component")) { + if (oldName.startsWith("data") || oldName.startsWith("Component")) { globalNonDuplicates.add(newName); } diff --git a/core/src/bricks/additional-css.ts b/core/src/bricks/additional-css.ts index 0fa5f96..fb60455 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -6,18 +6,17 @@ import { reorderNodesBasedOnDirection, getDirection, } from "./direction"; -import { Node, NodeType } from "./node"; +import { Node, NodeType, TextNode } from "./node"; import { getContainerLineFromNodes, getLinesFromNodes, Line, getLineBasedOnDirection, getContainerRenderingLineFromNodes, - getLineUsingRenderingBoxBasedOnDirection, } from "./line"; -import { filterCssValue } from "./util"; +import { filterCssValue, shouldUseAsBackgroundImage } from "./util"; import { absolutePositioningAnnotation } from "./overlap"; -import { getFigmaLineBasedOnDirection } from "../design/adapter/figma/adapter"; +import { nameRegistryGlobalInstance } from "../code/name-registry/name-registry"; export const selectBox = ( node: Node, @@ -36,11 +35,6 @@ export const selectBox = ( return node.getAbsBoundingBox(); } - if (node.getType() === NodeType.TEXT) { - return node.getAbsBoundingBox(); - } - - if (useBoundingBox) { return node.getAbsBoundingBox(); } @@ -82,7 +76,7 @@ export const addAdditionalCssAttributesToNodes = (node: Node) => { return; } - const direction = getDirection(node.children); + const direction = getDirection(node); reorderNodesBasedOnDirection(node.children, direction); node.addPositionalCssAttributes(getPositionalCssAttributes(node, direction)); adjustChildrenHeightAndWidthCssValue(node); @@ -93,23 +87,12 @@ export const addAdditionalCssAttributesToNodes = (node: Node) => { } }; -const adjustChildrenPositionalCssValue = (node: Node, direction: Direction) => { +const adjustChildrenPositionalCssValue = (node: Node, direction) => { const children = node.getChildren(); if (isEmpty(children)) { return; } - children.sort((a, b): number => { - const currentRenderingBox: BoxCoordinates = a.getAbsRenderingBox(); - const targetRenderingBox: BoxCoordinates = b.getAbsRenderingBox(); - - if (direction === Direction.HORIZONTAL) { - return currentRenderingBox.leftTop.y - targetRenderingBox.leftTop.y; - } - - return currentRenderingBox.leftTop.x - targetRenderingBox.leftTop.x; - }); - const zIndexArr: string[] = ["50", "40", "30", "20", "10"]; if (node.hasAnnotation(absolutePositioningAnnotation)) { @@ -413,6 +396,22 @@ const isCssValueEmpty = (value: string): boolean => { }; export const addAdditionalCssAttributes = (node: Node) => { + if (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + + let extension: string = "png"; + if (node.getType() === NodeType.VECTOR) { + extension = "svg"; + } + + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.${extension}')`, + }); + } + if (node.getType() === NodeType.IMAGE) { node.addCssAttributes({ "overflow": "hidden", @@ -479,7 +478,6 @@ const adjustChildrenHeightAndWidthCssValue = (node: Node) => { const flexDir = node.getAPositionalAttribute("flex-direction"); - const justifyContent = node.getAPositionalAttribute("justify-content"); const alignItems = node.getAPositionalAttribute("align-items"); let gap: number = 0; @@ -556,10 +554,28 @@ const adjustChildrenHeightAndWidthCssValue = (node: Node) => { child.addCssAttributes(attributes); + if (alignItems === "center" && child.getType() === NodeType.TEXT) { - const childAttributes: Attributes = child.getCssAttributes(); - delete (childAttributes["width"]); - child.setCssAttributes(childAttributes); + let moreThanOneRow: boolean = false; + const textNode: TextNode = child as TextNode; + const childAttributes: Attributes = textNode.getCssAttributes(); + + const [_, renderBoundsHeight] = child.getRenderingBoxWidthAndHeight(); + + let fontSize: number = -Infinity; + for (const segment of textNode.getStyledTextSegments()) { + if (segment.fontSize > fontSize) { + fontSize = segment.fontSize; + } + } + + moreThanOneRow = renderBoundsHeight > fontSize * 1.5; + + if (!moreThanOneRow) { + delete (childAttributes["width"]); + delete (childAttributes["min-width"]); + child.setCssAttributes(childAttributes); + } } } } @@ -752,6 +768,22 @@ const getJustifyContentValue = ( if (targetLines.length === 1) { const targetLine = targetLines[0]; const mid = parentLine.getMid(); + + const touchingStart: boolean = parentLine.lower + 2 >= targetLine.lower && targetLine.lower >= parentLine.lower - 2; + const touchingEnd: boolean = parentLine.upper + 2 >= targetLine.upper && targetLine.upper >= parentLine.upper - 2; + + if (touchingStart && touchingEnd) { + return JustifyContent.CENTER; + } + + if (touchingStart) { + return JustifyContent.FLEX_START; + } + + if (touchingEnd) { + return JustifyContent.FLEX_END; + } + switch (targetLine.getRelativeLinePosition(mid)) { case RelativePoisition.LEFT: return JustifyContent.FLEX_START; @@ -907,10 +939,6 @@ const getAlignItemsValue = ( } } - // console.log("numberOfItemsInTheMiddle: ", numberOfItemsInTheMiddle); - // console.log("numberOfItemsTippingLeft: ", numberOfItemsTippingLeft); - // console.log("numberOfItemsTippingRight: ", numberOfItemsTippingRight); - if (noGapItems === targetLines.length) { for (const targetLine of targetLines) { diff --git a/core/src/bricks/annotation.ts b/core/src/bricks/annotation.ts new file mode 100644 index 0000000..657d010 --- /dev/null +++ b/core/src/bricks/annotation.ts @@ -0,0 +1 @@ +export const replacedParentAnnotation: string = "replacedParent"; \ No newline at end of file diff --git a/core/src/bricks/direction.ts b/core/src/bricks/direction.ts index 03d0fae..15d360e 100644 --- a/core/src/bricks/direction.ts +++ b/core/src/bricks/direction.ts @@ -1,5 +1,5 @@ import { Node } from "./node"; -import { getLineBasedOnDirection } from "./line"; +import { getContainerLineFromNodes, getLineBasedOnDirection } from "./line"; import { BoxCoordinates } from "../design/adapter/node"; import { selectBox } from "./additional-css"; @@ -12,20 +12,33 @@ export enum Direction { } // getDirection figures out whether nodes are positioned using row vs column. -export const getDirection = (nodes: Node[]): Direction => { - if (nodes.length <= 1) { - return Direction.HORIZONTAL; +export const getDirection = (node: Node): Direction => { + const children: Node[] = node.getChildren(); + if (children.length <= 1) { + const targetLine = getContainerLineFromNodes(children, Direction.HORIZONTAL); + const parentLine = getContainerLineFromNodes([node], Direction.HORIZONTAL); + + const counterTargetLine = getContainerLineFromNodes(children, Direction.VERTICAL); + const counterParentLine = getContainerLineFromNodes([node], Direction.VERTICAL); + + let useHorizontal: boolean = Math.abs(parentLine.upper - parentLine.lower - (targetLine.upper - targetLine.lower)) > Math.abs(counterParentLine.upper - counterParentLine.lower - (counterTargetLine.upper - counterTargetLine.lower)); + + if (useHorizontal) { + return Direction.HORIZONTAL; + } + + return Direction.VERTICAL; } let noVerticalOverlap = true; - for (let i = 0; i < nodes.length; i++) { - const currentLine = getLineBasedOnDirection(nodes[i], Direction.HORIZONTAL); - for (let j = 0; j < nodes.length; j++) { + for (let i = 0; i < children.length; i++) { + const currentLine = getLineBasedOnDirection(children[i], Direction.HORIZONTAL); + for (let j = 0; j < children.length; j++) { if (i === j) { continue; } const targetLine = getLineBasedOnDirection( - nodes[j], + children[j], Direction.HORIZONTAL ); noVerticalOverlap = noVerticalOverlap && !currentLine.overlap(targetLine); diff --git a/core/src/bricks/grouping.ts b/core/src/bricks/grouping.ts index 3044b80..1a3ea46 100644 --- a/core/src/bricks/grouping.ts +++ b/core/src/bricks/grouping.ts @@ -24,38 +24,23 @@ export const groupNodes = (parentNode: Node) => { } let groupedNodes = groupNodesByInclusion(children); - - // console.log("groupNodesByInclusion: ", groupedNodes); - groupedNodes = groupNodesByOverlap(groupedNodes); - // console.log("groupNodesByOverlap: ", groupedNodes); - const horizontalSegmentedNodes = groupNodesByDirectionalOverlap( groupedNodes, Direction.HORIZONTAL ); - // console.log("horizontalSegmentedNodes: ", horizontalSegmentedNodes); - - const verticalSegmentedNodes = groupNodesByDirectionalOverlap( groupedNodes, Direction.VERTICAL ); - // console.log("verticalSegmentedNodes: ", verticalSegmentedNodes); - - const decided = decideBetweenDirectionalOverlappingNodes( horizontalSegmentedNodes, verticalSegmentedNodes ); - - // console.log("decided: ", decided); - - if (!isEmpty(decided)) { groupedNodes = decided; } diff --git a/core/src/bricks/line.ts b/core/src/bricks/line.ts index b5a857c..5d03149 100644 --- a/core/src/bricks/line.ts +++ b/core/src/bricks/line.ts @@ -111,7 +111,7 @@ export const getLinesFromNodes = ( ): Line[] => { const lines: Line[] = []; for (const node of nodes) { - const renderingBox = node.getAbsRenderingBox(); + const renderingBox = selectBox(node); if (direction === Direction.VERTICAL) { lines.push(new Line(renderingBox.leftTop.x, renderingBox.rightBot.x)); diff --git a/core/src/bricks/node.ts b/core/src/bricks/node.ts index 8b2f373..400b0e2 100644 --- a/core/src/bricks/node.ts +++ b/core/src/bricks/node.ts @@ -8,6 +8,7 @@ import { VectorNode as AdaptedVectorNode, VectorGroupNode as AdaptedVectorGroupNode, ImageNode as AdaptedImageNode, + StyledTextSegment, } from "../design/adapter/node"; import { isEmpty } from "../utils"; import { selectBox } from "./additional-css"; @@ -182,9 +183,6 @@ export const doOverlap = ( const intersectionWidth: number = Math.abs(intersection.leftTop.x - intersection.rightBot.x); const intersectionHeight: number = Math.abs(intersection.leftTop.y - intersection.rightBot.y); - // console.log("intersectionWidth: ", intersectionWidth); - // console.log("intersectionHeight: ", intersectionHeight); - if (intersectionWidth < 2 || intersectionHeight < 2) { return false; } @@ -362,6 +360,14 @@ export class GroupNode extends BaseNode { return this.absRenderingBox; } + getRenderingBoxWidthAndHeight(): number[] { + const coordinates = this.getAbsRenderingBox(); + const width = Math.abs(coordinates.rightTop.x - coordinates.leftBot.x); + const height = Math.abs(coordinates.rightBot.y - coordinates.leftTop.y); + return [width, height]; + } + + getAbsBoundingBox() { if (!isEmpty(this.node)) { return this.node.getAbsoluteBoundingBoxCoordinates(); @@ -571,6 +577,10 @@ export class TextNode extends VisibleNode { getType(): NodeType { return NodeType.TEXT; } + + getStyledTextSegments(): StyledTextSegment[] { + return this.node.getStyledTextSegments(); + } } export class VectorGroupNode extends GroupNode { diff --git a/core/src/bricks/overlap.ts b/core/src/bricks/overlap.ts index 6088a01..a9f2201 100644 --- a/core/src/bricks/overlap.ts +++ b/core/src/bricks/overlap.ts @@ -69,11 +69,6 @@ export const findOverlappingNodes = ( PostionalRelationship.OVERLAP ) { - console.log("targetNode: ", targetNode); - console.log("startingNode: ", startingNode); - console.log("startingNode: ", ); - console.log("startingNode.getPositionalRelationship(targetNode): ", startingNode.getPositionalRelationship(targetNode)); - overlappingNodes.push(targetNode); if (!currentPath.has(startingNode.getId())) { diff --git a/core/src/bricks/remove-css.ts b/core/src/bricks/remove-css.ts index bcfb806..7ce4be3 100644 --- a/core/src/bricks/remove-css.ts +++ b/core/src/bricks/remove-css.ts @@ -17,7 +17,7 @@ export const removeCssFromNode = (node: Node) => { const positionalAttributes: Attributes = node.getPositionalCssAttributes(); let cssAttributes: Attributes = node.getCssAttributes(); - if (isEmpty(positionalAttributes["position"]) && positionalAttributes["justify-content"] !== "space-between") { + if (isEmpty(positionalAttributes["position"]) && positionalAttributes["justify-content"] !== "space-between" && !isEmpty(cssAttributes["width"])) { const actualChildrenWidth: number = calculateActualChildrenWidth(node); const width: number = cssStrToNum(cssAttributes["width"]); @@ -37,12 +37,12 @@ export const removeCssFromNode = (node: Node) => { if (!isEmpty(childAttributes["height"]) && !isEmpty(cssAttributes["height"]) && childAttributes["height"] === cssAttributes["height"]) { delete (cssAttributes["height"]); } - - node.setCssAttributes(cssAttributes); } removeCssFromNode(child); } + + node.setCssAttributes(cssAttributes); }; const calculateActualChildrenWidth = (node: Node): number => { @@ -68,6 +68,8 @@ const calculateActualChildrenWidth = (node: Node): number => { if (childAttributes["width"]) { widthCum += cssStrToNum(childAttributes["width"]); + } else if (childAttributes["min-width"]) { + widthCum += cssStrToNum(childAttributes["min-width"]); } if (isEmpty(positionalAttributes["gap"])) { diff --git a/core/src/bricks/remove-node.ts b/core/src/bricks/remove-node.ts index eac42eb..7cfdb80 100644 --- a/core/src/bricks/remove-node.ts +++ b/core/src/bricks/remove-node.ts @@ -2,6 +2,7 @@ import { Node, NodeType } from "./node"; import { Attributes } from "../design/adapter/node"; import { isEmpty } from "../utils"; import { cssStrToNum } from "../code/generator/util"; +import { replacedParentAnnotation } from "./annotation"; export const removeNode = (node: Node): Node => { const children: Node[] = node.getChildren(); @@ -17,6 +18,7 @@ export const removeNode = (node: Node): Node => { child.setCssAttributes(cssAttributes); child.setPositionalCssAttributes(positionalCssAttributes); + child.addAnnotations(replacedParentAnnotation, true); return removeNode(child); } @@ -85,26 +87,6 @@ const haveSimlarWidthAndHeight = (currentNode: Node, targetNode: Node): boolean return similarHeight && similarWidth; - - - // const currentBorderRadius: string = currentNode.getACssAttribute("border-radius"); - // const targetBorderRadius: string = targetNode.getACssAttribute("border-radius"); - - // // console.log("currentNode: ", currentNode); - // // console.log("targetNode: ", targetNode); - // // console.log("currentBorderRadius: ", currentBorderRadius); - // // console.log("targetBorderRadius: ", targetBorderRadius); - // if (isEmpty(currentBorderRadius) || isEmpty(targetBorderRadius)) { - // return similarHeight && similarWidth; - // } - - // let similarCornerRadius: boolean = false; - // let diffInCornerRadius: number = Math.abs(cssStrToNum(currentBorderRadius) - cssStrToNum(targetHeight)); - // if (diffInCornerRadius <= 1) { - // similarCornerRadius = true; - // } - - // return similarHeight && similarWidth && similarCornerRadius; }; const filterAttributes = (attribtues: Attributes): Attributes => { diff --git a/core/src/bricks/util.ts b/core/src/bricks/util.ts index 2fa0177..bd05d5d 100644 --- a/core/src/bricks/util.ts +++ b/core/src/bricks/util.ts @@ -1,6 +1,6 @@ import { isEmpty } from "../utils"; import { Attributes } from "../design/adapter/node"; -import { Option } from "./node"; +import { Node, NodeType, Option } from "./node"; const toOneDecimal = (num: number): number => Math.round(num * 10) / 10; @@ -185,4 +185,16 @@ export const filterCssValue = (cssValue: string, option: Option): string => { } return updated; +}; + +export const shouldUseAsBackgroundImage = (node: Node): boolean => { + if (node.getType() === NodeType.VECTOR && !isEmpty(node.getChildren())) { + return true; + } + + if (node.getType() === NodeType.IMAGE && !isEmpty(node.getChildren())) { + return true; + } + + return false; }; \ No newline at end of file diff --git a/core/src/code/generator/css/generator.ts b/core/src/code/generator/css/generator.ts index 2e98733..78f9460 100644 --- a/core/src/code/generator/css/generator.ts +++ b/core/src/code/generator/css/generator.ts @@ -6,7 +6,6 @@ import { getFileExtensionFromLanguage, constructExtraFiles, snakeCaseToCamelCase, - shouldUseAsBackgroundImage, } from "../util"; import { Generator as HtmlGenerator, @@ -126,8 +125,15 @@ const getPropsFromNode = (node: Node, option: Option): string => { case NodeType.TEXT: return convertCssClassesToInlineStyle( { + ...{ + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, ...node.getCssAttributes(), - ...node.getPositionalCssAttributes(), }, option, node.getId() @@ -159,19 +165,12 @@ const getPropsFromNode = (node: Node, option: Option): string => { node.getId() ); case NodeType.IMAGE: - if (shouldUseAsBackgroundImage(node)) { - const id: string = node.getId(); - const imageComponentName: string = - nameRegistryGlobalInstance.getImageName(id); - - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.png')`, - }); - } - if (isEmpty(node.getChildren())) { return convertCssClassesToInlineStyle( { + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), ...filterAttributes(node.getPositionalCssAttributes(), { absolutePositioningFilter: true, }), @@ -195,16 +194,6 @@ const getPropsFromNode = (node: Node, option: Option): string => { node.getId() ); case NodeType.VECTOR: - if (shouldUseAsBackgroundImage(node)) { - const id: string = node.getId(); - const imageComponentName: string = - nameRegistryGlobalInstance.getImageName(id); - - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.svg')`, - }); - } - if (isEmpty(node.getChildren())) { return convertCssClassesToInlineStyle( { diff --git a/core/src/code/generator/font.ts b/core/src/code/generator/font.ts index d741bd8..ef0336c 100644 --- a/core/src/code/generator/font.ts +++ b/core/src/code/generator/font.ts @@ -26,7 +26,7 @@ const findAllFonts = (node: Node, fonts: FontMetadataMap) => { const styledTextSegments = textNode.node.getStyledTextSegments(); styledTextSegments.forEach((styledTextSegment) => { - const { fontName, fontSize, fontWeight } = styledTextSegment; + const { fontName, fontSize, fontWeight, fontFamily } = styledTextSegment; const { family, style } = fontName; const isItalic = style.toLowerCase().includes("italic"); @@ -35,7 +35,7 @@ const findAllFonts = (node: Node, fonts: FontMetadataMap) => { if (!font) { fonts[family] = { isItalic, - familyCss: family, + familyCss: fontFamily, sizes: [fontSize], weights: [fontWeight], }; diff --git a/core/src/code/generator/html/generator.ts b/core/src/code/generator/html/generator.ts index 53d7296..5e4ce2c 100644 --- a/core/src/code/generator/html/generator.ts +++ b/core/src/code/generator/html/generator.ts @@ -291,10 +291,6 @@ export class Generator { ]; } - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.png')`, - }); - return [`
                  `, `
                  `]; } @@ -352,6 +348,7 @@ export class Generator { }); let codeStr: string = ""; + if (renderInALoop) { let dataCodeStr: string = `const ${dataArr.name} = ${JSON.stringify( data diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index e87b82b..225bc9b 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -685,7 +685,6 @@ export const getTwcssClass = ( return `bg-${getImageFileNameFromUrl(cssValue)}`; case "box-shadow": { - console.log(cssValue); // A very naive conversion for now, because parsing box-shadow string is too complicated if (cssValue.includes("inset")) { // inner shadow @@ -823,6 +822,10 @@ export const getTwcssClass = ( } } + case "border-top": + case "border-bottom": + case "border-left": + case "border-right": case "border-style": { switch (cssValue) { case "solid": @@ -1093,7 +1096,6 @@ const findClosestTwcssRotate = (cssValue: string) => { const end: number = cssValue.indexOf("d"); const numStr: string = cssValue.substring(start, end); - console.log(cssValue.substring(start, end)); let numRaw: number = parseInt(numStr); if (isEmpty(numRaw)) { return ""; diff --git a/core/src/code/generator/tailwindcss/generator.ts b/core/src/code/generator/tailwindcss/generator.ts index 88c5f93..ba235e1 100644 --- a/core/src/code/generator/tailwindcss/generator.ts +++ b/core/src/code/generator/tailwindcss/generator.ts @@ -20,7 +20,6 @@ import { import { Generator as ReactGenerator } from "../react/generator"; import { filterAttributes } from "../../../bricks/util"; import { extraFileRegistryGlobalInstance } from "../../extra-file-registry/extra-file-registry"; -import { nameRegistryGlobalInstance } from "../../name-registry/name-registry"; import { shouldUseAsBackgroundImage } from "../util"; import { Attributes } from "../../../design/adapter/node"; @@ -136,16 +135,6 @@ const getPropsFromNode = (node: Node, option: Option): string => { ); case NodeType.IMAGE: - if (shouldUseAsBackgroundImage(node)) { - const id: string = node.getId(); - const imageComponentName: string = - nameRegistryGlobalInstance.getImageName(id); - - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.png')`, - }); - } - if (isEmpty(node.getChildren())) { return convertCssClassesToTwcssClasses( { @@ -176,16 +165,6 @@ const getPropsFromNode = (node: Node, option: Option): string => { ); case NodeType.VECTOR: - if (shouldUseAsBackgroundImage(node)) { - const id: string = node.getId(); - const imageComponentName: string = - nameRegistryGlobalInstance.getImageName(id); - - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.svg')`, - }); - } - if (isEmpty(node.getChildren())) { return convertCssClassesToTwcssClasses( { diff --git a/core/src/code/name-registry/name-registry.ts b/core/src/code/name-registry/name-registry.ts index ed85d6c..0f46ec3 100644 --- a/core/src/code/name-registry/name-registry.ts +++ b/core/src/code/name-registry/name-registry.ts @@ -1,3 +1,5 @@ +import { isEmpty } from "../../utils"; + export let nameRegistryGlobalInstance: NameRegistry; export const instantiateNameRegistryGlobalInstance = () => { nameRegistryGlobalInstance = new NameRegistry(); @@ -66,11 +68,11 @@ class NameRegistry { getDataArrName(id: string): string { let name: string = this.idToNameMap[id]; - if (name) { + if (!isEmpty(name)) { return name; } - name = "dataArr" + this.numberOfDataArr; + name = "data" + this.numberOfDataArr; this.idToNameMap[id] = name; this.numberOfDataArr++; return name; @@ -78,7 +80,7 @@ class NameRegistry { getPropName(id: string): string { let name: string = this.idToNameMap[id]; - if (name) { + if (!isEmpty(name)) { return name; } @@ -90,8 +92,7 @@ class NameRegistry { getVectorName(id: string): string { let name: string = this.idToNameMap[id]; - if (name) { - let name: string = this.idToNameMap[id]; + if (!isEmpty(name)) { return name; } diff --git a/core/src/design/adapter/figma/adapter.ts b/core/src/design/adapter/figma/adapter.ts index 4a61715..185bd64 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -57,6 +57,26 @@ const safelySetWidthAndHeight = ( return; } + // @ts-ignore + if (!isEmpty(figmaNode?.effects)) { + // @ts-ignore + const dropShadowStrings: string[] = figmaNode.effects + .filter( + (effect) => + effect.visible && + (effect.type === "DROP_SHADOW" || effect.type === "INNER_SHADOW")); + + + if (dropShadowStrings.length > 0) { + if (!isEmpty(figmaNode.absoluteBoundingBox)) { + attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; + attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; + } + + return; + } + } + if ( nodeType === NodeType.FRAME || nodeType === NodeType.IMAGE || @@ -314,8 +334,30 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } } - attributes["border-style"] = - figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + if (strokeTopWeight > 0 && strokeBottomWeight > 0 && strokeLeftWeight > 0 && strokeRightWeight > 0) { + attributes["border-style"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } else { + if (strokeTopWeight > 0) { + attributes["border-top"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } + + if (strokeBottomWeight > 0) { + attributes["border-bottom"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } + + if (strokeLeftWeight > 0) { + attributes["border-left"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } + + if (strokeRightWeight > 0) { + attributes["border-right"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } + } } safelySetWidthAndHeight(figmaNode.type, figmaNode, attributes); @@ -456,7 +498,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } - if (!moreThanOneRow && !isEmpty(absoluteRenderBounds) && (Math.abs(absoluteBoundingBox.width - absoluteRenderBounds.width) < fontSize)) { + if (!moreThanOneRow) { attributes["min-width"] = `${absoluteBoundingBox.width}px`; attributes["white-space"] = "nowrap"; } else { @@ -464,25 +506,25 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } // switch (figmaNode.textAutoResize) { - // // case "NONE": { - // // attributes["width"] = `${width}px`; - // // attributes["height"] = `${absoluteRenderBounds.height}px`; - // // break; - // // } - // // case "HEIGHT": { - // // attributes["width"] = `${width}px`; - // // break; - // // } + // case "NONE": { + // attributes["width"] = `${width}px`; + // attributes["height"] = `${absoluteRenderBounds.height}px`; + // break; + // } + // case "HEIGHT": { + // attributes["width"] = `${width}px`; + // break; + // } // case "WIDTH_AND_HEIGHT": { // attributes["width"] = `${width}px`; // break; // } - // // case "TRUNCATE": { - // // attributes["width"] = `${width}px`; - // // attributes["height"] = `${absoluteRenderBounds.height}px`; - // // attributes["text-overflow"] = "ellipsis"; - // // break; - // // } + // case "TRUNCATE": { + // attributes["width"] = `${width}px`; + // attributes["height"] = `${absoluteRenderBounds.height}px`; + // attributes["text-overflow"] = "ellipsis"; + // break; + // } // } // text decoration @@ -520,12 +562,27 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { } const textContainingOnlyOneWord = - figmaNode.characters.split(" ").length === 1; + figmaNode.characters.trim().split(" ").length === 1; if (moreThanOneRow && textContainingOnlyOneWord) { attributes["overflow-wrap"] = "break-word"; } + if (textContainingOnlyOneWord) { + // text alignment + switch (figmaNode.textAlignHorizontal) { + case "CENTER": + attributes["text-align"] = "center"; + break; + case "RIGHT": + attributes["text-align"] = "right"; + break; + case "JUSTIFIED": + attributes["text-align"] = "justify"; + break; + } + } + /* TODO: This field is causing styling differences between Figma design and rendered Bricks components. @@ -545,7 +602,7 @@ const getCssAttributes = (figmaNode: SceneNode): Attributes => { // line height const lineHeight = figmaNode.lineHeight; - if (lineHeight !== figma.mixed && lineHeight.unit !== "AUTO") { + if (lineHeight !== figma.mixed) { attributes["line-height"] = figmaLineHeightToCssString(lineHeight); } @@ -1062,61 +1119,9 @@ export const convertFigmaNodesToBricksNodes = ( newNodes.push(newNode); - - // //@ts-ignore - // if (!isEmpty(figmaNode?.children)) { - // let isExportableNode: boolean = false; - // //@ts-ignore - // const feedback = convertFigmaNodesToBricksNodes(figmaNode.children); - // if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { - // if (!feedback.doNodesHaveNonOverlappingChildren) { - // isExportableNode = true; - // if (feedback.doNodesContainImage) { - // newNode = new ImageNode( - // new FigmaVectorGroupNodeAdapter(figmaNode) - // ); - // } else { - // newNode = new BricksVector( - // new FigmaVectorGroupNodeAdapter(figmaNode) - // ); - // } - // } - // } - - // result.areAllNodesExportable = - // feedback.areAllNodesExportable && result.areAllNodesExportable; - - // result.doNodesContainImage = - // feedback.doNodesContainImage || result.doNodesContainImage; - - // if (!isExportableNode) { - // newNode.setChildren(feedback.nodes); - // } - // } - - // result.nodes.push(newNode); } } - // let horizontalOverlap: boolean = areNodesOverlappingByDirection( - // reordered, - // Direction.HORIZONTAL - // ); - - // let verticalOverlap: boolean = areNodesOverlappingByDirection( - // reordered, - // Direction.VERTICAL - // ); - - // result.doNodesHaveNonOverlappingChildren = - // !horizontalOverlap || !verticalOverlap; - - // console.log("result.doNodesHaveNonOverlappingChildren: ", result.doNodesHaveNonOverlappingChildren); - - - // console.log(figmaNodes); - // console.log(result); - for (let i = 0; i < reordered.length; i++) { const figmaNode = reordered[i]; @@ -1149,11 +1154,6 @@ export const convertFigmaNodesToBricksNodes = ( doNodesHaveNonOverlappingChildren = false; } - - // console.log("parent:<<<<<<<<<<<<<<<<<<< ", figmaNodes); - // console.log("feedback: ", feedback); - // console.log("figmaNode: ", figmaNode); - // console.log("!doNodesHaveNonOverlappingChildren: ", !doNodesHaveNonOverlappingChildren); if (feedback.areAllNodesExportable && !feedback.isSingleRectangle) { if (!doNodesHaveNonOverlappingChildren) { isExportableNode = true; diff --git a/core/src/design/adapter/figma/util.ts b/core/src/design/adapter/figma/util.ts index ed48358..3225362 100644 --- a/core/src/design/adapter/figma/util.ts +++ b/core/src/design/adapter/figma/util.ts @@ -145,12 +145,6 @@ export function getMostCommonFieldInString< } } - console.log( - "variation with longest length for field:", - field, - " = ", - variationWithLongestLength - ); return variationWithLongestLength; } @@ -191,7 +185,7 @@ export function getRgbaFromPaints(paints: readonly Paint[]) { export const figmaLineHeightToCssString = (lineHeight: LineHeight) => { switch (lineHeight.unit) { case "AUTO": - return "normal"; + return "100%"; case "PERCENT": return `${lineHeight.value}%`; case "PIXELS": diff --git a/core/src/index.ts b/core/src/index.ts index e4996bc..51cf70e 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -32,7 +32,6 @@ export const convertToCode = async ( const dedupedNodes: Node[] = []; for (const node of converted) { let newNode: Node = removeNode(node); - // console.log("after ssssss: ", newNode); removeCompletelyOverlappingNodes(newNode, null); removeChildrenNode(newNode); dedupedNodes.push(newNode); @@ -40,25 +39,19 @@ export const convertToCode = async ( let startingNode: Node = - dedupedNodes.length > 1 ? new GroupNode(dedupedNodes) : dedupedNodes[0]; - - console.log("after trim: ", startingNode); - + dedupedNodes.length > 1 ? new GroupNode(converted) : converted[0]; groupNodes(startingNode); - console.log("after group: ", startingNode); - startingNode = removeNode(startingNode); removeCompletelyOverlappingNodes(startingNode, null); removeChildrenNode(startingNode); - // console.log("startingNode: ", startingNode); + instantiateRegistries(startingNode, option); addAdditionalCssAttributesToNodes(startingNode); removeCssFromNode(startingNode); - instantiateRegistries(startingNode, option); return await generateCodingFiles(startingNode, option); }; @@ -90,10 +83,10 @@ export const convertToCodeWithAi = async ( removeCompletelyOverlappingNodes(startingNode, null); removeChildrenNode(startingNode); - addAdditionalCssAttributesToNodes(startingNode); - instantiateRegistries(startingNode, option); + addAdditionalCssAttributesToNodes(startingNode); + // ee features let startAnnotateHtmlTag: number = Date.now(); await annotateNodeForHtmlTag(startingNode); diff --git a/figma/src/code.ts b/figma/src/code.ts index 8e017e4..875bd04 100644 --- a/figma/src/code.ts +++ b/figma/src/code.ts @@ -36,10 +36,16 @@ figma.ui.onmessage = async (msg) => { files, }); } catch (e) { - console.error("Error from Figma core:\n", e.stack); + const errorDetails = { + error: e.name, + message: e.message, + stack: e.stack, + }; + + console.error("Error from Figma core:\n", errorDetails); trackEvent(EVENT_ERROR, { source: "figma", - error: e.stack, + ...errorDetails, }); figma.ui.postMessage({ @@ -67,10 +73,16 @@ figma.ui.onmessage = async (msg) => { applications, }); } catch (e) { - console.error("Error from Figma core:\n", e.stack); + const errorDetails = { + error: e.name, + message: e.message, + stack: e.stack, + }; + + console.error("Error from Figma core:\n", errorDetails); trackEvent(EVENT_ERROR, { source: "figma", - error: e.stack, + ...errorDetails, }); figma.ui.postMessage({ From ed9489e1b481c95b2b8a944ea099eb44dc256d67 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Wed, 17 May 2023 19:49:23 -0700 Subject: [PATCH 20/21] minor fixes --- core/ee/cv/component-recognition.ts | 2 +- core/src/bricks/additional-css.ts | 29 +++++++++++++- figma/src/code.ts | 44 ++++++++++----------- figma/src/pages/home.tsx | 13 +++--- figma/src/pages/post-code-generation-ai.tsx | 4 +- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/core/ee/cv/component-recognition.ts b/core/ee/cv/component-recognition.ts index 9b13687..baf6760 100644 --- a/core/ee/cv/component-recognition.ts +++ b/core/ee/cv/component-recognition.ts @@ -54,7 +54,7 @@ export const annotateNodeForHtmlTag = async (startingNode: Node) => { if (predictedHtmlTag) { aiApplicationRegistryGlobalInstance.addApplication(AiApplication.componentIdentification); node.addAnnotations("htmlTag", predictedHtmlTag); - return predictedHtmlTag !== "a" && predictedHtmlTag !== "button"; + return predictedHtmlTag !== "button"; } } diff --git a/core/src/bricks/additional-css.ts b/core/src/bricks/additional-css.ts index fb60455..0d0d629 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -13,6 +13,7 @@ import { Line, getLineBasedOnDirection, getContainerRenderingLineFromNodes, + getLineUsingRenderingBoxBasedOnDirection, } from "./line"; import { filterCssValue, shouldUseAsBackgroundImage } from "./util"; import { absolutePositioningAnnotation } from "./overlap"; @@ -87,7 +88,7 @@ export const addAdditionalCssAttributesToNodes = (node: Node) => { } }; -const adjustChildrenPositionalCssValue = (node: Node, direction) => { +const adjustChildrenPositionalCssValue = (node: Node, direction: Direction) => { const children = node.getChildren(); if (isEmpty(children)) { return; @@ -115,6 +116,32 @@ const adjustChildrenPositionalCssValue = (node: Node, direction) => { }); } } + + return; + } + + let prevChild: Node = null; + for (let i = 0; i < children.length; i++) { + const child: Node = children[i]; + if (i === 0) { + prevChild = child; + continue; + } + + const currentLine: Line = getLineUsingRenderingBoxBasedOnDirection(child, direction); + const prevLine: Line = getLineUsingRenderingBoxBasedOnDirection(prevChild, direction); + + if (currentLine.overlapStrict(prevLine)) { + if (child.getACssAttribute("box-shadow")) { + child.addCssAttributes({ + "z-index": "10", + }); + } else if (prevChild.getACssAttribute("box-shadow")) { + prevChild.addCssAttributes({ + "z-index": "10", + }); + } + } } }; diff --git a/figma/src/code.ts b/figma/src/code.ts index 875bd04..c4bdf6e 100644 --- a/figma/src/code.ts +++ b/figma/src/code.ts @@ -24,18 +24,19 @@ figma.showUI(__html__, { height: 300, width: 350 }); figma.ui.onmessage = async (msg) => { if (msg.type === "styled-bricks-nodes") { - try { - const files = await convertToCode(figma.currentPage.selection, { - language: msg.options.language, - cssFramework: msg.options.cssFramework, - uiFramework: msg.options.uiFramework, - }); + const promise = convertToCode(figma.currentPage.selection, { + language: msg.options.language, + cssFramework: msg.options.cssFramework, + uiFramework: msg.options.uiFramework, + }); + promise.then((files) => { figma.ui.postMessage({ type: "generated-files", files, }); - } catch (e) { + + }).catch((e) => { const errorDetails = { error: e.name, message: e.message, @@ -53,26 +54,27 @@ figma.ui.onmessage = async (msg) => { files: [], error: true, }); - } + }); } if (msg.type === "generate-code-with-ai") { - try { - const [files, applications] = await convertToCodeWithAi( - figma.currentPage.selection, - { - language: msg.options.language, - cssFramework: msg.options.cssFramework, - uiFramework: msg.options.uiFramework, - } - ); + const promise = convertToCodeWithAi( + figma.currentPage.selection, + { + language: msg.options.language, + cssFramework: msg.options.cssFramework, + uiFramework: msg.options.uiFramework, + } + ); + promise.then(([files, applications]) => { figma.ui.postMessage({ type: "generated-files", files, applications, }); - } catch (e) { + + }).catch((e) => { const errorDetails = { error: e.name, message: e.message, @@ -91,7 +93,7 @@ figma.ui.onmessage = async (msg) => { applications: [], error: true, }); - } + }); } if (msg.type === "update-settings") { @@ -130,10 +132,6 @@ figma.ui.onmessage = async (msg) => { } if (msg.type === "get-last-reset") { - if (figma.currentUser.id === "624412189236026359") { - await figma.clientStorage.setAsync("limit", 6); - } - const reset: number = await figma.clientStorage.getAsync("last-reset"); figma.ui.postMessage({ diff --git a/figma/src/pages/home.tsx b/figma/src/pages/home.tsx index 1584cc2..21ba864 100644 --- a/figma/src/pages/home.tsx +++ b/figma/src/pages/home.tsx @@ -43,6 +43,9 @@ const Home = (props: PropsWithChildren) => { const { setCurrentPage } = useContext(PageContext); const handleGenerateCodeButtonClick = () => { + setIsGeneratingCode(true); + setCurrentPage(PAGES.CODE_GENERATION); + parent.postMessage( { pluginMessage: { @@ -56,8 +59,6 @@ const Home = (props: PropsWithChildren) => { }, "*" ); - setIsGeneratingCode(true); - setCurrentPage(PAGES.CODE_GENERATION); parent.postMessage( { @@ -76,6 +77,10 @@ const Home = (props: PropsWithChildren) => { }; const handleGenerateCodeWithAiButtonClick = () => { + setIsGeneratingCodeWithAi(true); + setIsGeneratingCode(true); + setCurrentPage(PAGES.CODE_GENERATION); + parent.postMessage( { pluginMessage: { @@ -90,10 +95,6 @@ const Home = (props: PropsWithChildren) => { "*" ); - setIsGeneratingCodeWithAi(true); - setIsGeneratingCode(true); - setCurrentPage(PAGES.CODE_GENERATION); - parent.postMessage( { pluginMessage: { diff --git a/figma/src/pages/post-code-generation-ai.tsx b/figma/src/pages/post-code-generation-ai.tsx index dc41c36..73144a7 100644 --- a/figma/src/pages/post-code-generation-ai.tsx +++ b/figma/src/pages/post-code-generation-ai.tsx @@ -22,7 +22,7 @@ const PostCodeGenerationAi = (props: PropsWithChildren) => { if (isEmpty(aiApplications)) { return (

                  - Ai is not applied in this code generation. No credits deducted. + We didnt detect any looped components, buttons, or links so we switched to instant version. No credits have been deducted.

                  ); } @@ -33,7 +33,7 @@ const PostCodeGenerationAi = (props: PropsWithChildren) => { if (aiApplication === AiApplication.componentIdentification) { applications.push(

                  - * Auto identification of buttons and links. + * Auto identification of buttons.

                  ); } From d2a80c97591a3de57c297558dbc2821a147e51b5 Mon Sep 17 00:00:00 2001 From: Spike Lu Date: Wed, 17 May 2023 20:24:29 -0700 Subject: [PATCH 21/21] remove unnecessary code --- .../src/code/generator/tailwindcss/css-to-twcss.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/core/src/code/generator/tailwindcss/css-to-twcss.ts b/core/src/code/generator/tailwindcss/css-to-twcss.ts index 225bc9b..c228c03 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -450,9 +450,6 @@ const findClosestTwcssFontWeight = (fontWeight: string): string => { return twClassToUse; }; -// const maxTwcssSizeInPixels = 384; -// const maxTwcssSizeInRem = 24; - // findClosestTwcssSize finds the closest size in tailwindcss given css value. const findClosestTwcssSize = (cssSize: string): string => { const regexExecResult = /^([0-9]\d*(?:\.\d+)?)(px|rem)$/.exec(cssSize); @@ -470,9 +467,6 @@ const findClosestTwcssSize = (cssSize: string): string => { if (givenUnit === "px" && cssValue.endsWith("px")) { const val = parseFloat(cssValue.slice(0, -2)); - // if (val > maxTwcssSizeInPixels) { - // return `${val}px`; - // } diff = Math.abs(givenPadding - val); } @@ -481,9 +475,6 @@ const findClosestTwcssSize = (cssSize: string): string => { // assume root font size equals 16px, which is true in most cases const val = parseFloat(cssValue.slice(0, -3)) * 16; - // if (val > maxTwcssSizeInPixels) { - // return `${val}px`; - // } diff = Math.abs(givenPadding - val); } @@ -668,11 +659,6 @@ export const getTwcssClass = ( return ""; } - // const borderRadiusSize = extractPixelNumberFromString(cssValue); - // if (borderRadiusSize > MAX_BORDER_RADIUS_IN_PIXELS) { - // return `rounded-[${borderRadiusSize}px]`; - // } - return borderRadiusTwSize === "" ? "rounded" : "rounded-" + borderRadiusTwSize;