mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-04-18 01:34:17 -05:00
wip
This commit is contained in:
parent
06d56ad3c4
commit
b4e81a65c1
22 changed files with 198 additions and 79 deletions
35
plugin-src/ComponentPropertiesLibrary.ts
Normal file
35
plugin-src/ComponentPropertiesLibrary.ts
Normal 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();
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
|
|||
]);
|
||||
|
||||
return {
|
||||
characters: node.characters,
|
||||
content: {
|
||||
type: 'root',
|
||||
verticalAlign: translateVerticalAlign(node.textAlignVertical),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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:
|
||||
*
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -2,3 +2,4 @@ export * from './symbolBoolType';
|
|||
export * from './symbolFills';
|
||||
export * from './symbolPathContent';
|
||||
export * from './symbolStrokes';
|
||||
export * from './symbolTouched';
|
||||
|
|
32
ui-src/parser/creators/symbols/symbolTouched.ts
Normal file
32
ui-src/parser/creators/symbols/symbolTouched.ts
Normal 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;
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -27,3 +27,9 @@ export type ComponentInstance = ShapeGeomAttributes &
|
|||
showContent?: boolean;
|
||||
type: 'instance';
|
||||
};
|
||||
|
||||
export type ComponentPropertyReference =
|
||||
| {
|
||||
[nodeProperty in 'visible' | 'characters' | 'mainComponent']?: string;
|
||||
}
|
||||
| null;
|
||||
|
|
|
@ -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>;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue