import { AnyClassGroupIds, AnyConfig, AnyThemeGroupIds, ClassGroup, ClassValidator, Config, ThemeGetter, ThemeObject, } from './types' export interface ClassPartObject { nextPart: Map validators: ClassValidatorObject[] classGroupId?: AnyClassGroupIds } interface ClassValidatorObject { classGroupId: AnyClassGroupIds validator: ClassValidator } const CLASS_PART_SEPARATOR = '-' export const createClassGroupUtils = (config: AnyConfig) => { const classMap = createClassMap(config) const { conflictingClassGroups, conflictingClassGroupModifiers } = config const getClassGroupId = (className: string) => { const classParts = className.split(CLASS_PART_SEPARATOR) // Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and remove it from classParts. if (classParts[0] === '' && classParts.length !== 1) { classParts.shift() } return getGroupRecursive(classParts, classMap) || getGroupIdForArbitraryProperty(className) } const getConflictingClassGroupIds = ( classGroupId: AnyClassGroupIds, hasPostfixModifier: boolean, ) => { const conflicts = conflictingClassGroups[classGroupId] || [] if (hasPostfixModifier && conflictingClassGroupModifiers[classGroupId]) { return [...conflicts, ...conflictingClassGroupModifiers[classGroupId]!] } return conflicts } return { getClassGroupId, getConflictingClassGroupIds, } } const getGroupRecursive = ( classParts: string[], classPartObject: ClassPartObject, ): AnyClassGroupIds | undefined => { if (classParts.length === 0) { return classPartObject.classGroupId } const currentClassPart = classParts[0]! const nextClassPartObject = classPartObject.nextPart.get(currentClassPart) const classGroupFromNextClassPart = nextClassPartObject ? getGroupRecursive(classParts.slice(1), nextClassPartObject) : undefined if (classGroupFromNextClassPart) { return classGroupFromNextClassPart } if (classPartObject.validators.length === 0) { return undefined } const classRest = classParts.join(CLASS_PART_SEPARATOR) return classPartObject.validators.find(({ validator }) => validator(classRest))?.classGroupId } const arbitraryPropertyRegex = /^\[(.+)\]$/ const getGroupIdForArbitraryProperty = (className: string) => { if (arbitraryPropertyRegex.test(className)) { const arbitraryPropertyClassName = arbitraryPropertyRegex.exec(className)![1] const property = arbitraryPropertyClassName?.substring( 0, arbitraryPropertyClassName.indexOf(':'), ) if (property) { // I use two dots here because one dot is used as prefix for class groups in plugins return 'arbitrary..' + property } } } /** * Exported for testing only */ export const createClassMap = (config: Config) => { const { theme, classGroups } = config const classMap: ClassPartObject = { nextPart: new Map(), validators: [], } for (const classGroupId in classGroups) { processClassesRecursively(classGroups[classGroupId]!, classMap, classGroupId, theme) } return classMap } const processClassesRecursively = ( classGroup: ClassGroup, classPartObject: ClassPartObject, classGroupId: AnyClassGroupIds, theme: ThemeObject, ) => { classGroup.forEach((classDefinition) => { if (typeof classDefinition === 'string') { const classPartObjectToEdit = classDefinition === '' ? classPartObject : getPart(classPartObject, classDefinition) classPartObjectToEdit.classGroupId = classGroupId return } if (typeof classDefinition === 'function') { if (isThemeGetter(classDefinition)) { processClassesRecursively( classDefinition(theme), classPartObject, classGroupId, theme, ) return } classPartObject.validators.push({ validator: classDefinition, classGroupId, }) return } Object.entries(classDefinition).forEach(([key, classGroup]) => { processClassesRecursively( classGroup, getPart(classPartObject, key), classGroupId, theme, ) }) }) } const getPart = (classPartObject: ClassPartObject, path: string) => { let currentClassPartObject = classPartObject path.split(CLASS_PART_SEPARATOR).forEach((pathPart) => { if (!currentClassPartObject.nextPart.has(pathPart)) { currentClassPartObject.nextPart.set(pathPart, { nextPart: new Map(), validators: [], }) } currentClassPartObject = currentClassPartObject.nextPart.get(pathPart)! }) return currentClassPartObject } const isThemeGetter = (func: ClassValidator | ThemeGetter): func is ThemeGetter => (func as ThemeGetter).isThemeGetter