From 4b2e5630704ed44296db585f2e5e3c53dbf6819b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 9 Apr 2024 11:23:06 +0200 Subject: [PATCH 1/2] refactor --- src/common/interfaces.ts | 4 + src/plugin/code.ts | 106 +----------------- src/plugin/figma/index.ts | 1 + src/plugin/figma/traverseNode.ts | 24 ++++ .../messageHandlers/handleCancelMessage.ts | 3 + .../messageHandlers/handleExportMessage.ts | 8 ++ .../messageHandlers/handleResizeMessage.ts | 3 + src/plugin/messageHandlers/index.ts | 3 + src/plugin/utils/detectMimeType.ts | 16 +++ src/plugin/utils/getImageFill.ts | 18 +++ src/plugin/utils/getNodeData.ts | 16 +++ src/plugin/utils/getTextData.ts | 34 ++++++ src/plugin/utils/index.ts | 4 + 13 files changed, 138 insertions(+), 102 deletions(-) create mode 100644 src/plugin/figma/index.ts create mode 100644 src/plugin/figma/traverseNode.ts create mode 100644 src/plugin/messageHandlers/handleCancelMessage.ts create mode 100644 src/plugin/messageHandlers/handleExportMessage.ts create mode 100644 src/plugin/messageHandlers/handleResizeMessage.ts create mode 100644 src/plugin/messageHandlers/index.ts create mode 100644 src/plugin/utils/detectMimeType.ts create mode 100644 src/plugin/utils/getImageFill.ts create mode 100644 src/plugin/utils/getNodeData.ts create mode 100644 src/plugin/utils/getTextData.ts create mode 100644 src/plugin/utils/index.ts diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 23e152b..f8f3613 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -49,3 +49,7 @@ export type ExportFile = { penpotFile: PenpotFile; fontNames: Set; }; + +export interface Signatures { + [key: string]: string; +} diff --git a/src/plugin/code.ts b/src/plugin/code.ts index 0796bd6..e16e6ba 100644 --- a/src/plugin/code.ts +++ b/src/plugin/code.ts @@ -1,114 +1,16 @@ -import { NodeData, TextData } from '../common/interfaces'; - -interface Signatures { - [key: string]: string; -} - -const signatures: Signatures = { - 'R0lGODdh': 'image/gif', - 'R0lGODlh': 'image/gif', - 'iVBORw0KGgo': 'image/png', - '/9j/': 'image/jpg' -}; - -function detectMimeType(b64: string) { - for (const s in signatures) { - if (b64.indexOf(s) === 0) { - return signatures[s]; - } - } -} - -async function traverse(node: BaseNode): Promise { - const children: (NodeData | TextData)[] = []; - - if (node.type === 'PAGE') { - await node.loadAsync(); - } - - if ('children' in node) { - if (node.type !== 'INSTANCE') { - for (const child of node.children) { - children.push(await traverse(child)); - } - } - } - - const result = { - id: node.id, - type: node.type, - name: node.name, - children: children, - x: 'x' in node ? node.x : 0, - y: 'y' in node ? node.y : 0, - width: 'width' in node ? node.width : 0, - height: 'height' in node ? node.height : 0, - fills: 'fills' in node ? (node.fills === figma.mixed ? [] : node.fills) : [], // TODO: Support mixed fills - imageFill: '' - }; - - if (result.fills) { - // Find any fill of type image - const imageFill = result.fills.find(fill => fill.type === 'IMAGE'); - if (imageFill && 'exportAsync' in node) { - // An "image" in Figma is a shape with one or more image fills, potentially blended with other fill - // types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class - // objects, we're going to simplify this by exporting this shape as a PNG image. - const value = await node.exportAsync({ format: 'PNG' }); - const b64 = figma.base64Encode(value); - - result.imageFill = 'data:' + detectMimeType(b64) + ';base64,' + b64; - } - } - - if (node.type == 'TEXT') { - const styledTextSegments = node.getStyledTextSegments([ - 'fontName', - 'fontSize', - 'fontWeight', - 'lineHeight', - 'letterSpacing', - 'textCase', - 'textDecoration', - 'fills' - ]); - - if (styledTextSegments[0]) { - const font = { - ...result, - fontName: styledTextSegments[0].fontName, - fontSize: styledTextSegments[0].fontSize.toString(), - fontWeight: styledTextSegments[0].fontWeight.toString(), - characters: node.characters, - lineHeight: styledTextSegments[0].lineHeight, - letterSpacing: styledTextSegments[0].letterSpacing, - fills: styledTextSegments[0].fills, - textCase: styledTextSegments[0].textCase, - textDecoration: styledTextSegments[0].textDecoration, - textAlignHorizontal: node.textAlignHorizontal, - textAlignVertical: node.textAlignVertical, - children: styledTextSegments - }; - - return font as TextData; - } - } - - return result as NodeData; -} +import { handleCancelMessage, handleExportMessage, handleResizeMessage } from './messageHandlers'; figma.showUI(__html__, { themeColors: true, height: 200, width: 300 }); figma.ui.onmessage = async msg => { if (msg.type === 'export') { - const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root - figma.ui.postMessage({ type: 'FIGMAFILE', data: root }); + await handleExportMessage(); } if (msg.type === 'cancel') { - figma.closePlugin(); + handleCancelMessage(); } if (msg.type === 'resize') { - figma.ui.resize(msg.width, msg.height); + handleResizeMessage(msg.width, msg.height); } }; diff --git a/src/plugin/figma/index.ts b/src/plugin/figma/index.ts new file mode 100644 index 0000000..0a100b8 --- /dev/null +++ b/src/plugin/figma/index.ts @@ -0,0 +1 @@ +export * from './traverseNode'; diff --git a/src/plugin/figma/traverseNode.ts b/src/plugin/figma/traverseNode.ts new file mode 100644 index 0000000..43cbecb --- /dev/null +++ b/src/plugin/figma/traverseNode.ts @@ -0,0 +1,24 @@ +import { NodeData, TextData } from '../../common/interfaces'; +import { getImageFill, getNodeData, getTextData } from '../utils'; + +export async function traverse(baseNode: BaseNode): Promise { + const children: (NodeData | TextData)[] = []; + if ('children' in baseNode && baseNode.type !== 'INSTANCE') { + for (const child of baseNode.children) { + children.push(await traverse(child)); + } + } + + const nodeData = getNodeData(baseNode, children); + + if (nodeData.fills) { + nodeData.imageFill = await getImageFill(baseNode, nodeData); + } + + const textNode = getTextData(baseNode, nodeData); + if (textNode) { + return textNode; + } + + return nodeData; +} diff --git a/src/plugin/messageHandlers/handleCancelMessage.ts b/src/plugin/messageHandlers/handleCancelMessage.ts new file mode 100644 index 0000000..b91a3b7 --- /dev/null +++ b/src/plugin/messageHandlers/handleCancelMessage.ts @@ -0,0 +1,3 @@ +export function handleCancelMessage() { + figma.closePlugin(); +} diff --git a/src/plugin/messageHandlers/handleExportMessage.ts b/src/plugin/messageHandlers/handleExportMessage.ts new file mode 100644 index 0000000..aeabbf3 --- /dev/null +++ b/src/plugin/messageHandlers/handleExportMessage.ts @@ -0,0 +1,8 @@ +import { NodeData, TextData } from '../../common/interfaces'; +import { traverse } from '../figma'; + +export async function handleExportMessage() { + await figma.loadAllPagesAsync(); // ensures all PageNodes are loaded + const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root + figma.ui.postMessage({ type: 'FIGMAFILE', data: root }); +} diff --git a/src/plugin/messageHandlers/handleResizeMessage.ts b/src/plugin/messageHandlers/handleResizeMessage.ts new file mode 100644 index 0000000..b4e322b --- /dev/null +++ b/src/plugin/messageHandlers/handleResizeMessage.ts @@ -0,0 +1,3 @@ +export function handleResizeMessage(width: number, height: number) { + figma.ui.resize(width, height); +} diff --git a/src/plugin/messageHandlers/index.ts b/src/plugin/messageHandlers/index.ts new file mode 100644 index 0000000..4c54b10 --- /dev/null +++ b/src/plugin/messageHandlers/index.ts @@ -0,0 +1,3 @@ +export * from './handleCancelMessage'; +export * from './handleExportMessage'; +export * from './handleResizeMessage'; diff --git a/src/plugin/utils/detectMimeType.ts b/src/plugin/utils/detectMimeType.ts new file mode 100644 index 0000000..c3500d8 --- /dev/null +++ b/src/plugin/utils/detectMimeType.ts @@ -0,0 +1,16 @@ +import { Signatures } from '../../common/interfaces'; + +const signatures: Signatures = { + 'R0lGODdh': 'image/gif', + 'R0lGODlh': 'image/gif', + 'iVBORw0KGgo': 'image/png', + '/9j/': 'image/jpg' +}; + +export const detectMimeType = (b64: string) => { + for (const s in signatures) { + if (b64.indexOf(s) === 0) { + return signatures[s]; + } + } +}; diff --git a/src/plugin/utils/getImageFill.ts b/src/plugin/utils/getImageFill.ts new file mode 100644 index 0000000..871189c --- /dev/null +++ b/src/plugin/utils/getImageFill.ts @@ -0,0 +1,18 @@ +import { NodeData } from '../../common/interfaces'; +import { detectMimeType } from './detectMimeType'; + +export async function getImageFill(baseNode: BaseNode, node: NodeData): Promise { + // Find any fill of type image + const imageFill = node.fills.find(fill => fill.type === 'IMAGE'); + if (imageFill && 'exportAsync' in baseNode) { + // An "image" in Figma is a shape with one or more image fills, potentially blended with other fill + // types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class + // objects, we're going to simplify this by exporting this shape as a PNG image. + const value = await baseNode.exportAsync({ format: 'PNG' }); + const b64 = figma.base64Encode(value); + + return 'data:' + detectMimeType(b64) + ';base64,' + b64; + } + + return ''; +} diff --git a/src/plugin/utils/getNodeData.ts b/src/plugin/utils/getNodeData.ts new file mode 100644 index 0000000..fb588f7 --- /dev/null +++ b/src/plugin/utils/getNodeData.ts @@ -0,0 +1,16 @@ +import { NodeData, TextData } from '../../common/interfaces'; + +export const getNodeData = (node: BaseNode, children: (NodeData | TextData)[]): NodeData => { + return { + id: node.id, + type: node.type, + name: node.name, + children: children, + x: 'x' in node ? node.x : 0, + y: 'y' in node ? node.y : 0, + width: 'width' in node ? node.width : 0, + height: 'height' in node ? node.height : 0, + fills: 'fills' in node ? (node.fills === figma.mixed ? [] : node.fills) : [], // TODO: Support mixed fills + imageFill: '' + } as NodeData; +}; diff --git a/src/plugin/utils/getTextData.ts b/src/plugin/utils/getTextData.ts new file mode 100644 index 0000000..291f987 --- /dev/null +++ b/src/plugin/utils/getTextData.ts @@ -0,0 +1,34 @@ +import { NodeData, TextData } from '../../common/interfaces'; + +export const getTextData = (baseNode: BaseNode, nodeData: NodeData): TextData | undefined => { + if (baseNode.type === 'TEXT') { + const styledTextSegments = baseNode.getStyledTextSegments([ + 'fontName', + 'fontSize', + 'fontWeight', + 'lineHeight', + 'letterSpacing', + 'textCase', + 'textDecoration', + 'fills' + ]); + + if (styledTextSegments[0]) { + return { + ...nodeData, + fontName: styledTextSegments[0].fontName, + fontSize: styledTextSegments[0].fontSize.toString(), + fontWeight: styledTextSegments[0].fontWeight.toString(), + characters: baseNode.characters, + lineHeight: styledTextSegments[0].lineHeight, + letterSpacing: styledTextSegments[0].letterSpacing, + fills: styledTextSegments[0].fills, + textCase: styledTextSegments[0].textCase, + textDecoration: styledTextSegments[0].textDecoration, + textAlignHorizontal: baseNode.textAlignHorizontal, + textAlignVertical: baseNode.textAlignVertical, + children: styledTextSegments + } as TextData; + } + } +}; diff --git a/src/plugin/utils/index.ts b/src/plugin/utils/index.ts new file mode 100644 index 0000000..29de70c --- /dev/null +++ b/src/plugin/utils/index.ts @@ -0,0 +1,4 @@ +export * from './detectMimeType'; +export * from './getImageFill'; +export * from './getNodeData'; +export * from './getTextData'; From 9c2be58746a6dfd7541a9888407c53493e5a6f45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 9 Apr 2024 11:26:17 +0200 Subject: [PATCH 2/2] minor fix --- src/plugin/code.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugin/code.ts b/src/plugin/code.ts index e16e6ba..9c78fcb 100644 --- a/src/plugin/code.ts +++ b/src/plugin/code.ts @@ -9,7 +9,6 @@ figma.ui.onmessage = async msg => { if (msg.type === 'cancel') { handleCancelMessage(); } - if (msg.type === 'resize') { handleResizeMessage(msg.width, msg.height); }