diff --git a/packages/penpot-css-export/src/lib/css.ts b/packages/penpot-css-export/src/lib/css.ts index b7d8b1d..bfad605 100644 --- a/packages/penpot-css-export/src/lib/css.ts +++ b/packages/penpot-css-export/src/lib/css.ts @@ -2,6 +2,65 @@ 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: + * - Any Unicode code point that is not a hex digits or a newline. The escape sequence is replaced by that code point. + * - Or one to six hex digits, followed by an optional whitespace. The escape sequence is replaced by the Unicode code + * point whose value is given by the hexadecimal digits. This optional whitespace allow hexadecimal escape sequences + * to be followed by "real" hex digits. + */ +function escapeCssCharacter(char: string) { + return '\\' + char +} + +/** From: https://www.w3.org/TR/css-syntax-3/#syntax-description: + * Property names and at-rule names are always ident sequences, which have to start with an ident-start code point, + * two hyphens, or a hyphen followed by an ident-start code point, and then can contain zero or more ident code points. + * You can include any code point at all, even ones that CSS uses in its syntax, by escaping it. + * + * Railroad diagram: https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + * + * ident-start code point: A letter, a non-ASCII code point, or U+005F LOW LINE (_). + * ident code point: An ident-start code point, a digit, or U+002D HYPHEN-MINUS (-). + * digit: A code point between U+0030 DIGIT ZERO (0) and U+0039 DIGIT NINE (9) inclusive. + * letter: An uppercase letter or a lowercase letter. + * non-ASCII code point: A code point with a value equal to or greater than U+0080 . + */ +function textToCssIdentToken(str: string) { + const normalizedString = str.trim().replace(/\s/g, '_') + + const escapedString = normalizedString + .replace( + // NOTE All ASCII printable characters except -, 0-9, A-Z, _, a-z + /[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, + escapeCssCharacter, + ) + .replace( + /^(-?)([0-9])/, + (match: string, dashGroup: string, numberGroup: string) => + dashGroup + escapeCssCharacter(numberGroup), + ) + + return escapedString +} + +/** From: https://www.w3.org/TR/selectors-3/#w3cselgrammar + * class + * : '.' IDENT + * ; + * + * Tokenizer (Flex notation) + * ident [-]?{nmstart}{nmchar}* + * nmstart [_a-z]|{nonascii}|{escape} + * nmchar [_a-z0-9-]|{nonascii}|{escape} + * nonascii [^\0-\177] + */ +export function textToCssClassSelector(str: string) { + const ident = textToCssIdentToken(str) + return '.' + ident +} + export function cssClassDefinitionToCSS( cssClassDefinition: CSSClassDefinition, ): string { @@ -9,9 +68,7 @@ export function cssClassDefinitionToCSS( (key) => ` ${camelToKebab(key)}: ${cssClassDefinition.cssProps[key]};`, ) - return [`.${cssClassDefinition.className} {`, ...cssValidProps, '}'].join( - '\n', - ) + return [`${cssClassDefinition.selector} {`, ...cssValidProps, '}'].join('\n') } export function writeCssFile( diff --git a/packages/penpot-css-export/src/lib/index.ts b/packages/penpot-css-export/src/lib/index.ts index cb982b5..922201c 100644 --- a/packages/penpot-css-export/src/lib/index.ts +++ b/packages/penpot-css-export/src/lib/index.ts @@ -1,8 +1,7 @@ import Penpot from '../lib/api' -import { textToValidClassname } from './string' import { validateAndNormalizePenpotExportConfig } from './config' import { CSSClassDefinition, Config } from './types' -import { writeCssFile } from './css' +import { textToCssClassSelector, writeCssFile } from './css' import path from 'path' export async function generateCssFromConfig( @@ -18,15 +17,11 @@ export async function generateCssFromConfig( const { fileName, typographies } = await penpot.getFileTypographies({ fileId: typographiesConfig.fileId, }) - const fileClassname = textToValidClassname(fileName) for (const typography of typographies) { - if (typography.path === '📱Title') continue // FIXME Conflicting emoji names - const cssProps = Penpot.getTypographyAssetCssProps(typography) - const objectClassname = textToValidClassname(typography.name) - const className = `${fileClassname}--${objectClassname}` - cssClassDefinitions.push({ className, cssProps }) + const selector = textToCssClassSelector(`${fileName}--${typography.name}`) + cssClassDefinitions.push({ selector, cssProps }) } const cssPath = path.resolve(rootProjectPath, typographiesConfig.output) @@ -43,15 +38,13 @@ export async function generateCssFromConfig( fileId: page.fileId, pageId: page.pageId, }) - const pageClassname = textToValidClassname(pageName) for (const component of components) { for (const object of component.objects) { if (object.type === 'text') { const cssProps = Penpot.getTextObjectCssProps(object) - const objectClassname = textToValidClassname(object.name) - const className = `${pageClassname}--${objectClassname}` - cssClassDefinitions.push({ className, cssProps }) + const selector = textToCssClassSelector(`${pageName}--${object.name}`) + cssClassDefinitions.push({ selector, cssProps }) } } } diff --git a/packages/penpot-css-export/src/lib/string.ts b/packages/penpot-css-export/src/lib/string.ts index 820f0fd..1186463 100644 --- a/packages/penpot-css-export/src/lib/string.ts +++ b/packages/penpot-css-export/src/lib/string.ts @@ -1,15 +1,3 @@ export function camelToKebab(str: string) { return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() } - -export function textToValidClassname(str: string) { - // Remove invalid characters - let className = str.toLowerCase().replace(/[^a-zA-Z_-|0-9]/g, '_') - - // If it starts with a hyphen, ensure it's not followed by a digit - if (str.startsWith('-')) { - className = str.replace(/^-([0-9])/, '_$1') - } - - return className -} diff --git a/packages/penpot-css-export/src/lib/types.ts b/packages/penpot-css-export/src/lib/types.ts index 48175ba..60137b8 100644 --- a/packages/penpot-css-export/src/lib/types.ts +++ b/packages/penpot-css-export/src/lib/types.ts @@ -16,6 +16,6 @@ export interface Config { } export interface CSSClassDefinition { - className: string + selector: string cssProps: Record }