0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2025-04-18 01:34:17 -05:00
This commit is contained in:
Alex Sánchez 2024-06-21 14:29:29 +02:00
parent 06d56ad3c4
commit b4e81a65c1
No known key found for this signature in database
GPG key ID: 68A95170EEB87E16
22 changed files with 198 additions and 79 deletions

View file

@ -0,0 +1,35 @@
export type ComponentProperty = {
type: ComponentPropertyType;
defaultValue: string | boolean;
preferredValues?: InstanceSwapPreferredValue[];
variantOptions?: string[];
};
class ComponentPropertiesLibrary {
private properties: Map<string, ComponentProperty> = new Map();
public register(id: string, property: ComponentProperty) {
this.properties.set(id, property);
}
public registerAll(properties: Record<string, ComponentProperty>) {
Object.entries(properties).forEach(([key, value]) => {
if (value.type === 'TEXT' || value.type === 'BOOLEAN') {
this.register(key, value);
}
});
}
public get(id: string): ComponentProperty | undefined {
return this.properties.get(id);
}
public all(): Record<string, ComponentProperty> {
return Object.fromEntries(this.properties.entries());
}
public init(properties: Record<string, ComponentProperty>): void {
this.properties = new Map(Object.entries(properties));
}
}
export const componentPropertiesLibrary = new ComponentPropertiesLibrary();

View file

@ -1,23 +1,26 @@
import { overridesLibrary } from '@plugin/OverridesLibrary';
import { syncAttributes } from '@plugin/utils/syncAttributes';
import { ShapeAttributes } from '@ui/lib/types/shapes/shape';
import { SyncGroups } from '@ui/lib/types/utils/syncGroups';
export const transformOverrides = (node: SceneNode) => {
export const transformOverrides = (
node: SceneNode
): Pick<ShapeAttributes, 'touched' | 'componentPropertyReferences'> => {
const overrides = overridesLibrary.get(node.id);
if (!overrides) {
return {};
}
const touched: SyncGroups[] = [];
overrides.forEach(override => {
if (syncAttributes[override]) {
touched.push(...syncAttributes[override]);
}
});
if (overrides) {
overrides.forEach(override => {
if (syncAttributes[override]) {
touched.push(...syncAttributes[override]);
}
});
}
return {
touched
touched,
componentPropertyReferences: node.componentPropertyReferences
};
};

View file

@ -20,6 +20,7 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
]);
return {
characters: node.characters,
content: {
type: 'root',
verticalAlign: translateVerticalAlign(node.textAlignVertical),

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary';
import { componentPropertiesLibrary } from '@plugin/ComponentPropertiesLibrary';
import {
transformAutoLayout,
transformBlend,
@ -18,6 +19,10 @@ import {
import { ComponentRoot } from '@ui/types';
const isNonVariantComponentNode = (node: ComponentNode): boolean => {
return node.parent?.type !== 'COMPONENT_SET';
};
export const transformComponentNode = async (node: ComponentNode): Promise<ComponentRoot> => {
componentsLibrary.register(node.id, {
type: 'component',
@ -40,6 +45,14 @@ export const transformComponentNode = async (node: ComponentNode): Promise<Compo
...transformAutoLayout(node)
});
if (isNonVariantComponentNode(node)) {
try {
componentPropertiesLibrary.registerAll(node.componentPropertyDefinitions);
} catch (error) {
console.error('Error registering component properties', error);
}
}
return {
figmaId: node.id,
type: 'component',

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary';
import { componentPropertiesLibrary } from '@plugin/ComponentPropertiesLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary';
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
@ -83,11 +84,14 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
});
}
const componentProperties = componentPropertiesLibrary.all();
return {
name: node.name,
children,
components: componentsLibrary.all(),
images: await downloadImages(),
styles: styleLibrary.all()
styles: styleLibrary.all(),
componentProperties
};
};

View file

@ -1,3 +1,4 @@
import { componentPropertiesLibrary } from '@plugin/ComponentPropertiesLibrary';
import {
transformAutoLayout,
transformBlend,
@ -23,12 +24,26 @@ const isSectionNode = (node: FrameNode | SectionNode | ComponentSetNode): node i
return node.type === 'SECTION';
};
const isComponentSetNode = (
node: FrameNode | SectionNode | ComponentSetNode
): node is ComponentSetNode => {
return node.type === 'COMPONENT_SET';
};
export const transformFrameNode = async (
node: FrameNode | SectionNode | ComponentSetNode
): Promise<FrameShape> => {
let frameSpecificAttributes: Partial<FrameShape> = {};
let referencePoint: Point = { x: node.absoluteTransform[0][2], y: node.absoluteTransform[1][2] };
if (isComponentSetNode(node)) {
try {
componentPropertiesLibrary.registerAll(node.componentPropertyDefinitions);
} catch (error) {
console.error('Error registering component properties', error);
}
}
if (!isSectionNode(node)) {
const { x, y, ...transformAndRotation } = transformRotationAndPosition(node);

View file

@ -37,21 +37,12 @@ export const transformInstanceNode = async (
registerExternalComponents(primaryComponent);
}
registerTextVariableOverrides(node, primaryComponent);
if (node.overrides.length > 0) {
node.overrides.forEach(override =>
overridesLibrary.register(override.id, override.overriddenFields)
);
}
if (!node.visible) {
overridesLibrary.register(node.id, ['visible']);
}
if (node.locked) {
overridesLibrary.register(node.id, ['locked']);
}
return {
type: 'instance',
name: node.name,
@ -92,57 +83,6 @@ const registerExternalComponents = (primaryComponent: ComponentNode | ComponentS
remoteComponentLibrary.register(primaryComponent.id, primaryComponent);
};
const getComponentTextPropertyOverrides = (
node: InstanceNode,
primaryComponent: ComponentNode | ComponentSetNode
): ComponentTextPropertyOverride[] => {
try {
const componentPropertyDefinitions = Object.entries(
primaryComponent.componentPropertyDefinitions
).filter(([, value]) => value.type === 'TEXT');
const instanceComponentProperties = new Map(
Object.entries(node.componentProperties).filter(([, value]) => value.type === 'TEXT')
);
return componentPropertyDefinitions
.map(([key, value]) => {
const nodeValue = instanceComponentProperties.get(key);
return {
id: key,
...value,
value: nodeValue ? nodeValue.value : value.defaultValue
} as ComponentTextPropertyOverride;
})
.filter(({ value, defaultValue }) => value !== defaultValue);
} catch (error) {
return [];
}
};
const registerTextVariableOverrides = (
node: InstanceNode,
primaryComponent: ComponentNode | ComponentSetNode
) => {
const mergedOverridden = getComponentTextPropertyOverrides(node, primaryComponent);
if (mergedOverridden.length > 0) {
const textNodes = node
.findChildren(child => child.type === 'TEXT')
.filter(textNode => {
const componentPropertyReference = textNode.componentPropertyReferences?.characters;
return (
componentPropertyReference &&
mergedOverridden.some(property => property.id === componentPropertyReference)
);
});
textNodes.forEach(textNode => {
overridesLibrary.register(textNode.id, ['text']);
});
}
};
/**
* We do not want to process component instances in the following scenarios:
*

View file

@ -11,6 +11,7 @@ import { Shadow } from '@ui/lib/types/utils/shadow';
import { Stroke } from '@ui/lib/types/utils/stroke';
import { SyncGroups } from '@ui/lib/types/utils/syncGroups';
import { Uuid } from '@ui/lib/types/utils/uuid';
import { ComponentPropertyReference } from '@ui/types';
export type ShapeBaseAttributes = {
id?: Uuid;
@ -74,6 +75,7 @@ export type ShapeAttributes = {
blur?: Blur;
growType?: GrowType;
touched?: SyncGroups[];
componentPropertyReferences?: ComponentPropertyReference; // @TODO: move to any other place
};
export type ShapeGeomAttributes = {

View file

@ -15,6 +15,7 @@ export type TextShape = ShapeBaseAttributes &
export type TextAttributes = {
type?: 'text';
content?: TextContent;
characters?: string; // @ TODO: move to any other place
};
export type TextContent = {

View file

@ -2,7 +2,7 @@ import { PenpotFile } from '@ui/lib/types/penpotFile';
import { FrameShape } from '@ui/lib/types/shapes/frameShape';
import { Uuid } from '@ui/lib/types/utils/uuid';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import { symbolFills, symbolStrokes, symbolTouched } from '@ui/parser/creators/symbols';
import { createItems } from '.';
@ -16,6 +16,12 @@ export const createArtboard = (
shape.shapeRef ??= parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.addArtboard(shape);

View file

@ -1,7 +1,12 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { BoolShape } from '@ui/lib/types/shapes/boolShape';
import { parseFigmaId } from '@ui/parser';
import { symbolBoolType, symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import {
symbolBoolType,
symbolFills,
symbolStrokes,
symbolTouched
} from '@ui/parser/creators/symbols';
import { createItems } from '.';
@ -14,6 +19,12 @@ export const createBool = (
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes);
shape.boolType = symbolBoolType(shape.boolType);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.addBool(shape);

View file

@ -1,7 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { CircleShape } from '@ui/lib/types/shapes/circleShape';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import { symbolFills, symbolStrokes, symbolTouched } from '@ui/parser/creators/symbols';
export const createCircle = (
file: PenpotFile,
@ -11,6 +11,12 @@ export const createCircle = (
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.createCircle(shape);
};

View file

@ -1,5 +1,6 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { parseFigmaId } from '@ui/parser';
import { symbolTouched } from '@ui/parser/creators/symbols';
import { uiComponents } from '@ui/parser/libraries';
import { ComponentInstance } from '@ui/types';
@ -22,6 +23,12 @@ export const createComponentInstance = (
shape.componentFile = file.getId();
shape.componentRoot = isComponentRoot;
shape.componentId = uiComponent.componentId;
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
createArtboard(file, shape);
};

View file

@ -1,6 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { GroupShape } from '@ui/lib/types/shapes/groupShape';
import { parseFigmaId } from '@ui/parser';
import { symbolTouched } from '@ui/parser/creators/symbols';
import { createItems } from '.';
@ -10,6 +11,12 @@ export const createGroup = (
) => {
shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.addGroup(shape);

View file

@ -1,7 +1,12 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolPathContent, symbolStrokes } from '@ui/parser/creators/symbols';
import {
symbolFills,
symbolPathContent,
symbolStrokes,
symbolTouched
} from '@ui/parser/creators/symbols';
export const createPath = (
file: PenpotFile,
@ -12,6 +17,12 @@ export const createPath = (
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes);
shape.content = symbolPathContent(shape.content);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.createPath(shape);
};

View file

@ -1,7 +1,7 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { RectShape } from '@ui/lib/types/shapes/rectShape';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import { symbolFills, symbolStrokes, symbolTouched } from '@ui/parser/creators/symbols';
export const createRectangle = (
file: PenpotFile,
@ -11,6 +11,12 @@ export const createRectangle = (
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.fills = symbolFills(shape.fillStyleId, shape.fills);
shape.strokes = symbolStrokes(shape.strokes);
shape.touched = symbolTouched(
!shape.hidden,
undefined,
shape.touched,
shape.componentPropertyReferences
);
file.createRect(shape);
};

View file

@ -1,16 +1,22 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { TextContent, TextShape } from '@ui/lib/types/shapes/textShape';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import { symbolFills, symbolStrokes, symbolTouched } from '@ui/parser/creators/symbols';
export const createText = (
file: PenpotFile,
{ type, figmaId, figmaRelatedId, ...shape }: TextShape
{ type, figmaId, figmaRelatedId, characters, ...shape }: TextShape
) => {
shape.id = parseFigmaId(file, figmaId);
shape.shapeRef = parseFigmaId(file, figmaRelatedId, true);
shape.content = parseContent(shape.content);
shape.strokes = symbolStrokes(shape.strokes);
shape.touched = symbolTouched(
!shape.hidden,
characters,
shape.touched,
shape.componentPropertyReferences
);
file.createText(shape);
};

View file

@ -2,3 +2,4 @@ export * from './symbolBoolType';
export * from './symbolFills';
export * from './symbolPathContent';
export * from './symbolStrokes';
export * from './symbolTouched';

View file

@ -0,0 +1,32 @@
import { componentPropertiesLibrary } from '@plugin/ComponentPropertiesLibrary';
import { SyncGroups } from '@ui/lib/types/utils/syncGroups';
import { ComponentPropertyReference } from '@ui/types';
export const symbolTouched = (
visible: boolean | undefined,
characters: string | undefined,
touched: SyncGroups[] | undefined,
componentPropertyReferences: ComponentPropertyReference | undefined
): SyncGroups[] | undefined => {
if (componentPropertyReferences) {
Object.entries(componentPropertyReferences).forEach(([key, value]) => {
switch (key) {
case 'visible':
if (visible !== componentPropertiesLibrary.get(value)?.defaultValue) {
touched?.push(':visibility-group');
}
break;
case 'characters':
if (characters !== componentPropertiesLibrary.get(value)?.defaultValue) {
touched?.push(':content-group');
}
break;
default:
break;
}
});
}
return touched;
};

View file

@ -1,4 +1,5 @@
import { componentsLibrary } from '@plugin/ComponentLibrary';
import { componentPropertiesLibrary } from '@plugin/ComponentPropertiesLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
// @TODO: Direct import on purpose, to avoid problems with the tsc linting
import { sleep } from '@plugin/utils/sleep';
@ -46,10 +47,12 @@ export const parse = async ({
children = [],
components,
images,
styles
styles,
componentProperties
}: PenpotDocument) => {
componentsLibrary.init(components);
styleLibrary.init(styles);
componentPropertiesLibrary.init(componentProperties);
await optimizeImages(images);

View file

@ -27,3 +27,9 @@ export type ComponentInstance = ShapeGeomAttributes &
showContent?: boolean;
type: 'instance';
};
export type ComponentPropertyReference =
| {
[nodeProperty in 'visible' | 'characters' | 'mainComponent']?: string;
}
| null;

View file

@ -1,3 +1,5 @@
import { ComponentProperty } from '@plugin/ComponentPropertiesLibrary';
import { PenpotPage } from '@ui/lib/types/penpotPage';
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
import { Fill } from '@ui/lib/types/utils/fill';
@ -8,4 +10,5 @@ export type PenpotDocument = {
components: Record<string, ComponentShape>;
images: Record<string, Uint8Array>;
styles: Record<string, Fill[]>;
componentProperties: Record<string, ComponentProperty>;
};