diff --git a/.changeset/heavy-timers-sit.md b/.changeset/heavy-timers-sit.md new file mode 100644 index 0000000..3387d39 --- /dev/null +++ b/.changeset/heavy-timers-sit.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": minor +--- + +Add support for typographies diff --git a/.changeset/mean-clouds-jog.md b/.changeset/mean-clouds-jog.md new file mode 100644 index 0000000..e409946 --- /dev/null +++ b/.changeset/mean-clouds-jog.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": patch +--- + +Improve font weight translation diff --git a/.changeset/selfish-spies-cover.md b/.changeset/selfish-spies-cover.md new file mode 100644 index 0000000..6dcad8f --- /dev/null +++ b/.changeset/selfish-spies-cover.md @@ -0,0 +1,5 @@ +--- +"penpot-exporter": patch +--- + +Fix letter spacing diff --git a/plugin-src/TextLibrary.ts b/plugin-src/TextLibrary.ts new file mode 100644 index 0000000..647c611 --- /dev/null +++ b/plugin-src/TextLibrary.ts @@ -0,0 +1,21 @@ +class TextLibrary { + private styles: Map = 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 { + return Object.fromEntries(this.styles.entries()); + } +} + +export const textLibrary = new TextLibrary(); diff --git a/plugin-src/transformers/partials/transformFills.ts b/plugin-src/transformers/partials/transformFills.ts index 66af40a..a8ef081 100644 --- a/plugin-src/transformers/partials/transformFills.ts +++ b/plugin-src/transformers/partials/transformFills.ts @@ -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 | Pick => { 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 && diff --git a/plugin-src/transformers/partials/transformText.ts b/plugin-src/transformers/partials/transformText.ts index 207945b..b2ec2ae 100644 --- a/plugin-src/transformers/partials/transformText.ts +++ b/plugin-src/transformers/partials/transformText.ts @@ -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 { + return style.type === 'PAINT'; +}; + +const isTextStyle = (style: BaseStyle): style is TextStyle => { + return style.type === 'TEXT'; +}; + const downloadImages = async (): Promise> => { const imageToDownload = Object.entries(imagesLibrary.all()); const images: Record = {}; @@ -71,7 +81,7 @@ const getFillStyles = async (): Promise> => { 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> => { return styles; }; -const isPaintStyle = (style: BaseStyle): style is PaintStyle => { - return style.type === 'PAINT'; +const getTextStyles = async (): Promise> => { + const stylesToFetch = Object.entries(textLibrary.all()); + const styles: Record = {}; + + 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 => { @@ -122,6 +165,11 @@ export const transformDocumentNode = async (node: DocumentNode): Promise { + 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 { +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; }; diff --git a/plugin-src/translators/styles/translateTextStyle.ts b/plugin-src/translators/styles/translateTextStyle.ts new file mode 100644 index 0000000..fa17754 --- /dev/null +++ b/plugin-src/translators/styles/translateTextStyle.ts @@ -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 + } + }; +}; diff --git a/plugin-src/translators/text/font/custom/translateCustomFont.ts b/plugin-src/translators/text/font/custom/translateCustomFont.ts index d000339..8c3cf89 100644 --- a/plugin-src/translators/text/font/custom/translateCustomFont.ts +++ b/plugin-src/translators/text/font/custom/translateCustomFont.ts @@ -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 | undefined => { + const customFontId = getCustomFontId(fontName); return { - fontId: `custom-${getCustomFontId(fontName)}`, - fontVariantId: translateFontVariantId(fontName, fontWeight) + fontId: customFontId ? `custom-${customFontId}` : '', + fontVariantId: translateFontVariantId(fontName, fontWeight), + fontWeight }; }; diff --git a/plugin-src/translators/text/font/custom/translateFontVariantId.ts b/plugin-src/translators/text/font/custom/translateFontVariantId.ts index 112e5db..d14967d 100644 --- a/plugin-src/translators/text/font/custom/translateFontVariantId.ts +++ b/plugin-src/translators/text/font/custom/translateFontVariantId.ts @@ -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}`; }; diff --git a/plugin-src/translators/text/font/gfonts/translateFontVariantId.ts b/plugin-src/translators/text/font/gfonts/translateFontVariantId.ts index fe1fd3e..1feac4b 100644 --- a/plugin-src/translators/text/font/gfonts/translateFontVariantId.ts +++ b/plugin-src/translators/text/font/gfonts/translateFontVariantId.ts @@ -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; diff --git a/plugin-src/translators/text/font/gfonts/translateGoogleFont.ts b/plugin-src/translators/text/font/gfonts/translateGoogleFont.ts index 641ce8b..c75a4d5 100644 --- a/plugin-src/translators/text/font/gfonts/translateGoogleFont.ts +++ b/plugin-src/translators/text/font/gfonts/translateGoogleFont.ts @@ -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({ max: 30 }); -export const translateGoogleFont = (fontName: FontName, fontWeight: number): FontId | undefined => { +export const translateGoogleFont = ( + fontName: FontName, + fontWeight: string +): Pick | 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 }; }; diff --git a/plugin-src/translators/text/font/index.ts b/plugin-src/translators/text/font/index.ts index 404d1f7..8c195a4 100644 --- a/plugin-src/translators/text/font/index.ts +++ b/plugin-src/translators/text/font/index.ts @@ -1 +1 @@ -export * from './translateFontId'; +export * from './translateFontName'; diff --git a/plugin-src/translators/text/font/local/translateFontVariantId.ts b/plugin-src/translators/text/font/local/translateFontVariantId.ts index b8c220b..9e7a101 100644 --- a/plugin-src/translators/text/font/local/translateFontVariantId.ts +++ b/plugin-src/translators/text/font/local/translateFontVariantId.ts @@ -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; diff --git a/plugin-src/translators/text/font/local/translateLocalFont.ts b/plugin-src/translators/text/font/local/translateLocalFont.ts index b77a0c2..aa4ea52 100644 --- a/plugin-src/translators/text/font/local/translateLocalFont.ts +++ b/plugin-src/translators/text/font/local/translateLocalFont.ts @@ -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 | undefined => { const localFont = getLocalFont(fontName); if (localFont === undefined) return; return { fontId: localFont.id, - fontVariantId: translateFontVariantId(localFont, fontName, fontWeight) + fontVariantId: translateFontVariantId(localFont, fontName, fontWeight), + fontWeight }; }; diff --git a/plugin-src/translators/text/font/translateFontId.ts b/plugin-src/translators/text/font/translateFontId.ts deleted file mode 100644 index 68d1001..0000000 --- a/plugin-src/translators/text/font/translateFontId.ts +++ /dev/null @@ -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) - ); -}; diff --git a/plugin-src/translators/text/font/translateFontName.ts b/plugin-src/translators/text/font/translateFontName.ts new file mode 100644 index 0000000..654e774 --- /dev/null +++ b/plugin-src/translators/text/font/translateFontName.ts @@ -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 | undefined => { + const fontWeight = translateFontWeight(fontName); + + return ( + translateGoogleFont(fontName, fontWeight) ?? + translateLocalFont(fontName, fontWeight) ?? + translateCustomFont(fontName, fontWeight) + ); +}; diff --git a/plugin-src/translators/text/index.ts b/plugin-src/translators/text/index.ts index c599f36..25f95d1 100644 --- a/plugin-src/translators/text/index.ts +++ b/plugin-src/translators/text/index.ts @@ -1 +1 @@ -export * from './translateStyleTextSegments'; +export * from './translateTextSegments'; diff --git a/plugin-src/translators/text/paragraph/List.ts b/plugin-src/translators/text/paragraph/List.ts index f220bc0..e6044cc 100644 --- a/plugin-src/translators/text/paragraph/List.ts +++ b/plugin-src/translators/text/paragraph/List.ts @@ -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'); diff --git a/plugin-src/translators/text/paragraph/Paragraph.ts b/plugin-src/translators/text/paragraph/Paragraph.ts index fc7b71c..757a26b 100644 --- a/plugin-src/translators/text/paragraph/Paragraph.ts +++ b/plugin-src/translators/text/paragraph/Paragraph.ts @@ -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' }; } } diff --git a/plugin-src/translators/text/paragraph/translateParagraphProperties.ts b/plugin-src/translators/text/paragraph/translateParagraphProperties.ts index 35153a7..53589da 100644 --- a/plugin-src/translators/text/paragraph/translateParagraphProperties.ts +++ b/plugin-src/translators/text/paragraph/translateParagraphProperties.ts @@ -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[] = []; diff --git a/plugin-src/translators/text/properties/index.ts b/plugin-src/translators/text/properties/index.ts index d607616..d117bd8 100644 --- a/plugin-src/translators/text/properties/index.ts +++ b/plugin-src/translators/text/properties/index.ts @@ -1,4 +1,5 @@ export * from './translateFontStyle'; +export * from './translateFontWeight'; export * from './translateGrowType'; export * from './translateHorizontalAlign'; export * from './translateLetterSpacing'; diff --git a/plugin-src/translators/text/properties/translateFontWeight.ts b/plugin-src/translators/text/properties/translateFontWeight.ts new file mode 100644 index 0000000..f01d5a8 --- /dev/null +++ b/plugin-src/translators/text/properties/translateFontWeight.ts @@ -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'; + } +}; diff --git a/plugin-src/translators/text/properties/translateLetterSpacing.ts b/plugin-src/translators/text/properties/translateLetterSpacing.ts index a2dbc71..9c27159 100644 --- a/plugin-src/translators/text/properties/translateLetterSpacing.ts +++ b/plugin-src/translators/text/properties/translateLetterSpacing.ts @@ -1,12 +1,12 @@ export const translateLetterSpacing = ( segment: Pick -): 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'; } }; diff --git a/plugin-src/translators/text/properties/translateLineHeight.ts b/plugin-src/translators/text/properties/translateLineHeight.ts index ee8bc6c..0371846 100644 --- a/plugin-src/translators/text/properties/translateLineHeight.ts +++ b/plugin-src/translators/text/properties/translateLineHeight.ts @@ -1,10 +1,12 @@ export const translateLineHeight = ( segment: Pick -): 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'; } }; diff --git a/plugin-src/translators/text/translateStyleTextSegments.ts b/plugin-src/translators/text/translateStyleTextSegments.ts deleted file mode 100644 index 2112764..0000000 --- a/plugin-src/translators/text/translateStyleTextSegments.ts +++ /dev/null @@ -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) - }; -}; diff --git a/plugin-src/translators/text/translateTextSegments.ts b/plugin-src/translators/text/translateTextSegments.ts new file mode 100644 index 0000000..fee84cc --- /dev/null +++ b/plugin-src/translators/text/translateTextSegments.ts @@ -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; +}; diff --git a/ui-src/components/ExporterProgress.tsx b/ui-src/components/ExporterProgress.tsx index bc9e64a..dcae13d 100644 --- a/ui-src/components/ExporterProgress.tsx +++ b/ui-src/components/ExporterProgress.tsx @@ -45,6 +45,15 @@ const stepMessages: Record = { 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} diff --git a/ui-src/context/useFigma.ts b/ui-src/context/useFigma.ts index 462f545..393d611 100644 --- a/ui-src/context/useFigma.ts +++ b/ui-src/context/useFigma.ts @@ -29,7 +29,10 @@ export type Steps = | 'exporting' | 'fills' | 'format' - | 'libraries'; + | 'libraries' + | 'typographies' + | 'typoFormat' + | 'typoLibraries'; export const useFigma = (): UseFigmaHook => { const [missingFonts, setMissingFonts] = useState(); diff --git a/ui-src/lib/types/penpotFile.ts b/ui-src/lib/types/penpotFile.ts index 20952b4..4a03480 100644 --- a/ui-src/lib/types/penpotFile.ts +++ b/ui-src/lib/types/penpotFile.ts @@ -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; } diff --git a/ui-src/lib/types/shapes/textShape.ts b/ui-src/lib/types/shapes/textShape.ts index c6f02b2..117aef6 100644 --- a/ui-src/lib/types/shapes/textShape.ts +++ b/ui-src/lib/types/shapes/textShape.ts @@ -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; +}; diff --git a/ui-src/lib/types/utils/typography.ts b/ui-src/lib/types/utils/typography.ts new file mode 100644 index 0000000..42ef91e --- /dev/null +++ b/ui-src/lib/types/utils/typography.ts @@ -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; +}; diff --git a/ui-src/parser/creators/buildFile.ts b/ui-src/parser/creators/buildFile.ts index 73d69e0..a259f42 100644 --- a/ui-src/parser/creators/buildFile.ts +++ b/ui-src/parser/creators/buildFile.ts @@ -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); diff --git a/ui-src/parser/creators/createText.ts b/ui-src/parser/creators/createText.ts index cba4501..e71117d 100644 --- a/ui-src/parser/creators/createText.ts +++ b/ui-src/parser/creators/createText.ts @@ -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; +}; diff --git a/ui-src/parser/creators/createTextLibrary.ts b/ui-src/parser/creators/createTextLibrary.ts new file mode 100644 index 0000000..b9fa30f --- /dev/null +++ b/ui-src/parser/creators/createTextLibrary.ts @@ -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); + } +}; diff --git a/ui-src/parser/creators/index.ts b/ui-src/parser/creators/index.ts index ece31ef..56df467 100644 --- a/ui-src/parser/creators/index.ts +++ b/ui-src/parser/creators/index.ts @@ -12,3 +12,4 @@ export * from './createPage'; export * from './createPath'; export * from './createRectangle'; export * from './createText'; +export * from './createTextLibrary'; diff --git a/ui-src/parser/libraries/UiTextLibraries.ts b/ui-src/parser/libraries/UiTextLibraries.ts new file mode 100644 index 0000000..208ac57 --- /dev/null +++ b/ui-src/parser/libraries/UiTextLibraries.ts @@ -0,0 +1,19 @@ +import { TypographyStyle } from '@ui/lib/types/shapes/textShape'; + +class UiTextLibraries { + private libraries: Map = 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(); diff --git a/ui-src/parser/parse.ts b/ui-src/parser/parse.ts index 7e4a2fb..9ccf507 100644 --- a/ui-src/parser/parse.ts +++ b/ui-src/parser/parse.ts @@ -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) => { } }; +const prepareTypographyLibraries = async ( + file: PenpotFile, + styles: Record +) => { + 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) => { 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); }; diff --git a/ui-src/types/penpotDocument.ts b/ui-src/types/penpotDocument.ts index 6a5e9c1..0bdc317 100644 --- a/ui-src/types/penpotDocument.ts +++ b/ui-src/types/penpotDocument.ts @@ -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; images: Record; styles: Record; + typographies: Record; };