mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-01-03 05:10:13 -05:00
Merge pull request #13 from Runroom/feature/code-refactor
Code Refactor
This commit is contained in:
commit
ffcf040bae
13 changed files with 138 additions and 103 deletions
|
@ -49,3 +49,7 @@ export type ExportFile = {
|
|||
penpotFile: PenpotFile;
|
||||
fontNames: Set<FontName>;
|
||||
};
|
||||
|
||||
export interface Signatures {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
|
|
@ -1,114 +1,15 @@
|
|||
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<NodeData | TextData> {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
|
1
src/plugin/figma/index.ts
Normal file
1
src/plugin/figma/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './traverseNode';
|
24
src/plugin/figma/traverseNode.ts
Normal file
24
src/plugin/figma/traverseNode.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { NodeData, TextData } from '../../common/interfaces';
|
||||
import { getImageFill, getNodeData, getTextData } from '../utils';
|
||||
|
||||
export async function traverse(baseNode: BaseNode): Promise<NodeData | TextData> {
|
||||
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;
|
||||
}
|
3
src/plugin/messageHandlers/handleCancelMessage.ts
Normal file
3
src/plugin/messageHandlers/handleCancelMessage.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function handleCancelMessage() {
|
||||
figma.closePlugin();
|
||||
}
|
8
src/plugin/messageHandlers/handleExportMessage.ts
Normal file
8
src/plugin/messageHandlers/handleExportMessage.ts
Normal file
|
@ -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 });
|
||||
}
|
3
src/plugin/messageHandlers/handleResizeMessage.ts
Normal file
3
src/plugin/messageHandlers/handleResizeMessage.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function handleResizeMessage(width: number, height: number) {
|
||||
figma.ui.resize(width, height);
|
||||
}
|
3
src/plugin/messageHandlers/index.ts
Normal file
3
src/plugin/messageHandlers/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './handleCancelMessage';
|
||||
export * from './handleExportMessage';
|
||||
export * from './handleResizeMessage';
|
16
src/plugin/utils/detectMimeType.ts
Normal file
16
src/plugin/utils/detectMimeType.ts
Normal file
|
@ -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];
|
||||
}
|
||||
}
|
||||
};
|
18
src/plugin/utils/getImageFill.ts
Normal file
18
src/plugin/utils/getImageFill.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { NodeData } from '../../common/interfaces';
|
||||
import { detectMimeType } from './detectMimeType';
|
||||
|
||||
export async function getImageFill(baseNode: BaseNode, node: NodeData): Promise<string> {
|
||||
// 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 '';
|
||||
}
|
16
src/plugin/utils/getNodeData.ts
Normal file
16
src/plugin/utils/getNodeData.ts
Normal file
|
@ -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;
|
||||
};
|
34
src/plugin/utils/getTextData.ts
Normal file
34
src/plugin/utils/getTextData.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
4
src/plugin/utils/index.ts
Normal file
4
src/plugin/utils/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './detectMimeType';
|
||||
export * from './getImageFill';
|
||||
export * from './getNodeData';
|
||||
export * from './getTextData';
|
Loading…
Reference in a new issue