import { ParserToolbox } from '../../../parsersEngine/ParserToolbox.js'; import { specifyErrors } from '../../../errors/index.js'; import { SDTF_PATH_SEPARATOR, TokenState } from '@specifyapp/specify-design-token-format'; import { removeModesIfSameValue } from '../../utils/removeModesIfSameValue.js'; import { urlToTailwind } from './converters/url.js'; import { tokenTypeToTailwind } from './tokenTypeToTailwind.js'; import { dimensionToTailwind } from './converters/dimension.js'; import { colorToTailwind } from './converters/color.js'; import { cubicBezierToTailwind } from './converters/cubicBezier.js'; import { durationToTailwind } from './converters/duration.js'; import { simpleValueToTailwind } from './converters/simpleValue.js'; import { gradientToTailwind } from './converters/gradient.js'; import { gradientsToTailwind } from './converters/gradients.js'; import { spacingsToTailwind } from './converters/spacings.js'; import { stepsTimingFunctionToTailwind } from './converters/stepTimingFunction.js'; import { radiiToTailwind } from './converters/radii.js'; import { matchJsonValue, jsonValueMatcher } from '../../utils/jsonValueMatcher.js'; import { borderToTailwind } from './converters/border.js'; import { shadowToTailwind } from './converters/shadow.js'; import { transitionToTailwind, transitionToTailwindWithCssVariables, } from './converters/transition.js'; import { fontToTailwindWithCssVariables, fontToTailwind } from './converters/font.js'; import { textStyleToTailwind, textStyleToTailwindWithCssVariables, } from './converters/textStyle.js'; import { toNestedObject, tokensToFlatObject, makeDefaultPath, } from '../../utils/flatNestedObject.js'; import { makeRenderer, dataOfToken } from '../../shared/to-css-custom-properties/template.js'; import { makeCSSAlias } from '../../shared/to-css-custom-properties/makeCSSAlias.js'; // Array is for shadows type tailwindValue = string | Array; // Colors can be nested type tailwindModeValue = { [mode: string]: tailwindValue; }; type tailwindValues = tailwindValue | tailwindModeValue; type tailwindValuesWithoutMode = Exclude; type tailwindToken = { value: tailwindValue; type: string; mode?: string; }; type convertedTailwindToken = { value: tailwindValues; type: string; mode?: string; }; type parserOptions = { useCssVariable: boolean; cssVariableTemplate: { tokenNameTemplate: string; tokenNotInCollectionNameTemplate: string; }; removeModesIfSameValue: boolean; removeSingleMode: boolean; }; function tokenToTailwindWithAlias( token: TokenState, parserOptions: parserOptions, toolbox: ParserToolbox, ): Array | undefined { const renderVariable = makeRenderer( !!token.getCollection() ? parserOptions.cssVariableTemplate.tokenNameTemplate : parserOptions.cssVariableTemplate.tokenNotInCollectionNameTemplate, parserOptions.cssVariableTemplate, toolbox, ); switch (token.type) { case 'font': { return fontToTailwindWithCssVariables( token as TokenState<'font'>, parserOptions.removeSingleMode, renderVariable, ); } case 'textStyle': { return textStyleToTailwindWithCssVariables( token as TokenState<'textStyle'>, parserOptions.removeSingleMode, renderVariable, ); } case 'transition': { return transitionToTailwindWithCssVariables( token as TokenState<'transition'>, parserOptions.removeSingleMode, renderVariable, ); } case 'color': { const tokenWithModes = token.modes.reduce((acc, mode) => { acc[mode] = makeCSSAlias(renderVariable(dataOfToken(token, mode))); return acc; }, {} as any); const modes = Object.keys(tokenWithModes); return [ { type: 'colors', value: parserOptions.removeSingleMode && modes.length === 1 ? tokenWithModes[modes[0]] : parserOptions.removeModesIfSameValue ? removeModesIfSameValue(tokenWithModes) : tokenWithModes, }, ]; } default: { return token.modes.map(mode => { return { value: makeCSSAlias(renderVariable(dataOfToken(token, mode))), type: tokenTypeToTailwind(token.type), mode: parserOptions.removeSingleMode && token.modes.length === 1 ? undefined : mode, }; }); } } } function tokenToTailwindWithoutAlias( token: TokenState, options: { removeModesIfSameValue: boolean; removeSingleMode: boolean }, toolbox: ParserToolbox, ): Array | undefined { const modes = token.modes; if (modes.length === 0) return undefined; else if (token.type === 'color') { // Color token is the only type that allows nested object, so we render modes as well (but not default one) const modesByTailwindType = modes.reduce>((acc, mode) => { const convertedToken = tokenJsonValueToTailwind(token, mode, toolbox); if (!convertedToken) return acc; const tokens = Array.isArray(convertedToken) ? typeof convertedToken[0] === 'string' ? [ { value: convertedToken as tailwindValue, type: tokenTypeToTailwind(token.type), }, ] : (convertedToken as Array) : [{ value: convertedToken, type: tokenTypeToTailwind(token.type) }]; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; let modeValue = acc.get(token.type); if (!modeValue) { modeValue = {}; } modeValue[mode] = token.value; acc.set(token.type, modeValue); } return acc; }, new Map()); return modesByTailwindType.size === 0 ? undefined : Array.from(modesByTailwindType.entries()).map(([type, value]) => { const modes = Object.keys(value); return { type, value: modes.length === 1 && options.removeSingleMode ? value[modes[0]] : options.removeModesIfSameValue ? removeModesIfSameValue(value) : value, }; }); } else { const tokens = modes.flatMap(m => { const convertedToken = tokenJsonValueToTailwind(token, m, toolbox); if (!convertedToken || convertedToken.length === 0) return []; const mode = options.removeSingleMode && modes.length === 1 ? undefined : m; const tokens = Array.isArray(convertedToken) ? convertedToken.map(value => typeof value === 'string' ? { value, type: tokenTypeToTailwind(token.type), mode } : { ...value, mode }, ) : [{ value: convertedToken, type: tokenTypeToTailwind(token.type), mode }]; return tokens; }); return tokens.length === 0 ? undefined : tokens; } } export function tokenToTailwind( token: TokenState, parserOptions: parserOptions, toolbox: ParserToolbox, ): Array | undefined { if (!token.isFullyResolvable) { toolbox.populateMessage({ type: 'warning', content: `Design token "${token.path.toString()}" is not fully resolvable. Please check that aliases points to a valid value.`, errorKey: specifyErrors.PARSERS_ENGINE_INVALID_ALIAS.errorKey, }); return undefined; } // Some tokens are not supported by the CSS parser, so we don't need them to be rendered with alias switch (token.type) { case 'bitmap': case 'vector': case 'fontFamily': // Even if border is supported by css parser, it's not splitted in different properties, but is needed by Tailwind case 'border': return tokenToTailwindWithoutAlias(token, parserOptions, toolbox); default: if (parserOptions.useCssVariable) { return tokenToTailwindWithAlias(token, parserOptions, toolbox); } else { return tokenToTailwindWithoutAlias(token, parserOptions, toolbox); } } } function tokenJsonValueToTailwind( token: TokenState, mode: string, toolbox: ParserToolbox, ): tailwindValuesWithoutMode | Array | undefined { const matcher: jsonValueMatcher> = { bitmap: urlToTailwind, vector: urlToTailwind, vectors: vectors => vectors.files.map(urlToTailwind).join(', '), blur: dimensionToTailwind, breakpoint: dimensionToTailwind, dimension: dimensionToTailwind, radius: dimensionToTailwind, spacing: dimensionToTailwind, spacings: spacingsToTailwind, border: borderToTailwind, color: colorToTailwind, cubicBezier: cubicBezierToTailwind, duration: durationToTailwind, font: fontToTailwind, fontFamily: simpleValueToTailwind, fontWeight: simpleValueToTailwind, opacity: simpleValueToTailwind, zIndex: simpleValueToTailwind, gradient: gradientToTailwind, gradients: gradientsToTailwind, radii: radiiToTailwind, shadow: shadowToTailwind, shadows: v => v.map(shadowToTailwind).join(', '), stepsTimingFunction: stepsTimingFunctionToTailwind, textStyle: textStyleToTailwind, transition: transitionToTailwind, }; return matchJsonValue( matcher, _ => { toolbox.populateMessage({ type: 'warning', content: `Design token "${token.path.toString()}" (${token.type}) cannot be converted to a Tailwind format.`, errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, }); return undefined; }, token, mode, ); } export function tokensToTailwindPreset( tokens: Array, parserOptions: parserOptions, toolbox: ParserToolbox, ) { const flatNestedObject = tokensToFlatObject(tokens, token => { const convertedToken = tokenToTailwind(token, parserOptions, toolbox); if (!convertedToken || convertedToken.length === 0) return undefined; return convertedToken.map(({ value, type, mode }) => { const defaultPath = makeDefaultPath(token); return type === 'colors' ? // Tailwind colors tokens can be nested so we can define a path that'll be nested and let default name { value, path: `${type}${defaultPath === '' ? '' : SDTF_PATH_SEPARATOR + defaultPath}`, } : { value, path: type, name: `${defaultPath === '' ? '' : defaultPath.replace(/\./g, '-') + '-'}${token.name}${ !!mode ? '-' + mode : '' }`, }; }); }); return { theme: { extend: toNestedObject(flatNestedObject) }, }; }