From 7f43fc1d988f27cebfe64be1656b5dcae5b21577 Mon Sep 17 00:00:00 2001 From: Roberto Redradix Date: Thu, 31 Aug 2023 22:28:37 +0200 Subject: [PATCH] refactor(cli): extract adapters and a CSS outputter --- .../adapters/inbound/colorsToCssVariables.ts | 23 +++++++ .../inbound/pageComponentsToCssClasses.ts | 61 +++++++++++++++++ .../inbound/typographyToCssClasses.ts | 33 ++++++++++ .../penpot-css-export/src/lib/api/penpot.ts | 46 ------------- .../penpot-css-export/src/lib/api/types.ts | 2 +- .../src/lib/{css.ts => css/helpers.ts} | 29 -------- packages/penpot-css-export/src/lib/index.ts | 66 ++++--------------- .../src/lib/outputters/css.ts | 30 +++++++++ 8 files changed, 160 insertions(+), 130 deletions(-) create mode 100644 packages/penpot-css-export/src/lib/adapters/inbound/colorsToCssVariables.ts create mode 100644 packages/penpot-css-export/src/lib/adapters/inbound/pageComponentsToCssClasses.ts create mode 100644 packages/penpot-css-export/src/lib/adapters/inbound/typographyToCssClasses.ts rename packages/penpot-css-export/src/lib/{css.ts => css/helpers.ts} (81%) create mode 100644 packages/penpot-css-export/src/lib/outputters/css.ts diff --git a/packages/penpot-css-export/src/lib/adapters/inbound/colorsToCssVariables.ts b/packages/penpot-css-export/src/lib/adapters/inbound/colorsToCssVariables.ts new file mode 100644 index 0000000..0a3ee85 --- /dev/null +++ b/packages/penpot-css-export/src/lib/adapters/inbound/colorsToCssVariables.ts @@ -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), + } +} diff --git a/packages/penpot-css-export/src/lib/adapters/inbound/pageComponentsToCssClasses.ts b/packages/penpot-css-export/src/lib/adapters/inbound/pageComponentsToCssClasses.ts new file mode 100644 index 0000000..35a72f0 --- /dev/null +++ b/packages/penpot-css-export/src/lib/adapters/inbound/pageComponentsToCssClasses.ts @@ -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 +} diff --git a/packages/penpot-css-export/src/lib/adapters/inbound/typographyToCssClasses.ts b/packages/penpot-css-export/src/lib/adapters/inbound/typographyToCssClasses.ts new file mode 100644 index 0000000..0b3b288 --- /dev/null +++ b/packages/penpot-css-export/src/lib/adapters/inbound/typographyToCssClasses.ts @@ -0,0 +1,33 @@ +import { textToCssClassSelector } from '../../css/helpers' + +import { PenpotApiTypography, CssTextProperty } from '../../api' +import { CSSClassDefinition, PenpotExportFile } from '../../types' + +const getTypographyAssetCssProps = ( + typography: PenpotApiTypography, +): Record => { + 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 +} diff --git a/packages/penpot-css-export/src/lib/api/penpot.ts b/packages/penpot-css-export/src/lib/api/penpot.ts index 4c47c18..ba0f03b 100644 --- a/packages/penpot-css-export/src/lib/api/penpot.ts +++ b/packages/penpot-css-export/src/lib/api/penpot.ts @@ -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 { @@ -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}"`, - } - } } diff --git a/packages/penpot-css-export/src/lib/api/types.ts b/packages/penpot-css-export/src/lib/api/types.ts index 82c32f1..703a9af 100644 --- a/packages/penpot-css-export/src/lib/api/types.ts +++ b/packages/penpot-css-export/src/lib/api/types.ts @@ -33,7 +33,7 @@ export interface PenpotApiColor extends PenpotApiAsset { opacity: number } -type CssTextProperty = +export type CssTextProperty = | 'lineHeight' | 'fontStyle' | 'textTransform' diff --git a/packages/penpot-css-export/src/lib/css.ts b/packages/penpot-css-export/src/lib/css/helpers.ts similarity index 81% rename from packages/penpot-css-export/src/lib/css.ts rename to packages/penpot-css-export/src/lib/css/helpers.ts index 02a8c9d..8475b3f 100644 --- a/packages/penpot-css-export/src/lib/css.ts +++ b/packages/penpot-css-export/src/lib/css/helpers.ts @@ -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') -} diff --git a/packages/penpot-css-export/src/lib/index.ts b/packages/penpot-css-export/src/lib/index.ts index 5ada011..4b1fa28 100644 --- a/packages/penpot-css-export/src/lib/index.ts +++ b/packages/penpot-css-export/src/lib/index.ts @@ -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) diff --git a/packages/penpot-css-export/src/lib/outputters/css.ts b/packages/penpot-css-export/src/lib/outputters/css.ts new file mode 100644 index 0000000..28c48bb --- /dev/null +++ b/packages/penpot-css-export/src/lib/outputters/css.ts @@ -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') +}