mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2025-03-11 15:21:26 -05:00
Google Fonts (#80)
* moved validate font logic * google fonts working * fixes * minor improvements * fix linter * fixes * Changeset * refactor * refactor * move files around --------- Co-authored-by: Jordi Sala Morales <jordism91@gmail.com>
This commit is contained in:
parent
881ccabe86
commit
8021da2623
24 changed files with 27114 additions and 1582 deletions
5
.changeset/good-teachers-clap.md
Normal file
5
.changeset/good-teachers-clap.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"penpot-exporter": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add support for Google Fonts
|
26942
plugin-src/gfonts.json
Normal file
26942
plugin-src/gfonts.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -7,5 +7,5 @@ export * from './transformFills';
|
||||||
export * from './transformProportion';
|
export * from './transformProportion';
|
||||||
export * from './transformSceneNode';
|
export * from './transformSceneNode';
|
||||||
export * from './transformStrokes';
|
export * from './transformStrokes';
|
||||||
export * from './transformTextStyle';
|
export * from './transformText';
|
||||||
export * from './transformVectorPaths';
|
export * from './transformVectorPaths';
|
||||||
|
|
89
plugin-src/transformers/partials/transformText.ts
Normal file
89
plugin-src/transformers/partials/transformText.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { transformFills } from '@plugin/transformers/partials';
|
||||||
|
import {
|
||||||
|
translateFills,
|
||||||
|
translateHorizontalAlign,
|
||||||
|
translateVerticalAlign
|
||||||
|
} from '@plugin/translators';
|
||||||
|
import {
|
||||||
|
translateFontId,
|
||||||
|
translateFontStyle,
|
||||||
|
translateFontVariantId,
|
||||||
|
translateGrowType,
|
||||||
|
translateLetterSpacing,
|
||||||
|
translateLineHeight,
|
||||||
|
translateTextDecoration,
|
||||||
|
translateTextTransform
|
||||||
|
} from '@plugin/translators/text';
|
||||||
|
|
||||||
|
import { TextStyle } from '@ui/lib/types/text/textContent';
|
||||||
|
import { TextShape } from '@ui/lib/types/text/textShape';
|
||||||
|
|
||||||
|
export const transformText = (node: TextNode): Partial<TextShape> => {
|
||||||
|
const styledTextSegments = node.getStyledTextSegments([
|
||||||
|
'fontName',
|
||||||
|
'fontSize',
|
||||||
|
'fontWeight',
|
||||||
|
'lineHeight',
|
||||||
|
'letterSpacing',
|
||||||
|
'textCase',
|
||||||
|
'textDecoration',
|
||||||
|
'fills'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: {
|
||||||
|
type: 'root',
|
||||||
|
verticalAlign: translateVerticalAlign(node.textAlignVertical),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'paragraph-set',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: styledTextSegments.map(segment => ({
|
||||||
|
fills: translateFills(segment.fills, node.width, node.height),
|
||||||
|
text: segment.characters,
|
||||||
|
...transformTextStyle(node, segment)
|
||||||
|
})),
|
||||||
|
...(styledTextSegments.length ? transformTextStyle(node, styledTextSegments[0]) : {}),
|
||||||
|
...transformFills(node)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
growType: translateGrowType(node)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformTextStyle = (
|
||||||
|
node: TextNode,
|
||||||
|
segment: Pick<
|
||||||
|
StyledTextSegment,
|
||||||
|
| 'characters'
|
||||||
|
| 'start'
|
||||||
|
| 'end'
|
||||||
|
| 'fontName'
|
||||||
|
| 'fontSize'
|
||||||
|
| 'fontWeight'
|
||||||
|
| 'lineHeight'
|
||||||
|
| 'letterSpacing'
|
||||||
|
| 'textCase'
|
||||||
|
| 'textDecoration'
|
||||||
|
| 'fills'
|
||||||
|
>
|
||||||
|
): Partial<TextStyle> => {
|
||||||
|
return {
|
||||||
|
fontFamily: segment.fontName.family,
|
||||||
|
fontId: translateFontId(segment.fontName),
|
||||||
|
fontVariantId: translateFontVariantId(segment.fontName, segment.fontWeight),
|
||||||
|
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)
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,45 +0,0 @@
|
||||||
import slugify from 'slugify';
|
|
||||||
|
|
||||||
import {
|
|
||||||
translateFontStyle,
|
|
||||||
translateFontVariantId,
|
|
||||||
translateHorizontalAlign,
|
|
||||||
translateLetterSpacing,
|
|
||||||
translateLineHeight,
|
|
||||||
translateTextDecoration,
|
|
||||||
translateTextTransform
|
|
||||||
} from '@plugin/translators';
|
|
||||||
|
|
||||||
import { TextStyle } from '@ui/lib/types/text/textContent';
|
|
||||||
|
|
||||||
export const transformTextStyle = (
|
|
||||||
node: TextNode,
|
|
||||||
segment: Pick<
|
|
||||||
StyledTextSegment,
|
|
||||||
| 'characters'
|
|
||||||
| 'start'
|
|
||||||
| 'end'
|
|
||||||
| 'fontName'
|
|
||||||
| 'fontSize'
|
|
||||||
| 'fontWeight'
|
|
||||||
| 'lineHeight'
|
|
||||||
| 'letterSpacing'
|
|
||||||
| 'textCase'
|
|
||||||
| 'textDecoration'
|
|
||||||
| 'fills'
|
|
||||||
>
|
|
||||||
): Partial<TextStyle> => {
|
|
||||||
return {
|
|
||||||
fontFamily: segment.fontName.family,
|
|
||||||
fontId: `gfont-${slugify(segment.fontName.family.toLowerCase())}`,
|
|
||||||
fontSize: segment.fontSize.toString(),
|
|
||||||
fontStyle: translateFontStyle(segment.fontName.style),
|
|
||||||
fontWeight: segment.fontWeight.toString(),
|
|
||||||
fontVariantId: translateFontVariantId(segment.fontName.style),
|
|
||||||
textAlign: translateHorizontalAlign(node.textAlignHorizontal),
|
|
||||||
textDecoration: translateTextDecoration(segment),
|
|
||||||
textTransform: translateTextTransform(segment),
|
|
||||||
letterSpacing: translateLetterSpacing(segment),
|
|
||||||
lineHeight: translateLineHeight(segment)
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -2,53 +2,19 @@ import {
|
||||||
transformBlend,
|
transformBlend,
|
||||||
transformDimensionAndPosition,
|
transformDimensionAndPosition,
|
||||||
transformEffects,
|
transformEffects,
|
||||||
transformFills,
|
|
||||||
transformProportion,
|
transformProportion,
|
||||||
transformSceneNode,
|
transformSceneNode,
|
||||||
transformStrokes,
|
transformStrokes,
|
||||||
transformTextStyle
|
transformText
|
||||||
} from '@plugin/transformers/partials';
|
} from '@plugin/transformers/partials';
|
||||||
import {
|
|
||||||
translateGrowType,
|
|
||||||
translateStyledTextSegments,
|
|
||||||
translateVerticalAlign
|
|
||||||
} from '@plugin/translators';
|
|
||||||
|
|
||||||
import { TextShape } from '@ui/lib/types/text/textShape';
|
import { TextShape } from '@ui/lib/types/text/textShape';
|
||||||
|
|
||||||
export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => {
|
export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => {
|
||||||
const styledTextSegments = node.getStyledTextSegments([
|
|
||||||
'fontName',
|
|
||||||
'fontSize',
|
|
||||||
'fontWeight',
|
|
||||||
'lineHeight',
|
|
||||||
'letterSpacing',
|
|
||||||
'textCase',
|
|
||||||
'textDecoration',
|
|
||||||
'fills'
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: node.name,
|
name: node.name,
|
||||||
content: {
|
...transformText(node),
|
||||||
type: 'root',
|
|
||||||
verticalAlign: translateVerticalAlign(node.textAlignVertical),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'paragraph-set',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'paragraph',
|
|
||||||
children: translateStyledTextSegments(node, styledTextSegments),
|
|
||||||
...(styledTextSegments.length ? transformTextStyle(node, styledTextSegments[0]) : {}),
|
|
||||||
...transformFills(node)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
growType: translateGrowType(node),
|
|
||||||
...transformDimensionAndPosition(node, baseX, baseY),
|
...transformDimensionAndPosition(node, baseX, baseY),
|
||||||
...transformEffects(node),
|
...transformEffects(node),
|
||||||
...transformSceneNode(node),
|
...transformSceneNode(node),
|
||||||
|
|
|
@ -1,15 +1,7 @@
|
||||||
export * from './translateBlendMode';
|
export * from './translateBlendMode';
|
||||||
export * from './translateShadowEffects';
|
export * from './translateShadowEffects';
|
||||||
export * from './translateFills';
|
export * from './translateFills';
|
||||||
export * from './translateFontStyle';
|
|
||||||
export * from './translateFontVariantId';
|
|
||||||
export * from './translateGrowType';
|
|
||||||
export * from './translateHorizontalAlign';
|
export * from './translateHorizontalAlign';
|
||||||
export * from './translateLetterSpacing';
|
|
||||||
export * from './translateLineHeight';
|
|
||||||
export * from './translateStrokes';
|
export * from './translateStrokes';
|
||||||
export * from './translateStyledTextSegments';
|
|
||||||
export * from './translateTextDecoration';
|
|
||||||
export * from './translateTextTransform';
|
|
||||||
export * from './translateVectorPaths';
|
export * from './translateVectorPaths';
|
||||||
export * from './translateVerticalAlign';
|
export * from './translateVerticalAlign';
|
||||||
|
|
7
plugin-src/translators/text/index.ts
Normal file
7
plugin-src/translators/text/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export * from './translateFontIds';
|
||||||
|
export * from './translateFontStyle';
|
||||||
|
export * from './translateGrowType';
|
||||||
|
export * from './translateLetterSpacing';
|
||||||
|
export * from './translateLineHeight';
|
||||||
|
export * from './translateTextDecoration';
|
||||||
|
export * from './translateTextTransform';
|
60
plugin-src/translators/text/translateFontIds.ts
Normal file
60
plugin-src/translators/text/translateFontIds.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import slugify from 'slugify';
|
||||||
|
|
||||||
|
import { items as gfonts } from '@plugin/gfonts.json';
|
||||||
|
|
||||||
|
export const translateFontId = (fontName: FontName): string => {
|
||||||
|
if (isGfont(fontName)) {
|
||||||
|
return `gfont-${slugify(fontName.family.toLowerCase())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: check if source sans pro
|
||||||
|
|
||||||
|
// always send font name if not gfont or source sans pro
|
||||||
|
figma.ui.postMessage({ type: 'FONT_NAME', data: fontName.family });
|
||||||
|
|
||||||
|
// @TODO: custom font
|
||||||
|
return slugify(fontName.family.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const translateFontVariantId = (fontName: FontName, fontWeight: number) => {
|
||||||
|
const variantId = translateGfontVariantId(fontName, fontWeight);
|
||||||
|
if (variantId !== undefined) {
|
||||||
|
return variantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @TODO: Custom font
|
||||||
|
// @TODO: Source Sans pro
|
||||||
|
return fontName.style.toLowerCase().replace(/\s/g, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const findGoogleFont = (fontName: FontName) => {
|
||||||
|
return gfonts.find(font => font.family === fontName.family);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGfont = (fontName: FontName): boolean => {
|
||||||
|
return findGoogleFont(fontName) !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateGfontVariantId = (fontName: FontName, fontWeight: number): string | undefined => {
|
||||||
|
const gfont = findGoogleFont(fontName);
|
||||||
|
|
||||||
|
if (gfont === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check match directly by style
|
||||||
|
const variant = gfont.variants.find(variant => variant === fontName.style.toLowerCase());
|
||||||
|
if (variant !== undefined) {
|
||||||
|
return variant;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check match by style and weight
|
||||||
|
const italic = fontName.style.toLowerCase().includes('italic') ? 'italic' : '';
|
||||||
|
const variantWithWeight = gfont.variants.find(
|
||||||
|
variant => variant === `${fontWeight.toString()}${italic}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variantWithWeight !== undefined) {
|
||||||
|
return variantWithWeight;
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,3 +0,0 @@
|
||||||
export const translateFontVariantId = (style: string) => {
|
|
||||||
return style.toLowerCase().replace(/\s/g, '');
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { transformTextStyle } from '@plugin/transformers/partials';
|
|
||||||
import { translateFills } from '@plugin/translators/translateFills';
|
|
||||||
|
|
||||||
import { TextNode as PenpotTextNode } from '@ui/lib/types/text/textContent';
|
|
||||||
|
|
||||||
export const translateStyledTextSegments = (
|
|
||||||
node: TextNode,
|
|
||||||
segments: Pick<
|
|
||||||
StyledTextSegment,
|
|
||||||
| 'characters'
|
|
||||||
| 'start'
|
|
||||||
| 'end'
|
|
||||||
| 'fontName'
|
|
||||||
| 'fontSize'
|
|
||||||
| 'fontWeight'
|
|
||||||
| 'lineHeight'
|
|
||||||
| 'letterSpacing'
|
|
||||||
| 'textCase'
|
|
||||||
| 'textDecoration'
|
|
||||||
| 'fills'
|
|
||||||
>[]
|
|
||||||
): PenpotTextNode[] => {
|
|
||||||
return segments.map(segment => {
|
|
||||||
figma.ui.postMessage({ type: 'FONT_NAME', data: segment.fontName.family });
|
|
||||||
|
|
||||||
return {
|
|
||||||
fills: translateFills(segment.fills, node.width, node.height),
|
|
||||||
text: segment.characters,
|
|
||||||
...transformTextStyle(node, segment)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -5,6 +5,7 @@
|
||||||
"lib": ["es6"],
|
"lib": ["es6"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"typeRoots": ["../node_modules/@figma"],
|
"typeRoots": ["../node_modules/@figma"],
|
||||||
"moduleResolution": "Node"
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
export * from './detectMimeType';
|
export * from './applyMatrixToPoint';
|
||||||
export * from './rgbToHex';
|
|
||||||
export * from './calculateAdjustment';
|
export * from './calculateAdjustment';
|
||||||
|
export * from './calculateLinearGradient';
|
||||||
|
export * from './detectMimeType';
|
||||||
|
export * from './matrixInvert';
|
||||||
|
export * from './rgbToHex';
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import slugify from 'slugify';
|
|
||||||
|
|
||||||
import { createPenpotFile } from '@ui/converters';
|
import { createPenpotFile } from '@ui/converters';
|
||||||
import { PenpotDocument } from '@ui/lib/types/penpotDocument';
|
import { PenpotDocument } from '@ui/lib/types/penpotDocument';
|
||||||
import { validateFont } from '@ui/validators';
|
|
||||||
|
|
||||||
import Logo from './logo.svg?react';
|
import Logo from './logo.svg?react';
|
||||||
|
|
||||||
|
@ -24,11 +22,7 @@ export const PenpotExporter = () => {
|
||||||
|
|
||||||
setExporting(false);
|
setExporting(false);
|
||||||
} else if (event.data.pluginMessage?.type == 'FONT_NAME') {
|
} else if (event.data.pluginMessage?.type == 'FONT_NAME') {
|
||||||
const fontName = event.data.pluginMessage.data as string;
|
addFontWarning(event.data.pluginMessage.data as string);
|
||||||
|
|
||||||
if (!validateFont(fontName)) {
|
|
||||||
addFontWarning(slugify(fontName.toLowerCase()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
1434
ui-src/gfonts.json
1434
ui-src/gfonts.json
File diff suppressed because it is too large
Load diff
|
@ -7,12 +7,10 @@
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
"skipLibCheck": false,
|
"skipLibCheck": false,
|
||||||
"esModuleInterop": false,
|
"esModuleInterop": false,
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './validateFont';
|
|
|
@ -1,10 +0,0 @@
|
||||||
import slugify from 'slugify';
|
|
||||||
|
|
||||||
import fonts from '@ui/gfonts.json';
|
|
||||||
|
|
||||||
const gfonts = new Set(fonts);
|
|
||||||
|
|
||||||
export const validateFont = (fontFamily: string): boolean => {
|
|
||||||
const name = slugify(fontFamily.toLowerCase());
|
|
||||||
return gfonts.has(name);
|
|
||||||
};
|
|
Loading…
Add table
Reference in a new issue