From c27b25a16d05dc388c74f6db0e21eb1bd94d6353 Mon Sep 17 00:00:00 2001 From: Nicolas Goutay Date: Fri, 31 Dec 2021 15:03:38 +0100 Subject: [PATCH] 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" ``` --- src/tailwindModifiers.ts | 74 ++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 57 ++++++++++++++++++++++++------- tests/utils.spec.ts | 34 ++++++++++++++++-- 3 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 src/tailwindModifiers.ts diff --git a/src/tailwindModifiers.ts b/src/tailwindModifiers.ts new file mode 100644 index 0000000..7bc4b5c --- /dev/null +++ b/src/tailwindModifiers.ts @@ -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; \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts index 04bc54a..c750c0d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ import { LangConfig } from './extension'; +import { TAILWIND_MODIFIERS } from './tailwindModifiers'; export interface Options { shouldRemoveDuplicates: boolean; @@ -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), @@ -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') diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 46c01f6..b44a70d 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -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', () => { @@ -262,7 +292,7 @@ describe('extract className (jsx) string with single regex', () => { }`), classString, startPosition + - `{ clsx( + `{ clsx( foo, bar, '`.length, @@ -285,7 +315,7 @@ describe('extract className (jsx) string with single regex', () => { }`), classString, startPosition + - `{ clsx( + `{ clsx( foo, bar, "`.length,