183 lines
5.3 KiB
TypeScript
183 lines
5.3 KiB
TypeScript
import {
|
|
AnyClassGroupIds,
|
|
AnyConfig,
|
|
AnyThemeGroupIds,
|
|
ClassGroup,
|
|
ClassValidator,
|
|
Config,
|
|
ThemeGetter,
|
|
ThemeObject,
|
|
} from './types'
|
|
|
|
export interface ClassPartObject {
|
|
nextPart: Map<string, ClassPartObject>
|
|
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<AnyClassGroupIds, AnyThemeGroupIds>) => {
|
|
const { theme, classGroups } = config
|
|
const classMap: ClassPartObject = {
|
|
nextPart: new Map<string, ClassPartObject>(),
|
|
validators: [],
|
|
}
|
|
|
|
for (const classGroupId in classGroups) {
|
|
processClassesRecursively(classGroups[classGroupId]!, classMap, classGroupId, theme)
|
|
}
|
|
|
|
return classMap
|
|
}
|
|
|
|
const processClassesRecursively = (
|
|
classGroup: ClassGroup<AnyThemeGroupIds>,
|
|
classPartObject: ClassPartObject,
|
|
classGroupId: AnyClassGroupIds,
|
|
theme: ThemeObject<AnyThemeGroupIds>,
|
|
) => {
|
|
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
|