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 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 732cc19..baf6760 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,17 +32,10 @@ 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([ + const [predictImagesResult] = await Promise.allSettled([ predictImage(idImageMap), - predictText(idTextMap), ]); - const textPredictions = - predictTextsResult.status === "fulfilled" ? predictTextsResult.value : {}; - const imagePredictions = predictImagesResult.status === "fulfilled" ? predictImagesResult.value @@ -52,23 +45,16 @@ 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); - } - - // console.log("imagePredictions", imagePredictions); - // console.log("textPredictions", textPredictions); - 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); node.addAnnotations("htmlTag", predictedHtmlTag); - return predictedHtmlTag !== "a" && predictedHtmlTag !== "button"; + return predictedHtmlTag !== "button"; } } 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 36fa3d7..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]; @@ -203,8 +238,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"]; @@ -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 cc09402..394b65f 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', @@ -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,16 +68,10 @@ export const getNameMap = async (): Promise => { const parsedNameMapArr: NameMap[] = []; for (const nameMapStr of parsedArr) { - parsedNameMapArr.push(JSON.parse(nameMapStr)); + parsedNameMapArr.push(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; @@ -103,10 +97,50 @@ const dedupNames = (nameMap: NameMap) => { return; } - if (oldName.startsWith("dataArr") || oldName.startsWith("Component")) { + if (oldName.startsWith("data") || oldName.startsWith("Component")) { globalNonDuplicates.add(newName); } 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..0d0d629 100644 --- a/core/src/bricks/additional-css.ts +++ b/core/src/bricks/additional-css.ts @@ -6,27 +6,34 @@ import { reorderNodesBasedOnDirection, getDirection, } from "./direction"; -import { ImageNode, Node, NodeType, VisibleNode } 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 { nameRegistryGlobalInstance } from "../code/name-registry/name-registry"; 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(); + return node.getAbsBoundingBox(); } if (node.getType() === NodeType.IMAGE) { - const imageNode = node as ImageNode; - return imageNode.getAbsBoundingBox(); + return node.getAbsBoundingBox(); } if (useBoundingBox) { @@ -58,22 +65,86 @@ 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; } - const direction = getDirection(node.children); + const direction = getDirection(node); reorderNodesBasedOnDirection(node.children, direction); - node.addCssAttributes(getAdditionalCssAttributes(node)); node.addPositionalCssAttributes(getPositionalCssAttributes(node, direction)); adjustChildrenHeightAndWidthCssValue(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; + } + + 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}`, + }); + } + } + + 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", + }); + } + } + } +}; + // getPaddingInPixels calculates paddings given a node. export const getPaddingInPixels = ( node: Node, @@ -86,18 +157,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 +265,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 +298,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: @@ -347,27 +422,91 @@ 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 (shouldUseAsBackgroundImage(node)) { + const id: string = node.getId(); + const imageComponentName: string = + nameRegistryGlobalInstance.getImageName(id); + + + let extension: string = "png"; + if (node.getType() === NodeType.VECTOR) { + extension = "svg"; + } - if ( - (!isCssValueEmpty(node.getACssAttribute("border-radius")) || - !isCssValueEmpty(node.getACssAttribute("border-width"))) && - node.areThereOverflowingChildren() - ) { - attributes["overflow"] = "hidden"; + node.addCssAttributes({ + "background-image": `url('./assets/${imageComponentName}.${extension}')`, + }); } - return attributes; + if (node.getType() === NodeType.IMAGE) { + node.addCssAttributes({ + "overflow": "hidden", + }); + return; + } + + if (isEmpty(node.getChildren())) { + return; + } + + 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) => { + 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`; + } + + 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); +}; + + const adjustChildrenHeightAndWidthCssValue = (node: Node) => { if (!isEmpty(node.getPositionalCssAttributes())) { const [maxWidth, maxHeight] = getAllowedMaxWidthAndHeight(node); const flexDir = node.getAPositionalAttribute("flex-direction"); + const alignItems = node.getAPositionalAttribute("align-items"); + let gap: number = 0; let gapCssVal: string = node.getACssAttribute("gap"); if (!isCssValueEmpty(gapCssVal)) { @@ -441,6 +580,30 @@ const adjustChildrenHeightAndWidthCssValue = (node: Node) => { } child.addCssAttributes(attributes); + + + if (alignItems === "center" && child.getType() === NodeType.TEXT) { + 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); + } + } } } @@ -560,10 +723,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); @@ -575,6 +738,7 @@ export const getPositionalCssAttributes = ( childAttributes["right"] = `${right}px`; childAttributes["left"] = `${left}px`; + child.addPositionalCssAttributes(childAttributes); } @@ -631,6 +795,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; @@ -650,7 +830,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 +905,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 +966,31 @@ 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/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/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..5d03149 100644 --- a/core/src/bricks/line.ts +++ b/core/src/bricks/line.ts @@ -10,8 +10,20 @@ 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); + } + + 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); @@ -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. @@ -87,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)); @@ -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 bb66e3b..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"; @@ -23,7 +24,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 +68,8 @@ export class BaseNode { option: Option = { zeroValueAllowed: false, truncateNumbers: true, - absolutePositioningOnly: false, + absolutePositioningFilter: false, + marginFilter: false, } ): Attributes { return filterAttributes(this.positionalCssAttributes, option); @@ -90,7 +94,8 @@ export class BaseNode { option: Option = { zeroValueAllowed: false, truncateNumbers: true, - absolutePositioningOnly: false, + absolutePositioningFilter: false, + marginFilter: false, } ): Attributes { return filterAttributes(this.cssAttributes, option); @@ -136,11 +141,53 @@ 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); + + if (intersectionWidth < 2 || intersectionHeight < 2) { + return false; + } + } + if ( currentCoordinate.leftTop.x === currentCoordinate.rightBot.x || currentCoordinate.leftTop.y === currentCoordinate.rightBot.y @@ -296,6 +343,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(); } @@ -303,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(); @@ -311,7 +376,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); @@ -319,9 +384,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 ); } @@ -341,12 +417,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; } @@ -373,17 +443,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, @@ -420,7 +484,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); @@ -435,10 +499,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 ); } @@ -470,7 +552,6 @@ export class VisibleNode extends BaseNode { } export class TextNode extends VisibleNode { - fontSource: string; node: AdaptedTextNode; constructor(node: AdaptedTextNode) { super(node); @@ -489,14 +570,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(); } @@ -504,6 +577,10 @@ export class TextNode extends VisibleNode { getType(): NodeType { return NodeType.TEXT; } + + getStyledTextSegments(): StyledTextSegment[] { + return this.node.getStyledTextSegments(); + } } export class VectorGroupNode extends GroupNode { @@ -552,4 +629,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/bricks/overlap.ts b/core/src/bricks/overlap.ts index 878fd49..a9f2201 100644 --- a/core/src/bricks/overlap.ts +++ b/core/src/bricks/overlap.ts @@ -68,6 +68,7 @@ export const findOverlappingNodes = ( startingNode.getPositionalRelationship(targetNode) === PostionalRelationship.OVERLAP ) { + 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..7ce4be3 --- /dev/null +++ b/core/src/bricks/remove-css.ts @@ -0,0 +1,87 @@ +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" && !isEmpty(cssAttributes["width"])) { + 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"]); + } + } + + removeCssFromNode(child); + } + + node.setCssAttributes(cssAttributes); +}; + +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"]); + } else if (childAttributes["min-width"]) { + widthCum += cssStrToNum(childAttributes["min-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 583096a..7cfdb80 100644 --- a/core/src/bricks/remove-node.ts +++ b/core/src/bricks/remove-node.ts @@ -1,71 +1,161 @@ -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"; +import { replacedParentAnnotation } from "./annotation"; 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); + child.addAnnotations(replacedParentAnnotation, true); + + 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) && isEmpty(child.getChildren())) { + const cssAttributes: Attributes = { + ...node.getCssAttributes(), + ...child.getCssAttributes(), + }; + + const positionalCssAttributes: Attributes = mergeAttributes(node.getPositionalCssAttributes(), node.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 => { + 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 (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..bd05d5d 100644 --- a/core/src/bricks/util.ts +++ b/core/src/bricks/util.ts @@ -1,9 +1,18 @@ 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; +// 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) { @@ -152,38 +187,14 @@ 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; -// }; +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/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 a8aa41c..78f9460 100644 --- a/core/src/code/generator/css/generator.ts +++ b/core/src/code/generator/css/generator.ts @@ -19,13 +19,17 @@ 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; reactGenerator: ReactGenerator; constructor() { - this.htmlGenerator = new HtmlGenerator(getProps); + this.htmlGenerator = new HtmlGenerator( + getPropsFromNode, + convertCssClassesToInlineStyle + ); this.reactGenerator = new ReactGenerator(); } @@ -35,15 +39,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 +57,7 @@ export class Generator { isCssFileNeeded, [], inFileData, - inFileComponents, + inFileComponents ), importComponents, ]; @@ -114,18 +120,23 @@ 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( { + ...{ + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, ...node.getCssAttributes(), - ...filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), }, option, - node.getId(), + node.getId() ); case NodeType.GROUP: return convertCssClassesToInlineStyle( @@ -134,45 +145,92 @@ const getProps = (node: Node, option: Option): string => { ...node.getCssAttributes(), }, option, - node.getId(), + 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(), ...node.getPositionalCssAttributes(), }, option, - node.getId(), + node.getId() ); case NodeType.IMAGE: - return convertCssClassesToInlineStyle( - filterAttributes( + if (isEmpty(node.getChildren())) { + return convertCssClassesToInlineStyle( { - ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + ...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(), + node.getId() ); case NodeType.VECTOR: + 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(), + 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(), + node.getId() ); } }; @@ -181,11 +239,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..ef0336c 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, fontFamily } = styledTextSegment; + const { family, style } = fontName; + const isItalic = style.toLowerCase().includes("italic"); + + if (family && fontSize) { + const font = fonts[family]; + if (!font) { + fonts[family] = { + isItalic, + familyCss: fontFamily, + 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 89bb424..5e4ce2c 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"; @@ -20,7 +24,14 @@ 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; +export type GetPropsFromAttributes = ( + attributes: Attributes, + 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 = { node: VectorGroupNode | VectorNode | ImageNode; @@ -37,27 +48,60 @@ 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); - return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}`; + const textProp = this.getText(node, option); + + // 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}`; + } + + 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: // this edge case should never happen @@ -65,7 +109,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 +117,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}> `; } @@ -84,12 +128,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 @@ -102,9 +147,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: @@ -220,16 +274,16 @@ export class Generator { } return [ - this.renderNodeWithAbsolutePosition( + this.renderNodeWithPositionalAttributes( node, - `${alt}`, + `${alt}`, option ), ]; } return [ - this.renderNodeWithAbsolutePosition( + this.renderNodeWithPositionalAttributes( node, `${alt}`, option @@ -237,23 +291,31 @@ export class Generator { ]; } - node.addCssAttributes({ - "background-image": `url('./assets/${imageComponentName}.png')`, - }); - - return [`
    `, `
    `]; + return [`
    `, `
    `]; } - renderNodeWithAbsolutePosition( + renderNodeWithPositionalAttributes( node: ImageNode | VectorNode | VectorGroupNode, inner: string, option: Option ): string { const positionalCssAttribtues: Attributes = node.getPositionalCssAttributes(); - if (positionalCssAttribtues["position"] === "absolute") { - return `
    ` + inner + `
    `; + + const cssAttribtues: Attributes = + node.getCssAttributes(); + + if ( + positionalCssAttribtues["position"] === "absolute" || + positionalCssAttribtues["margin-left"] || + positionalCssAttribtues["margin-right"] || + positionalCssAttribtues["margin-top"] || + positionalCssAttribtues["margin-bottom"] || + cssAttribtues["border-radius"] + ) { + return `
    ` + inner + `
    `; } + return inner; } @@ -286,6 +348,7 @@ export class Generator { }); let codeStr: string = ""; + if (renderInALoop) { let dataCodeStr: string = `const ${dataArr.name} = ${JSON.stringify( data @@ -316,8 +379,312 @@ 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(); + + // 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 { 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 + ); + + resultText += `<${listType} ${listProps}>`; + } else { + resultText += `<${listType}>`; + } + + 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. "; + } + + return result; + }) + .join(""); + + return resultText; + } + ) + .join(""); + + // close all open tags + while (htmlTagStack.length) { + resultText += ``; + } + + return resultText; + } + + 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; + } + + 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" ? "
            " : "
            "); + } + + start = end; + i = start; + } + } + } + + return newStrParts.join(""); +}; + +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; + } + + 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 (text !== "" && 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; +}; + const getWidthAndHeightProp = (node: Node): string => { const cssAttribtues: Attributes = node.getCssAttributes(); let widthAndHeight: string = getWidthAndHeightVariableProp(node.getId()); @@ -333,7 +700,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 +719,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 +731,28 @@ 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 textNode.getText(); +const 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 = ( +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..c228c03 100644 --- a/core/src/code/generator/tailwindcss/css-to-twcss.ts +++ b/core/src/code/generator/tailwindcss/css-to-twcss.ts @@ -13,16 +13,19 @@ import { twUnitMap, twWidthMap, tailwindTextDecorationMap, - MAX_BORDER_RADIUS_IN_PIXELS, + twcssDropShadowToSumMap, + twcssRotateToDegMap, + twcssZIndexMap, } from "./twcss-conversion-map"; 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, - filledClassIndexes: Set, + numberOfTwcssClasses: number; + filledClassIndexes: Set; }; export type TwcssPropRenderingMap = { @@ -30,17 +33,23 @@ export type TwcssPropRenderingMap = { }; // convertCssClassesToTwcssClasses converts css classes to tailwindcss classes -export const convertCssClassesToTwcssClasses = ( +export const convertCssClassesToTwcssClasses: GetPropsFromAttributes = ( attributes: Attributes, - id: string, option: Option, -): string => { + id?: string, + parentAttributes?: Attributes +) => { 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, + parentAttributes + ).split(" "); twcssPropRenderingMap[property] = { numberOfTwcssClasses: twcssClasses.length, filledClassIndexes: new Set(), @@ -54,13 +63,22 @@ 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, + parentAttributes + ).split(" "); if (twcssPropRenderingMeta.filledClassIndexes.has(i)) { continue; } @@ -113,8 +131,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) => { @@ -214,6 +233,10 @@ const findClosestTwcssFontSize = (cssFontSize: string) => { } }); + if (!(smallestDiff < 2)) { + return `text-[${cssFontSize}]`; + } + return closestTailwindFontSize; }; @@ -228,7 +251,7 @@ const findClosestTwcssClassUsingPixel = ( targetPixelStr: string, twClassToPixelMap: Record, defaultClass: string -) => { +): [string, number] => { let closestTwClass = defaultClass; const targetPixelNum = extractPixelNumberFromString(targetPixelStr); @@ -244,7 +267,7 @@ const findClosestTwcssClassUsingPixel = ( } }); - return closestTwClass; + return [closestTwClass, smallestDiff]; }; // findTwcssTextDecoration translates text-decoration from css to tailwindcss @@ -320,6 +343,92 @@ const findClosestTwcssLetterSpacing = ( return twClassToUse; }; +const getRadiusFromBoxShadow = (boxShadowValue: string): number => { + const spaceParts: string[] = boxShadowValue.split(" "); + + if (spaceParts.length < 4) { + return 2; + } + + if (spaceParts[2].endsWith("px")) { + return parseInt(spaceParts[2].slice(0, -2)); + } + + return 2; +}; + +const getYOffSetFromBoxShadow = (boxShadowValue: string): number => { + const spaceParts: string[] = boxShadowValue.split(" "); + + if (spaceParts.length < 4) { + return 1; + } + + if (spaceParts[1].endsWith("px")) { + return parseInt(spaceParts[1].slice(0, -2)); + } + + return 1; +}; + +const findClosestTwcssDropShadowClassUsingPixel = ( + cssValue: string, +) => { + let closestTwClass = ""; + const dropShadowParts: string[] = cssValue.split("),"); + + if (isEmpty(dropShadowParts)) { + return "shadow"; + } + + 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 largestRadius: number = -Infinity; + let largestYOffset: number = -Infinity; + + for (let i = 0; i < newShadowParts.length; i++) { + const radius: number = getRadiusFromBoxShadow(newShadowParts[i]); + const yOffset: number = getYOffSetFromBoxShadow(newShadowParts[i]); + + if (radius > largestRadius) { + largestRadius = radius; + } + + if (yOffset > largestYOffset) { + largestYOffset = yOffset; + } + } + + let smallestDiff: number = Infinity; + Object.entries(twcssDropShadowToSumMap).forEach(([key, val]) => { + const pixelNum = extractPixelNumberFromString(val); + if (val.endsWith("px")) { + const diff: number = Math.abs(largestRadius + largestYOffset - pixelNum); + if (diff < smallestDiff) { + smallestDiff = diff; + closestTwClass = key; + } + } + }); + + if (isEmpty(closestTwClass)) { + return "shadow"; + } + + return "shadow" + "-" + closestTwClass; +}; + + // findClosestTwcssFontWeight finds the closest tailwincss font weight given the css font weight const findClosestTwcssFontWeight = (fontWeight: string): string => { const givenFontWeight = parseInt(fontWeight); @@ -341,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); @@ -361,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); } @@ -372,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); } @@ -417,7 +517,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 ""; @@ -426,11 +527,11 @@ export const getTwcssClass = ( switch (cssProperty) { case "height": const heightNum = extractPixelNumberFromString(cssValue); - if (heightNum > largestTWCHeightInPixels) { + if (cssValue.endsWith("px") && heightNum > largestTwcssHeightInPixels) { return `h-[${heightNum}px]`; } - const approximatedTwcssHeightClass = findClosestTwcssClassUsingPixel( + const [approximatedTwcssHeightClass] = findClosestTwcssClassUsingPixel( cssValue, twHeightMap, "h-0" @@ -445,13 +546,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 (widthNum > largestTWCWidthInPixels) { + if (cssValue.endsWith("px") && widthNum > largestTwcssWidthInPixels) { return `w-[${widthNum}px]`; } - const approximatedTwcssWidthClass = findClosestTwcssClassUsingPixel( + const [approximatedTwcssWidthClass] = findClosestTwcssClassUsingPixel( cssValue, twWidthMap, "w-0" @@ -470,7 +575,7 @@ export const getTwcssClass = ( return "border-" + findClosestTwcssColor(cssValue); case "border-width": { - const borderWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -484,7 +589,7 @@ export const getTwcssClass = ( } case "border-top-width": { - const borderTopWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderTopWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -498,7 +603,7 @@ export const getTwcssClass = ( } case "border-bottom-width": { - const borderBottomWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderBottomWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -512,7 +617,7 @@ export const getTwcssClass = ( } case "border-left-width": { - const borderLeftWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderLeftWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -526,7 +631,7 @@ export const getTwcssClass = ( } case "border-right-width": { - const borderRightWidthTwSize = findClosestTwcssClassUsingPixel( + const [borderRightWidthTwSize] = findClosestTwcssClassUsingPixel( cssValue, twBroderWidthMap, "0" @@ -540,18 +645,18 @@ export const getTwcssClass = ( } case "border-radius": { - const borderRadiusTwSize = findClosestTwcssClassUsingPixel( + const [borderRadiusTwSize, smallestDiff] = findClosestTwcssClassUsingPixel( cssValue, twBorderRadiusMap, "none" ); - if (borderRadiusTwSize === "0") { - return ""; + + if (smallestDiff > 2) { + return `rounded-[${cssValue}]`; } - const borderRadiusSize = extractPixelNumberFromString(cssValue); - if (borderRadiusSize > MAX_BORDER_RADIUS_IN_PIXELS) { - return `rounded-[${borderRadiusSize}px]`; + if (borderRadiusTwSize === "0") { + return ""; } return borderRadiusTwSize === "" @@ -572,7 +677,7 @@ export const getTwcssClass = ( return "shadow-inner"; } else { // drop shadow - return "shadow-xl"; + return findClosestTwcssDropShadowClassUsingPixel(cssValue); } } @@ -703,6 +808,10 @@ export const getTwcssClass = ( } } + case "border-top": + case "border-bottom": + case "border-left": + case "border-right": case "border-style": { switch (cssValue) { case "solid": @@ -819,6 +928,14 @@ export const getTwcssClass = ( } case "line-height": { + const lineHeightNum = extractPixelNumberFromString(cssValue); + if ( + cssValue.endsWith("px") && + lineHeightNum > largestTwcssLineheightInPixels + ) { + return `leading-[${lineHeightNum}px]`; + } + return findClosestTwcssLineHeight(cssValue); } @@ -850,8 +967,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; } @@ -903,7 +1023,95 @@ export const getTwcssClass = ( return findClosestTwcssFontWeight(cssValue); } + case "list-style-type": { + if (cssValue === "disc") { + return "list-disc"; + } + if (cssValue === "decimal") { + return "list-decimal"; + } + } + + 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); + + 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/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..ba235e1 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, @@ -22,13 +20,18 @@ import { import { Generator as ReactGenerator } from "../react/generator"; import { filterAttributes } from "../../../bricks/util"; import { extraFileRegistryGlobalInstance } from "../../extra-file-registry/extra-file-registry"; +import { shouldUseAsBackgroundImage } from "../util"; +import { Attributes } from "../../../design/adapter/node"; export class Generator { htmlGenerator: HtmlGenerator; reactGenerator: ReactGenerator; constructor() { - this.htmlGenerator = new HtmlGenerator(getProps); + this.htmlGenerator = new HtmlGenerator( + getPropsFromNode, + convertCssClassesToTwcssClasses + ); this.reactGenerator = new ReactGenerator(); } @@ -37,11 +40,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,28 +98,31 @@ 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( - { - ...node.getCssAttributes(), + case NodeType.TEXT: { + const attributes: Attributes = { + ...{ ...filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }) + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), }, - node.getId(), - option, - ); + ...node.getCssAttributes(), + }; + + return convertCssClassesToTwcssClasses(attributes, option, node.getId()); + } case NodeType.GROUP: return convertCssClassesToTwcssClasses( { ...node.getPositionalCssAttributes(), ...node.getCssAttributes(), }, - node.getId(), option, + node.getId() ); case NodeType.VISIBLE: return convertCssClassesToTwcssClasses( @@ -121,34 +130,79 @@ const getProps = (node: Node, option: Option): string => { ...node.getPositionalCssAttributes(), ...node.getCssAttributes(), }, - node.getId(), - option + option, + node.getId() ); case NodeType.IMAGE: + if (isEmpty(node.getChildren())) { + return convertCssClassesToTwcssClasses( + { + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, + option, + node.getId() + ); + } + return convertCssClassesToTwcssClasses( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), - node.getId(), - option + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, + option, + node.getId() ); case NodeType.VECTOR: + 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.getId(), - option + { + ...node.getPositionalCssAttributes(), + ...filterAttributes(node.getCssAttributes(), { + excludeBackgroundColor: true, + }), + }, + option, + node.getId() ); + // TODO: VECTOR_GROUP node type is deprecated case NodeType.VECTOR_GROUP: return convertCssClassesToTwcssClasses( - filterAttributes(node.getPositionalCssAttributes(), { - absolutePositioningOnly: true, - }), - node.getId(), - option + { + ...filterAttributes(node.getPositionalCssAttributes(), { + absolutePositioningFilter: true, + }), + ...filterAttributes(node.getPositionalCssAttributes(), { + marginFilter: true, + }), + }, + option, + node.getId() ); default: @@ -179,6 +233,15 @@ export const buildTwcssConfigFileContent = ( importComponent.importPath )}": "url(.${importComponent.importPath})",`; } + + if ( + extension === "svg" && + shouldUseAsBackgroundImage(importComponent.node) + ) { + backgroundImages += `"${getImageFileNameFromUrl( + importComponent.importPath + )}": "url(.${importComponent.importPath})",`; + } }); } @@ -216,11 +279,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/code/generator/tailwindcss/twcss-conversion-map.ts b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts index 94ab973..c4e42c5 100644 --- a/core/src/code/generator/tailwindcss/twcss-conversion-map.ts +++ b/core/src/code/generator/tailwindcss/twcss-conversion-map.ts @@ -4,6 +4,38 @@ export const tailwindTextDecorationMap = { none: "no-underline", }; + +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", @@ -418,7 +450,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/code/generator/util.ts b/core/src/code/generator/util.ts index f3f84a4..d5eb30a 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 => { @@ -34,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)); } @@ -96,3 +101,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/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 658db96..185bd64 100644 --- a/core/src/design/adapter/figma/adapter.ts +++ b/core/src/design/adapter/figma/adapter.ts @@ -4,9 +4,10 @@ import { ImageNode, Node, TextNode as BricksTextNode, - VectorGroupNode, VectorNode as BricksVector, VisibleNode, + computePositionalRelationship, + VectorNode, } from "../../../bricks/node"; import { isEmpty } from "../../../utils"; import { BoxCoordinates, Attributes, ExportFormat } from "../node"; @@ -16,20 +17,116 @@ import { rgbaToString, isFrameNodeTransparent, doesNodeContainsAnImage, + getMostCommonFieldInString, + getRgbaFromPaints, + figmaLineHeightToCssString, + figmaLetterSpacingToCssString, + figmaFontNameToCssString, } from "./util"; -import { GoogleFontsInstance } from "../../../google/google-fonts"; +import { PostionalRelationship } from "../../../bricks/node"; +import { Direction } from "../../../bricks/direction"; +import { StyledTextSegment } from "../node"; +import { Line } from "../../../bricks/line"; 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 +) => { + // @ts-ignore + if (!isEmpty(figmaNode.rotation) && figmaNode.rotation !== 0) { + // @ts-ignore + attributes["width"] = `${figmaNode.width}px`; + // @ts-ignore + attributes["height"] = `${figmaNode.height}px`; + 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 || + 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`; + } + + // @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; + } + + // @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 @@ -103,7 +200,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"; @@ -112,24 +209,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`; } @@ -158,24 +254,35 @@ 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); } + // @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 ) { - 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 + ); + } } } @@ -227,15 +334,33 @@ 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"; + } - // width - attributes["width"] = `${figmaNode.absoluteBoundingBox.width}px`; + if (strokeBottomWeight > 0) { + attributes["border-bottom"] = + figmaNode.dashPattern.length === 0 ? "solid" : "dashed"; + } - // height - attributes["height"] = `${figmaNode.absoluteBoundingBox.height}px`; + 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); // box shadow addDropShadowCssProperty(figmaNode, attributes); @@ -274,79 +399,134 @@ 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", + { + areVariationsEqual: (fontName1, fontName2) => { + return ( + fontName1.family === fontName2.family && + fontName1.style === fontName2.style + ); + }, + } + ); + + if (mostCommonFontName) { + attributes["font-family"] = + figmaFontNameToCssString(mostCommonFontName); + } } // 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 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 moreThanOneRow: boolean = false; - if (figmaNode.fontSize !== figma.mixed) { - moreThanOneRow = renderBoundsHeight > figmaNode.fontSize; - } + let fontSize: number = 0; - if (!moreThanOneRow) { - attributes["white-space"] = "nowrap"; - } + if (!isEmpty(absoluteRenderBounds)) { + const renderBoundsHeight = absoluteRenderBounds.height; - let width = absoluteRenderBounds.width + 2; + if (figmaNode.fontSize !== figma.mixed) { + fontSize = figmaNode.fontSize; + } else { + for (const segment of figmaNode.getStyledTextSegments(["fontSize"])) { + if (segment.fontSize > fontSize) { + fontSize = segment.fontSize; + } + } + } - if ( - Math.abs( - figmaNode.absoluteBoundingBox.width - absoluteRenderBounds.width - ) / - figmaNode.absoluteBoundingBox.width > - 0.2 - ) { - width = absoluteRenderBounds.width + 4; - } - // @ts-ignore - if (isAutoLayout(figmaNode.parent)) { - attributes["width"] = `${absoluteBoundingBox.width}px`; + moreThanOneRow = renderBoundsHeight > fontSize * 1.5; } - 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; + if (!isEmpty(absoluteBoundingBox) && !isEmpty(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(absoluteBoundingBox.width - absoluteRenderBounds.width) / + absoluteBoundingBox.width > + 0.2 + ) { + width = absoluteRenderBounds.width + 6; + } } + + if (!moreThanOneRow) { + attributes["min-width"] = `${absoluteBoundingBox.width}px`; + attributes["white-space"] = "nowrap"; + } else { + attributes["width"] = `${absoluteBoundingBox.width}px`; + } + + // 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": @@ -358,31 +538,37 @@ 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 finalColor = getRgbaFromPaints(paints); + if (finalColor) { + 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); + if (finalColor) { + attributes["color"] = rgbaToString(finalColor); + } } const textContainingOnlyOneWord = - figmaNode.characters.split(" ").length === 1; + figmaNode.characters.trim().split(" ").length === 1; if (moreThanOneRow && textContainingOnlyOneWord) { 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 - ) { + if (textContainingOnlyOneWord) { // text alignment switch (figmaNode.textAlignHorizontal) { case "CENTER": @@ -416,9 +602,8 @@ 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}`; + if (lineHeight !== figma.mixed) { + attributes["line-height"] = figmaLineHeightToCssString(lineHeight); } // text transform @@ -443,18 +628,38 @@ 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 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 @@ -469,6 +674,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; @@ -477,6 +711,7 @@ export class FigmaNodeAdapter { constructor(node: SceneNode) { this.node = node; this.cssAttributes = getCssAttributes(node); + this.positionalCssAttribtues = getPositionalCssAttributes(node); } @@ -499,6 +734,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, @@ -521,9 +798,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; } @@ -573,8 +856,8 @@ export class FigmaNodeAdapter { } export class FigmaVectorNodeAdapter extends FigmaNodeAdapter { - node: VectorNode; - constructor(node: VectorNode) { + node: SceneNode; + constructor(node: SceneNode) { super(node); this.node = node; } @@ -623,27 +906,123 @@ export class FigmaTextNodeAdapter extends FigmaNodeAdapter { // @ts-ignore return this.node.characters; } + + getStyledTextSegments(): StyledTextSegment[] { + const styledTextSegments = this.node.getStyledTextSegments([ + "fontSize", + "fontName", + "fontWeight", + "textDecoration", + "textCase", + "fills", + "letterSpacing", + "listOptions", + "indentation", + "hyperlink", + ]); + + // for converting figma textDecoration to css textDecoration + 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; + + const figmaListOptionsToHtmlTagMap = { + NONE: "none", + UNORDERED: "ul", + ORDERED: "ol", + } as const; + + 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 : "", + }; + }); + } } const EXPORTABLE_NODE_TYPES: string[] = [ NodeType.ELLIPSE, NodeType.VECTOR, + NodeType.IMAGE, + NodeType.INSTANCE, + NodeType.GROUP, + NodeType.STAR, NodeType.FRAME, NodeType.RECTANGLE, + NodeType.BOOLEAN_OPERATION, +]; + +const VECTOR_NODE_TYPES: string[] = [ + NodeType.ELLIPSE, + NodeType.VECTOR, + NodeType.STAR, + NodeType.BOOLEAN_OPERATION, + NodeType.RECTANGLE, ]; -type Feedbacks = { +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 => { - let reordered = [...figmaNodes]; +): Feedback => { + 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) => { + 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; } @@ -652,11 +1031,26 @@ 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 && !doesNodeContainsAnImage(figmaNode)) { + result.isSingleRectangle = true; + } + } + + const newNodes: Node[] = []; + for (let i = 0; i < reordered.length; i++) { const figmaNode = reordered[i]; @@ -665,59 +1059,184 @@ 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: 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: 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; + } + + if (!isEmpty(figmaNode.rotation)) { + newNode = new VectorNode(new FigmaImageNodeAdapter(figmaNode)); break; } } + + newNodes.push(newNode); + } + } + + 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)) { + const feedback: Feedback = convertFigmaNodesToBricksNodes(figmaNode.children); + + let doNodesHaveNonOverlappingChildren: boolean = true; + let horizontalNonOverlap: boolean = areThereNonOverlappingByDirection( //@ts-ignore - const feedbacks = convertFigmaNodesToBricksNodes(figmaNode.children); - if (feedbacks.areAllNodesExportable) { - newNode = new VectorGroupNode( - new FigmaVectorGroupNodeAdapter(figmaNode), - feedbacks.nodes - ); + 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; + } + + 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 = - feedbacks.areAllNodesExportable && result.areAllNodesExportable; + result.doNodesContainImage = + feedback.doNodesContainImage || result.doNodesContainImage; - newNode.setChildren(feedbacks.nodes); + result.doNodesHaveNonOverlappingChildren = doNodesHaveNonOverlappingChildren; + + if (!isExportableNode) { + newNode.setChildren(feedback.nodes); } - result.nodes.push(newNode); + newNodes[i] = newNode; } } + result.nodes = newNodes; + + if (!isEmpty(sliceNode)) { + result.nodes = [new BricksVector(new FigmaVectorNodeAdapter(sliceNode))]; + result.doNodesHaveNonOverlappingChildren = false; + result.areAllNodesExportable = false; + } + return result; }; + +const areThereNonOverlappingByDirection = ( + nodes: readonly SceneNode[], + direction: Direction +): boolean => { + + for (let i = 0; i < nodes.length; i++) { + const currentNode: SceneNode = nodes[i]; + let currentLine = getFigmaLineBasedOnDirection(currentNode, direction); + + let nonOverlap: boolean = true; + for (let j = 0; j < nodes.length; j++) { + const targetNode: SceneNode = nodes[j]; + if (i === j) { + continue; + } + + + const targetLine = getFigmaLineBasedOnDirection(targetNode, direction); + + if (currentLine.overlapStrict(targetLine)) { + nonOverlap = false; + break; + } + } + + if (nonOverlap) { + return true; + } + } + + 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 0abeb39..3225362 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; @@ -49,13 +50,165 @@ 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) { - if (!isEmpty(node.fills) && node.fills[0].type === "IMAGE") { - return true; + for (const fill of node.fills) { + if (fill.type === "IMAGE") { + return true; + } } } 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, + 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]); + + // 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.) + const fieldNumOfChars = new Map, number>(); + styledTextSegments.forEach((segment) => { + const variation = variationModifier + ? variationModifier(segment[field]) + : segment[field]; + + if (variation === null) { + return; + } + + 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 currentLongestLength = -Infinity; + for (const [variation, sum] of fieldNumOfChars) { + if (sum > currentLongestLength) { + currentLongestLength = sum; + variationWithLongestLength = variation; + } + } + + 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 getRgbaFromPaints(paints: readonly Paint[]) { + // TODO: support GradientPaint + const solidPaints = paints.filter( + (paint) => paint.type === "SOLID" + ) as SolidPaint[]; + + if (solidPaints.length === 0) { + return null; + } + + 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; +} + +export const figmaLineHeightToCssString = (lineHeight: LineHeight) => { + switch (lineHeight.unit) { + case "AUTO": + return "100%"; + case "PERCENT": + return `${lineHeight.value}%`; + case "PIXELS": + return `${lineHeight.value}px`; + } +}; + +export const figmaLetterSpacingToCssString = (letterSpacing: LetterSpacing) => { + if (letterSpacing.value === 0) { + return "normal"; + } + + switch (letterSpacing.unit) { + case "PERCENT": + return `${roundToTwoDps(letterSpacing.value / 100)}em`; + case "PIXELS": + 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 b64c10e..5a0bcf1 100644 --- a/core/src/design/adapter/node.ts +++ b/core/src/design/adapter/node.ts @@ -29,10 +29,31 @@ export interface Node { export(exportFormat: ExportFormat): Promise; } +export interface StyledTextSegment { + characters: string; + start: number; + end: number; + fontName: { + family: string; + style: string; + }; + fontSize: number; + fontFamily: string; + fontWeight: number; + textDecoration: "normal" | "line-through" | "underline"; + textTransform: "none" | "uppercase" | "lowercase" | "capitalize"; + color: string; + letterSpacing: string; + listType: "none" | "ul" | "ol"; + indentation: number; + href: string; +} + 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/src/index.ts b/core/src/index.ts index cfd5181..51cf70e 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"; @@ -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,18 +29,29 @@ 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(converted) : converted[0]; groupNodes(startingNode); startingNode = removeNode(startingNode); removeCompletelyOverlappingNodes(startingNode, null); - - addAdditionalCssAttributesToNodes(startingNode); + removeChildrenNode(startingNode); instantiateRegistries(startingNode, option); + addAdditionalCssAttributesToNodes(startingNode); + removeCssFromNode(startingNode); + return await generateCodingFiles(startingNode, option); }; @@ -54,18 +66,27 @@ 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); - - addAdditionalCssAttributesToNodes(startingNode); + removeChildrenNode(startingNode); instantiateRegistries(startingNode, option); + addAdditionalCssAttributesToNodes(startingNode); + // ee features let startAnnotateHtmlTag: number = Date.now(); await annotateNodeForHtmlTag(startingNode); 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) ); 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, diff --git a/figma/src/code.ts b/figma/src/code.ts index b880664..c4bdf6e 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"; @@ -27,22 +24,29 @@ 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) { - console.error("Error from Figma core:\n", e.stack); + + }).catch((e) => { + 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({ @@ -50,27 +54,37 @@ figma.ui.onmessage = async (msg) => { files: [], error: true, }); - } + }); } if (msg.type === "generate-code-with-ai") { - try { - const [files, applications] = await convertToCodeWithAi(figma.currentPage.selection, { + 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) { - console.error("Error from Figma core:\n", e.stack); + + }).catch((e) => { + 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({ @@ -79,7 +93,7 @@ figma.ui.onmessage = async (msg) => { applications: [], error: true, }); - } + }); } if (msg.type === "update-settings") { @@ -154,7 +168,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..21ba864 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, @@ -38,6 +43,9 @@ const Home = (props: PropsWithChildren) => { const { setCurrentPage } = useContext(PageContext); const handleGenerateCodeButtonClick = () => { + setIsGeneratingCode(true); + setCurrentPage(PAGES.CODE_GENERATION); + parent.postMessage( { pluginMessage: { @@ -51,8 +59,6 @@ const Home = (props: PropsWithChildren) => { }, "*" ); - setIsGeneratingCode(true); - setCurrentPage(PAGES.CODE_GENERATION); parent.postMessage( { @@ -71,6 +77,10 @@ const Home = (props: PropsWithChildren) => { }; const handleGenerateCodeWithAiButtonClick = () => { + setIsGeneratingCodeWithAi(true); + setIsGeneratingCode(true); + setCurrentPage(PAGES.CODE_GENERATION); + parent.postMessage( { pluginMessage: { @@ -85,10 +95,6 @@ const Home = (props: PropsWithChildren) => { "*" ); - setIsGeneratingCodeWithAi(true); - setIsGeneratingCode(true); - setCurrentPage(PAGES.CODE_GENERATION); - parent.postMessage( { pluginMessage: { @@ -146,7 +152,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..73144a7 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); @@ -25,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.

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

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

            ); } @@ -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..1659428 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 @@ -143,7 +152,6 @@ const UI = () => { }, []); const resetLimit = () => { - console.log("called!!!"); parent.postMessage( { pluginMessage: { @@ -179,7 +187,6 @@ const UI = () => { }, 1000); if (settings) { - } setSelectedLanguage(settings.language); @@ -189,8 +196,6 @@ const UI = () => { } if (pluginMessage.type === "get-limit") { - // resetLimit(); - if (Number.isInteger(pluginMessage.limit) && pluginMessage.limit >= 0) { setLimit(pluginMessage.limit); } else { @@ -348,7 +353,9 @@ const UI = () => { /> )} {currentPage === PAGES.POST_CODE_GENERATION && } - {currentPage === PAGES.POST_CODE_GENERATION_AI && } + {currentPage === PAGES.POST_CODE_GENERATION_AI && ( + + )} {currentPage === PAGES.ERROR && }