mirror of
https://github.com/penpot/penpot-export.git
synced 2025-02-12 18:18:01 -05:00
feat(core): describe typography requirements and Google Fonts implementation aid
This commit is contained in:
parent
562e1d77e3
commit
1fcb0e0860
8 changed files with 150 additions and 26 deletions
|
@ -0,0 +1,36 @@
|
||||||
|
import { PenpotApiFile } from '../../api'
|
||||||
|
import { FontsSummary } from '../../types'
|
||||||
|
|
||||||
|
export const summarizeTypographies = (
|
||||||
|
penpotFile: PenpotApiFile,
|
||||||
|
): FontsSummary => {
|
||||||
|
const typographies = Object.values(penpotFile.data.typographies ?? {})
|
||||||
|
|
||||||
|
const separator = '|>'
|
||||||
|
const dedupedFontsKeys = Array.from(
|
||||||
|
typographies.reduce((set, typography) => {
|
||||||
|
const { fontId, fontFamily, fontWeight } = typography
|
||||||
|
const typographyKey = [fontId, fontFamily, fontWeight].join(separator)
|
||||||
|
|
||||||
|
set.add(typographyKey)
|
||||||
|
return set
|
||||||
|
}, new Set<string>()),
|
||||||
|
)
|
||||||
|
|
||||||
|
const fontsSummary = dedupedFontsKeys.reduce<FontsSummary>(
|
||||||
|
(summary, typographyKey) => {
|
||||||
|
const [fontId, fontFamily, fontWeight] = typographyKey.split(separator)
|
||||||
|
const fontSource = fontId.startsWith('gfont-')
|
||||||
|
? 'googleFonts'
|
||||||
|
: 'userCustomFonts'
|
||||||
|
|
||||||
|
summary[fontSource][fontFamily] ??= []
|
||||||
|
summary[fontSource][fontFamily].push(fontWeight)
|
||||||
|
|
||||||
|
return summary
|
||||||
|
},
|
||||||
|
{ googleFonts: {}, userCustomFonts: {} },
|
||||||
|
)
|
||||||
|
|
||||||
|
return fontsSummary
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographiesToCssClasses'
|
import { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographiesToCssClasses'
|
||||||
|
import { summarizeTypographies } from './adapters/inbound/typographiesToFontSummary'
|
||||||
import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables'
|
import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables'
|
||||||
import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses'
|
import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses'
|
||||||
|
|
||||||
|
@ -16,30 +17,41 @@ import {
|
||||||
cssOutputter,
|
cssOutputter,
|
||||||
scssOutputter,
|
scssOutputter,
|
||||||
jsonOutputter,
|
jsonOutputter,
|
||||||
|
OutputterFunction,
|
||||||
} from './outputters'
|
} from './outputters'
|
||||||
import { CSSClassDefinition, CSSCustomPropertyDefinition } from './types'
|
import {
|
||||||
|
CSSClassDefinition,
|
||||||
|
CSSCustomPropertyDefinition,
|
||||||
|
FontsSummary,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
const processOutput = ({
|
const processOutput = ({
|
||||||
outputFormat = 'css',
|
outputFormat = 'css',
|
||||||
outputPath,
|
outputPath,
|
||||||
content,
|
content,
|
||||||
|
metadata,
|
||||||
}: {
|
}: {
|
||||||
outputFormat: AssetConfig['format']
|
outputFormat: AssetConfig['format']
|
||||||
outputPath: string
|
outputPath: string
|
||||||
content: CSSClassDefinition[] | CSSCustomPropertyDefinition[]
|
content: CSSClassDefinition[] | CSSCustomPropertyDefinition[]
|
||||||
|
metadata?: FontsSummary
|
||||||
}) => {
|
}) => {
|
||||||
if (outputFormat === 'css') {
|
const outputter: OutputterFunction | null =
|
||||||
return writeTextFile(outputPath, cssOutputter, content)
|
outputFormat === 'css'
|
||||||
}
|
? cssOutputter
|
||||||
if (outputFormat === 'scss') {
|
: outputFormat === 'scss'
|
||||||
return writeTextFile(outputPath, scssOutputter, content)
|
? scssOutputter
|
||||||
}
|
: outputFormat === 'json'
|
||||||
if (outputFormat === 'json') {
|
? jsonOutputter
|
||||||
return writeTextFile(outputPath, jsonOutputter, content)
|
: null
|
||||||
}
|
|
||||||
throw new Error(
|
if (outputter === null)
|
||||||
'Unable to process output format. This is an error in penpot-export code, please contact their authors.',
|
throw new Error(
|
||||||
)
|
'Unable to process output format. This is an error in penpot-export code, please contact their authors.',
|
||||||
|
)
|
||||||
|
|
||||||
|
const textContents = outputter(content, metadata)
|
||||||
|
return writeTextFile(outputPath, textContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type * from './types'
|
export type * from './types'
|
||||||
|
@ -78,6 +90,7 @@ export default async function penpotExport(
|
||||||
outputFormat: typographiesConfig.format,
|
outputFormat: typographiesConfig.format,
|
||||||
outputPath: path.resolve(rootProjectPath, typographiesConfig.output),
|
outputPath: path.resolve(rootProjectPath, typographiesConfig.output),
|
||||||
content: adaptTypographiesToCssClassDefinitions(penpotFile),
|
content: adaptTypographiesToCssClassDefinitions(penpotFile),
|
||||||
|
metadata: summarizeTypographies(penpotFile),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('✅ Typographies: %s', typographiesConfig.output)
|
console.log('✅ Typographies: %s', typographiesConfig.output)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import {
|
import {
|
||||||
CSSClassDefinition,
|
CSSClassDefinition,
|
||||||
CSSCustomPropertyDefinition,
|
CSSCustomPropertyDefinition,
|
||||||
|
FontsSummary,
|
||||||
isCssClassDefinition,
|
isCssClassDefinition,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
|
|
||||||
|
import { describeFontsRequirements } from '../fileHeader'
|
||||||
import { OutputterFunction } from '../types'
|
import { OutputterFunction } from '../types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -40,9 +42,15 @@ const serializeCssCustomProperty = (
|
||||||
return `${padding}${key}: ${value};`
|
return `${padding}${key}: ${value};`
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializeCss: OutputterFunction = (
|
const composeFileHeader = (fontsSummary: FontsSummary) => {
|
||||||
|
const message = describeFontsRequirements(fontsSummary)
|
||||||
|
|
||||||
|
return ['/*', ...message.map((line) => ' * ' + line), '*/'].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeFileBody = (
|
||||||
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
): string => {
|
) => {
|
||||||
if (areCssCustomPropertiesDefinitions(cssDefinitions)) {
|
if (areCssCustomPropertiesDefinitions(cssDefinitions)) {
|
||||||
const pad = 2
|
const pad = 2
|
||||||
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
|
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
|
||||||
|
@ -54,4 +62,16 @@ const serializeCss: OutputterFunction = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serializeCss: OutputterFunction = (
|
||||||
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
|
fontsSummary?: FontsSummary,
|
||||||
|
): string => {
|
||||||
|
const body: string = composeFileBody(cssDefinitions)
|
||||||
|
|
||||||
|
if (fontsSummary === undefined) return body
|
||||||
|
|
||||||
|
const header: string = composeFileHeader(fontsSummary)
|
||||||
|
return header + '\n\n' + body
|
||||||
|
}
|
||||||
|
|
||||||
export default serializeCss
|
export default serializeCss
|
||||||
|
|
26
packages/core/src/lib/outputters/fileHeader.ts
Normal file
26
packages/core/src/lib/outputters/fileHeader.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { FontFamily, FontWeights, FontsSummary } from '../types'
|
||||||
|
|
||||||
|
const createGoogleFontLink = (
|
||||||
|
fontFamily: FontFamily,
|
||||||
|
fontWeights: FontWeights,
|
||||||
|
) => {
|
||||||
|
const family = encodeURIComponent(fontFamily)
|
||||||
|
const weights = fontWeights.join(',')
|
||||||
|
|
||||||
|
return `<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=${family}:${weights}">`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const describeFontsRequirements = (
|
||||||
|
fontSummary: FontsSummary,
|
||||||
|
): string[] => {
|
||||||
|
const userFontsCount = Object.keys(fontSummary.userCustomFonts).length
|
||||||
|
const googleFontsCount = Object.keys(fontSummary.googleFonts).length
|
||||||
|
return [
|
||||||
|
'These generated typography declarations rely on the given font being already available in your front end code.',
|
||||||
|
`There are ${userFontsCount} custom fonts and ${googleFontsCount} fonts from Google Fonts.`,
|
||||||
|
`You can choose to load the latter from the Google Fonts CSS API in a HTML document using:`,
|
||||||
|
...Object.entries(fontSummary.googleFonts).map(([family, weights]) =>
|
||||||
|
createGoogleFontLink(family, weights),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,18 +1,13 @@
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { OutputterFunction } from './types'
|
|
||||||
|
|
||||||
export { default as cssOutputter } from './css'
|
export { default as cssOutputter } from './css'
|
||||||
export { default as scssOutputter } from './scss'
|
export { default as scssOutputter } from './scss'
|
||||||
export { default as jsonOutputter } from './json'
|
export { default as jsonOutputter } from './json'
|
||||||
|
|
||||||
export function writeTextFile(
|
export type { OutputterFunction } from './types'
|
||||||
outputPath: string,
|
|
||||||
outputter: OutputterFunction,
|
export function writeTextFile(outputPath: string, textContents: string) {
|
||||||
...outputterParams: Parameters<OutputterFunction>
|
|
||||||
) {
|
|
||||||
const textContents = outputter(...outputterParams)
|
|
||||||
const dirname = path.dirname(outputPath)
|
const dirname = path.dirname(outputPath)
|
||||||
|
|
||||||
if (!fs.existsSync(dirname)) {
|
if (!fs.existsSync(dirname)) {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import {
|
import {
|
||||||
CSSClassDefinition,
|
CSSClassDefinition,
|
||||||
CSSCustomPropertyDefinition,
|
CSSCustomPropertyDefinition,
|
||||||
|
FontsSummary,
|
||||||
isCssClassDefinition,
|
isCssClassDefinition,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
|
|
||||||
import { camelToKebab } from '../css/syntax'
|
import { camelToKebab } from '../css/syntax'
|
||||||
|
|
||||||
|
import { describeFontsRequirements } from '../fileHeader'
|
||||||
import { OutputterFunction } from '../types'
|
import { OutputterFunction } from '../types'
|
||||||
|
|
||||||
import { textToScssVariableName } from './syntax'
|
import { textToScssVariableName } from './syntax'
|
||||||
|
@ -39,9 +41,15 @@ const serializeScssVariable = (
|
||||||
return `${property}: ${value};`
|
return `${property}: ${value};`
|
||||||
}
|
}
|
||||||
|
|
||||||
const serializeScss: OutputterFunction = (
|
const composeFileHeader = (fontsSummary: FontsSummary) => {
|
||||||
|
const message = describeFontsRequirements(fontsSummary)
|
||||||
|
|
||||||
|
return message.map((line) => '// ' + line).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeFileBody = (
|
||||||
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
): string => {
|
) => {
|
||||||
if (areCssCustomPropertiesDefinitions(cssDefinitions)) {
|
if (areCssCustomPropertiesDefinitions(cssDefinitions)) {
|
||||||
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
|
const cssDeclarations = cssDefinitions.map((customPropertyDefinition) =>
|
||||||
serializeScssVariable(customPropertyDefinition),
|
serializeScssVariable(customPropertyDefinition),
|
||||||
|
@ -52,4 +60,16 @@ const serializeScss: OutputterFunction = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serializeScss: OutputterFunction = (
|
||||||
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
|
fontsSummary?: FontsSummary,
|
||||||
|
): string => {
|
||||||
|
const body: string = composeFileBody(cssDefinitions)
|
||||||
|
|
||||||
|
if (fontsSummary === undefined) return body
|
||||||
|
|
||||||
|
const header: string = composeFileHeader(fontsSummary)
|
||||||
|
return header + '\n\n' + body
|
||||||
|
}
|
||||||
|
|
||||||
export default serializeScss
|
export default serializeScss
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { CSSClassDefinition, CSSCustomPropertyDefinition } from '../types'
|
import {
|
||||||
|
CSSClassDefinition,
|
||||||
|
CSSCustomPropertyDefinition,
|
||||||
|
FontsSummary,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
export type OutputterFunction = (
|
export type OutputterFunction = (
|
||||||
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
cssDefinitions: CSSClassDefinition[] | CSSCustomPropertyDefinition[],
|
||||||
|
metadata?: FontsSummary,
|
||||||
) => string
|
) => string
|
||||||
|
|
|
@ -15,3 +15,12 @@ export interface CSSCustomPropertyDefinition {
|
||||||
name: string
|
name: string
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FontFamily = string
|
||||||
|
export type FontWeights = string[]
|
||||||
|
export type FontsDetails = Record<FontFamily, FontWeights>
|
||||||
|
|
||||||
|
export interface FontsSummary {
|
||||||
|
googleFonts: FontsDetails
|
||||||
|
userCustomFonts: FontsDetails
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue