import { AnyConfig, ParsedClassName } from './types'
export const IMPORTANT_MODIFIER = '!'
const MODIFIER_SEPARATOR = ':'
const MODIFIER_SEPARATOR_LENGTH = MODIFIER_SEPARATOR.length
export const createParseClassName = (config: AnyConfig) => {
const { prefix, experimentalParseClassName } = config
/**
* Parse class name into parts.
*
* Inspired by `splitAtTopLevelOnly` used in Tailwind CSS
* @see https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js
*/
let parseClassName = (className: string): ParsedClassName => {
const modifiers = []
let bracketDepth = 0
let parenDepth = 0
let modifierStart = 0
let postfixModifierPosition: number | undefined
for (let index = 0; index < className.length; index++) {
let currentCharacter = className[index]
if (bracketDepth === 0 && parenDepth === 0) {
if (currentCharacter === MODIFIER_SEPARATOR) {
modifiers.push(className.slice(modifierStart, index))
modifierStart = index + MODIFIER_SEPARATOR_LENGTH
continue
}
if (currentCharacter === '/') {
postfixModifierPosition = index
continue
}
}
if (currentCharacter === '[') {
bracketDepth++
} else if (currentCharacter === ']') {
bracketDepth--
} else if (currentCharacter === '(') {
parenDepth++
} else if (currentCharacter === ')') {
parenDepth--
}
}
const baseClassNameWithImportantModifier =
modifiers.length === 0 ? className : className.substring(modifierStart)
const baseClassName = stripImportantModifier(baseClassNameWithImportantModifier)
const hasImportantModifier = baseClassName !== baseClassNameWithImportantModifier
const maybePostfixModifierPosition =
postfixModifierPosition && postfixModifierPosition > modifierStart
? postfixModifierPosition - modifierStart
: undefined
return {
modifiers,
hasImportantModifier,
baseClassName,
maybePostfixModifierPosition,
}
}
if (prefix) {
const fullPrefix = prefix + MODIFIER_SEPARATOR
const parseClassNameOriginal = parseClassName
parseClassName = (className) =>
className.startsWith(fullPrefix)
? parseClassNameOriginal(className.substring(fullPrefix.length))
: {
isExternal: true,
modifiers: [],
hasImportantModifier: false,
baseClassName: className,
maybePostfixModifierPosition: undefined,
}
}
if (experimentalParseClassName) {
const parseClassNameOriginal = parseClassName
parseClassName = (className) =>
experimentalParseClassName({ className, parseClassName: parseClassNameOriginal })
}
return parseClassName
}
const stripImportantModifier = (baseClassName: string) => {
if (baseClassName.endsWith(IMPORTANT_MODIFIER)) {
return baseClassName.substring(0, baseClassName.length - 1)
}
/**
* In Tailwind CSS v3 the important modifier was at the start of the base class name. This is still supported for legacy reasons.
* @see https://github.com/dcastil/tailwind-merge/issues/513#issuecomment-2614029864
*/
if (baseClassName.startsWith(IMPORTANT_MODIFIER)) {
return baseClassName.substring(1)
}
return baseClassName
}