Skip to content

Commit

Permalink
feat: add prefer-logical-properties rule
Browse files Browse the repository at this point in the history
  • Loading branch information
azat-io committed Feb 22, 2025
1 parent a9692b0 commit 7df50ab
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 0 deletions.
28 changes: 28 additions & 0 deletions docs/rules/prefer-logical-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# prefer-logical-properties

Prefer logical properties over physical properties.

## Background

Logical properties are a set of CSS properties that map to their physical counterparts. They are designed to make it easier to create styles that work in both left-to-right and right-to-left languages. Logical properties are useful for creating styles that are more flexible and easier to maintain.

## Rule Details

This rule checks for the use of physical properties and suggests using their logical counterparts instead.

Examples of **incorrect** code for this rule:

```css
/* incorrect use of physical properties */
a {
margin-left: 10px;
}
```

Examples of **correct** code for this rule:

```css
a {
margin-inline-start: 10px;
}
```
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import noEmptyBlocks from "./rules/no-empty-blocks.js";
import noDuplicateImports from "./rules/no-duplicate-imports.js";
import noInvalidProperties from "./rules/no-invalid-properties.js";
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
import useLayers from "./rules/use-layers.js";
import requireBaseline from "./rules/require-baseline.js";

Expand All @@ -33,6 +34,7 @@ const plugin = {
"no-duplicate-imports": noDuplicateImports,
"no-invalid-at-rules": noInvalidAtRules,
"no-invalid-properties": noInvalidProperties,
"prefer-logical-properties": preferLogicalProperties,
"use-layers": useLayers,
"require-baseline": requireBaseline,
},
Expand Down
175 changes: 175 additions & 0 deletions src/rules/prefer-logical-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const propertiesReplacements = {
bottom: "inset-block-end",
"border-bottom": "border-block-end",
"border-bottom-color": "border-block-end-color",
"border-bottom-left-radius": "border-end-start-radius",
"border-bottom-right-radius": "border-end-end-radius",
"border-bottom-style": "border-block-end-style",
"border-bottom-width": "border-block-end-width",
"border-left": "border-inline-start",
"border-left-color": "border-inline-start-color",
"border-left-style": "border-inline-start-style",
"border-left-width": "border-inline-start-width",
"border-right": "border-inline-end",
"border-right-color": "border-inline-end-color",
"border-right-style": "border-inline-end-style",
"border-right-width": "border-inline-end-width",
"border-top": "border-block-start",
"border-top-color": "border-block-start-color",
"border-top-left-radius": "border-start-start-radius",
"border-top-right-radius": "border-start-end-radius",
"border-top-style": "border-block-start-style",
"border-top-width": "border-block-start-width",
"contain-intrinsic-height": "contain-intrinsic-block-size",
"contain-intrinsic-width": "contain-intrinsic-inline-size",
height: "block-size",
left: "inset-inline-start",
"margin-bottom": "margin-block-end",
"margin-left": "margin-inline-start",
"margin-right": "margin-inline-end",
"margin-top": "margin-block-start",
"max-height": "max-block-size",
"max-width": "max-inline-size",
"min-height": "min-block-size",
"min-width": "min-inline-size",
"overflow-x": "overflow-inline",
"overflow-y": "overflow-block",
"overscroll-behavior-x": "overscroll-behavior-inline",
"overscroll-behavior-y": "overscroll-behavior-block",
"padding-bottom": "padding-block-end",
"padding-left": "padding-inline-start",
"padding-right": "padding-inline-end",
"padding-top": "padding-block-start",
right: "inset-inline-end",
"scroll-margin-bottom": "scroll-margin-block-end",
"scroll-margin-left": "scroll-margin-inline-start",
"scroll-margin-right": "scroll-margin-inline-end",
"scroll-margin-top": "scroll-margin-block-start",
"scroll-padding-bottom": "scroll-padding-block-end",
"scroll-padding-left": "scroll-padding-inline-start",
"scroll-padding-right": "scroll-padding-inline-end",
"scroll-padding-top": "scroll-padding-block-start",
top: "inset-block-start",
width: "inline-size",
};

const propertyValuesReplacements = {
"text-align": {
left: "start",
right: "end",
},
resize: {
horizontal: "inline",
vertical: "block",
},
"caption-side": {
left: "inline-start",
right: "inline-end",
},
"box-orient": {
horizontal: "inline-axis",
vertical: "block-axis",
},
float: {
left: "inline-start",
right: "inline-end",
},
clear: {
left: "inline-start",
right: "inline-end",
},
};

const unitReplacements = {
cqh: "cqb",
cqw: "cqi",
dvh: "dvb",
dvw: "dvi",
lvh: "lvb",
lvw: "lvi",
svh: "svb",
svw: "svi",
vh: "vb",
vw: "vi",
};

//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default {
meta: {
type: /** @type {const} */ ("problem"),

fixable: "code",

docs: {
description: "Require use of layers",
url: "https://github.com/eslint/css/blob/main/docs/rules/prefer-logical-properties.md",
},

messages: {
notLogicalProperty:
"Expected logical property '{{replacement}}' instead of '{{property}}'.",
notLogicalValue:
"Expected logical value '{{replacement}}' instead of '{{value}}'.",
notLogicalUnit:
"Expected logical unit '{{replacement}}' instead of '{{unit}}'.",
},
},

create(context) {
return {
Declaration(node) {
if (propertiesReplacements[node.property]) {
context.report({
loc: node.loc,
messageId: "notLogicalProperty",
data: {
property: node.property,
replacement: propertiesReplacements[node.property],
},
});
}

if (
propertyValuesReplacements[node.property] &&
node.value.children[0].type === "Identifier"
) {
const nodeValue = node.value.children[0].name;
if (propertyValuesReplacements[node.property][nodeValue]) {
const replacement =
propertyValuesReplacements[node.property][
nodeValue
];
if (replacement) {
context.report({
loc: node.value.children[0].loc,
messageId: "notLogicalValue",
data: {
value: nodeValue,
replacement,
},
});
}
}
}
},
Dimension(node) {
if (unitReplacements[node.unit]) {
context.report({
loc: node.loc,
messageId: "notLogicalUnit",
data: {
unit: node.unit,
replacement: unitReplacements[node.unit],
},
});
}
},
};
},
};
94 changes: 94 additions & 0 deletions tests/rules/prefer-logical-properties.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//------------------------------------------------------------------------------
// Imports
//------------------------------------------------------------------------------

import rule from "../../src/rules/prefer-logical-properties.js";
import css from "../../src/index.js";
import { RuleTester } from "eslint";

//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester({
plugins: {
css,
},
language: "css/css",
});

ruleTester.run("prefer-logical-properties", rule, {
valid: [
"a { margin-block: 10px; }",
"a { padding-inline: 20px; }",
"a { margin: 10px; }",
"a { padding: 20px; }",
"a { text-align: start }",
],
invalid: [
{
code: "a { margin-top: 10px; }",
errors: [
{
messageId: "notLogicalProperty",
line: 1,
column: 5,
endLine: 1,
endColumn: 21,
data: {
property: "margin-top",
replacement: "margin-block-start",
},
},
],
},
{
code: "a { padding-top: 20px; }",
errors: [
{
messageId: "notLogicalProperty",
line: 1,
column: 5,
endLine: 1,
endColumn: 22,
data: {
property: "padding-top",
replacement: "padding-block-start",
},
},
],
},
{
code: "a { text-align: left }",
errors: [
{
messageId: "notLogicalValue",
line: 1,
column: 17,
endLine: 1,
endColumn: 21,
data: {
value: "left",
replacement: "start",
},
},
],
},
{
code: "a { block-size: 100vh }",
errors: [
{
messageId: "notLogicalUnit",
line: 1,
column: 17,
endLine: 1,
endColumn: 22,
data: {
unit: "vh",
replacement: "vb",
},
},
],
},
],
});

0 comments on commit 7df50ab

Please sign in to comment.