Skip to content

Commit

Permalink
Add sorting for modifier classes
Browse files Browse the repository at this point in the history
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"
```
  • Loading branch information
phacks committed Dec 31, 2021
1 parent ae5d26f commit c27b25a
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 15 deletions.
74 changes: 74 additions & 0 deletions src/tailwindModifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const TAILWIND_PSEUDO_CLASSES = [
'hover',
'focus',
'focus-within',
'focus-visible',
'active',
'visited',
'target',
'first',
'last',
'only',
'odd',
'even',
'first-of-type',
'last-of-type',
'only-of-type',
'empty',
'disabled',
'checked',
'indeterminate',
'default',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'placeholder-shown',
'autofill',
'read-only'
]

const TAILWIND_PSEUDO_ELEMENTS = [
'before',
'after',
'placeholder',
'file',
'marker',
'selection',
'first-line',
'first-letter'
]

const TAILWIND_RESPONSIVE_BREAKPOINTS = [
'sm',
'md',
'lg',
'xl',
'2xl'
]

const TAILWIND_MEDIA_QUERIES = [
'dark',
'motion-reduce',
'motion-safe',
'portrait',
'landscape',
'print'
]

const TAILWIND_OTHER_MODIFIERS = [
'ltr',
'rtl',
'open'
]

export const TAILWIND_MODIFIERS = [
...TAILWIND_PSEUDO_CLASSES,
...TAILWIND_PSEUDO_ELEMENTS,
...TAILWIND_RESPONSIVE_BREAKPOINTS,
...TAILWIND_MEDIA_QUERIES,
...TAILWIND_OTHER_MODIFIERS
]

export default TAILWIND_MODIFIERS;
57 changes: 44 additions & 13 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { LangConfig } from './extension';
import { TAILWIND_MODIFIERS } from './tailwindModifiers';

export interface Options {
shouldRemoveDuplicates: boolean;
Expand Down Expand Up @@ -43,21 +44,51 @@ export const sortClassString = (
return classArray.join(options.replacement || ' ').trim();
};

const isTailwindClass = (el: string, sortOrder: string[]) => sortOrder.indexOf(el) !== -1
const isTailwindModifierClass = (el: string, sortOrder: string[]) => el.includes(':') && TAILWIND_MODIFIERS.indexOf(el.split(':')[0]) !== -1 && sortOrder.indexOf(el.split(':')[1]) !== -1

const sortClassArray = (
classArray: string[],
sortOrder: string[],
shouldPrependCustomClasses: boolean
): string[] => [
...classArray.filter(
(el) => shouldPrependCustomClasses && sortOrder.indexOf(el) === -1
), // append the classes that were not in the sort order if configured this way
...classArray
.filter((el) => sortOrder.indexOf(el) !== -1) // take the classes that are in the sort order
.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b)), // and sort them
...classArray.filter(
(el) => !shouldPrependCustomClasses && sortOrder.indexOf(el) === -1
), // prepend the classes that were not in the sort order if configured this way
];
): string[] => {
const [tailwindClasses, allTailwindModifiersClasses, customClasses] = [
classArray.filter(
(el) => isTailwindClass(el, sortOrder)
),
classArray.filter(
(el) => isTailwindModifierClass(el, sortOrder)
),
classArray.filter(
(el) => !isTailwindClass(el, sortOrder) && !isTailwindModifierClass(el, sortOrder)
),
]

/**
* This array contains the classes with tailwind modifiers, sorted first by modifiers
* and then by the sort in sortOrder:
*
* 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"
* 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"
*
* The Tailwind modifier order is defined in ./tailwindModifiers.ts
*/
const allSortedTailwindModifiersClasses = TAILWIND_MODIFIERS
.map((modifier) =>
allTailwindModifiersClasses.filter((el) => el.split(':')[0] === modifier)
)
.map((tailwindModifierClass) => tailwindModifierClass.sort((a, b) => sortOrder.indexOf(a.split(':')[1]) - sortOrder.indexOf(b.split(':')[1])))
.reduce((allSortedTailwindModifiersClasses, sortedTailwindModifiersClasses) => {
return allSortedTailwindModifiersClasses.concat(sortedTailwindModifiersClasses)
}, [])

return [
...(shouldPrependCustomClasses ? customClasses : []),
...tailwindClasses.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b)),
...allSortedTailwindModifiersClasses,
...(!shouldPrependCustomClasses ? customClasses : []),
]
};

const removeDuplicates = (classArray: string[]): string[] => [
...new Set(classArray),
Expand Down Expand Up @@ -94,8 +125,8 @@ function buildMatcher(value: LangConfig): Matcher {
typeof value.regex === 'string'
? [new RegExp(value.regex, 'gi')]
: isArrayOfStrings(value.regex)
? value.regex.map((v) => new RegExp(v, 'gi'))
: [],
? value.regex.map((v) => new RegExp(v, 'gi'))
: [],
separator:
typeof value.separator === 'string'
? new RegExp(value.separator, 'g')
Expand Down
34 changes: 32 additions & 2 deletions tests/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,36 @@ describe('sortClassString', () => {
expect(result).toBe(validClasses.join(replacement || ' '));
}
);

it('should sort classes with modifiers independently and append those to sorted classes', () => {
const result = sortClassString(
'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',
sortOrder,
{
shouldRemoveDuplicates: true,
shouldPrependCustomClasses: false,
customTailwindPrefix: '',
}
);
expect(result).toBe(
'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'
);
});

it('should sort classes even if non-modifier classes are after modifier classes (issue #104)', () => {
const result = sortClassString(
'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',
sortOrder,
{
shouldRemoveDuplicates: true,
shouldPrependCustomClasses: false,
customTailwindPrefix: '',
}
);
expect(result).toBe(
'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'
);
})
});

describe('removeDuplicates', () => {
Expand Down Expand Up @@ -262,7 +292,7 @@ describe('extract className (jsx) string with single regex', () => {
}`),
classString,
startPosition +
`{ clsx(
`{ clsx(
foo,
bar,
'`.length,
Expand All @@ -285,7 +315,7 @@ describe('extract className (jsx) string with single regex', () => {
}`),
classString,
startPosition +
`{ clsx(
`{ clsx(
foo,
bar,
"`.length,
Expand Down

0 comments on commit c27b25a

Please sign in to comment.