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
-
\ No newline at end of file
+
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}${htmlTag}>`;
+ 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}${listTag}>`;
+ }
+
+ const children: Node[] = node.getChildren();
+
+ if (isEmpty(children)) {
+ return `<${htmlTag} ${attributes}${textNodeClassProps}>${textProp}${htmlTag}>`;
+ }
+
+ for (const child of children) {
+ if (child.getType() === NodeType.TEXT) {
+ child.addAnnotations("htmlTag", "span");
+ }
+ }
+
+ return await this.generateHtmlFromNodes(
+ children,
+ [`<${htmlTag} ${attributes} ${textNodeClassProps}>${textProp} {" "}`, `${htmlTag}>`],
+ option
+ );
+ }
case NodeType.GROUP:
// this edge case should never happen
@@ -65,7 +109,7 @@ export class Generator {
return `<${htmlTag}>${htmlTag}>`;
}
- const groupNodeClassProps = this.getProps(node, option);
+ const groupNodeClassProps = this.getPropsFromNode(node, option);
return await this.generateHtmlFromNodes(
node.getChildren(),
[`<${htmlTag} ${groupNodeClassProps}>`, `${htmlTag}>`],
@@ -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}> ${htmlTag}>`;
}
@@ -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,
- `
`,
+ `
`,
option
),
];
}
return [
- this.renderNodeWithAbsolutePosition(
+ this.renderNodeWithPositionalAttributes(
node,
`
`,
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 .
+ // Hence, we are appending a tag if the last open tag is or .
+ if (lastOpenTag === "ul" || lastOpenTag === "ol") {
+ resultText += ``;
+ 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 += ` `;
+ } else {
+ resultText += `${htmlTag}>`;
+ 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 += "";
+ }
+
+ const lastListItem =
+ listItemArr[listItemIndex - 1] ||
+ styledTextSegmentArr[styledTextSegmentIndex - 1].characters ||
+ "";
+ if (hasOpenListItem && lastListItem.endsWith("\n")) {
+ result += " ";
+ }
+
+ 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 += " ";
+ }
+
+ return result;
+ })
+ .join("");
+
+ return resultText;
+ }
+ )
+ .join("");
+
+ // close all open tags
+ while (htmlTagStack.length) {
+ resultText += `${htmlTagStack.pop()}>`;
+ }
+
+ 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}${htmlTag}>`;
+ }
}
+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 (
) => {
);
};
- const ranOutOfAiCredits = limit === 0 ? (
-
- Ran out of daily AI credits?
-
- spike@bricks-tech.com
-
- }
- trigger="hover"
- arrow={false}
- >
-
- Contact us.
-
-
-
-
- ) : null;
+ const ranOutOfAiCredits =
+ limit === 0 ? (
+
+ Ran out of daily AI credits?
+
spike@bricks-tech.com}
+ trigger="hover"
+ arrow={false}
+ >
+ Contact us.
+
+
+ ) : null;
return (
@@ -241,14 +245,16 @@ const Home = (props: PropsWithChildren
) => {
{getGenerateCodeButton()}
- {connectedToVSCode ?
-
- Setting
- : null}
+ {connectedToVSCode ? (
+
+
+ Setting
+
+ ) : 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 && }