0
Fork 0
mirror of https://github.com/penpot/penpot-export.git synced 2025-01-04 13:50:05 -05:00

refactor(cli): extract adapters and a CSS outputter

This commit is contained in:
Roberto Redradix 2023-08-31 22:28:37 +02:00 committed by Roberto RedRadix
parent 66f0b48fa4
commit 7f43fc1d98
8 changed files with 160 additions and 130 deletions

View file

@ -0,0 +1,23 @@
import { textToCssCustomProperyName } from '../../css/helpers'
import { CSSClassDefinition, PenpotExportFile } from '../../types'
export const adaptColorsToCssVariables = (
penpotFile: PenpotExportFile,
): CSSClassDefinition => {
const { colors } = penpotFile
const cssPropsEntries = Object.values(colors).map((color) => {
const objectClassName = textToCssCustomProperyName(color.name)
return [
objectClassName,
color.color, // FIXME Add opacity with rgba()
]
})
return {
selector: ':root',
cssProps: Object.fromEntries(cssPropsEntries),
}
}

View file

@ -0,0 +1,61 @@
import {
getObjectShapesFromPage,
isComponent,
pickObjectProps,
} from '../../api/helpers'
import { textToCssClassSelector } from '../../css/helpers'
import { PenpotApiObject } from '../../api'
import { CSSClassDefinition, PenpotExportFile } from '../../types'
const extractObjectCssProps = (object: PenpotApiObject) => {
let { textDecoration, ...styles } = object.positionData[0]
const isTextObject = object.type === 'text'
if (isTextObject) {
if (!textDecoration.startsWith('none')) {
styles = { ...styles, textDecoration }
}
}
return styles
}
const getTextObjectCssProps = (object: PenpotApiObject) => {
const textCssProps = [
'fontStyle',
'fontSize',
'fontWeight',
'direction',
'fontFamily',
]
const objectCssProps = extractObjectCssProps(object)
return pickObjectProps(objectCssProps, textCssProps)
}
export const adaptPageComponentsToCssClassDefinitions = (
penpotFile: PenpotExportFile,
options: { pageId: string },
): CSSClassDefinition[] => {
const cssClassDefinitions = []
const page = penpotFile.pages[options.pageId]
const components = Object.values(page.objects)
.filter(isComponent)
.map((object) => getObjectShapesFromPage(object, page))
for (const component of components) {
for (const objectId in component.objects) {
const object = component.objects[objectId]
if (object.type === 'text') {
const cssProps = getTextObjectCssProps(object)
const selector = textToCssClassSelector(`${page.name}--${object.name}`)
cssClassDefinitions.push({ selector, cssProps })
}
}
}
return cssClassDefinitions
}

View file

@ -0,0 +1,33 @@
import { textToCssClassSelector } from '../../css/helpers'
import { PenpotApiTypography, CssTextProperty } from '../../api'
import { CSSClassDefinition, PenpotExportFile } from '../../types'
const getTypographyAssetCssProps = (
typography: PenpotApiTypography,
): Record<CssTextProperty, string> => {
return {
lineHeight: typography.lineHeight,
fontStyle: typography.fontStyle,
textTransform: typography.textTransform,
fontWeight: typography.fontWeight,
fontSize: `${typography.fontSize}px`,
letterSpacing: `${typography.letterSpacing}px`,
fontFamily: `"${typography.fontFamily}"`,
}
}
export const adaptTypographiesToCssClassDefinitions = (
penpotFile: PenpotExportFile,
): CSSClassDefinition[] => {
const { fileName, typographies } = penpotFile
const cssClassDefinitions = Object.values(typographies).map((typography) => {
const cssProps = getTypographyAssetCssProps(typography)
const selector = textToCssClassSelector(`${fileName}--${typography.name}`)
return { selector, cssProps }
})
return cssClassDefinitions
}

View file

@ -1,10 +1,7 @@
import fetch, { RequestInit } from 'node-fetch'
import { pickObjectProps } from './helpers'
import type {
PenpotApiErrorResponse,
PenpotApiFile,
PenpotApiObject,
PenpotApiTypography,
PenpotClientFetcherOptions,
PenpotClientGetFileOptions,
PenpotClientSettings,
@ -68,32 +65,6 @@ export class Penpot {
return json as ResultType
}
static extractObjectCssProps(object: PenpotApiObject) {
let { textDecoration, ...styles } = object.positionData[0]
const isTextObject = object.type === 'text'
if (isTextObject) {
if (!textDecoration.startsWith('none')) {
styles = { ...styles, textDecoration }
}
}
return styles
}
static getTextObjectCssProps(object: PenpotApiObject) {
const textCssProps = [
'fontStyle',
'fontSize',
'fontWeight',
'direction',
'fontFamily',
]
const objectCssProps = Penpot.extractObjectCssProps(object)
return pickObjectProps(objectCssProps, textCssProps)
}
async getFile(
options: PenpotClientGetFileOptions,
): Promise<PenpotExportFile> {
@ -111,21 +82,4 @@ export class Penpot {
pages: file.data.pagesIndex ?? {},
}
}
static getTypographyAssetCssProps(typography: PenpotApiTypography) {
const textCssProps = [
'lineHeight',
'fontStyle',
'textTransform',
'fontWeight',
'direction',
]
return {
...pickObjectProps(typography, textCssProps),
fontSize: `${typography.fontSize}px`,
letterSpacing: `${typography.letterSpacing}px`,
fontFamily: `"${typography.fontFamily}"`,
}
}
}

View file

@ -33,7 +33,7 @@ export interface PenpotApiColor extends PenpotApiAsset {
opacity: number
}
type CssTextProperty =
export type CssTextProperty =
| 'lineHeight'
| 'fontStyle'
| 'textTransform'

View file

@ -1,7 +1,3 @@
import fs from 'fs'
import { camelToKebab } from './string'
import { CSSClassDefinition } from './types'
/** From: https://www.w3.org/TR/css-syntax-3/#escaping
* Any Unicode code point can be included in an ident sequence or quoted string by escaping it. CSS escape sequences
* start with a backslash (\), and continue with:
@ -78,28 +74,3 @@ export function textToCssCustomProperyName(str: string) {
const unescapedDashedIdentifier = '--' + str.trimStart()
return textToCssIdentToken(unescapedDashedIdentifier)
}
export function cssClassDefinitionToCSS(
cssClassDefinition: CSSClassDefinition,
): string {
const cssValidProps = Object.keys(cssClassDefinition.cssProps).map(
(key) => ` ${camelToKebab(key)}: ${cssClassDefinition.cssProps[key]};`,
)
return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n')
}
export function writeCssFile(
path: string,
cssClassDefinitions: CSSClassDefinition[],
) {
const css = cssClassDefinitions.map(cssClassDefinitionToCSS).join('\n\n')
const pathDirs = path.trim().split('/')
const dirname = pathDirs.slice(0, pathDirs.length - 1).join('/')
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
fs.writeFileSync(path, css, 'utf-8')
}

View file

@ -1,13 +1,10 @@
import path from 'node:path'
import Penpot from '../lib/api'
import { validateUserConfig, normalizePenpotExportUserConfig } from './config'
import { CSSClassDefinition } from './types'
import {
textToCssClassSelector,
textToCssCustomProperyName,
writeCssFile,
} from './css'
import path from 'path'
import { getObjectShapesFromPage, isComponent } from './api/helpers'
import { writeCssFile } from './outputters/css'
import { adaptTypographiesToCssClassDefinitions } from './adapters/inbound/typographyToCssClasses'
import { adaptColorsToCssVariables } from './adapters/inbound/colorsToCssVariables'
import { adaptPageComponentsToCssClassDefinitions } from './adapters/inbound/pageComponentsToCssClasses'
export async function generateCssFromConfig(
userConfig: object,
@ -32,20 +29,8 @@ export async function generateCssFromConfig(
console.log('🎨 Processing Penpot file: %s', penpotFile.fileName)
for (const colorsConfig of fileConfig.colors) {
const cssClassDefinition: CSSClassDefinition = {
selector: ':root',
cssProps: {},
}
const { colors } = penpotFile
for (const colorId in colors) {
const color = colors[colorId]
const objectClassname = textToCssCustomProperyName(color.name)
cssClassDefinition.cssProps[objectClassname] = color.color // FIXME Add opacity with rgba()
}
const cssPath = path.resolve(rootProjectPath, colorsConfig.output)
const cssClassDefinition = adaptColorsToCssVariables(penpotFile)
writeCssFile(cssPath, [cssClassDefinition])
@ -53,20 +38,9 @@ export async function generateCssFromConfig(
}
for (const typographiesConfig of fileConfig.typographies) {
const cssClassDefinitions: CSSClassDefinition[] = []
const { fileName, typographies } = penpotFile
for (const typographyId in typographies) {
const typography = typographies[typographyId]
const cssProps = Penpot.getTypographyAssetCssProps(typography)
const selector = textToCssClassSelector(
`${fileName}--${typography.name}`,
)
cssClassDefinitions.push({ selector, cssProps })
}
const cssPath = path.resolve(rootProjectPath, typographiesConfig.output)
const cssClassDefinitions =
adaptTypographiesToCssClassDefinitions(penpotFile)
writeCssFile(cssPath, cssClassDefinitions)
@ -74,26 +48,10 @@ export async function generateCssFromConfig(
}
for (const pagesConfig of fileConfig.pages) {
const cssClassDefinitions: CSSClassDefinition[] = []
const page = penpotFile.pages[pagesConfig.pageId]
const components = Object.values(page.objects)
.filter(isComponent)
.map((object) => getObjectShapesFromPage(object, page))
for (const component of components) {
for (const objectId in component.objects) {
const object = component.objects[objectId]
if (object.type === 'text') {
const cssProps = Penpot.getTextObjectCssProps(object)
const selector = textToCssClassSelector(
`${page.name}--${object.name}`,
)
cssClassDefinitions.push({ selector, cssProps })
}
}
}
const cssClassDefinitions = adaptPageComponentsToCssClassDefinitions(
penpotFile,
{ pageId: pagesConfig.pageId },
)
const cssPath = path.resolve(rootProjectPath, pagesConfig.output)

View file

@ -0,0 +1,30 @@
import fs from 'node:fs'
import path from 'node:path'
import { camelToKebab } from '../string'
import { CSSClassDefinition } from '../types'
const serializeCssClass = (cssClassDefinition: CSSClassDefinition): string => {
const cssValidProps = Object.keys(cssClassDefinition.cssProps).map(
(key) => ` ${camelToKebab(key)}: ${cssClassDefinition.cssProps[key]};`,
)
return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n')
}
const serializeCss = (cssClassDefinitions: CSSClassDefinition[]): string => {
return cssClassDefinitions.map(serializeCssClass).join('\n\n')
}
export function writeCssFile(
outputPath: string,
cssClassDefinitions: CSSClassDefinition[],
) {
const css = serializeCss(cssClassDefinitions)
const dirname = path.dirname(outputPath)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
fs.writeFileSync(outputPath, css, 'utf-8')
}