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;
|
penpotFile: PenpotFile;
|
||||||
fontNames: Set<FontName>;
|
fontNames: Set<FontName>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface Signatures {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,114 +1,15 @@
|
||||||
import { NodeData, TextData } from '../common/interfaces';
|
import { handleCancelMessage, handleExportMessage, handleResizeMessage } from './messageHandlers';
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
|
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
|
||||||
|
|
||||||
figma.ui.onmessage = async msg => {
|
figma.ui.onmessage = async msg => {
|
||||||
if (msg.type === 'export') {
|
if (msg.type === 'export') {
|
||||||
const root: NodeData | TextData = await traverse(figma.root); // start the traversal at the root
|
await handleExportMessage();
|
||||||
figma.ui.postMessage({ type: 'FIGMAFILE', data: root });
|
|
||||||
}
|
}
|
||||||
if (msg.type === 'cancel') {
|
if (msg.type === 'cancel') {
|
||||||
figma.closePlugin();
|
handleCancelMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === 'resize') {
|
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