import { ParserToolbox } from '../../../parsersEngine/ParserToolbox.js'; import { specifyErrors } from '../../../errors/index.js'; import { SDTF_PATH_SEPARATOR, TokenState } from '@specifyapp/specify-design-token-format'; import { DEFAULT_MODE } from '../../utils/constants.js'; import { dimensionToJson } from './converters/dimension.js'; import { colorToJson } from './converters/color.js'; import { durationToJson } from './converters/duration.js'; import { gradientToJson } from './converters/gradient.js'; import { gradientsToJson } from './converters/gradients.js'; import { spacingsToJson } from './converters/spacings.js'; import { radiiToJson } from './converters/radii.js'; import { matchJsonValue, jsonValueMatcher } from '../../utils/jsonValueMatcher.js'; import { borderToJson } from './converters/border.js'; import { shadowToJson } from './converters/shadow.js'; import { transitionToJson } from './converters/transition.js'; import { textStyleToJson } from './converters/textStyle.js'; import { toNestedObject, tokensToFlatObject, makeDefaultPath, } from '../../utils/flatNestedObject.js'; import { urlToJson } from './converters/url.js'; import { cubicBezierToJson } from './converters/cubicBezier.js'; import { fontToJson } from './converters/font.js'; export type jsonValue = null | boolean | string | number | object | Array; type jsonModeValue = { [mode: string]: jsonValue; }; export type jsonToken = { value: jsonValue; nameExtension?: string; }; type jsonTokenWithMode = { value: jsonModeValue; nameExtension?: string; }; type outputType = 'raw' | 'css'; export function tokenToJson( token: TokenState, toolbox: ParserToolbox, outputType: outputType, ): 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; } if (token.modes.length === 0) return undefined; // The reason we use a map is because when converting token it may output an array, and it'll be a list of token for a given mode // So we need a way to group the array values together and with the right mode const output = token.modes.reduce((acc, mode) => { const converted = tokenJsonValueToJson(token, mode, outputType); if (!converted || (Array.isArray(converted) && converted.length === 0)) return acc; if (Array.isArray(converted)) { converted.forEach(jsonToken => { if (!jsonToken.nameExtension) { throw new Error('Unreachable'); } let modeValue = acc.get(jsonToken.nameExtension); if (!modeValue) { modeValue = { value: {}, nameExtension: jsonToken.nameExtension }; } modeValue.value[mode] = jsonToken.value; acc.set(jsonToken.nameExtension, modeValue); }); } else { // If the token is not an array, then it won't ever be an array, so we can use a constant key to group everything let modeValue = acc.get('key'); if (!modeValue) { modeValue = { value: {} }; } modeValue.value[mode] = converted.value; acc.set('key', modeValue); } return acc; }, new Map()); return Array.from(output.values()); } function wrapOutput(value: jsonValue | undefined) { return value === undefined ? undefined : { value }; } const cssMatcher: jsonValueMatcher> = { bitmap: v => wrapOutput(urlToJson(v)), bitmaps: v => wrapOutput(v.files.map(urlToJson)), vector: v => wrapOutput(urlToJson(v)), vectors: v => wrapOutput(v.files.map(urlToJson)), blur: v => wrapOutput(dimensionToJson(v)), breakpoint: v => wrapOutput(dimensionToJson(v)), dimension: v => wrapOutput(dimensionToJson(v)), radius: v => wrapOutput(dimensionToJson(v)), spacing: v => wrapOutput(dimensionToJson(v)), spacings: v => wrapOutput(spacingsToJson(v)), border: v => borderToJson(v), color: v => wrapOutput(colorToJson(v)), cubicBezier: v => wrapOutput(cubicBezierToJson(v)), duration: v => wrapOutput(durationToJson(v)), fontFamily: v => wrapOutput(v), fontWeight: v => wrapOutput(v), opacity: v => wrapOutput(v), zIndex: v => wrapOutput(v), gradient: v => wrapOutput(gradientToJson(v)), gradients: v => wrapOutput(gradientsToJson(v)), radii: v => wrapOutput(radiiToJson(v)), shadow: v => wrapOutput(shadowToJson(v)), shadows: v => wrapOutput(v.map(shadowToJson).join(', ')), stepsTimingFunction: v => wrapOutput(v), textStyle: textStyleToJson, transition: transitionToJson, font: v => fontToJson(v), }; function tokenJsonValueToJson(token: TokenState, mode: string, output: 'css' | 'raw') { if (output === 'raw') { return wrapOutput( token.getJSONValue({ resolveAliases: true, allowUnresolvable: false, targetMode: mode, }), ); } return matchJsonValue(cssMatcher, v => (!v ? undefined : wrapOutput(v)), token, mode); } export function tokensToJson( tokens: Array, toolbox: ParserToolbox, outputType: outputType, ) { const flatNestedObject = tokensToFlatObject(tokens, token => { const convertedToken = tokenToJson(token, toolbox, outputType); if (!convertedToken || convertedToken.length === 0) return undefined; return convertedToken.map(({ value, nameExtension }) => { const defaultPath = makeDefaultPath(token); return { // We get rid of the default mode to avoid rendering it value: DEFAULT_MODE in value ? value[DEFAULT_MODE] : value, path: `${defaultPath === '' ? '' : defaultPath}${ defaultPath !== '' && !!nameExtension ? SDTF_PATH_SEPARATOR : '' }${!!nameExtension ? token.name : ''}`, name: !!nameExtension ? nameExtension : token.name, }; }); }); return toNestedObject(flatNestedObject); }