0
Fork 0
mirror of https://github.com/penpot/penpot-exporter-figma-plugin.git synced 2024-12-22 05:33:02 -05:00

Text libraries (#185)

* color library

* fixes

* wip

* wip

* wip

* wip

* working

* improvements

* changeset

* changeset

* changeset & cleaning

* changeset & cleaning

* improvements

* fixes

* rebase
This commit is contained in:
Alex Sánchez 2024-06-25 16:08:59 +02:00 committed by GitHub
parent a58f9e913d
commit d3c144e5e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 506 additions and 155 deletions

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": minor
---
Add support for typographies

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": patch
---
Improve font weight translation

View file

@ -0,0 +1,5 @@
---
"penpot-exporter": patch
---
Fix letter spacing

21
plugin-src/TextLibrary.ts Normal file
View file

@ -0,0 +1,21 @@
class TextLibrary {
private styles: Map<string, TextStyle | undefined> = new Map();
public register(id: string, styles?: TextStyle | undefined) {
this.styles.set(id, styles);
}
public get(id: string): TextStyle | undefined {
return this.styles.get(id);
}
public has(id: string): boolean {
return this.styles.has(id);
}
public all(): Record<string, TextStyle | undefined> {
return Object.fromEntries(this.styles.entries());
}
}
export const textLibrary = new TextLibrary();

View file

@ -1,15 +1,11 @@
import { translateFillStyleId, translateFills } from '@plugin/translators/fills';
import { StyleTextSegment } from '@plugin/translators/text/paragraph';
import { TextSegment } from '@plugin/translators/text/paragraph';
import { ShapeAttributes } from '@ui/lib/types/shapes/shape';
import { TextStyle } from '@ui/lib/types/shapes/textShape';
export const transformFills = (
node:
| (MinimalFillsMixin & DimensionAndPositionMixin)
| VectorRegion
| VectorNode
| StyleTextSegment
node: (MinimalFillsMixin & DimensionAndPositionMixin) | VectorRegion | VectorNode | TextSegment
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> | Pick<TextStyle, 'fills' | 'fillStyleId'> => {
if (hasFillStyle(node)) {
return {
@ -39,11 +35,7 @@ export const transformVectorFills = (
};
const hasFillStyle = (
node:
| (MinimalFillsMixin & DimensionAndPositionMixin)
| VectorRegion
| VectorNode
| StyleTextSegment
node: (MinimalFillsMixin & DimensionAndPositionMixin) | VectorRegion | VectorNode | TextSegment
): boolean => {
return (
node.fillStyleId !== figma.mixed &&

View file

@ -1,5 +1,5 @@
import { transformFills } from '@plugin/transformers/partials';
import { transformTextStyle, translateStyleTextSegments } from '@plugin/translators/text';
import { transformTextStyle, translateTextSegments } from '@plugin/translators/text';
import { translateGrowType, translateVerticalAlign } from '@plugin/translators/text/properties';
import { TextAttributes, TextShape } from '@ui/lib/types/shapes/textShape';
@ -16,7 +16,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
'indentation',
'listOptions',
'fills',
'fillStyleId'
'fillStyleId',
'textStyleId'
]);
return {
@ -30,7 +31,7 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
children: [
{
type: 'paragraph',
children: translateStyleTextSegments(node, styledTextSegments),
children: translateTextSegments(node, styledTextSegments),
...transformTextStyle(node, styledTextSegments[0]),
...transformFills(node)
}

View file

@ -2,16 +2,26 @@ import { componentsLibrary } from '@plugin/ComponentLibrary';
import { imagesLibrary } from '@plugin/ImageLibrary';
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
import { styleLibrary } from '@plugin/StyleLibrary';
import { textLibrary } from '@plugin/TextLibrary';
import { translateRemoteChildren } from '@plugin/translators';
import { translatePaintStyles } from '@plugin/translators/fills';
import { translatePaintStyle, translateTextStyle } from '@plugin/translators/styles';
import { sleep } from '@plugin/utils';
import { PenpotPage } from '@ui/lib/types/penpotPage';
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
import { FillStyle } from '@ui/lib/types/utils/fill';
import { PenpotDocument } from '@ui/types';
import { transformPageNode } from '.';
const isPaintStyle = (style: BaseStyle): style is PaintStyle => {
return style.type === 'PAINT';
};
const isTextStyle = (style: BaseStyle): style is TextStyle => {
return style.type === 'TEXT';
};
const downloadImages = async (): Promise<Record<string, Uint8Array>> => {
const imageToDownload = Object.entries(imagesLibrary.all());
const images: Record<string, Uint8Array> = {};
@ -71,7 +81,7 @@ const getFillStyles = async (): Promise<Record<string, FillStyle>> => {
for (const [styleId, paintStyle] of stylesToFetch) {
const figmaStyle = paintStyle ?? (await figma.getStyleByIdAsync(styleId));
if (figmaStyle && isPaintStyle(figmaStyle)) {
styles[styleId] = translatePaintStyles(figmaStyle);
styles[styleId] = translatePaintStyle(figmaStyle);
}
figma.ui.postMessage({
@ -87,8 +97,41 @@ const getFillStyles = async (): Promise<Record<string, FillStyle>> => {
return styles;
};
const isPaintStyle = (style: BaseStyle): style is PaintStyle => {
return style.type === 'PAINT';
const getTextStyles = async (): Promise<Record<string, TypographyStyle>> => {
const stylesToFetch = Object.entries(textLibrary.all());
const styles: Record<string, TypographyStyle> = {};
if (stylesToFetch.length === 0) return styles;
let currentStyle = 1;
figma.ui.postMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: stylesToFetch.length
});
figma.ui.postMessage({
type: 'PROGRESS_STEP',
data: 'typographies'
});
for (const [styleId, style] of stylesToFetch) {
const figmaStyle = style ?? (await figma.getStyleByIdAsync(styleId));
if (figmaStyle && isTextStyle(figmaStyle)) {
styles[styleId] = translateTextStyle(figmaStyle);
}
figma.ui.postMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: currentStyle++
});
await sleep(0);
}
await sleep(20);
return styles;
};
const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
@ -122,6 +165,11 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
styleLibrary.register(style.id, style);
});
const localTextStyles = await figma.getLocalTextStylesAsync();
localTextStyles.forEach(style => {
textLibrary.register(style.id, style);
});
const children = await processPages(node);
if (remoteComponentLibrary.remaining() > 0) {
@ -135,11 +183,14 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
const images = await downloadImages();
const typographies = await getTextStyles();
return {
name: node.name,
children,
components: componentsLibrary.all(),
images,
styles
styles,
typographies
};
};

View file

@ -1,4 +1,3 @@
export * from './translateFills';
export * from './translateImageFill';
export * from './translatePaintStyles';
export * from './translateSolidFill';

View file

@ -0,0 +1,2 @@
export * from './translatePaintStyle';
export * from './translateTextStyle';

View file

@ -2,7 +2,7 @@ import { translateFill } from '@plugin/translators/fills/translateFills';
import { FillStyle } from '@ui/lib/types/utils/fill';
export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => {
export const translatePaintStyle = (figmaStyle: PaintStyle): FillStyle => {
const fillStyle: FillStyle = {
name: figmaStyle.name,
fills: [],
@ -10,7 +10,6 @@ export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => {
};
const colorName = (figmaStyle: PaintStyle, index: number): string => {
// @TODO: Think something better
return figmaStyle.paints.length > 1 ? `Color ${index + 1}` : figmaStyle.name;
};

View file

@ -0,0 +1,32 @@
import { translateFontName } from '@plugin/translators/text/font';
import {
translateFontStyle,
translateLetterSpacing,
translateLineHeight,
translateTextDecoration,
translateTextTransform
} from '@plugin/translators/text/properties';
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
export const translateTextStyle = (figmaStyle: TextStyle): TypographyStyle => {
const path = figmaStyle.remote ? 'Remote / ' : '';
return {
name: figmaStyle.name,
textStyle: {
...translateFontName(figmaStyle.fontName),
fontFamily: figmaStyle.fontName.family,
fontSize: figmaStyle.fontSize.toString(),
fontStyle: translateFontStyle(figmaStyle.fontName.style),
textDecoration: translateTextDecoration(figmaStyle),
letterSpacing: translateLetterSpacing(figmaStyle),
textTransform: translateTextTransform(figmaStyle),
lineHeight: translateLineHeight(figmaStyle)
},
typography: {
path,
name: figmaStyle.name
}
};
};

View file

@ -1,10 +1,15 @@
import { getCustomFontId, translateFontVariantId } from '@plugin/translators/text/font/custom';
import { FontId } from '@ui/lib/types/shapes/textShape';
import { TextTypography } from '@ui/lib/types/shapes/textShape';
export const translateCustomFont = (fontName: FontName, fontWeight: number): FontId | undefined => {
export const translateCustomFont = (
fontName: FontName,
fontWeight: string
): Pick<TextTypography, 'fontId' | 'fontVariantId' | 'fontWeight'> | undefined => {
const customFontId = getCustomFontId(fontName);
return {
fontId: `custom-${getCustomFontId(fontName)}`,
fontVariantId: translateFontVariantId(fontName, fontWeight)
fontId: customFontId ? `custom-${customFontId}` : '',
fontVariantId: translateFontVariantId(fontName, fontWeight),
fontWeight
};
};

View file

@ -1,5 +1,5 @@
export const translateFontVariantId = (fontName: FontName, fontWeight: number) => {
export const translateFontVariantId = (fontName: FontName, fontWeight: string) => {
const style = fontName.style.toLowerCase().includes('italic') ? 'italic' : 'normal';
return `${style}-${fontWeight.toString()}`;
return `${style}-${fontWeight}`;
};

View file

@ -3,7 +3,7 @@ import { GoogleFont } from './googleFont';
export const translateFontVariantId = (
googleFont: GoogleFont,
fontName: FontName,
fontWeight: number
fontWeight: string
) => {
// check match directly by style
const variant = googleFont.variants?.find(variant => variant === fontName.style.toLowerCase());
@ -13,7 +13,7 @@ export const translateFontVariantId = (
// check match by style and weight
const italic = fontName.style.toLowerCase().includes('italic') ? 'italic' : '';
const variantWithWeight = googleFont.variants?.find(
variant => variant === `${fontWeight.toString()}${italic}`
variant => variant === `${fontWeight}${italic}`
);
if (variantWithWeight !== undefined) return variantWithWeight;

View file

@ -3,21 +3,25 @@ import slugify from 'slugify';
import { Cache } from '@plugin/Cache';
import { translateFontVariantId } from '@plugin/translators/text/font/gfonts';
import { FontId } from '@ui/lib/types/shapes/textShape';
import { TextTypography } from '@ui/lib/types/shapes/textShape';
import { items as gfonts } from './gfonts.json';
import { GoogleFont } from './googleFont';
const fontsCache = new Cache<string, GoogleFont>({ max: 30 });
export const translateGoogleFont = (fontName: FontName, fontWeight: number): FontId | undefined => {
export const translateGoogleFont = (
fontName: FontName,
fontWeight: string
): Pick<TextTypography, 'fontId' | 'fontVariantId' | 'fontWeight'> | undefined => {
const googleFont = getGoogleFont(fontName);
if (googleFont === undefined) return;
return {
fontId: `gfont-${slugify(fontName.family.toLowerCase())}`,
fontVariantId: translateFontVariantId(googleFont, fontName, fontWeight)
fontVariantId: translateFontVariantId(googleFont, fontName, fontWeight),
fontWeight
};
};

View file

@ -1 +1 @@
export * from './translateFontId';
export * from './translateFontName';

View file

@ -3,13 +3,12 @@ import { LocalFont } from './localFont';
export const translateFontVariantId = (
localFont: LocalFont,
fontName: FontName,
fontWeight: number
fontWeight: string
): string | undefined => {
// check match by style and weight
const italic = fontName.style.toLowerCase().includes('italic');
const variantWithStyleWeight = localFont.variants?.find(
variant =>
variant.weight === fontWeight.toString() && variant.style === (italic ? 'italic' : 'normal')
variant => variant.weight === fontWeight && variant.style === (italic ? 'italic' : 'normal')
);
if (variantWithStyleWeight !== undefined) return variantWithStyleWeight.id;

View file

@ -1,17 +1,21 @@
import { LocalFont, translateFontVariantId } from '@plugin/translators/text/font/local';
import { FontId } from '@ui/lib/types/shapes/textShape';
import { TextTypography } from '@ui/lib/types/shapes/textShape';
import { items as localFonts } from './localFonts.json';
export const translateLocalFont = (fontName: FontName, fontWeight: number): FontId | undefined => {
export const translateLocalFont = (
fontName: FontName,
fontWeight: string
): Pick<TextTypography, 'fontId' | 'fontVariantId' | 'fontWeight'> | undefined => {
const localFont = getLocalFont(fontName);
if (localFont === undefined) return;
return {
fontId: localFont.id,
fontVariantId: translateFontVariantId(localFont, fontName, fontWeight)
fontVariantId: translateFontVariantId(localFont, fontName, fontWeight),
fontWeight
};
};

View file

@ -1,13 +0,0 @@
import { FontId } from '@ui/lib/types/shapes/textShape';
import { translateCustomFont } from './custom';
import { translateGoogleFont } from './gfonts';
import { translateLocalFont } from './local';
export const translateFontId = (fontName: FontName, fontWeight: number): FontId | undefined => {
return (
translateGoogleFont(fontName, fontWeight) ??
translateLocalFont(fontName, fontWeight) ??
translateCustomFont(fontName, fontWeight)
);
};

View file

@ -0,0 +1,19 @@
import { translateFontWeight } from '@plugin/translators/text/properties';
import { TextTypography } from '@ui/lib/types/shapes/textShape';
import { translateCustomFont } from './custom';
import { translateGoogleFont } from './gfonts';
import { translateLocalFont } from './local';
export const translateFontName = (
fontName: FontName
): Pick<TextTypography, 'fontId' | 'fontVariantId' | 'fontWeight'> | undefined => {
const fontWeight = translateFontWeight(fontName);
return (
translateGoogleFont(fontName, fontWeight) ??
translateLocalFont(fontName, fontWeight) ??
translateCustomFont(fontName, fontWeight)
);
};

View file

@ -1 +1 @@
export * from './translateStyleTextSegments';
export * from './translateTextSegments';

View file

@ -1,4 +1,4 @@
import { StyleTextSegment } from '@plugin/translators/text/paragraph/translateParagraphProperties';
import { TextSegment } from '@plugin/translators/text/paragraph/translateParagraphProperties';
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
@ -18,7 +18,7 @@ export class List {
protected counter: number[] = [];
private listTypeFactory = new ListTypeFactory();
public update(textNode: PenpotTextNode, segment: StyleTextSegment): void {
public update(textNode: PenpotTextNode, segment: TextSegment): void {
if (segment.indentation < this.indentation) {
for (let i = segment.indentation + 1; i <= this.indentation; i++) {
this.levels.delete(i);
@ -41,7 +41,7 @@ export class List {
this.indentation = segment.indentation;
}
public getCurrentList(textNode: PenpotTextNode, segment: StyleTextSegment): PenpotTextNode {
public getCurrentList(textNode: PenpotTextNode, segment: TextSegment): PenpotTextNode {
const level = this.levels.get(segment.indentation);
if (level === undefined) {
throw new Error('Levels not updated');

View file

@ -1,7 +1,7 @@
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
import { List } from './List';
import { StyleTextSegment } from './translateParagraphProperties';
import { TextSegment } from './translateParagraphProperties';
export class Paragraph {
private isParagraphStarting = false;
@ -9,11 +9,7 @@ export class Paragraph {
private firstTextNode: PenpotTextNode | null = null;
private list = new List();
public format(
node: TextNode,
textNode: PenpotTextNode,
segment: StyleTextSegment
): PenpotTextNode[] {
public format(node: TextNode, textNode: PenpotTextNode, segment: TextSegment): PenpotTextNode[] {
const textNodes: PenpotTextNode[] = [];
const spacing = this.applySpacing(segment, node);
@ -32,7 +28,7 @@ export class Paragraph {
private applyIndentation(
textNode: PenpotTextNode,
segment: StyleTextSegment,
segment: TextSegment,
node: TextNode
): PenpotTextNode | undefined {
if (this.isParagraphStarting || this.isFirstTextNode(textNode)) {
@ -44,7 +40,7 @@ export class Paragraph {
}
}
private applySpacing(segment: StyleTextSegment, node: TextNode): PenpotTextNode | undefined {
private applySpacing(segment: TextSegment, node: TextNode): PenpotTextNode | undefined {
if (this.isParagraphStarting) {
const isList = segment.listOptions.type !== 'NONE';
@ -73,8 +69,8 @@ export class Paragraph {
fontSize: '5',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: 1,
letterSpacing: 0
lineHeight: '1',
letterSpacing: '0'
};
}
@ -88,8 +84,8 @@ export class Paragraph {
fontSize: paragraphSpacing.toString(),
fontStyle: 'normal',
fontWeight: '400',
lineHeight: 1,
letterSpacing: 0
lineHeight: '1',
letterSpacing: '0'
};
}
}

View file

@ -2,7 +2,7 @@ import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
import { Paragraph } from './Paragraph';
export type StyleTextSegment = Pick<
export type TextSegment = Pick<
StyledTextSegment,
| 'characters'
| 'start'
@ -18,16 +18,17 @@ export type StyleTextSegment = Pick<
| 'listOptions'
| 'fills'
| 'fillStyleId'
| 'textStyleId'
>;
type PartialTranslation = {
textNodes: PenpotTextNode[];
segment: StyleTextSegment;
segment: TextSegment;
};
export const translateParagraphProperties = (
node: TextNode,
partials: { textNode: PenpotTextNode; segment: StyleTextSegment }[]
partials: { textNode: PenpotTextNode; segment: TextSegment }[]
): PenpotTextNode[] => {
const splitSegments: PartialTranslation[] = [];

View file

@ -1,4 +1,5 @@
export * from './translateFontStyle';
export * from './translateFontWeight';
export * from './translateGrowType';
export * from './translateHorizontalAlign';
export * from './translateLetterSpacing';

View file

@ -0,0 +1,39 @@
export const translateFontWeight = (fontName: FontName): string => {
switch (fontName.style) {
case 'Thin':
case 'Thin Italic':
return '100';
case 'Extra Light':
case 'ExtraLight':
case 'Extra Light Italic':
case 'ExtraLight Italic':
return '200';
case 'Light':
case 'Light Italic':
return '300';
case 'Regular':
case 'Italic':
return '400';
case 'Medium':
case 'Medium Italic':
return '500';
case 'Semi Bold':
case 'SemiBold':
case 'Semi Bold Italic':
case 'SemiBold Italic':
return '600';
case 'Bold':
case 'Bold Italic':
return '700';
case 'ExtraBold':
case 'Extra Bold':
case 'ExtraBold Italic':
case 'Extra Bold Italic':
return '800';
case 'Black':
case 'Black Italic':
return '900';
default:
return '400';
}
};

View file

@ -1,12 +1,12 @@
export const translateLetterSpacing = (
segment: Pick<StyledTextSegment, 'letterSpacing' | 'fontSize'>
): number => {
): string => {
switch (segment.letterSpacing.unit) {
case 'PIXELS':
return segment.letterSpacing.value;
return segment.letterSpacing.value.toString();
case 'PERCENT':
return (segment.fontSize * segment.letterSpacing.value) / 100;
return ((segment.fontSize * segment.letterSpacing.value) / 100).toString();
default:
return 0;
return '0';
}
};

View file

@ -1,10 +1,12 @@
export const translateLineHeight = (
segment: Pick<StyledTextSegment, 'lineHeight' | 'fontSize'>
): number | undefined => {
): string => {
switch (segment.lineHeight.unit) {
case 'PIXELS':
return segment.lineHeight.value / segment.fontSize;
return (segment.lineHeight.value / segment.fontSize).toString();
case 'PERCENT':
return segment.lineHeight.value / 100;
return (segment.lineHeight.value / 100).toString();
default:
return '1.2';
}
};

View file

@ -1,48 +0,0 @@
import { transformFills } from '@plugin/transformers/partials';
import { translateFontId } from '@plugin/translators/text/font';
import { StyleTextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph';
import {
translateFontStyle,
translateHorizontalAlign,
translateLetterSpacing,
translateLineHeight,
translateTextDecoration,
translateTextTransform
} from '@plugin/translators/text/properties';
import { TextNode as PenpotTextNode, TextStyle } from '@ui/lib/types/shapes/textShape';
export const translateStyleTextSegments = (
node: TextNode,
segments: StyleTextSegment[]
): PenpotTextNode[] => {
const partials = segments.map(segment => ({
textNode: translateStyleTextSegment(node, segment),
segment
}));
return translateParagraphProperties(node, partials);
};
export const transformTextStyle = (node: TextNode, segment: StyleTextSegment): TextStyle => {
return {
...translateFontId(segment.fontName, segment.fontWeight),
fontFamily: segment.fontName.family,
fontSize: segment.fontSize.toString(),
fontStyle: translateFontStyle(segment.fontName.style),
fontWeight: segment.fontWeight.toString(),
textAlign: translateHorizontalAlign(node.textAlignHorizontal),
textDecoration: translateTextDecoration(segment),
textTransform: translateTextTransform(segment),
letterSpacing: translateLetterSpacing(segment),
lineHeight: translateLineHeight(segment)
};
};
const translateStyleTextSegment = (node: TextNode, segment: StyleTextSegment): PenpotTextNode => {
return {
text: segment.characters,
...transformTextStyle(node, segment),
...transformFills(segment)
};
};

View file

@ -0,0 +1,75 @@
import { textLibrary } from '@plugin/TextLibrary';
import { transformFills } from '@plugin/transformers/partials';
import { translateFontName } from '@plugin/translators/text/font';
import { TextSegment, translateParagraphProperties } from '@plugin/translators/text/paragraph';
import {
translateFontStyle,
translateHorizontalAlign,
translateLetterSpacing,
translateLineHeight,
translateTextDecoration,
translateTextTransform
} from '@plugin/translators/text/properties';
import { TextNode as PenpotTextNode, TextStyle } from '@ui/lib/types/shapes/textShape';
export const translateTextSegments = (
node: TextNode,
segments: TextSegment[]
): PenpotTextNode[] => {
const partials = segments.map(segment => ({
textNode: translateStyleTextSegment(node, segment),
segment
}));
return translateParagraphProperties(node, partials);
};
export const transformTextStyle = (node: TextNode, segment: TextSegment): TextStyle => {
if (hasTextStyle(segment)) {
return {
...partialTransformTextStyle(node, segment),
textStyleId: translateTextStyleId(segment.textStyleId)
};
}
return {
...partialTransformTextStyle(node, segment),
fontFamily: segment.fontName.family,
fontSize: segment.fontSize.toString(),
fontStyle: translateFontStyle(segment.fontName.style),
textDecoration: translateTextDecoration(segment),
letterSpacing: translateLetterSpacing(segment),
lineHeight: translateLineHeight(segment),
textTransform: translateTextTransform(segment)
};
};
const partialTransformTextStyle = (node: TextNode, segment: TextSegment): TextStyle => {
return {
...translateFontName(segment.fontName),
textAlign: translateHorizontalAlign(node.textAlignHorizontal)
};
};
const translateStyleTextSegment = (node: TextNode, segment: TextSegment): PenpotTextNode => {
return {
text: segment.characters,
...transformTextStyle(node, segment),
...transformFills(segment)
};
};
const hasTextStyle = (segment: TextSegment): boolean => {
return segment.textStyleId !== undefined && segment.textStyleId.length > 0;
};
const translateTextStyleId = (textStyleId: string | undefined): string | undefined => {
if (textStyleId === undefined) return;
if (!textLibrary.has(textStyleId)) {
textLibrary.register(textStyleId);
}
return textStyleId;
};

View file

@ -45,6 +45,15 @@ const stepMessages: Record<Steps, Messages> = {
exporting: {
total: 'Generating Penpot file 🚀',
current: 'Please wait, this process might take a while...'
},
typographies: {
total: 'text styles fetched 📝'
},
typoFormat: {
total: 'formatting text styles 📝'
},
typoLibraries: {
total: 'text styles built 📝'
}
};
@ -73,6 +82,9 @@ const StepProgress = (): JSX.Element | null => {
case 'components':
case 'format':
case 'libraries':
case 'typographies':
case 'typoFormat':
case 'typoLibraries':
return (
<>
{processedItems} of {totalItems} {stepMessages[step].total}

View file

@ -29,7 +29,10 @@ export type Steps =
| 'exporting'
| 'fills'
| 'format'
| 'libraries';
| 'libraries'
| 'typographies'
| 'typoFormat'
| 'typoLibraries';
export const useFigma = (): UseFigmaHook => {
const [missingFonts, setMissingFonts] = useState<string[]>();

View file

@ -8,6 +8,7 @@ import { PathShape } from '@ui/lib/types/shapes/pathShape';
import { RectShape } from '@ui/lib/types/shapes/rectShape';
import { TextShape } from '@ui/lib/types/shapes/textShape';
import { Color } from '@ui/lib/types/utils/color';
import { Typography } from '@ui/lib/types/utils/typography';
import { Uuid } from '@ui/lib/types/utils/uuid';
export interface PenpotFile {
@ -24,18 +25,11 @@ export interface PenpotFile {
createPath(path: PathShape): Uuid;
createText(options: TextShape): Uuid;
addLibraryColor(color: Color): void;
updateLibraryColor(color: Color): void;
deleteLibraryColor(color: Color): void;
// addLibraryTypography(typography: any): void;
// deleteLibraryTypography(typography: any): void;
addLibraryTypography(typography: Typography): void;
startComponent(component: ComponentShape): Uuid;
finishComponent(): void;
// lookupShape(shapeId: string): PenpotNode;
// updateObject(id: string, object: any): void;
// deleteObject(id: string): void;
getId(): Uuid;
getCurrentPageId(): Uuid;
newId(): Uuid;
// asMap(): unknown;
export(): Promise<Blob>;
}

View file

@ -5,6 +5,7 @@ import {
ShapeGeomAttributes
} from '@ui/lib/types/shapes/shape';
import { Fill } from '@ui/lib/types/utils/fill';
import { Typography } from '@ui/lib/types/utils/typography';
export type TextShape = ShapeBaseAttributes &
ShapeGeomAttributes &
@ -45,25 +46,35 @@ export type TextNode = {
key?: string;
} & TextStyle;
export type TextStyle = FontId & {
fontFamily?: string;
fontSize?: string;
fontStyle?: TextFontStyle;
fontWeight?: string;
export type TextStyle = TextTypography & {
textDecoration?: string;
textTransform?: string;
direction?: string;
typographyRefId?: string;
typographyRefFile?: string;
lineHeight?: number;
letterSpacing?: number;
textAlign?: TextHorizontalAlign;
textDirection?: 'ltr' | 'rtl' | 'auto';
fills?: Fill[];
fillStyleId?: string; // @TODO: move to any other place
textStyleId?: string; // @TODO: move to any other place
};
export type TextTypography = FontId & {
fontFamily?: string;
fontSize?: string;
fontWeight?: string;
fontStyle?: TextFontStyle;
lineHeight?: string;
letterSpacing?: string;
textTransform?: string;
};
export type FontId = {
fontId?: string;
fontVariantId?: string;
};
export type TypographyStyle = {
name: string;
textStyle: TextStyle;
typography: Typography;
};

View file

@ -0,0 +1,8 @@
import { TextTypography } from '@ui/lib/types/shapes/textShape';
import { Uuid } from '@ui/lib/types/utils/uuid';
export type Typography = TextTypography & {
id?: Uuid;
name?: string;
path?: string;
};

View file

@ -4,7 +4,12 @@ import { sendMessage } from '@ui/context';
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { PenpotPage } from '@ui/lib/types/penpotPage';
import { idLibrary } from '@ui/parser';
import { createColorsLibrary, createComponentsLibrary, createPage } from '@ui/parser/creators';
import {
createColorsLibrary,
createComponentsLibrary,
createPage,
createTextLibrary
} from '@ui/parser/creators';
import { uiComponents } from '@ui/parser/libraries';
export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
@ -35,6 +40,7 @@ export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
}
await createColorsLibrary(file);
await createTextLibrary(file);
await createComponentsLibrary(file);

View file

@ -1,7 +1,8 @@
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { TextContent, TextShape } from '@ui/lib/types/shapes/textShape';
import { Paragraph, TextContent, TextNode, TextShape } from '@ui/lib/types/shapes/textShape';
import { parseFigmaId } from '@ui/parser';
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
import { uiTextLibraries } from '@ui/parser/libraries/UiTextLibraries';
export const createText = (
file: PenpotFile,
@ -18,15 +19,31 @@ export const createText = (
const parseContent = (content: TextContent | undefined): TextContent | undefined => {
if (!content) return;
content.children?.forEach(paragraphSet => {
paragraphSet.children.forEach(paragraph => {
paragraph.children.forEach(textNode => {
textNode.fills = symbolFills(textNode.fillStyleId, textNode.fills);
content.children = content.children?.map(paragraphSet => {
paragraphSet.children = paragraphSet.children.map(paragraph => {
paragraph.children = paragraph.children.map(textNode => {
return parseTextStyle(textNode, textNode.textStyleId) as TextNode;
});
paragraph.fills = symbolFills(paragraph.fillStyleId, paragraph.fills);
return parseTextStyle(paragraph, paragraph.textStyleId) as Paragraph;
});
return paragraphSet;
});
return content;
};
const parseTextStyle = (text: Paragraph | TextNode, textStyleId?: string): Paragraph | TextNode => {
let textStyle = text;
textStyle.fills = symbolFills(text.fillStyleId, text.fills);
const libraryStyle = textStyleId ? uiTextLibraries.get(textStyleId) : undefined;
if (libraryStyle) {
textStyle = {
...libraryStyle.textStyle,
...textStyle
};
}
return textStyle;
};

View file

@ -0,0 +1,42 @@
import { sleep } from '@plugin/utils/sleep';
import { sendMessage } from '@ui/context';
import { PenpotFile } from '@ui/lib/types/penpotFile';
import { uiTextLibraries } from '@ui/parser/libraries/UiTextLibraries';
export const createTextLibrary = async (file: PenpotFile) => {
let librariesBuilt = 1;
const libraries = uiTextLibraries.all();
sendMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: libraries.length
});
sendMessage({
type: 'PROGRESS_STEP',
data: 'typoLibraries'
});
for (const library of libraries) {
file.addLibraryTypography({
...library.typography,
fontId: library.textStyle.fontId,
fontVariantId: library.textStyle.fontVariantId,
letterSpacing: library.textStyle.letterSpacing,
fontWeight: library.textStyle.fontWeight,
fontStyle: library.textStyle.fontStyle,
fontFamily: library.textStyle.fontFamily,
fontSize: library.textStyle.fontSize,
textTransform: library.textStyle.textTransform,
lineHeight: library.textStyle.lineHeight
});
sendMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: librariesBuilt++
});
await sleep(0);
}
};

View file

@ -12,3 +12,4 @@ export * from './createPage';
export * from './createPath';
export * from './createRectangle';
export * from './createText';
export * from './createTextLibrary';

View file

@ -0,0 +1,19 @@
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
class UiTextLibraries {
private libraries: Map<string, TypographyStyle> = new Map();
public register(id: string, textStyle: TypographyStyle) {
this.libraries.set(id, textStyle);
}
public get(id: string): TypographyStyle | undefined {
return this.libraries.get(id);
}
public all(): TypographyStyle[] {
return Array.from(this.libraries.values());
}
}
export const uiTextLibraries = new UiTextLibraries();

View file

@ -5,10 +5,11 @@ import { sleep } from '@plugin/utils/sleep';
import { sendMessage } from '@ui/context';
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 { buildFile } from '@ui/parser/creators';
import { uiImages } from '@ui/parser/libraries';
import { uiColorLibraries } from '@ui/parser/libraries';
import { uiColorLibraries, uiImages } from '@ui/parser/libraries';
import { uiTextLibraries } from '@ui/parser/libraries/UiTextLibraries';
import { PenpotDocument } from '@ui/types';
import { parseImage } from '.';
@ -44,6 +45,43 @@ const optimizeImages = async (images: Record<string, Uint8Array>) => {
}
};
const prepareTypographyLibraries = async (
file: PenpotFile,
styles: Record<string, TypographyStyle>
) => {
const stylesToRegister = Object.entries(styles);
if (stylesToRegister.length === 0) return;
let stylesRegistered = 1;
sendMessage({
type: 'PROGRESS_TOTAL_ITEMS',
data: stylesToRegister.length
});
sendMessage({
type: 'PROGRESS_STEP',
data: 'typoFormat'
});
for (const [key, style] of stylesToRegister) {
const typographyId = file.newId();
style.textStyle.typographyRefId = typographyId;
style.textStyle.typographyRefFile = file.getId();
style.typography.id = typographyId;
uiTextLibraries.register(key, style);
sendMessage({
type: 'PROGRESS_PROCESSED_ITEMS',
data: stylesRegistered++
});
await sleep(0);
}
};
const prepareColorLibraries = async (file: PenpotFile, styles: Record<string, FillStyle>) => {
const stylesToRegister = Object.entries(styles);
@ -86,7 +124,8 @@ export const parse = async ({
children = [],
components,
images,
styles
styles,
typographies
}: PenpotDocument) => {
componentsLibrary.init(components);
@ -94,6 +133,7 @@ export const parse = async ({
await optimizeImages(images);
await prepareColorLibraries(file, styles);
await prepareTypographyLibraries(file, typographies);
return buildFile(file, children);
};

View file

@ -1,5 +1,6 @@
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';
export type PenpotDocument = {
@ -8,4 +9,5 @@ export type PenpotDocument = {
components: Record<string, ComponentShape>;
images: Record<string, Uint8Array>;
styles: Record<string, FillStyle>;
typographies: Record<string, TypographyStyle>;
};