Skip to content

Commit 95a3d4e

Browse files
LokiWasHerethecrypticaceRobinMalfait
authored
Support regex matches for attributes and function names (#405)
* Add support for regex patterns in Tailwind attributes sorting * Remove debug logging for tailwindAttributes in getCustomizations function * Refactor wip * Update tests * Update readme * Update changelog * Refactor * Add support for Astro * Refactor * Add test * Support regex matching of function names * Cleanup tests * move tests * Update src/options.ts Co-authored-by: Robin Malfait <malfait.robin@gmail.com> --------- Co-authored-by: Loki <66067772+fahdlaabi@users.noreply.github.com> Co-authored-by: Jordan Pittman <jordan@cryptica.me> Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
1 parent a195f71 commit 95a3d4e

File tree

7 files changed

+257
-47
lines changed

7 files changed

+257
-47
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Fix whitespace removal inside nested concat and template expressions ([#396](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/396))
1919
- Fallback to Tailwind CSS v4 instead of v3 by default ([#390](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/390))
2020
- Support sorting in function calls in Twig ([#358](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/358))
21+
- Support regular expression patterns for attributes ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))
22+
- Support regular expression patterns for function names ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))
2123

2224
## [0.6.14] - 2025-07-09
2325

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,33 @@ function MyButton({ children }) {
9191
}
9292
```
9393

94+
### Using regex patterns
95+
96+
You can also use regular expressions to match multiple attributes. Patterns should be enclosed in forward slashes. Note that JS regex literals are not supported with Prettier:
97+
98+
```json5
99+
// .prettierrc
100+
{
101+
"tailwindAttributes": ["myClassList", "/data-.*/"]
102+
}
103+
```
104+
105+
This example will sort classes in the `myClassList` attribute as well as any attribute starting with `data-`:
106+
107+
```jsx
108+
function MyButton({ children }) {
109+
return (
110+
<button
111+
myClassList="rounded bg-blue-500 px-4 py-2 text-base text-white"
112+
data-theme="dark:bg-gray-800 bg-white"
113+
data-classes="flex items-center"
114+
>
115+
{children}
116+
</button>
117+
);
118+
}
119+
```
120+
94121
## Sorting classes in function calls
95122

96123
In addition to sorting classes in attributes, you can also sort classes in strings provided to function calls. This is useful when working with libraries like [clsx](https://github.com/lukeed/clsx) or [cva](https://cva.style/).
@@ -165,6 +192,10 @@ Once added, tag your strings with the function and the plugin will sort them:
165192
const mySortedClasses = tw`bg-white p-4 dark:bg-black`
166193
```
167194

195+
### Using regex patterns
196+
197+
Like `tailwindAttributes` this function all supports regular expressions to match multiple function names. Patterns should be enclosed in forward slashes. Note that JS regex literals are not supported with Prettier.
198+
168199
## Preserving whitespace
169200

170201
This plugin automatically removes unnecessary whitespace between classes to ensure consistent formatting. If you prefer to preserve whitespace, you can use the `tailwindPreserveWhitespace` option:

src/index.ts

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as prettierParserBabel from 'prettier/plugins/babel'
1111
// @ts-ignore
1212
import * as recast from 'recast'
1313
import { getTailwindConfig } from './config.js'
14-
import { getCustomizations } from './options.js'
14+
import { createMatcher, type Matcher } from './options.js'
1515
import { loadPlugins } from './plugins.js'
1616
import { sortClasses, sortClassList } from './sorting.js'
1717
import type { Customizations, StringChange, TransformerContext, TransformerEnv, TransformerMetadata } from './types'
@@ -30,6 +30,9 @@ function createParser(
3030
staticAttrs: new Set(meta.staticAttrs ?? []),
3131
dynamicAttrs: new Set(meta.dynamicAttrs ?? []),
3232
functions: new Set(meta.functions ?? []),
33+
staticAttrsRegex: [],
34+
dynamicAttrsRegex: [],
35+
functionsRegex: [],
3336
}
3437

3538
return {
@@ -53,12 +56,12 @@ function createParser(
5356
// @ts-ignore: We pass three options in the case of plugins that support Prettier 2 _and_ 3.
5457
let ast = await original.parse(text, options, options)
5558

56-
let customizations = getCustomizations(options, parserFormat, customizationDefaults)
59+
let matcher = createMatcher(options, parserFormat, customizationDefaults)
5760

5861
let changes: any[] = []
5962

6063
transform(ast, {
61-
env: { context, customizations, parsers: {}, options },
64+
env: { context, matcher, parsers: {}, options },
6265
changes,
6366
})
6467

@@ -158,7 +161,7 @@ function transformDynamicAngularAttribute(attr: any, env: TransformerEnv) {
158161
}
159162

160163
function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
161-
let { functions } = env.customizations
164+
let { matcher } = env
162165

163166
let ast = recast.parse(`let __prettier_temp__ = ${attr.value}`, {
164167
parser: prettierParserBabel.parsers['babel-ts'],
@@ -249,7 +252,7 @@ function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
249252
)
250253
})
251254

252-
if (isSortableTemplateExpression(path.node, functions)) {
255+
if (isSortableTemplateExpression(path.node, matcher)) {
253256
let sorted = sortTemplateLiteral(path.node.quasi, {
254257
env,
255258
collapseWhitespace: {
@@ -273,13 +276,13 @@ function transformDynamicJsAttribute(attr: any, env: TransformerEnv) {
273276
}
274277

275278
function transformHtml(ast: any, { env, changes }: TransformerContext) {
276-
let { staticAttrs, dynamicAttrs } = env.customizations
279+
let { matcher } = env
277280
let { parser } = env.options
278281

279282
for (let attr of ast.attrs ?? []) {
280-
if (staticAttrs.has(attr.name)) {
283+
if (matcher.hasStaticAttr(attr.name)) {
281284
attr.value = sortClasses(attr.value, { env })
282-
} else if (dynamicAttrs.has(attr.name)) {
285+
} else if (matcher.hasDynamicAttr(attr.name)) {
283286
if (!/[`'"]/.test(attr.value)) {
284287
continue
285288
}
@@ -298,11 +301,11 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) {
298301
}
299302

300303
function transformGlimmer(ast: any, { env }: TransformerContext) {
301-
let { staticAttrs } = env.customizations
304+
let { matcher } = env
302305

303306
visit(ast, {
304307
AttrNode(attr, _path, meta) {
305-
if (staticAttrs.has(attr.name) && attr.value) {
308+
if (matcher.hasStaticAttr(attr.name) && attr.value) {
306309
meta.sortTextNodes = true
307310
}
308311
},
@@ -354,12 +357,12 @@ function transformGlimmer(ast: any, { env }: TransformerContext) {
354357
}
355358

356359
function transformLiquid(ast: any, { env }: TransformerContext) {
357-
let { staticAttrs } = env.customizations
360+
let { matcher } = env
358361

359362
function isClassAttr(node: { name: string | { type: string; value: string }[] }) {
360363
return Array.isArray(node.name)
361-
? node.name.every((n) => n.type === 'TextNode' && staticAttrs.has(n.value))
362-
: staticAttrs.has(node.name)
364+
? node.name.every((n) => n.type === 'TextNode' && matcher.hasStaticAttr(n.value))
365+
: matcher.hasStaticAttr(node.name)
363366
}
364367

365368
function hasSurroundingQuotes(str: string) {
@@ -568,26 +571,26 @@ function sortTemplateLiteral(
568571

569572
function isSortableTemplateExpression(
570573
node: import('@babel/types').TaggedTemplateExpression | import('ast-types').namedTypes.TaggedTemplateExpression,
571-
functions: Set<string>,
574+
matcher: Matcher,
572575
): boolean {
573-
return isSortableExpression(node.tag, functions)
576+
return isSortableExpression(node.tag, matcher)
574577
}
575578

576579
function isSortableCallExpression(
577580
node: import('@babel/types').CallExpression | import('ast-types').namedTypes.CallExpression,
578-
functions: Set<string>,
581+
matcher: Matcher,
579582
): boolean {
580583
if (!node.arguments?.length) return false
581584

582-
return isSortableExpression(node.callee, functions)
585+
return isSortableExpression(node.callee, matcher)
583586
}
584587

585588
function isSortableExpression(
586589
node:
587590
| import('@babel/types').Expression
588591
| import('@babel/types').V8IntrinsicIdentifier
589592
| import('ast-types').namedTypes.ASTNode,
590-
functions: Set<string>,
593+
matcher: Matcher,
591594
): boolean {
592595
// Traverse property accesses and function calls to find the leading ident
593596
while (node.type === 'CallExpression' || node.type === 'MemberExpression') {
@@ -599,7 +602,7 @@ function isSortableExpression(
599602
}
600603

601604
if (node.type === 'Identifier') {
602-
return functions.has(node.name)
605+
return matcher.hasFunction(node.name)
603606
}
604607

605608
return false
@@ -653,7 +656,7 @@ function canCollapseWhitespaceIn(path: Path<import('@babel/types').Node, any>) {
653656
// We cross several parsers that share roughly the same shape so things are
654657
// good enough. The actual AST we should be using is probably estree + ts.
655658
function transformJavaScript(ast: import('@babel/types').Node, { env }: TransformerContext) {
656-
let { staticAttrs, functions } = env.customizations
659+
let { matcher } = env
657660

658661
function sortInside(ast: import('@babel/types').Node) {
659662
visit(ast, (node, path) => {
@@ -664,7 +667,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor
664667
} else if (node.type === 'TemplateLiteral') {
665668
sortTemplateLiteral(node, { env, collapseWhitespace })
666669
} else if (node.type === 'TaggedTemplateExpression') {
667-
if (isSortableTemplateExpression(node, functions)) {
670+
if (isSortableTemplateExpression(node, matcher)) {
668671
sortTemplateLiteral(node.quasi, { env, collapseWhitespace })
669672
}
670673
}
@@ -685,7 +688,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor
685688
return
686689
}
687690

688-
if (!staticAttrs.has(node.name.name)) {
691+
if (!matcher.hasStaticAttr(node.name.name)) {
689692
return
690693
}
691694

@@ -699,7 +702,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor
699702
CallExpression(node) {
700703
node = node as import('@babel/types').CallExpression
701704

702-
if (!isSortableCallExpression(node, functions)) {
705+
if (!isSortableCallExpression(node, matcher)) {
703706
return
704707
}
705708

@@ -709,7 +712,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor
709712
TaggedTemplateExpression(node, path) {
710713
node = node as import('@babel/types').TaggedTemplateExpression
711714

712-
if (!isSortableTemplateExpression(node, functions)) {
715+
if (!isSortableTemplateExpression(node, matcher)) {
713716
return
714717
}
715718

@@ -792,16 +795,16 @@ function transformCss(ast: any, { env }: TransformerContext) {
792795
}
793796

794797
function transformAstro(ast: any, { env, changes }: TransformerContext) {
795-
let { staticAttrs, dynamicAttrs } = env.customizations
798+
let { matcher } = env
796799

797800
if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') {
798801
for (let attr of ast.attributes ?? []) {
799-
if (staticAttrs.has(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') {
802+
if (matcher.hasStaticAttr(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') {
800803
attr.value = sortClasses(attr.value, {
801804
env,
802805
})
803806
} else if (
804-
dynamicAttrs.has(attr.name) &&
807+
matcher.hasDynamicAttr(attr.name) &&
805808
attr.type === 'attribute' &&
806809
attr.kind === 'expression' &&
807810
typeof attr.value === 'string'
@@ -817,7 +820,7 @@ function transformAstro(ast: any, { env, changes }: TransformerContext) {
817820
}
818821

819822
function transformMarko(ast: any, { env }: TransformerContext) {
820-
let { staticAttrs } = env.customizations
823+
let { matcher } = env
821824

822825
const nodesToVisit = [ast]
823826
while (nodesToVisit.length > 0) {
@@ -837,7 +840,7 @@ function transformMarko(ast: any, { env }: TransformerContext) {
837840
nodesToVisit.push(...currentNode.body)
838841
break
839842
case 'MarkoAttribute':
840-
if (!staticAttrs.has(currentNode.name)) break
843+
if (!matcher.hasStaticAttr(currentNode.name)) break
841844
switch (currentNode.value.type) {
842845
case 'ArrayExpression':
843846
const classList = currentNode.value.elements
@@ -859,25 +862,22 @@ function transformMarko(ast: any, { env }: TransformerContext) {
859862
}
860863

861864
function transformTwig(ast: any, { env, changes }: TransformerContext) {
862-
let { staticAttrs, functions } = env.customizations
865+
let { matcher } = env
863866

864867
for (let child of ast.expressions ?? []) {
865868
transformTwig(child, { env, changes })
866869
}
867870

868871
visit(ast, {
869872
Attribute(node, _path, meta) {
870-
if (!staticAttrs.has(node.name.name)) return
873+
if (!matcher.hasStaticAttr(node.name.name)) return
871874

872875
meta.sortTextNodes = true
873876
},
874877

875878
CallExpression(node, _path, meta) {
876879
// Traverse property accesses and function calls to find the *trailing* ident
877-
while (
878-
node.type === 'CallExpression' ||
879-
node.type === 'MemberExpression'
880-
) {
880+
while (node.type === 'CallExpression' || node.type === 'MemberExpression') {
881881
if (node.type === 'CallExpression') {
882882
node = node.callee
883883
} else if (node.type === 'MemberExpression') {
@@ -891,7 +891,7 @@ function transformTwig(ast: any, { env, changes }: TransformerContext) {
891891
}
892892

893893
if (node.type === 'Identifier') {
894-
if (!functions.has(node.name)) return
894+
if (!matcher.hasFunction(node.name)) return
895895
}
896896

897897
meta.sortTextNodes = true
@@ -923,7 +923,7 @@ function transformTwig(ast: any, { env, changes }: TransformerContext) {
923923
}
924924

925925
function transformPug(ast: any, { env }: TransformerContext) {
926-
let { staticAttrs } = env.customizations
926+
let { matcher } = env
927927

928928
// This isn't optimal
929929
// We should merge the classes together across class attributes and class tokens
@@ -932,7 +932,7 @@ function transformPug(ast: any, { env }: TransformerContext) {
932932

933933
// First sort the classes in attributes
934934
for (const token of ast.tokens) {
935-
if (token.type === 'attribute' && staticAttrs.has(token.name)) {
935+
if (token.type === 'attribute' && matcher.hasStaticAttr(token.name)) {
936936
token.val = [token.val.slice(0, 1), sortClasses(token.val.slice(1, -1), { env }), token.val.slice(-1)].join('')
937937
}
938938
}
@@ -977,10 +977,10 @@ function transformPug(ast: any, { env }: TransformerContext) {
977977
}
978978

979979
function transformSvelte(ast: any, { env, changes }: TransformerContext) {
980-
let { staticAttrs } = env.customizations
980+
let { matcher } = env
981981

982982
for (let attr of ast.attributes ?? []) {
983-
if (!staticAttrs.has(attr.name) || attr.type !== 'Attribute') {
983+
if (!matcher.hasStaticAttr(attr.name) || attr.type !== 'Attribute') {
984984
continue
985985
}
986986

0 commit comments

Comments
 (0)