Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/rules/enforce-logical-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ The rule reports physical classes and auto-fixes them to their logical equivalen
| `float-right` | `float-end` |
| `clear-left` | `clear-start` |
| `clear-right` | `clear-end` |
| `h-*` | `block-*` |
| `w-*` | `inline-*` |
| `min-h-*` | `min-block-*` |
| `min-w-*` | `min-inline-*` |
| `max-h-*` | `max-block-*` |
| `max-w-*` | `max-inline-*` |
| `size-*` | `block-* inline-*` |

<br/>

Expand Down
47 changes: 46 additions & 1 deletion src/rules/enforce-logical-properties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,14 @@ const testCases = [
["float-left", "float-start"],
["float-right", "float-end"],
["clear-left", "clear-start"],
["clear-right", "clear-end"]
["clear-right", "clear-end"],

["h-4", "block-4"],
["w-4", "inline-4"],
["min-h-4", "min-block-4"],
["min-w-4", "min-inline-4"],
["max-h-4", "max-block-4"],
["max-w-4", "max-inline-4"]
] satisfies [string, string][];

describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name, () => {
Expand Down Expand Up @@ -136,6 +143,20 @@ describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name
vue: `<template><img class="text-right!" /></template>`,
vueOutput: `<template><img class="text-end!" /></template>`,

errors: 1
},
{
angular: `<img class="size-4!" />`,
angularOutput: `<img class="block-4! inline-4!" />`,
html: `<img class="size-4!" />`,
htmlOutput: `<img class="block-4! inline-4!" />`,
jsx: `() => <img class="size-4!" />`,
jsxOutput: `() => <img class="block-4! inline-4!" />`,
svelte: `<img class="size-4!" />`,
svelteOutput: `<img class="block-4! inline-4!" />`,
vue: `<template><img class="size-4!" /></template>`,
vueOutput: `<template><img class="block-4! inline-4!" /></template>`,

errors: 1
}
]
Expand Down Expand Up @@ -247,4 +268,28 @@ describe.runIf(getTailwindCSSVersion().major >= 4)(enforceLogicalProperties.name
);
});

it("should split size classes into logical block and inline classes", () => {
lint(
enforceLogicalProperties,
{
invalid: [
{
angular: `<img class="size-4 hover:size-[12px]" />`,
angularOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
html: `<img class="size-4 hover:size-[12px]" />`,
htmlOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
jsx: `() => <img class="size-4 hover:size-[12px]" />`,
jsxOutput: `() => <img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
svelte: `<img class="size-4 hover:size-[12px]" />`,
svelteOutput: `<img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" />`,
vue: `<template><img class="size-4 hover:size-[12px]" /></template>`,
vueOutput: `<template><img class="block-4 inline-4 hover:block-[12px] hover:inline-[12px]" /></template>`,

errors: 2
}
]
}
);
});

});
41 changes: 28 additions & 13 deletions src/rules/enforce-logical-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const enforceLogicalProperties = createRule({
recommended: false,

messages: {
replaceable: "Physical class detected. Replace \"{{ className }}\" with logical class \"{{fix}}\"."
multiple: "Physical class detected. Replace \"{{ className }}\" with logical classes \"{{fix}}\".",
single: "Physical class detected. Replace \"{{ className }}\" with logical class \"{{fix}}\"."
},

initialize: ctx => {
Expand Down Expand Up @@ -82,8 +83,16 @@ const mappings = [
[/^float-left$/, "float-start"],
[/^float-right$/, "float-end"],
[/^clear-left$/, "clear-start"],
[/^clear-right$/, "clear-end"]
] satisfies [before: RegExp, after: string][];
[/^clear-right$/, "clear-end"],

[/^h-(.*)$/, "block-$1"],
[/^w-(.*)$/, "inline-$1"],
[/^min-h-(.*)$/, "min-block-$1"],
[/^min-w-(.*)$/, "min-inline-$1"],
[/^max-h-(.*)$/, "max-block-$1"],
[/^max-w-(.*)$/, "max-inline-$1"],
[/^size-(.*)$/, ["block-$1", "inline-$1"]]
] satisfies [before: RegExp, after: string[] | string][];


function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: Literal[]) {
Expand All @@ -93,13 +102,13 @@ function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: L
const { dissectedClasses, warnings } = getDissectedClasses(async(ctx), classes);

const possibleFixes = Object.values(dissectedClasses).flatMap(dissectedClass => {
const replacementBase = getReplacementBase(dissectedClass.base);
const replacementBases = getReplacementBases(dissectedClass.base);

if(!replacementBase){
if(!replacementBases){
return [];
}

return [buildClass(ctx, { ...dissectedClass, base: replacementBase })];
return replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));
});

const { unknownClasses } = getUnknownClasses(async(ctx), possibleFixes);
Expand All @@ -111,39 +120,45 @@ function lintLiterals(ctx: Context<typeof enforceLogicalProperties>, literals: L
return;
}

const replacementBase = getReplacementBase(dissectedClass.base);
const replacementBases = getReplacementBases(dissectedClass.base);

if(!replacementBase){
if(!replacementBases){
return;
}

const fix = buildClass(ctx, { ...dissectedClass, base: replacementBase });
const fixClasses = replacementBases.map(base => buildClass(ctx, { ...dissectedClass, base }));
const hasUnknownFix = fixClasses.some(fixClass => unknownClasses.includes(fixClass));

if(unknownClasses.includes(fix)){
if(hasUnknownFix){
return;
}

const fix = fixClasses.join(" ");
const id = fixClasses.length > 1 ? "multiple" : "single";

return {
data: {
className,
fix
},
fix,
id: "replaceable",
id,
warnings
} as const;
});
}
}

function getReplacementBase(base: string) {
function getReplacementBases(base: string): string[] | undefined {
for(const [pattern, replacement] of mappings){
const match = base.match(pattern);

if(!match){
continue;
}

return replacePlaceholders(replacement, match);
return Array.isArray(replacement)
? replacement.map(part => replacePlaceholders(part, match))
: [replacePlaceholders(replacement, match)];
}
}
Loading