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:
parent
66f0b48fa4
commit
7f43fc1d98
8 changed files with 160 additions and 130 deletions
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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}"`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export interface PenpotApiColor extends PenpotApiAsset {
|
|||
opacity: number
|
||||
}
|
||||
|
||||
type CssTextProperty =
|
||||
export type CssTextProperty =
|
||||
| 'lineHeight'
|
||||
| 'fontStyle'
|
||||
| 'textTransform'
|
||||
|
|
|
@ -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')
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
30
packages/penpot-css-export/src/lib/outputters/css.ts
Normal file
30
packages/penpot-css-export/src/lib/outputters/css.ts
Normal 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')
|
||||
}
|
Loading…
Reference in a new issue