import { sanitizeName } from './sanitizeName.js'; import { ResolvableTopLevelAlias, ResolvableValueLevelAlias, ResolvableModeLevelAlias, TokenState, } from '@specifyapp/specify-design-token-format'; import { ALIAS_SEPARATOR, parserOptions } from './constants.js'; import { DEFAULT_MODE } from '../../utils/constants.js'; import { parseMustacheTemplate } from '../../utils/parseMustacheTemplate.js'; import { SpecifyError, specifyErrors } from '../../../errors/index.js'; import { ParserToolbox } from '../../../parsersEngine/index.js'; enum Variable { COLLECTION = 'collection', MODE = 'mode', GROUPS = 'groups', TOKEN = 'token', } const GROUPLESS_TOKEN_NOT_IN_COLLECTION_TEMPLATE = '--{{token}}-{{mode}}'; export const DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE = '--{{groups}}-{{token}}-{{mode}}'; const GROUPLESS_TOKEN_TEMPLATE = '--{{token}}'; export const DEFAULT_SELECTOR_TEMPLATE = ':root[data-{{collection}}="{{mode}}"]'; export const DEFAULT_TOKEN_TEMPLATE = '--{{groups}}-{{token}}'; const templateMatch: { [DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE]: { ['true:true']: string; ['false:true']: string; ['true:false']: string; ['false:false']: string; }; [DEFAULT_TOKEN_TEMPLATE]: { ['true']: string; ['false']: string; }; } = { [DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE]: { // hasGroup:hasMode ['true:true']: DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE, ['false:true']: GROUPLESS_TOKEN_NOT_IN_COLLECTION_TEMPLATE, ['true:false']: DEFAULT_TOKEN_TEMPLATE, ['false:false']: GROUPLESS_TOKEN_TEMPLATE, }, // hasGroup [DEFAULT_TOKEN_TEMPLATE]: { ['true']: DEFAULT_TOKEN_TEMPLATE, ['false']: GROUPLESS_TOKEN_TEMPLATE, }, }; export const validateSelectorTemplate = (template: string) => parseMustacheTemplate(template).forEach(variable => { if (variable === Variable.GROUPS || variable === Variable.TOKEN) throw new SpecifyError({ errorKey: specifyErrors.PARSERS_ENGINE_INVALID_OPTION.errorKey, publicMessage: `'${variable}' is not a valid variable for the 'selectorTemplate' option.`, }); }); export const dataOfResolvedStatefulAlias = ( resolved: ResolvableTopLevelAlias | ResolvableModeLevelAlias | ResolvableValueLevelAlias, mode?: string, ) => dataOfToken( resolved.tokenState, resolved instanceof ResolvableTopLevelAlias ? mode : resolved.targetMode, ); export const dataOfToken = (token: TokenState, mode: string | undefined) => { const collection = token.getCollection()?.name ?? undefined; const groups = (() => { if (token.path.length < 2) return undefined; if (token.path.at(0) === collection && token.path.at(1) === token.name) return undefined; else if (token.path.at(0) === collection) { return token.path.toArray().slice(1, token.path.length - 1); } else return token.path.toArray().slice(0, token.path.length - 1); })()?.join(ALIAS_SEPARATOR); return { collection, groups, mode, token: token.name, }; }; export const renderTemplate = ( template: string, data: { [k in Variable]?: string }, toolbox: ParserToolbox, ) => { const variables = parseMustacheTemplate(template); // We want to remove the 'default' mode for tokens not in collections let rendered = template === DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE ? templateMatch[template][`${!!data.groups}:${!!data.mode && data.mode !== DEFAULT_MODE}`] : template === DEFAULT_TOKEN_TEMPLATE ? templateMatch[template][`${!!data.groups}`] : template; for (let i = 0; i < variables.length; ++i) { const variable = variables[i]; if (!data[variable as Variable]) { toolbox.populateMessage({ type: 'warning', content: `Undefined template variable '${variable}'`, errorKey: specifyErrors.PARSERS_ENGINE_INVALID_OPTION.errorKey, }); } const regexp = new RegExp(`{{${variable}}}`, 'g'); rendered = rendered.replace(regexp, sanitizeName(data[variable as Variable] ?? '')); } return rendered; }; export type templateRenderer = (data: { [k in Variable]?: string }) => string; export type aliasRenderer = ( alias: ResolvableTopLevelAlias | ResolvableModeLevelAlias | ResolvableValueLevelAlias, ) => string; export const makeRenderer = ( template: string, parserOptions: parserOptions, toolbox: ParserToolbox, isSelectorTemplate: boolean = false, ): templateRenderer => (data: { [k in Variable]?: string }) => { // Enforcing the collectionless template because when rendering the alias of a token // We're using the same renderer for simplicity, but depending on token state, collection might be present or not // So rather than creating a renderer for each token, I prefer update it on the fly const finalTemplate = !data.collection ? parserOptions.tokenNotInCollectionNameTemplate ?? DEFAULT_TOKEN_NOT_IN_COLLECTION_TEMPLATE : isSelectorTemplate ? template : parserOptions.tokenNameTemplate ?? DEFAULT_TOKEN_TEMPLATE; return renderTemplate(finalTemplate, data, toolbox); };