0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-21 21:23:06 -05:00

Fix component overrides (#200)

* wip

* wip

* wip

* wip

* wip

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes

* fixes
This commit is contained in:
Alex Sánchez 2024-06-28 12:17:56 +02:00 committed by GitHub
parent c5dd5d011e
commit 303cc833a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 223 additions and 100 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Improvements in overrides management

View file

@ -1,7 +1,9 @@
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
import { ComponentProperty } from '@ui/types';
export const textStyles: Map<string, TextStyle | undefined> = new Map();
export const paintStyles: Map<string, PaintStyle | undefined> = new Map();
export const overrides: Map<string, NodeChangeProperty[]> = new Map();
export const images: Map<string, Image | null> = new Map();
export const components: Map<string, ComponentShape> = new Map();
export const componentProperties: Map<string, ComponentProperty> = new Map();

View file

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

View file

@ -21,6 +21,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,4 @@
import { components } from '@plugin/libraries';
import { componentProperties, components } from '@plugin/libraries';
import {
transformAutoLayout,
transformBlend,
@ -18,6 +18,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> => {
components.set(node.id, {
type: 'component',
@ -40,6 +44,18 @@ export const transformComponentNode = async (node: ComponentNode): Promise<Compo
...transformAutoLayout(node)
});
if (isNonVariantComponentNode(node)) {
try {
Object.entries(node.componentPropertyDefinitions).forEach(([key, value]) => {
if (value.type === 'TEXT' || value.type === 'BOOLEAN') {
componentProperties.set(key, value);
}
});
} catch (error) {
console.error('Error registering component properties', error);
}
}
return {
figmaId: node.id,
type: 'component',

View file

@ -1,6 +1,6 @@
import { toObject } from '@common/map';
import { components } from '@plugin/libraries';
import { componentProperties, components } from '@plugin/libraries';
import {
processImages,
processPages,
@ -27,6 +27,7 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
components: toObject(components),
images,
paintStyles,
textStyles
textStyles,
componentProperties: toObject(componentProperties)
};
};

View file

@ -1,3 +1,4 @@
import { componentProperties } from '@plugin/libraries';
import {
transformAutoLayout,
transformBlend,
@ -23,12 +24,30 @@ 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 {
Object.entries(node.componentPropertyDefinitions).forEach(([key, value]) => {
if (value.type === 'TEXT' || value.type === 'BOOLEAN') {
componentProperties.set(key, value);
}
});
} catch (error) {
console.error('Error registering component properties', error);
}
}
if (!isSectionNode(node)) {
const { x, y, ...transformAndRotation } = transformRotationAndPosition(node);

View file

@ -17,7 +17,7 @@ import {
transformStrokes
} from '@plugin/transformers/partials';
import { ComponentInstance, ComponentTextPropertyOverride } from '@ui/types';
import { ComponentInstance } from '@ui/types';
export const transformInstanceNode = async (
node: InstanceNode
@ -30,14 +30,20 @@ export const transformInstanceNode = async (
const primaryComponent = getPrimaryComponent(mainComponent);
const isOrphan = isOrphanInstance(primaryComponent);
let nodeOverrides = {};
if (!isOrphan) {
registerTextVariableOverrides(node, primaryComponent);
if (node.overrides.length > 0) {
node.overrides.forEach(override => overrides.set(override.id, override.overriddenFields));
}
if (!isOrphan && node.overrides.length > 0) {
node.overrides.forEach(override => overrides.set(override.id, override.overriddenFields));
nodeOverrides = transformOverrides(node);
}
const fetchedOverrides = overrides.get(node.id) ?? [];
if (node.visible !== mainComponent.visible) {
fetchedOverrides.push('visible');
}
if (node.locked !== mainComponent.locked) {
fetchedOverrides.push('locked');
}
overrides.set(node.id, fetchedOverrides);
return {
type: 'instance',
name: node.name,
@ -71,57 +77,6 @@ const getPrimaryComponent = (mainComponent: ComponentNode): ComponentNode | Comp
return mainComponent;
};
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 => {
overrides.set(textNode.id, ['text']);
});
}
};
const isOrphanInstance = (primaryComponent: ComponentNode | ComponentSetNode): boolean => {
return primaryComponent.parent === null || primaryComponent.remote;
};

View file

@ -7,3 +7,4 @@ export * from './translateLayout';
export * from './translateRotation';
export * from './translateShadowEffects';
export * from './translateStrokes';
export * from './translateTouched';

View file

@ -1,10 +1,10 @@
import { SyncGroups } from '@ui/lib/types/utils/syncGroups';
export type SyncAttributes = {
type SyncAttributes = {
[key in NodeChangeProperty]: SyncGroups[];
};
export const syncAttributes: SyncAttributes = {
const syncAttributes: SyncAttributes = {
name: [':name-group'],
fills: [':fill-group'],
backgrounds: [':fill-group'],
@ -118,8 +118,8 @@ export const syncAttributes: SyncAttributes = {
minWidth: [],
minHeight: [],
maxWidth: [],
maxLines: [],
maxHeight: [],
maxLines: [],
counterAxisSpacing: [],
counterAxisAlignContent: [],
openTypeFeatures: [],
@ -148,3 +148,21 @@ export const syncAttributes: SyncAttributes = {
authorName: [],
code: []
};
export const translateTouched = (
changedProperties: NodeChangeProperty[] | undefined
): SyncGroups[] => {
const syncGroups: Set<SyncGroups> = new Set();
if (!changedProperties) return [];
changedProperties.forEach(changedProperty => {
const syncGroup = syncAttributes[changedProperty];
if (syncGroup && syncGroup.length > 0) {
syncGroup.forEach(group => syncGroups.add(group));
}
});
return Array.from(syncGroups);
};

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

@ -16,6 +16,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

@ -9,14 +9,7 @@ let remoteFileId: Uuid | undefined = undefined;
export const createComponentInstance = (
file: PenpotFile,
{
type,
mainComponentFigmaId,
figmaId,
figmaRelatedId,
isComponentRoot,
...shape
}: ComponentInstance
{ type, mainComponentFigmaId, isComponentRoot, ...shape }: ComponentInstance
) => {
const uiComponent =
components.get(mainComponentFigmaId) ?? createUiComponent(file, mainComponentFigmaId);
@ -25,7 +18,9 @@ export const createComponentInstance = (
return;
}
shape.shapeRef = uiComponent.mainInstanceId;
if (!shape.figmaRelatedId) {
shape.shapeRef = uiComponent.mainInstanceId;
}
shape.componentFile = shape.isOrphan ? getRemoteFileId(file) : file.getId();
shape.componentRoot = isComponentRoot;
shape.componentId = uiComponent.componentId;

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 { Paragraph, TextContent, TextNode, TextShape } from '@ui/lib/types/shapes/textShape';
import { parseFigmaId, typographies } 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,35 @@
import { SyncGroups } from '@ui/lib/types/utils/syncGroups';
import { componentProperties } from '@ui/parser';
import { ComponentPropertyReference } from '@ui/types';
export const symbolTouched = (
visible: boolean | undefined,
characters: string | undefined,
touched: SyncGroups[] | undefined,
componentPropertyReferences: ComponentPropertyReference | undefined
): SyncGroups[] | undefined => {
if (!componentPropertyReferences) {
return touched;
}
const propertyReferenceVisible = componentPropertyReferences.visible;
const propertyReferenceCharacters = componentPropertyReferences.characters;
if (
propertyReferenceVisible &&
visible !== componentProperties.get(propertyReferenceVisible)?.defaultValue &&
!touched?.includes(':visibility-group')
) {
touched?.push(':visibility-group');
}
if (
propertyReferenceCharacters &&
characters !== componentProperties.get(propertyReferenceCharacters)?.defaultValue &&
!touched?.includes(':content-group')
) {
touched?.push(':content-group');
}
return touched;
};

View file

@ -3,7 +3,7 @@ import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { ImageColor } from '@ui/lib/types/utils/imageColor';
import { Uuid } from '@ui/lib/types/utils/uuid';
import { UiComponent } from '@ui/types';
import { ComponentProperty, UiComponent } from '@ui/types';
export const typographies: Map<string, TypographyStyle> = new Map();
export const images: Map<string, ImageColor> = new Map();
@ -11,3 +11,4 @@ export const identifiers: Map<string, Uuid> = new Map();
export const components: Map<string, UiComponent> = new Map();
export const componentShapes: Map<string, ComponentShape> = new Map();
export const colors: Map<string, FillStyle> = new Map();
export const componentProperties: Map<string, ComponentProperty> = new Map();

View file

@ -6,7 +6,13 @@ import { createFile } from '@ui/lib/penpot';
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { colors, componentShapes, images, typographies } from '@ui/parser';
import {
colors,
componentShapes,
images,
typographies,
componentProperties as uiComponentProperties
} from '@ui/parser';
import { buildFile } from '@ui/parser/creators';
import { PenpotDocument } from '@ui/types';
@ -123,9 +129,11 @@ export const parse = async ({
components,
images,
paintStyles,
textStyles
textStyles,
componentProperties
}: PenpotDocument) => {
init(componentShapes, components);
init(uiComponentProperties, componentProperties);
const file = createFile(name);

View file

@ -36,3 +36,20 @@ export type UiComponent = {
mainInstanceId: Uuid;
componentFigmaId: string;
};
export type ComponentProperty = {
type: 'BOOLEAN' | 'TEXT' | 'INSTANCE_SWAP' | 'VARIANT';
defaultValue: string | boolean;
preferredValues?: {
type: 'COMPONENT' | 'COMPONENT_SET';
key: string;
}[];
variantOptions?: string[];
};
// This type comes directly from Figma. We have it here because we need to reference it from the UI
export type ComponentPropertyReference =
| {
[nodeProperty in 'visible' | 'characters' | 'mainComponent']?: string;
}
| null;

View file

@ -2,6 +2,7 @@ import { PenpotPage } from '@ui/lib/types/penpotPage';
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { ComponentProperty } from '@ui/types/component';
export type PenpotDocument = {
name: string;
@ -10,4 +11,5 @@ export type PenpotDocument = {
images: Record<string, Uint8Array>;
paintStyles: Record<string, FillStyle>;
textStyles: Record<string, TypographyStyle>;
componentProperties: Record<string, ComponentProperty>;
};