Skip to content

Commit c27b25a

Browse files
committed
Add sorting for modifier classes
This commit adds sorting for Tailwind modifier classes: `md:w-12`, `hover:bg-gray-500`... The sort is as follows: - Modifier classes are added after non-modifier classes, but before customClasses if those are appended - For a given modifier (e.g. `md:`), the sort is the same as `sortOrder` - Modifiers are sorted among one another in the order they appear in the Tailwind documentation Example: ```javascript // Input: "xl:mx-6 bg-gray-100 lg:mx-4 mt-1 sm:bg-gray-200 hover:bg-blue-100 lg:bg-gray-400 hover:text-blue-100 xl:bg-gray-600 sm:mx-2" // Output: "mt-1 bg-gray-100 hover:text-blue-100 hover:bg-blue-100 sm:mx-2 sm:bg-gray-200 lg:mx-4 lg:bg-gray-400 xl:mx-6 xl:bg-gray-600" ```
1 parent ae5d26f commit c27b25a

File tree

3 files changed

+150
-15
lines changed

3 files changed

+150
-15
lines changed

src/tailwindModifiers.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const TAILWIND_PSEUDO_CLASSES = [
2+
'hover',
3+
'focus',
4+
'focus-within',
5+
'focus-visible',
6+
'active',
7+
'visited',
8+
'target',
9+
'first',
10+
'last',
11+
'only',
12+
'odd',
13+
'even',
14+
'first-of-type',
15+
'last-of-type',
16+
'only-of-type',
17+
'empty',
18+
'disabled',
19+
'checked',
20+
'indeterminate',
21+
'default',
22+
'required',
23+
'valid',
24+
'invalid',
25+
'in-range',
26+
'out-of-range',
27+
'placeholder-shown',
28+
'autofill',
29+
'read-only'
30+
]
31+
32+
const TAILWIND_PSEUDO_ELEMENTS = [
33+
'before',
34+
'after',
35+
'placeholder',
36+
'file',
37+
'marker',
38+
'selection',
39+
'first-line',
40+
'first-letter'
41+
]
42+
43+
const TAILWIND_RESPONSIVE_BREAKPOINTS = [
44+
'sm',
45+
'md',
46+
'lg',
47+
'xl',
48+
'2xl'
49+
]
50+
51+
const TAILWIND_MEDIA_QUERIES = [
52+
'dark',
53+
'motion-reduce',
54+
'motion-safe',
55+
'portrait',
56+
'landscape',
57+
'print'
58+
]
59+
60+
const TAILWIND_OTHER_MODIFIERS = [
61+
'ltr',
62+
'rtl',
63+
'open'
64+
]
65+
66+
export const TAILWIND_MODIFIERS = [
67+
...TAILWIND_PSEUDO_CLASSES,
68+
...TAILWIND_PSEUDO_ELEMENTS,
69+
...TAILWIND_RESPONSIVE_BREAKPOINTS,
70+
...TAILWIND_MEDIA_QUERIES,
71+
...TAILWIND_OTHER_MODIFIERS
72+
]
73+
74+
export default TAILWIND_MODIFIERS;

src/utils.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { LangConfig } from './extension';
2+
import { TAILWIND_MODIFIERS } from './tailwindModifiers';
23

34
export interface Options {
45
shouldRemoveDuplicates: boolean;
@@ -43,21 +44,51 @@ export const sortClassString = (
4344
return classArray.join(options.replacement || ' ').trim();
4445
};
4546

47+
const isTailwindClass = (el: string, sortOrder: string[]) => sortOrder.indexOf(el) !== -1
48+
const isTailwindModifierClass = (el: string, sortOrder: string[]) => el.includes(':') && TAILWIND_MODIFIERS.indexOf(el.split(':')[0]) !== -1 && sortOrder.indexOf(el.split(':')[1]) !== -1
49+
4650
const sortClassArray = (
4751
classArray: string[],
4852
sortOrder: string[],
4953
shouldPrependCustomClasses: boolean
50-
): string[] => [
51-
...classArray.filter(
52-
(el) => shouldPrependCustomClasses && sortOrder.indexOf(el) === -1
53-
), // append the classes that were not in the sort order if configured this way
54-
...classArray
55-
.filter((el) => sortOrder.indexOf(el) !== -1) // take the classes that are in the sort order
56-
.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b)), // and sort them
57-
...classArray.filter(
58-
(el) => !shouldPrependCustomClasses && sortOrder.indexOf(el) === -1
59-
), // prepend the classes that were not in the sort order if configured this way
60-
];
54+
): string[] => {
55+
const [tailwindClasses, allTailwindModifiersClasses, customClasses] = [
56+
classArray.filter(
57+
(el) => isTailwindClass(el, sortOrder)
58+
),
59+
classArray.filter(
60+
(el) => isTailwindModifierClass(el, sortOrder)
61+
),
62+
classArray.filter(
63+
(el) => !isTailwindClass(el, sortOrder) && !isTailwindModifierClass(el, sortOrder)
64+
),
65+
]
66+
67+
/**
68+
* This array contains the classes with tailwind modifiers, sorted first by modifiers
69+
* and then by the sort in sortOrder:
70+
*
71+
* input: "xl:mx-6 lg:mx-4 sm:bg-gray-200 hover:bg-blue-100 lg:bg-gray-400 hover:text-blue-100 xl:bg-gray-600 sm:mx-2"
72+
* output: "hover:text-blue-100 hover:bg-blue-100 sm:mx-2 sm:bg-gray-200 lg:mx-4 lg:bg-gray-400 xl:mx-6 xl:bg-gray-600"
73+
*
74+
* The Tailwind modifier order is defined in ./tailwindModifiers.ts
75+
*/
76+
const allSortedTailwindModifiersClasses = TAILWIND_MODIFIERS
77+
.map((modifier) =>
78+
allTailwindModifiersClasses.filter((el) => el.split(':')[0] === modifier)
79+
)
80+
.map((tailwindModifierClass) => tailwindModifierClass.sort((a, b) => sortOrder.indexOf(a.split(':')[1]) - sortOrder.indexOf(b.split(':')[1])))
81+
.reduce((allSortedTailwindModifiersClasses, sortedTailwindModifiersClasses) => {
82+
return allSortedTailwindModifiersClasses.concat(sortedTailwindModifiersClasses)
83+
}, [])
84+
85+
return [
86+
...(shouldPrependCustomClasses ? customClasses : []),
87+
...tailwindClasses.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b)),
88+
...allSortedTailwindModifiersClasses,
89+
...(!shouldPrependCustomClasses ? customClasses : []),
90+
]
91+
};
6192

6293
const removeDuplicates = (classArray: string[]): string[] => [
6394
...new Set(classArray),
@@ -94,8 +125,8 @@ function buildMatcher(value: LangConfig): Matcher {
94125
typeof value.regex === 'string'
95126
? [new RegExp(value.regex, 'gi')]
96127
: isArrayOfStrings(value.regex)
97-
? value.regex.map((v) => new RegExp(v, 'gi'))
98-
: [],
128+
? value.regex.map((v) => new RegExp(v, 'gi'))
129+
: [],
99130
separator:
100131
typeof value.separator === 'string'
101132
? new RegExp(value.separator, 'g')

tests/utils.spec.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,36 @@ describe('sortClassString', () => {
8686
expect(result).toBe(validClasses.join(replacement || ' '));
8787
}
8888
);
89+
90+
it('should sort classes with modifiers independently and append those to sorted classes', () => {
91+
const result = sortClassString(
92+
'xl:mx-6 bg-gray-100 lg:mx-4 mt-1 sm:bg-gray-200 hover:bg-blue-100 lg:bg-gray-400 hover:text-blue-100 xl:bg-gray-600 sm:mx-2',
93+
sortOrder,
94+
{
95+
shouldRemoveDuplicates: true,
96+
shouldPrependCustomClasses: false,
97+
customTailwindPrefix: '',
98+
}
99+
);
100+
expect(result).toBe(
101+
'mt-1 bg-gray-100 hover:text-blue-100 hover:bg-blue-100 sm:mx-2 sm:bg-gray-200 lg:mx-4 lg:bg-gray-400 xl:mx-6 xl:bg-gray-600'
102+
);
103+
});
104+
105+
it('should sort classes even if non-modifier classes are after modifier classes (issue #104)', () => {
106+
const result = sortClassString(
107+
'block w-full px-3 py-2 mb-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-blue-gray-500',
108+
sortOrder,
109+
{
110+
shouldRemoveDuplicates: true,
111+
shouldPrependCustomClasses: false,
112+
customTailwindPrefix: '',
113+
}
114+
);
115+
expect(result).toBe(
116+
'block px-3 py-2 mb-2 w-full text-blue-gray-500 bg-white rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 focus:outline-none sm:text-sm'
117+
);
118+
})
89119
});
90120

91121
describe('removeDuplicates', () => {
@@ -262,7 +292,7 @@ describe('extract className (jsx) string with single regex', () => {
262292
}`),
263293
classString,
264294
startPosition +
265-
`{ clsx(
295+
`{ clsx(
266296
foo,
267297
bar,
268298
'`.length,
@@ -285,7 +315,7 @@ describe('extract className (jsx) string with single regex', () => {
285315
}`),
286316
classString,
287317
startPosition +
288-
`{ clsx(
318+
`{ clsx(
289319
foo,
290320
bar,
291321
"`.length,

0 commit comments

Comments
 (0)