diff --git a/README.md b/README.md index 0ecf87c..8d39c05 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,13 @@ If a local configuration file cannot be found the plugin will fallback to the de By default this plugin sorts classes in the `class` attribute, any framework-specific equivalents like `className`, `:class`, `[ngClass]`, and any Tailwind `@apply` directives. -You can sort additional attributes using the `tailwindAttributes` option, which takes an array of attribute names: +You can extend this behavior to sort classes in any attribute using the following options: + +- `tailwindAttributes`: An array of exact attribute names to sort. +- `tailwindAttributesStartWith`: An array of prefixes to match attributes that begin with a certain string. +- `tailwindAttributesEndWith`: An array of suffixes to match attributes that end with a certain string. + +#### Example 1 ```json5 // .prettierrc @@ -91,6 +97,27 @@ function MyButton({ children }) { } ``` +#### Example 2 + +```json5 +// .prettierrc +{ + "tailwindAttributesEndWith": ["ClassName"] +} +``` + +With this configuration, any class found with suffix `ClassName` will be sorted: + +```jsx +function MyButton({ children }) { + return ( + + ); +} +``` + ## Sorting classes in function calls 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/). diff --git a/src/index.ts b/src/index.ts index fc1cbc0..d241e06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,8 @@ function createParser( ) { let customizationDefaults: Customizations = { staticAttrs: new Set(meta.staticAttrs ?? []), + prefixAttrs: new Set(meta.prefixAttrs ?? []), + suffixAttrs: new Set(meta.suffixAttrs ?? []), dynamicAttrs: new Set(meta.dynamicAttrs ?? []), functions: new Set(meta.functions ?? []), } @@ -273,13 +275,11 @@ function transformDynamicJsAttribute(attr: any, env: TransformerEnv) { } function transformHtml(ast: any, { env, changes }: TransformerContext) { - let { staticAttrs, dynamicAttrs } = env.customizations + let { dynamicAttrs } = env.customizations let { parser } = env.options for (let attr of ast.attrs ?? []) { - if (staticAttrs.has(attr.name)) { - attr.value = sortClasses(attr.value, { env }) - } else if (dynamicAttrs.has(attr.name)) { + if (dynamicAttrs.has(attr.name)) { if (!/[`'"]/.test(attr.value)) { continue } @@ -289,6 +289,8 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) { } else { transformDynamicJsAttribute(attr, env) } + } else if (isSortableAttribute(attr.name, env.customizations)) { + attr.value = sortClasses(attr.value, { env }) } } @@ -298,11 +300,9 @@ function transformHtml(ast: any, { env, changes }: TransformerContext) { } function transformGlimmer(ast: any, { env }: TransformerContext) { - let { staticAttrs } = env.customizations - visit(ast, { AttrNode(attr, _path, meta) { - if (staticAttrs.has(attr.name) && attr.value) { + if (isSortableAttribute(attr.name, env.customizations) && attr.value) { meta.sortTextNodes = true } }, @@ -354,12 +354,10 @@ function transformGlimmer(ast: any, { env }: TransformerContext) { } function transformLiquid(ast: any, { env }: TransformerContext) { - let { staticAttrs } = env.customizations - function isClassAttr(node: { name: string | { type: string; value: string }[] }) { return Array.isArray(node.name) - ? node.name.every((n) => n.type === 'TextNode' && staticAttrs.has(n.value)) - : staticAttrs.has(node.name) + ? node.name.every((n) => n.type === 'TextNode' && isSortableAttribute(n.value, env.customizations)) + : isSortableAttribute(node.name, env.customizations) } function hasSurroundingQuotes(str: string) { @@ -566,6 +564,22 @@ function sortTemplateLiteral( return didChange } +function isSortableAttribute(name: string, customizations: Customizations) { + const { staticAttrs, suffixAttrs, prefixAttrs } = customizations + + if (staticAttrs.has(name)) return true + + for (const prefix of prefixAttrs) { + if (name.startsWith(prefix)) return true + } + + for (const suffix of suffixAttrs) { + if (name.endsWith(suffix)) return true + } + + return false +} + function isSortableTemplateExpression( node: import('@babel/types').TaggedTemplateExpression | import('ast-types').namedTypes.TaggedTemplateExpression, functions: Set, @@ -653,7 +667,7 @@ function canCollapseWhitespaceIn(path: Path) { // We cross several parsers that share roughly the same shape so things are // good enough. The actual AST we should be using is probably estree + ts. function transformJavaScript(ast: import('@babel/types').Node, { env }: TransformerContext) { - let { staticAttrs, functions } = env.customizations + let { functions } = env.customizations function sortInside(ast: import('@babel/types').Node) { visit(ast, (node, path) => { @@ -685,7 +699,7 @@ function transformJavaScript(ast: import('@babel/types').Node, { env }: Transfor return } - if (!staticAttrs.has(node.name.name)) { + if (!isSortableAttribute(node.name.name, env.customizations)) { return } @@ -787,11 +801,11 @@ function transformCss(ast: any, { env }: TransformerContext) { } function transformAstro(ast: any, { env, changes }: TransformerContext) { - let { staticAttrs, dynamicAttrs } = env.customizations + let { dynamicAttrs } = env.customizations if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') { for (let attr of ast.attributes ?? []) { - if (staticAttrs.has(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') { + if (isSortableAttribute(attr.name, env.customizations) && attr.type === 'attribute' && attr.kind === 'quoted') { attr.value = sortClasses(attr.value, { env, }) @@ -812,8 +826,6 @@ function transformAstro(ast: any, { env, changes }: TransformerContext) { } function transformMarko(ast: any, { env }: TransformerContext) { - let { staticAttrs } = env.customizations - const nodesToVisit = [ast] while (nodesToVisit.length > 0) { const currentNode = nodesToVisit.pop() @@ -832,7 +844,7 @@ function transformMarko(ast: any, { env }: TransformerContext) { nodesToVisit.push(...currentNode.body) break case 'MarkoAttribute': - if (!staticAttrs.has(currentNode.name)) break + if (!isSortableAttribute(currentNode.name, env.customizations)) break switch (currentNode.value.type) { case 'ArrayExpression': const classList = currentNode.value.elements @@ -854,15 +866,13 @@ function transformMarko(ast: any, { env }: TransformerContext) { } function transformTwig(ast: any, { env, changes }: TransformerContext) { - let { staticAttrs } = env.customizations - for (let child of ast.expressions ?? []) { transformTwig(child, { env, changes }) } visit(ast, { Attribute(node, _path, meta) { - if (!staticAttrs.has(node.name.name)) return + if (!isSortableAttribute(node.name.name, env.customizations)) return meta.sortTextNodes = true }, @@ -893,8 +903,6 @@ function transformTwig(ast: any, { env, changes }: TransformerContext) { } function transformPug(ast: any, { env }: TransformerContext) { - let { staticAttrs } = env.customizations - // This isn't optimal // We should merge the classes together across class attributes and class tokens // And then we sort them @@ -902,7 +910,7 @@ function transformPug(ast: any, { env }: TransformerContext) { // First sort the classes in attributes for (const token of ast.tokens) { - if (token.type === 'attribute' && staticAttrs.has(token.name)) { + if (token.type === 'attribute' && isSortableAttribute(token.name, env.customizations)) { token.val = [token.val.slice(0, 1), sortClasses(token.val.slice(1, -1), { env }), token.val.slice(-1)].join('') } } @@ -947,10 +955,8 @@ function transformPug(ast: any, { env }: TransformerContext) { } function transformSvelte(ast: any, { env, changes }: TransformerContext) { - let { staticAttrs } = env.customizations - for (let attr of ast.attributes ?? []) { - if (!staticAttrs.has(attr.name) || attr.type !== 'Attribute') { + if (!isSortableAttribute(attr.name, env.customizations) || attr.type !== 'Attribute') { continue } @@ -1238,6 +1244,16 @@ export interface PluginOptions { */ tailwindAttributes?: string[] + /** + * List of prefixes to match attributes that contain classes. + */ + tailwindAttributesStartWith?: string[] + + /** + * List of suffixes to match attributes that contain classes. + */ + tailwindAttributesEndWith?: string[] + /** * Preserve whitespace around Tailwind classes when sorting. */ diff --git a/src/options.ts b/src/options.ts index 98cd138..4c38319 100644 --- a/src/options.ts +++ b/src/options.ts @@ -31,6 +31,22 @@ export const options: Record = { description: 'List of attributes/props that contain sortable Tailwind classes', }, + tailwindAttributesStartWith: { + type: 'string', + array: true, + default: [{ value: [] }], + category: 'Tailwind CSS', + description: 'List of prefixes for attributes that contain sortable Tailwind classes', + }, + + tailwindAttributesEndWith: { + type: 'string', + array: true, + default: [{ value: [] }], + category: 'Tailwind CSS', + description: 'List of suffixes for attributes that contain sortable Tailwind classes', + }, + tailwindFunctions: { type: 'string', array: true, @@ -63,6 +79,8 @@ export const options: Record = { export function getCustomizations(options: RequiredOptions, parser: string, defaults: Customizations): Customizations { let staticAttrs = new Set(defaults.staticAttrs) + let prefixAttrs = new Set(defaults.prefixAttrs) + let suffixAttrs = new Set(defaults.suffixAttrs) let dynamicAttrs = new Set(defaults.dynamicAttrs) let functions = new Set(defaults.functions) @@ -96,9 +114,19 @@ export function getCustomizations(options: RequiredOptions, parser: string, defa functions.add(fn) } + for (let attr of options.tailwindAttributesStartWith ?? []) { + prefixAttrs.add(attr) + } + + for (let attr of options.tailwindAttributesEndWith ?? []) { + suffixAttrs.add(attr) + } + return { functions, staticAttrs, dynamicAttrs, + prefixAttrs, + suffixAttrs, } } diff --git a/src/types.ts b/src/types.ts index 763aab0..87392fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,12 +4,16 @@ export interface TransformerMetadata { // Default customizations for a given transformer functions?: string[] staticAttrs?: string[] + prefixAttrs?: string[] + suffixAttrs?: string[] dynamicAttrs?: string[] } export interface Customizations { functions: Set staticAttrs: Set + prefixAttrs: Set + suffixAttrs: Set dynamicAttrs: Set }