diff --git a/package.json b/package.json index 6552a6a..b270ef6 100755 --- a/package.json +++ b/package.json @@ -5018,10 +5018,10 @@ "default": { "html": "\\bclass\\s*=\\s*[\\\"\\']([_a-zA-Z0-9\\s\\-\\:\\/]+)[\\\"\\']", "css": "\\B@apply\\s+([_a-zA-Z0-9\\s\\-\\:\\/]+);", - "javascript": "(?:\\bclassName\\s*=\\s*[\\\"\\']([_a-zA-Z0-9\\s\\-\\:\\/]+)[\\\"\\'])|(?:\\btw\\s*`([_a-zA-Z0-9\\s\\-\\:\\/]*)`)", - "javascriptreact": "(?:\\bclassName\\s*=\\s*[\\\"\\']([_a-zA-Z0-9\\s\\-\\:\\/]+)[\\\"\\'])|(?:\\btw\\s*`([_a-zA-Z0-9\\s\\-\\:\\/]*)`)", - "typescript": "(?:\\bclassName\\s*=\\s*[\\\"\\']([_a-zA-Z0-9\\s\\-\\:\\/]+)[\\\"\\'])|(?:\\btw\\s*`([_a-zA-Z0-9\\s\\-\\:\\/]*)`)", - "typescriptreact": "(?:\\bclassName\\s*=\\s*[\\\"\\']([_a-zA-Z0-9\\s\\-\\:\\/]+)[\\\"\\'])|(?:\\btw\\s*`([_a-zA-Z0-9\\s\\-\\:\\/]*)`)" + "javascript": "(?:\\bclass(?:Name)?\\s*=[\\w\\d\\s_,{}\\(\\)\\[\\]]*[\"'`]([\\w\\d\\s_\\-\\:\\/${}]+)[\"'`][\\w\\d\\s_,{}\\(\\)\\[\\]]*)|(?:\\btw\\s*`([\\w\\d\\s_\\-\\:\\/]*)`)", + "javascriptreact": "(?:\\bclass(?:Name)?\\s*=[\\w\\d\\s_,{}\\(\\)\\[\\]]*[\"'`]([\\w\\d\\s_\\-\\:\\/${}]+)[\"'`][\\w\\d\\s_,{}\\(\\)\\[\\]]*)|(?:\\btw\\s*`([\\w\\d\\s_\\-\\:\\/]*)`)", + "typescript": "(?:\\bclass(?:Name)?\\s*=[\\w\\d\\s_,{}\\(\\)\\[\\]]*[\"'`]([\\w\\d\\s_\\-\\:\\/${}]+)[\"'`][\\w\\d\\s_,{}\\(\\)\\[\\]]*)|(?:\\btw\\s*`([\\w\\d\\s_\\-\\:\\/]*)`)", + "typescriptreact": "(?:\\bclass(?:Name)?\\s*=[\\w\\d\\s_,{}\\(\\)\\[\\]]*[\"'`]([\\w\\d\\s_\\-\\:\\/${}]+)[\"'`][\\w\\d\\s_,{}\\(\\)\\[\\]]*)|(?:\\btw\\s*`([\\w\\d\\s_\\-\\:\\/]*)`)" }, "description": "An object with language IDs as keys and their values determining the regex to search for Tailwind CSS classes.", "scope": "window" diff --git a/src/extension.ts b/src/extension.ts index 3510bd7..158909e 100755 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, workspace, ExtensionContext, Range, window } from 'vscode'; -import { sortClassString } from './utils'; +import { sortClassString, getClassMatch } from './utils'; import { spawn } from 'child_process'; import { rustyWindPath } from 'rustywind'; @@ -39,39 +39,35 @@ export function activate(context: ExtensionContext) { const editorText = editor.document.getText(); const editorLangId = editor.document.languageId; - const classWrapperRegex = new RegExp(configRegex[editorLangId] || configRegex['html'], 'gi'); - let classWrapper: RegExpExecArray | null; - while ( - (classWrapper = classWrapperRegex.exec(editorText)) !== null - ) { - const wrapperMatch = classWrapper[0]; - const valueMatchIndex = classWrapper.findIndex((match, idx) => idx !== 0 && match); - const valueMatch = classWrapper[valueMatchIndex]; - - const startPosition = - classWrapper.index + wrapperMatch.lastIndexOf(valueMatch); - const endPosition = startPosition + valueMatch.length; - - const range = new Range( - editor.document.positionAt(startPosition), - editor.document.positionAt(endPosition) - ); - - const options = { - shouldRemoveDuplicates, - shouldPrependCustomClasses, - customTailwindPrefix - }; - - edit.replace( - range, - sortClassString( - valueMatch, - Array.isArray(sortOrder) ? sortOrder : [], - options - ) - ); - } + getClassMatch( + configRegex[editorLangId] || configRegex['html'], + editorText, + (classWrapper, wrapperMatch, valueMatch) => { + const startPosition = + classWrapper.index + wrapperMatch.lastIndexOf(valueMatch); + const endPosition = startPosition + valueMatch.length; + + const range = new Range( + editor.document.positionAt(startPosition), + editor.document.positionAt(endPosition) + ); + + const options = { + shouldRemoveDuplicates, + shouldPrependCustomClasses, + customTailwindPrefix, + }; + + edit.replace( + range, + sortClassString( + valueMatch, + Array.isArray(sortOrder) ? sortOrder : [], + options + ) + ); + } + ); } ); diff --git a/src/utils.ts b/src/utils.ts index 1bfcc92..acbff74 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -50,3 +50,25 @@ const sortClassArray = ( const removeDuplicates = (classArray: string[]): string[] => [ ...new Set(classArray) ]; + +export function getClassMatch( + regex: string, + editorText: string, + callback: ( + classWrapper: RegExpExecArray, + wrapperMatch: string, + valueMatch: string + ) => void +) { + const classWrapperRegex = new RegExp(regex, 'gi'); + let classWrapper: RegExpExecArray | null; + while ((classWrapper = classWrapperRegex.exec(editorText)) !== null) { + const wrapperMatch = classWrapper[0]; + const valueMatchIndex = classWrapper.findIndex( + (match, idx) => idx !== 0 && match + ); + const valueMatch = classWrapper[valueMatchIndex]; + + callback(classWrapper, wrapperMatch, valueMatch); + } +} diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 965f1c6..4db3806 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -1,4 +1,4 @@ -import { sortClassString } from '../src/utils'; +import { sortClassString, getClassMatch } from '../src/utils'; import 'jest'; import * as _ from 'lodash'; @@ -76,3 +76,172 @@ describe('removeDuplicates', () => { ); }); }); + +describe('extract className (jsx) string', () => { + const configRegex: { [key: string]: string } = + pjson.contributes.configuration[0].properties['headwind.classRegex'] + .default; + + const jsxLanguages = [ + 'javascript', + 'javascriptreact', + 'typescript', + 'typescriptreact', + ]; + + const classString = 'w-64 h-full bg-blue-400 relative'; + + const generateEditorText = (classNameString: string) => ` + export const Layout = ({ children }) => ( +
+
+
{children}
+
+ )`; + + const multiLineClassString = ` + w-64 + h-full + bg-blue-400 + relative + `; + it.each([ + [ + 'simple single quotes', + generateEditorText(`'${classString}'`), + classString, + ], + [ + 'simple double quotes', + generateEditorText(`"${classString}"`), + classString, + ], + [ + 'curly braces around single quotes', + generateEditorText(`{ '${classString}' }`), + classString, + ], + [ + 'curly braces around double quotes', + generateEditorText(`{ "${classString}" }`), + classString, + ], + [ + 'simple clsx single quotes', + generateEditorText(`{ clsx('${classString}' }`), + classString, + ], + [ + 'simple clsx double quotes', + generateEditorText(`{ clsx("${classString}" }`), + classString, + ], + [ + 'simple classname single quotes', + generateEditorText(`{ classname('${classString}' }`), + classString, + ], + [ + 'simple classname double quotes', + generateEditorText(`{ classname("${classString}" }`), + classString, + ], + [ + 'simple foo func single quotes', + generateEditorText(`{ foo('${classString}' }`), + classString, + ], + [ + 'simple foo func double quotes', + generateEditorText(`{ foo("${classString}" }`), + classString, + ], + [ + 'foo func multi str single quotes (only extracts first string)', + generateEditorText(`{ foo('${classString}', 'class1 class2' }`), + classString, + ], + [ + 'foo func multi str double quotes (only extracts first string)', + generateEditorText(`{ foo("${classString}", "class1, class2" }`), + classString, + ], + [ + 'foo func multi var single quotes', + generateEditorText(`{ clsx(foo, bar, '${classString}', foo, bar }`), + classString, + ], + [ + 'foo func multi var double quotes', + generateEditorText(`{ clsx(foo, bar, "${classString}", foo, bar }`), + classString, + ], + [ + 'foo func multi var multi str single quotes', + generateEditorText( + `{ clsx(foo, bar, '${classString}', foo, 'class1 class2', bar }` + ), + classString, + ], + [ + 'foo func multi var multi str double quotes', + generateEditorText( + `{ clsx(foo, bar, "${classString}", foo, "class1 class2", bar }` + ), + classString, + ], + [ + 'complex foo func single quotes multi lines', + generateEditorText(` + { clsx( + foo, + bar, + '${classString}', + foo, + 'class1 class2', + bar + }`), + classString, + ], + [ + 'simple multi line double quotes', + generateEditorText(multiLineClassString), + multiLineClassString, + ], + [ + 'complex foo func double quotes multi lines', + generateEditorText(` + { clsx( + foo, + bar, + "${classString}", + foo, + "class1 class2", + bar + }`), + classString, + ], + [ + 'class attribute', + `class="${classString}"`, + classString + ], + [ + 'string literal', + `export function FormGroup({className = '', ...props}) { + return
+ }`, + `${classString} \$\{className\}` + ] + ])('%s', (testName, editorText, expectedValueMatch) => { + for (const jsxLanguage of jsxLanguages) { + getClassMatch( + configRegex[jsxLanguage], + editorText, + (classWrapper, wrapperMatch, valueMatch) => { + expect(valueMatch).toBe(expectedValueMatch); + } + ); + } + }); +});