-
-
Notifications
You must be signed in to change notification settings - Fork 68
/
merge-classlist.ts
141 lines (114 loc) · 4.23 KB
/
merge-classlist.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import { ConfigUtils } from './config-utils'
const SPLIT_CLASSES_REGEX = /\s+/
const IMPORTANT_MODIFIER = '!'
export function mergeClassList(classList: string, configUtils: ConfigUtils) {
const { getClassGroupId, getConflictingClassGroupIds } = configUtils
/**
* Set of classGroupIds in following format:
* `{importantModifier}{variantModifiers}{classGroupId}`
* @example 'float'
* @example 'hover:focus:bg-color'
* @example 'md:!pr'
*/
const classGroupsInConflict = new Set<string>()
return (
classList
.trim()
.split(SPLIT_CLASSES_REGEX)
.map((originalClassName) => {
const { modifiers, hasImportantModifier, baseClassName } =
splitModifiers(originalClassName)
const classGroupId = getClassGroupId(baseClassName)
if (!classGroupId) {
return {
isTailwindClass: false as const,
originalClassName,
}
}
const variantModifier = sortModifiers(modifiers).join('')
const modifierId = hasImportantModifier
? variantModifier + IMPORTANT_MODIFIER
: variantModifier
return {
isTailwindClass: true as const,
modifierId,
classGroupId,
originalClassName,
}
})
.reverse()
// Last class in conflict wins, so we need to filter conflicting classes in reverse order.
.filter((parsed) => {
if (!parsed.isTailwindClass) {
return true
}
const { modifierId, classGroupId } = parsed
const classId = modifierId + classGroupId
if (classGroupsInConflict.has(classId)) {
return false
}
classGroupsInConflict.add(classId)
getConflictingClassGroupIds(classGroupId).forEach((group) =>
classGroupsInConflict.add(modifierId + group),
)
return true
})
.reverse()
.map((parsed) => parsed.originalClassName)
.join(' ')
)
}
const SPLIT_MODIFIER_REGEX = /[:[\]]/g
function splitModifiers(className: string) {
const modifiers = []
let bracketDepth = 0
let modifierStart = 0
let match: RegExpExecArray | null
while ((match = SPLIT_MODIFIER_REGEX.exec(className))) {
if (match[0] === ':') {
if (bracketDepth === 0) {
const nextModifierStart = match.index! + 1
modifiers.push(className.substring(modifierStart, nextModifierStart))
modifierStart = nextModifierStart
}
} else if (match[0] === '[') {
bracketDepth++
} else if (match[0] === ']') {
bracketDepth--
}
}
const baseClassNameWithImportantModifier =
modifiers.length === 0 ? className : className.substring(modifierStart)
const hasImportantModifier = baseClassNameWithImportantModifier.startsWith(IMPORTANT_MODIFIER)
const baseClassName = hasImportantModifier
? baseClassNameWithImportantModifier.substring(1)
: baseClassNameWithImportantModifier
return {
modifiers,
hasImportantModifier,
baseClassName,
}
}
/**
* Sorts modifiers according to following schema:
* - Predefined modifiers are sorted alphabetically
* - When an arbitrary variant appears, it must be preserved which modifiers are before and after it
*/
function sortModifiers(modifiers: string[]) {
if (modifiers.length <= 1) {
return modifiers
}
const sortedModifiers: string[] = []
let unsortedModifiers: string[] = []
modifiers.forEach((modifier) => {
const isArbitraryVariant = modifier[0] === '['
if (isArbitraryVariant) {
sortedModifiers.push(...unsortedModifiers.sort(), modifier)
unsortedModifiers = []
} else {
unsortedModifiers.push(modifier)
}
})
sortedModifiers.push(...unsortedModifiers.sort())
return sortedModifiers
}