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:
parent
a58f9e913d
commit
d3c144e5e9
42 changed files with 506 additions and 155 deletions
5
.changeset/heavy-timers-sit.md
Normal file
5
.changeset/heavy-timers-sit.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for typographies
|
5
.changeset/mean-clouds-jog.md
Normal file
5
.changeset/mean-clouds-jog.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve font weight translation
|
5
.changeset/selfish-spies-cover.md
Normal file
5
.changeset/selfish-spies-cover.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix letter spacing
|
21
plugin-src/TextLibrary.ts
Normal file
21
plugin-src/TextLibrary.ts
Normal 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();
|
|
@ -1,15 +1,11 @@
|
||||||
import { translateFillStyleId, translateFills } from '@plugin/translators/fills';
|
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 { ShapeAttributes } from '@ui/lib/types/shapes/shape';
|
||||||
import { TextStyle } from '@ui/lib/types/shapes/textShape';
|
import { TextStyle } from '@ui/lib/types/shapes/textShape';
|
||||||
|
|
||||||
export const transformFills = (
|
export const transformFills = (
|
||||||
node:
|
node: (MinimalFillsMixin & DimensionAndPositionMixin) | VectorRegion | VectorNode | TextSegment
|
||||||
| (MinimalFillsMixin & DimensionAndPositionMixin)
|
|
||||||
| VectorRegion
|
|
||||||
| VectorNode
|
|
||||||
| StyleTextSegment
|
|
||||||
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> | Pick<TextStyle, 'fills' | 'fillStyleId'> => {
|
): Pick<ShapeAttributes, 'fills' | 'fillStyleId'> | Pick<TextStyle, 'fills' | 'fillStyleId'> => {
|
||||||
if (hasFillStyle(node)) {
|
if (hasFillStyle(node)) {
|
||||||
return {
|
return {
|
||||||
|
@ -39,11 +35,7 @@ export const transformVectorFills = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasFillStyle = (
|
const hasFillStyle = (
|
||||||
node:
|
node: (MinimalFillsMixin & DimensionAndPositionMixin) | VectorRegion | VectorNode | TextSegment
|
||||||
| (MinimalFillsMixin & DimensionAndPositionMixin)
|
|
||||||
| VectorRegion
|
|
||||||
| VectorNode
|
|
||||||
| StyleTextSegment
|
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return (
|
return (
|
||||||
node.fillStyleId !== figma.mixed &&
|
node.fillStyleId !== figma.mixed &&
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { transformFills } from '@plugin/transformers/partials';
|
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 { translateGrowType, translateVerticalAlign } from '@plugin/translators/text/properties';
|
||||||
|
|
||||||
import { TextAttributes, TextShape } from '@ui/lib/types/shapes/textShape';
|
import { TextAttributes, TextShape } from '@ui/lib/types/shapes/textShape';
|
||||||
|
@ -16,7 +16,8 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
|
||||||
'indentation',
|
'indentation',
|
||||||
'listOptions',
|
'listOptions',
|
||||||
'fills',
|
'fills',
|
||||||
'fillStyleId'
|
'fillStyleId',
|
||||||
|
'textStyleId'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +31,7 @@ export const transformText = (node: TextNode): TextAttributes & Pick<TextShape,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
children: translateStyleTextSegments(node, styledTextSegments),
|
children: translateTextSegments(node, styledTextSegments),
|
||||||
...transformTextStyle(node, styledTextSegments[0]),
|
...transformTextStyle(node, styledTextSegments[0]),
|
||||||
...transformFills(node)
|
...transformFills(node)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,26 @@ import { componentsLibrary } from '@plugin/ComponentLibrary';
|
||||||
import { imagesLibrary } from '@plugin/ImageLibrary';
|
import { imagesLibrary } from '@plugin/ImageLibrary';
|
||||||
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
import { remoteComponentLibrary } from '@plugin/RemoteComponentLibrary';
|
||||||
import { styleLibrary } from '@plugin/StyleLibrary';
|
import { styleLibrary } from '@plugin/StyleLibrary';
|
||||||
|
import { textLibrary } from '@plugin/TextLibrary';
|
||||||
import { translateRemoteChildren } from '@plugin/translators';
|
import { translateRemoteChildren } from '@plugin/translators';
|
||||||
import { translatePaintStyles } from '@plugin/translators/fills';
|
import { translatePaintStyle, translateTextStyle } from '@plugin/translators/styles';
|
||||||
import { sleep } from '@plugin/utils';
|
import { sleep } from '@plugin/utils';
|
||||||
|
|
||||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||||
|
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
|
||||||
import { FillStyle } from '@ui/lib/types/utils/fill';
|
import { FillStyle } from '@ui/lib/types/utils/fill';
|
||||||
import { PenpotDocument } from '@ui/types';
|
import { PenpotDocument } from '@ui/types';
|
||||||
|
|
||||||
import { transformPageNode } from '.';
|
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 downloadImages = async (): Promise<Record<string, Uint8Array>> => {
|
||||||
const imageToDownload = Object.entries(imagesLibrary.all());
|
const imageToDownload = Object.entries(imagesLibrary.all());
|
||||||
const images: Record<string, Uint8Array> = {};
|
const images: Record<string, Uint8Array> = {};
|
||||||
|
@ -71,7 +81,7 @@ const getFillStyles = async (): Promise<Record<string, FillStyle>> => {
|
||||||
for (const [styleId, paintStyle] of stylesToFetch) {
|
for (const [styleId, paintStyle] of stylesToFetch) {
|
||||||
const figmaStyle = paintStyle ?? (await figma.getStyleByIdAsync(styleId));
|
const figmaStyle = paintStyle ?? (await figma.getStyleByIdAsync(styleId));
|
||||||
if (figmaStyle && isPaintStyle(figmaStyle)) {
|
if (figmaStyle && isPaintStyle(figmaStyle)) {
|
||||||
styles[styleId] = translatePaintStyles(figmaStyle);
|
styles[styleId] = translatePaintStyle(figmaStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
figma.ui.postMessage({
|
figma.ui.postMessage({
|
||||||
|
@ -87,8 +97,41 @@ const getFillStyles = async (): Promise<Record<string, FillStyle>> => {
|
||||||
return styles;
|
return styles;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isPaintStyle = (style: BaseStyle): style is PaintStyle => {
|
const getTextStyles = async (): Promise<Record<string, TypographyStyle>> => {
|
||||||
return style.type === 'PAINT';
|
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[]> => {
|
const processPages = async (node: DocumentNode): Promise<PenpotPage[]> => {
|
||||||
|
@ -122,6 +165,11 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
|
||||||
styleLibrary.register(style.id, style);
|
styleLibrary.register(style.id, style);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const localTextStyles = await figma.getLocalTextStylesAsync();
|
||||||
|
localTextStyles.forEach(style => {
|
||||||
|
textLibrary.register(style.id, style);
|
||||||
|
});
|
||||||
|
|
||||||
const children = await processPages(node);
|
const children = await processPages(node);
|
||||||
|
|
||||||
if (remoteComponentLibrary.remaining() > 0) {
|
if (remoteComponentLibrary.remaining() > 0) {
|
||||||
|
@ -135,11 +183,14 @@ export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotD
|
||||||
|
|
||||||
const images = await downloadImages();
|
const images = await downloadImages();
|
||||||
|
|
||||||
|
const typographies = await getTextStyles();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: node.name,
|
name: node.name,
|
||||||
children,
|
children,
|
||||||
components: componentsLibrary.all(),
|
components: componentsLibrary.all(),
|
||||||
images,
|
images,
|
||||||
styles
|
styles,
|
||||||
|
typographies
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './translateFills';
|
export * from './translateFills';
|
||||||
export * from './translateImageFill';
|
export * from './translateImageFill';
|
||||||
export * from './translatePaintStyles';
|
|
||||||
export * from './translateSolidFill';
|
export * from './translateSolidFill';
|
||||||
|
|
2
plugin-src/translators/styles/index.ts
Normal file
2
plugin-src/translators/styles/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './translatePaintStyle';
|
||||||
|
export * from './translateTextStyle';
|
|
@ -2,7 +2,7 @@ import { translateFill } from '@plugin/translators/fills/translateFills';
|
||||||
|
|
||||||
import { FillStyle } from '@ui/lib/types/utils/fill';
|
import { FillStyle } from '@ui/lib/types/utils/fill';
|
||||||
|
|
||||||
export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => {
|
export const translatePaintStyle = (figmaStyle: PaintStyle): FillStyle => {
|
||||||
const fillStyle: FillStyle = {
|
const fillStyle: FillStyle = {
|
||||||
name: figmaStyle.name,
|
name: figmaStyle.name,
|
||||||
fills: [],
|
fills: [],
|
||||||
|
@ -10,7 +10,6 @@ export const translatePaintStyles = (figmaStyle: PaintStyle): FillStyle => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorName = (figmaStyle: PaintStyle, index: number): string => {
|
const colorName = (figmaStyle: PaintStyle, index: number): string => {
|
||||||
// @TODO: Think something better
|
|
||||||
return figmaStyle.paints.length > 1 ? `Color ${index + 1}` : figmaStyle.name;
|
return figmaStyle.paints.length > 1 ? `Color ${index + 1}` : figmaStyle.name;
|
||||||
};
|
};
|
||||||
|
|
32
plugin-src/translators/styles/translateTextStyle.ts
Normal file
32
plugin-src/translators/styles/translateTextStyle.ts
Normal 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,10 +1,15 @@
|
||||||
import { getCustomFontId, translateFontVariantId } from '@plugin/translators/text/font/custom';
|
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 {
|
return {
|
||||||
fontId: `custom-${getCustomFontId(fontName)}`,
|
fontId: customFontId ? `custom-${customFontId}` : '',
|
||||||
fontVariantId: translateFontVariantId(fontName, fontWeight)
|
fontVariantId: translateFontVariantId(fontName, fontWeight),
|
||||||
|
fontWeight
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
const style = fontName.style.toLowerCase().includes('italic') ? 'italic' : 'normal';
|
||||||
|
|
||||||
return `${style}-${fontWeight.toString()}`;
|
return `${style}-${fontWeight}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { GoogleFont } from './googleFont';
|
||||||
export const translateFontVariantId = (
|
export const translateFontVariantId = (
|
||||||
googleFont: GoogleFont,
|
googleFont: GoogleFont,
|
||||||
fontName: FontName,
|
fontName: FontName,
|
||||||
fontWeight: number
|
fontWeight: string
|
||||||
) => {
|
) => {
|
||||||
// check match directly by style
|
// check match directly by style
|
||||||
const variant = googleFont.variants?.find(variant => variant === fontName.style.toLowerCase());
|
const variant = googleFont.variants?.find(variant => variant === fontName.style.toLowerCase());
|
||||||
|
@ -13,7 +13,7 @@ export const translateFontVariantId = (
|
||||||
// check match by style and weight
|
// check match by style and weight
|
||||||
const italic = fontName.style.toLowerCase().includes('italic') ? 'italic' : '';
|
const italic = fontName.style.toLowerCase().includes('italic') ? 'italic' : '';
|
||||||
const variantWithWeight = googleFont.variants?.find(
|
const variantWithWeight = googleFont.variants?.find(
|
||||||
variant => variant === `${fontWeight.toString()}${italic}`
|
variant => variant === `${fontWeight}${italic}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variantWithWeight !== undefined) return variantWithWeight;
|
if (variantWithWeight !== undefined) return variantWithWeight;
|
||||||
|
|
|
@ -3,21 +3,25 @@ import slugify from 'slugify';
|
||||||
import { Cache } from '@plugin/Cache';
|
import { Cache } from '@plugin/Cache';
|
||||||
import { translateFontVariantId } from '@plugin/translators/text/font/gfonts';
|
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 { items as gfonts } from './gfonts.json';
|
||||||
import { GoogleFont } from './googleFont';
|
import { GoogleFont } from './googleFont';
|
||||||
|
|
||||||
const fontsCache = new Cache<string, GoogleFont>({ max: 30 });
|
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);
|
const googleFont = getGoogleFont(fontName);
|
||||||
|
|
||||||
if (googleFont === undefined) return;
|
if (googleFont === undefined) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fontId: `gfont-${slugify(fontName.family.toLowerCase())}`,
|
fontId: `gfont-${slugify(fontName.family.toLowerCase())}`,
|
||||||
fontVariantId: translateFontVariantId(googleFont, fontName, fontWeight)
|
fontVariantId: translateFontVariantId(googleFont, fontName, fontWeight),
|
||||||
|
fontWeight
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './translateFontId';
|
export * from './translateFontName';
|
||||||
|
|
|
@ -3,13 +3,12 @@ import { LocalFont } from './localFont';
|
||||||
export const translateFontVariantId = (
|
export const translateFontVariantId = (
|
||||||
localFont: LocalFont,
|
localFont: LocalFont,
|
||||||
fontName: FontName,
|
fontName: FontName,
|
||||||
fontWeight: number
|
fontWeight: string
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
// check match by style and weight
|
// check match by style and weight
|
||||||
const italic = fontName.style.toLowerCase().includes('italic');
|
const italic = fontName.style.toLowerCase().includes('italic');
|
||||||
const variantWithStyleWeight = localFont.variants?.find(
|
const variantWithStyleWeight = localFont.variants?.find(
|
||||||
variant =>
|
variant => variant.weight === fontWeight && variant.style === (italic ? 'italic' : 'normal')
|
||||||
variant.weight === fontWeight.toString() && variant.style === (italic ? 'italic' : 'normal')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (variantWithStyleWeight !== undefined) return variantWithStyleWeight.id;
|
if (variantWithStyleWeight !== undefined) return variantWithStyleWeight.id;
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { LocalFont, translateFontVariantId } from '@plugin/translators/text/font/local';
|
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';
|
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);
|
const localFont = getLocalFont(fontName);
|
||||||
|
|
||||||
if (localFont === undefined) return;
|
if (localFont === undefined) return;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fontId: localFont.id,
|
fontId: localFont.id,
|
||||||
fontVariantId: translateFontVariantId(localFont, fontName, fontWeight)
|
fontVariantId: translateFontVariantId(localFont, fontName, fontWeight),
|
||||||
|
fontWeight
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
);
|
|
||||||
};
|
|
19
plugin-src/translators/text/font/translateFontName.ts
Normal file
19
plugin-src/translators/text/font/translateFontName.ts
Normal 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)
|
||||||
|
);
|
||||||
|
};
|
|
@ -1 +1 @@
|
||||||
export * from './translateStyleTextSegments';
|
export * from './translateTextSegments';
|
||||||
|
|
|
@ -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';
|
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export class List {
|
||||||
protected counter: number[] = [];
|
protected counter: number[] = [];
|
||||||
private listTypeFactory = new ListTypeFactory();
|
private listTypeFactory = new ListTypeFactory();
|
||||||
|
|
||||||
public update(textNode: PenpotTextNode, segment: StyleTextSegment): void {
|
public update(textNode: PenpotTextNode, segment: TextSegment): void {
|
||||||
if (segment.indentation < this.indentation) {
|
if (segment.indentation < this.indentation) {
|
||||||
for (let i = segment.indentation + 1; i <= this.indentation; i++) {
|
for (let i = segment.indentation + 1; i <= this.indentation; i++) {
|
||||||
this.levels.delete(i);
|
this.levels.delete(i);
|
||||||
|
@ -41,7 +41,7 @@ export class List {
|
||||||
this.indentation = segment.indentation;
|
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);
|
const level = this.levels.get(segment.indentation);
|
||||||
if (level === undefined) {
|
if (level === undefined) {
|
||||||
throw new Error('Levels not updated');
|
throw new Error('Levels not updated');
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
|
import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
|
||||||
|
|
||||||
import { List } from './List';
|
import { List } from './List';
|
||||||
import { StyleTextSegment } from './translateParagraphProperties';
|
import { TextSegment } from './translateParagraphProperties';
|
||||||
|
|
||||||
export class Paragraph {
|
export class Paragraph {
|
||||||
private isParagraphStarting = false;
|
private isParagraphStarting = false;
|
||||||
|
@ -9,11 +9,7 @@ export class Paragraph {
|
||||||
private firstTextNode: PenpotTextNode | null = null;
|
private firstTextNode: PenpotTextNode | null = null;
|
||||||
private list = new List();
|
private list = new List();
|
||||||
|
|
||||||
public format(
|
public format(node: TextNode, textNode: PenpotTextNode, segment: TextSegment): PenpotTextNode[] {
|
||||||
node: TextNode,
|
|
||||||
textNode: PenpotTextNode,
|
|
||||||
segment: StyleTextSegment
|
|
||||||
): PenpotTextNode[] {
|
|
||||||
const textNodes: PenpotTextNode[] = [];
|
const textNodes: PenpotTextNode[] = [];
|
||||||
|
|
||||||
const spacing = this.applySpacing(segment, node);
|
const spacing = this.applySpacing(segment, node);
|
||||||
|
@ -32,7 +28,7 @@ export class Paragraph {
|
||||||
|
|
||||||
private applyIndentation(
|
private applyIndentation(
|
||||||
textNode: PenpotTextNode,
|
textNode: PenpotTextNode,
|
||||||
segment: StyleTextSegment,
|
segment: TextSegment,
|
||||||
node: TextNode
|
node: TextNode
|
||||||
): PenpotTextNode | undefined {
|
): PenpotTextNode | undefined {
|
||||||
if (this.isParagraphStarting || this.isFirstTextNode(textNode)) {
|
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) {
|
if (this.isParagraphStarting) {
|
||||||
const isList = segment.listOptions.type !== 'NONE';
|
const isList = segment.listOptions.type !== 'NONE';
|
||||||
|
|
||||||
|
@ -73,8 +69,8 @@ export class Paragraph {
|
||||||
fontSize: '5',
|
fontSize: '5',
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
lineHeight: 1,
|
lineHeight: '1',
|
||||||
letterSpacing: 0
|
letterSpacing: '0'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +84,8 @@ export class Paragraph {
|
||||||
fontSize: paragraphSpacing.toString(),
|
fontSize: paragraphSpacing.toString(),
|
||||||
fontStyle: 'normal',
|
fontStyle: 'normal',
|
||||||
fontWeight: '400',
|
fontWeight: '400',
|
||||||
lineHeight: 1,
|
lineHeight: '1',
|
||||||
letterSpacing: 0
|
letterSpacing: '0'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { TextNode as PenpotTextNode } from '@ui/lib/types/shapes/textShape';
|
||||||
|
|
||||||
import { Paragraph } from './Paragraph';
|
import { Paragraph } from './Paragraph';
|
||||||
|
|
||||||
export type StyleTextSegment = Pick<
|
export type TextSegment = Pick<
|
||||||
StyledTextSegment,
|
StyledTextSegment,
|
||||||
| 'characters'
|
| 'characters'
|
||||||
| 'start'
|
| 'start'
|
||||||
|
@ -18,16 +18,17 @@ export type StyleTextSegment = Pick<
|
||||||
| 'listOptions'
|
| 'listOptions'
|
||||||
| 'fills'
|
| 'fills'
|
||||||
| 'fillStyleId'
|
| 'fillStyleId'
|
||||||
|
| 'textStyleId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type PartialTranslation = {
|
type PartialTranslation = {
|
||||||
textNodes: PenpotTextNode[];
|
textNodes: PenpotTextNode[];
|
||||||
segment: StyleTextSegment;
|
segment: TextSegment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const translateParagraphProperties = (
|
export const translateParagraphProperties = (
|
||||||
node: TextNode,
|
node: TextNode,
|
||||||
partials: { textNode: PenpotTextNode; segment: StyleTextSegment }[]
|
partials: { textNode: PenpotTextNode; segment: TextSegment }[]
|
||||||
): PenpotTextNode[] => {
|
): PenpotTextNode[] => {
|
||||||
const splitSegments: PartialTranslation[] = [];
|
const splitSegments: PartialTranslation[] = [];
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './translateFontStyle';
|
export * from './translateFontStyle';
|
||||||
|
export * from './translateFontWeight';
|
||||||
export * from './translateGrowType';
|
export * from './translateGrowType';
|
||||||
export * from './translateHorizontalAlign';
|
export * from './translateHorizontalAlign';
|
||||||
export * from './translateLetterSpacing';
|
export * from './translateLetterSpacing';
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,12 +1,12 @@
|
||||||
export const translateLetterSpacing = (
|
export const translateLetterSpacing = (
|
||||||
segment: Pick<StyledTextSegment, 'letterSpacing' | 'fontSize'>
|
segment: Pick<StyledTextSegment, 'letterSpacing' | 'fontSize'>
|
||||||
): number => {
|
): string => {
|
||||||
switch (segment.letterSpacing.unit) {
|
switch (segment.letterSpacing.unit) {
|
||||||
case 'PIXELS':
|
case 'PIXELS':
|
||||||
return segment.letterSpacing.value;
|
return segment.letterSpacing.value.toString();
|
||||||
case 'PERCENT':
|
case 'PERCENT':
|
||||||
return (segment.fontSize * segment.letterSpacing.value) / 100;
|
return ((segment.fontSize * segment.letterSpacing.value) / 100).toString();
|
||||||
default:
|
default:
|
||||||
return 0;
|
return '0';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
export const translateLineHeight = (
|
export const translateLineHeight = (
|
||||||
segment: Pick<StyledTextSegment, 'lineHeight' | 'fontSize'>
|
segment: Pick<StyledTextSegment, 'lineHeight' | 'fontSize'>
|
||||||
): number | undefined => {
|
): string => {
|
||||||
switch (segment.lineHeight.unit) {
|
switch (segment.lineHeight.unit) {
|
||||||
case 'PIXELS':
|
case 'PIXELS':
|
||||||
return segment.lineHeight.value / segment.fontSize;
|
return (segment.lineHeight.value / segment.fontSize).toString();
|
||||||
case 'PERCENT':
|
case 'PERCENT':
|
||||||
return segment.lineHeight.value / 100;
|
return (segment.lineHeight.value / 100).toString();
|
||||||
|
default:
|
||||||
|
return '1.2';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
|
||||||
};
|
|
||||||
};
|
|
75
plugin-src/translators/text/translateTextSegments.ts
Normal file
75
plugin-src/translators/text/translateTextSegments.ts
Normal 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;
|
||||||
|
};
|
|
@ -45,6 +45,15 @@ const stepMessages: Record<Steps, Messages> = {
|
||||||
exporting: {
|
exporting: {
|
||||||
total: 'Generating Penpot file 🚀',
|
total: 'Generating Penpot file 🚀',
|
||||||
current: 'Please wait, this process might take a while...'
|
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 'components':
|
||||||
case 'format':
|
case 'format':
|
||||||
case 'libraries':
|
case 'libraries':
|
||||||
|
case 'typographies':
|
||||||
|
case 'typoFormat':
|
||||||
|
case 'typoLibraries':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{processedItems} of {totalItems} {stepMessages[step].total}
|
{processedItems} of {totalItems} {stepMessages[step].total}
|
||||||
|
|
|
@ -29,7 +29,10 @@ export type Steps =
|
||||||
| 'exporting'
|
| 'exporting'
|
||||||
| 'fills'
|
| 'fills'
|
||||||
| 'format'
|
| 'format'
|
||||||
| 'libraries';
|
| 'libraries'
|
||||||
|
| 'typographies'
|
||||||
|
| 'typoFormat'
|
||||||
|
| 'typoLibraries';
|
||||||
|
|
||||||
export const useFigma = (): UseFigmaHook => {
|
export const useFigma = (): UseFigmaHook => {
|
||||||
const [missingFonts, setMissingFonts] = useState<string[]>();
|
const [missingFonts, setMissingFonts] = useState<string[]>();
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { PathShape } from '@ui/lib/types/shapes/pathShape';
|
||||||
import { RectShape } from '@ui/lib/types/shapes/rectShape';
|
import { RectShape } from '@ui/lib/types/shapes/rectShape';
|
||||||
import { TextShape } from '@ui/lib/types/shapes/textShape';
|
import { TextShape } from '@ui/lib/types/shapes/textShape';
|
||||||
import { Color } from '@ui/lib/types/utils/color';
|
import { Color } from '@ui/lib/types/utils/color';
|
||||||
|
import { Typography } from '@ui/lib/types/utils/typography';
|
||||||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||||
|
|
||||||
export interface PenpotFile {
|
export interface PenpotFile {
|
||||||
|
@ -24,18 +25,11 @@ export interface PenpotFile {
|
||||||
createPath(path: PathShape): Uuid;
|
createPath(path: PathShape): Uuid;
|
||||||
createText(options: TextShape): Uuid;
|
createText(options: TextShape): Uuid;
|
||||||
addLibraryColor(color: Color): void;
|
addLibraryColor(color: Color): void;
|
||||||
updateLibraryColor(color: Color): void;
|
addLibraryTypography(typography: Typography): void;
|
||||||
deleteLibraryColor(color: Color): void;
|
|
||||||
// addLibraryTypography(typography: any): void;
|
|
||||||
// deleteLibraryTypography(typography: any): void;
|
|
||||||
startComponent(component: ComponentShape): Uuid;
|
startComponent(component: ComponentShape): Uuid;
|
||||||
finishComponent(): void;
|
finishComponent(): void;
|
||||||
// lookupShape(shapeId: string): PenpotNode;
|
|
||||||
// updateObject(id: string, object: any): void;
|
|
||||||
// deleteObject(id: string): void;
|
|
||||||
getId(): Uuid;
|
getId(): Uuid;
|
||||||
getCurrentPageId(): Uuid;
|
getCurrentPageId(): Uuid;
|
||||||
newId(): Uuid;
|
newId(): Uuid;
|
||||||
// asMap(): unknown;
|
|
||||||
export(): Promise<Blob>;
|
export(): Promise<Blob>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ShapeGeomAttributes
|
ShapeGeomAttributes
|
||||||
} from '@ui/lib/types/shapes/shape';
|
} from '@ui/lib/types/shapes/shape';
|
||||||
import { Fill } from '@ui/lib/types/utils/fill';
|
import { Fill } from '@ui/lib/types/utils/fill';
|
||||||
|
import { Typography } from '@ui/lib/types/utils/typography';
|
||||||
|
|
||||||
export type TextShape = ShapeBaseAttributes &
|
export type TextShape = ShapeBaseAttributes &
|
||||||
ShapeGeomAttributes &
|
ShapeGeomAttributes &
|
||||||
|
@ -45,25 +46,35 @@ export type TextNode = {
|
||||||
key?: string;
|
key?: string;
|
||||||
} & TextStyle;
|
} & TextStyle;
|
||||||
|
|
||||||
export type TextStyle = FontId & {
|
export type TextStyle = TextTypography & {
|
||||||
fontFamily?: string;
|
|
||||||
fontSize?: string;
|
|
||||||
fontStyle?: TextFontStyle;
|
|
||||||
fontWeight?: string;
|
|
||||||
textDecoration?: string;
|
textDecoration?: string;
|
||||||
textTransform?: string;
|
|
||||||
direction?: string;
|
direction?: string;
|
||||||
typographyRefId?: string;
|
typographyRefId?: string;
|
||||||
typographyRefFile?: string;
|
typographyRefFile?: string;
|
||||||
lineHeight?: number;
|
|
||||||
letterSpacing?: number;
|
|
||||||
textAlign?: TextHorizontalAlign;
|
textAlign?: TextHorizontalAlign;
|
||||||
textDirection?: 'ltr' | 'rtl' | 'auto';
|
textDirection?: 'ltr' | 'rtl' | 'auto';
|
||||||
fills?: Fill[];
|
fills?: Fill[];
|
||||||
fillStyleId?: string; // @TODO: move to any other place
|
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 = {
|
export type FontId = {
|
||||||
fontId?: string;
|
fontId?: string;
|
||||||
fontVariantId?: string;
|
fontVariantId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TypographyStyle = {
|
||||||
|
name: string;
|
||||||
|
textStyle: TextStyle;
|
||||||
|
typography: Typography;
|
||||||
|
};
|
||||||
|
|
8
ui-src/lib/types/utils/typography.ts
Normal file
8
ui-src/lib/types/utils/typography.ts
Normal 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;
|
||||||
|
};
|
|
@ -4,7 +4,12 @@ import { sendMessage } from '@ui/context';
|
||||||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||||
import { idLibrary } from '@ui/parser';
|
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';
|
import { uiComponents } from '@ui/parser/libraries';
|
||||||
|
|
||||||
export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
|
export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
|
||||||
|
@ -35,6 +40,7 @@ export const buildFile = async (file: PenpotFile, children: PenpotPage[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await createColorsLibrary(file);
|
await createColorsLibrary(file);
|
||||||
|
await createTextLibrary(file);
|
||||||
|
|
||||||
await createComponentsLibrary(file);
|
await createComponentsLibrary(file);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
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 { parseFigmaId } from '@ui/parser';
|
||||||
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
|
import { symbolFills, symbolStrokes } from '@ui/parser/creators/symbols';
|
||||||
|
import { uiTextLibraries } from '@ui/parser/libraries/UiTextLibraries';
|
||||||
|
|
||||||
export const createText = (
|
export const createText = (
|
||||||
file: PenpotFile,
|
file: PenpotFile,
|
||||||
|
@ -18,15 +19,31 @@ export const createText = (
|
||||||
const parseContent = (content: TextContent | undefined): TextContent | undefined => {
|
const parseContent = (content: TextContent | undefined): TextContent | undefined => {
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
|
|
||||||
content.children?.forEach(paragraphSet => {
|
content.children = content.children?.map(paragraphSet => {
|
||||||
paragraphSet.children.forEach(paragraph => {
|
paragraphSet.children = paragraphSet.children.map(paragraph => {
|
||||||
paragraph.children.forEach(textNode => {
|
paragraph.children = paragraph.children.map(textNode => {
|
||||||
textNode.fills = symbolFills(textNode.fillStyleId, textNode.fills);
|
return parseTextStyle(textNode, textNode.textStyleId) as TextNode;
|
||||||
});
|
});
|
||||||
|
return parseTextStyle(paragraph, paragraph.textStyleId) as Paragraph;
|
||||||
paragraph.fills = symbolFills(paragraph.fillStyleId, paragraph.fills);
|
|
||||||
});
|
});
|
||||||
|
return paragraphSet;
|
||||||
});
|
});
|
||||||
|
|
||||||
return content;
|
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;
|
||||||
|
};
|
||||||
|
|
42
ui-src/parser/creators/createTextLibrary.ts
Normal file
42
ui-src/parser/creators/createTextLibrary.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -12,3 +12,4 @@ export * from './createPage';
|
||||||
export * from './createPath';
|
export * from './createPath';
|
||||||
export * from './createRectangle';
|
export * from './createRectangle';
|
||||||
export * from './createText';
|
export * from './createText';
|
||||||
|
export * from './createTextLibrary';
|
||||||
|
|
19
ui-src/parser/libraries/UiTextLibraries.ts
Normal file
19
ui-src/parser/libraries/UiTextLibraries.ts
Normal 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();
|
|
@ -5,10 +5,11 @@ import { sleep } from '@plugin/utils/sleep';
|
||||||
import { sendMessage } from '@ui/context';
|
import { sendMessage } from '@ui/context';
|
||||||
import { createFile } from '@ui/lib/penpot';
|
import { createFile } from '@ui/lib/penpot';
|
||||||
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
import { PenpotFile } from '@ui/lib/types/penpotFile';
|
||||||
|
import { TypographyStyle } from '@ui/lib/types/shapes/textShape';
|
||||||
import { FillStyle } from '@ui/lib/types/utils/fill';
|
import { FillStyle } from '@ui/lib/types/utils/fill';
|
||||||
import { buildFile } from '@ui/parser/creators';
|
import { buildFile } from '@ui/parser/creators';
|
||||||
import { uiImages } from '@ui/parser/libraries';
|
import { uiColorLibraries, uiImages } from '@ui/parser/libraries';
|
||||||
import { uiColorLibraries } from '@ui/parser/libraries';
|
import { uiTextLibraries } from '@ui/parser/libraries/UiTextLibraries';
|
||||||
import { PenpotDocument } from '@ui/types';
|
import { PenpotDocument } from '@ui/types';
|
||||||
|
|
||||||
import { parseImage } from '.';
|
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 prepareColorLibraries = async (file: PenpotFile, styles: Record<string, FillStyle>) => {
|
||||||
const stylesToRegister = Object.entries(styles);
|
const stylesToRegister = Object.entries(styles);
|
||||||
|
|
||||||
|
@ -86,7 +124,8 @@ export const parse = async ({
|
||||||
children = [],
|
children = [],
|
||||||
components,
|
components,
|
||||||
images,
|
images,
|
||||||
styles
|
styles,
|
||||||
|
typographies
|
||||||
}: PenpotDocument) => {
|
}: PenpotDocument) => {
|
||||||
componentsLibrary.init(components);
|
componentsLibrary.init(components);
|
||||||
|
|
||||||
|
@ -94,6 +133,7 @@ export const parse = async ({
|
||||||
|
|
||||||
await optimizeImages(images);
|
await optimizeImages(images);
|
||||||
await prepareColorLibraries(file, styles);
|
await prepareColorLibraries(file, styles);
|
||||||
|
await prepareTypographyLibraries(file, typographies);
|
||||||
|
|
||||||
return buildFile(file, children);
|
return buildFile(file, children);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||||
import { ComponentShape } from '@ui/lib/types/shapes/componentShape';
|
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 { FillStyle } from '@ui/lib/types/utils/fill';
|
||||||
|
|
||||||
export type PenpotDocument = {
|
export type PenpotDocument = {
|
||||||
|
@ -8,4 +9,5 @@ export type PenpotDocument = {
|
||||||
components: Record<string, ComponentShape>;
|
components: Record<string, ComponentShape>;
|
||||||
images: Record<string, Uint8Array>;
|
images: Record<string, Uint8Array>;
|
||||||
styles: Record<string, FillStyle>;
|
styles: Record<string, FillStyle>;
|
||||||
|
typographies: Record<string, TypographyStyle>;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue