import { createSDTFEngine, SDTFEngine } from '@specifyapp/specify-design-token-format'; import { groupBy } from 'lodash-es'; import { formatWithPrettier } from '../../shared/prettier/formatWithPrettier.js'; import { SELECTOR_SEPARATOR, DEFAULT_INCLUDE_CORE_TOKENS_IN_SCOPES, } from '../../shared/to-css-custom-properties/constants.js'; import { DEFAULT_SELECTOR_TEMPLATE, renderSelector, } from '../../../converters/css/utils/template.js'; import { SpecifyError, specifyErrors } from '../../../errors/index.js'; import type { DeriveBuiltInParserHandlerFromDefinition } from '../../internals/createBuiltInParserDefinition.js'; import type { ToCssCustomPropertiesParserDefinition } from './definition.js'; import { renderTokenToCss } from './renderTokenToCss.js'; export const toCssCustomPropertiesHandler: DeriveBuiltInParserHandlerFromDefinition< ToCssCustomPropertiesParserDefinition > = async (previousDataBox, toolbox, parserOptions, outputConfiguration, _context) => { let sdtfEngine: SDTFEngine; switch (previousDataBox.type) { case 'SDTF': { sdtfEngine = createSDTFEngine(previousDataBox.graph); break; } case 'SDTF Engine': { sdtfEngine = previousDataBox.engine; break; } default: { throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_INVALID_PARSER_INPUT.errorKey, publicMessage: `${ (previousDataBox as any).type } is not a valid input for the to-css-custom-properties parser.`, }); } } const currentView = parserOptions?.withSDTFView ?? null; const collections = sdtfEngine.query.getAllCollectionStates({ withView: currentView, }); const tokenStates = sdtfEngine.query.getAllTokenStates({ withView: currentView, }); const finalParserOptions = parserOptions ?? {}; finalParserOptions.includeCoreTokensInScopes = parserOptions?.includeCoreTokensInScopes ?? DEFAULT_INCLUDE_CORE_TOKENS_IN_SCOPES; const selectorTemplate = finalParserOptions.selectorTemplate ?? DEFAULT_SELECTOR_TEMPLATE; const tokenStatesByCollection = groupBy(tokenStates, tokenState => collections.find(collectionState => tokenState.path.toString().includes(collectionState.path.toString()), )?.name ? tokenState.path.toString().split(SELECTOR_SEPARATOR)[0] : 'root', ); let cssOutput: string = ''; const allowUnresolvable = parserOptions?.allowUnresolvable ?? false; if (Reflect.ownKeys(tokenStatesByCollection).length === 0) { toolbox.populateMessage({ type: 'warning', content: `No design tokens found in the input`, errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, }); cssOutput = await formatWithPrettier(':root{}', { parser: 'css' }); } else { const result = Object.entries(tokenStatesByCollection).reduce( (globalAcc, [collectionName, tokenStates]) => { // This variable is global storage to keep track of the variables we're adding in the scope if // `includeCoreTokensInScopes` is true. It allows to avoid having duplicated variables (although it doesn't really matter in CSS) const addedTokensStorage: { [mode: string]: { [variable: string]: true } } = {}; if (collectionName === 'root') { const rootCSS = tokenStates.reduce((acc, tokenState) => { if (!allowUnresolvable && !tokenState.isFullyResolvable) { toolbox.populateMessage({ type: 'warning', content: `Design token ${tokenState.path.toString()} cannot be resolved `, errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, }); return acc; } const resolvedTokenWithMode = renderTokenToCss( tokenState, finalParserOptions, toolbox, addedTokensStorage, ); if (!resolvedTokenWithMode) return acc; return ( acc + Object.values(resolvedTokenWithMode) .filter((value, index, array) => !!value && array.indexOf(value) === index) .join('') ); }, `:root {\n`) + '\n}'; return globalAcc + rootCSS; } else { const cssByMode = tokenStates.reduce( (acc, tokenState) => { if (!allowUnresolvable && !tokenState.isFullyResolvable) { toolbox.populateMessage({ type: 'warning', content: `Design token ${tokenState.path.toString()} cannot be resolved `, errorKey: specifyErrors.PARSERS_ENGINE_PARSER_EXECUTION_FAILED.errorKey, }); return acc; } const resolvedTokenWithMode = renderTokenToCss( tokenState, finalParserOptions, toolbox, addedTokensStorage, ); if (!resolvedTokenWithMode) return acc; Object.entries(resolvedTokenWithMode).forEach(([mode, css]) => { if (!css) return; acc[mode] ??= ''; acc[mode] += css; }); return acc; }, {} as { [mode: string]: string }, ); const toAppend = Object.entries(cssByMode).reduce((acc, [mode, css]) => { return ( acc + `${renderSelector(selectorTemplate, { collection: collectionName, mode, groupsBeforeCollection: [], groupsAfterCollection: [], token: '', groups: [], path: [], groupList: [], })} {\n` + css + '\n}' ); }, ''); return globalAcc + toAppend; } }, '', ); cssOutput = await formatWithPrettier(result, { parser: 'css' }); } toolbox.populateOutput({ type: 'files', files: [ { path: outputConfiguration.filePath, content: { type: 'text', text: cssOutput, }, }, ], }); return { type: 'SDTF Engine', engine: sdtfEngine, }; };