Skip to content

Commit 7df50ab

Browse files
committed
feat: add prefer-logical-properties rule
1 parent a9692b0 commit 7df50ab

File tree

4 files changed

+299
-0
lines changed

4 files changed

+299
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# prefer-logical-properties
2+
3+
Prefer logical properties over physical properties.
4+
5+
## Background
6+
7+
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.
8+
9+
## Rule Details
10+
11+
This rule checks for the use of physical properties and suggests using their logical counterparts instead.
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```css
16+
/* incorrect use of physical properties */
17+
a {
18+
margin-left: 10px;
19+
}
20+
```
21+
22+
Examples of **correct** code for this rule:
23+
24+
```css
25+
a {
26+
margin-inline-start: 10px;
27+
}
28+
```

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import noEmptyBlocks from "./rules/no-empty-blocks.js";
1313
import noDuplicateImports from "./rules/no-duplicate-imports.js";
1414
import noInvalidProperties from "./rules/no-invalid-properties.js";
1515
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
16+
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
1617
import useLayers from "./rules/use-layers.js";
1718
import requireBaseline from "./rules/require-baseline.js";
1819

@@ -33,6 +34,7 @@ const plugin = {
3334
"no-duplicate-imports": noDuplicateImports,
3435
"no-invalid-at-rules": noInvalidAtRules,
3536
"no-invalid-properties": noInvalidProperties,
37+
"prefer-logical-properties": preferLogicalProperties,
3638
"use-layers": useLayers,
3739
"require-baseline": requireBaseline,
3840
},
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
//-----------------------------------------------------------------------------
2+
// Helpers
3+
//-----------------------------------------------------------------------------
4+
5+
const propertiesReplacements = {
6+
bottom: "inset-block-end",
7+
"border-bottom": "border-block-end",
8+
"border-bottom-color": "border-block-end-color",
9+
"border-bottom-left-radius": "border-end-start-radius",
10+
"border-bottom-right-radius": "border-end-end-radius",
11+
"border-bottom-style": "border-block-end-style",
12+
"border-bottom-width": "border-block-end-width",
13+
"border-left": "border-inline-start",
14+
"border-left-color": "border-inline-start-color",
15+
"border-left-style": "border-inline-start-style",
16+
"border-left-width": "border-inline-start-width",
17+
"border-right": "border-inline-end",
18+
"border-right-color": "border-inline-end-color",
19+
"border-right-style": "border-inline-end-style",
20+
"border-right-width": "border-inline-end-width",
21+
"border-top": "border-block-start",
22+
"border-top-color": "border-block-start-color",
23+
"border-top-left-radius": "border-start-start-radius",
24+
"border-top-right-radius": "border-start-end-radius",
25+
"border-top-style": "border-block-start-style",
26+
"border-top-width": "border-block-start-width",
27+
"contain-intrinsic-height": "contain-intrinsic-block-size",
28+
"contain-intrinsic-width": "contain-intrinsic-inline-size",
29+
height: "block-size",
30+
left: "inset-inline-start",
31+
"margin-bottom": "margin-block-end",
32+
"margin-left": "margin-inline-start",
33+
"margin-right": "margin-inline-end",
34+
"margin-top": "margin-block-start",
35+
"max-height": "max-block-size",
36+
"max-width": "max-inline-size",
37+
"min-height": "min-block-size",
38+
"min-width": "min-inline-size",
39+
"overflow-x": "overflow-inline",
40+
"overflow-y": "overflow-block",
41+
"overscroll-behavior-x": "overscroll-behavior-inline",
42+
"overscroll-behavior-y": "overscroll-behavior-block",
43+
"padding-bottom": "padding-block-end",
44+
"padding-left": "padding-inline-start",
45+
"padding-right": "padding-inline-end",
46+
"padding-top": "padding-block-start",
47+
right: "inset-inline-end",
48+
"scroll-margin-bottom": "scroll-margin-block-end",
49+
"scroll-margin-left": "scroll-margin-inline-start",
50+
"scroll-margin-right": "scroll-margin-inline-end",
51+
"scroll-margin-top": "scroll-margin-block-start",
52+
"scroll-padding-bottom": "scroll-padding-block-end",
53+
"scroll-padding-left": "scroll-padding-inline-start",
54+
"scroll-padding-right": "scroll-padding-inline-end",
55+
"scroll-padding-top": "scroll-padding-block-start",
56+
top: "inset-block-start",
57+
width: "inline-size",
58+
};
59+
60+
const propertyValuesReplacements = {
61+
"text-align": {
62+
left: "start",
63+
right: "end",
64+
},
65+
resize: {
66+
horizontal: "inline",
67+
vertical: "block",
68+
},
69+
"caption-side": {
70+
left: "inline-start",
71+
right: "inline-end",
72+
},
73+
"box-orient": {
74+
horizontal: "inline-axis",
75+
vertical: "block-axis",
76+
},
77+
float: {
78+
left: "inline-start",
79+
right: "inline-end",
80+
},
81+
clear: {
82+
left: "inline-start",
83+
right: "inline-end",
84+
},
85+
};
86+
87+
const unitReplacements = {
88+
cqh: "cqb",
89+
cqw: "cqi",
90+
dvh: "dvb",
91+
dvw: "dvi",
92+
lvh: "lvb",
93+
lvw: "lvi",
94+
svh: "svb",
95+
svw: "svi",
96+
vh: "vb",
97+
vw: "vi",
98+
};
99+
100+
//-----------------------------------------------------------------------------
101+
// Rule Definition
102+
//-----------------------------------------------------------------------------
103+
export default {
104+
meta: {
105+
type: /** @type {const} */ ("problem"),
106+
107+
fixable: "code",
108+
109+
docs: {
110+
description: "Require use of layers",
111+
url: "https://github.com/eslint/css/blob/main/docs/rules/prefer-logical-properties.md",
112+
},
113+
114+
messages: {
115+
notLogicalProperty:
116+
"Expected logical property '{{replacement}}' instead of '{{property}}'.",
117+
notLogicalValue:
118+
"Expected logical value '{{replacement}}' instead of '{{value}}'.",
119+
notLogicalUnit:
120+
"Expected logical unit '{{replacement}}' instead of '{{unit}}'.",
121+
},
122+
},
123+
124+
create(context) {
125+
return {
126+
Declaration(node) {
127+
if (propertiesReplacements[node.property]) {
128+
context.report({
129+
loc: node.loc,
130+
messageId: "notLogicalProperty",
131+
data: {
132+
property: node.property,
133+
replacement: propertiesReplacements[node.property],
134+
},
135+
});
136+
}
137+
138+
if (
139+
propertyValuesReplacements[node.property] &&
140+
node.value.children[0].type === "Identifier"
141+
) {
142+
const nodeValue = node.value.children[0].name;
143+
if (propertyValuesReplacements[node.property][nodeValue]) {
144+
const replacement =
145+
propertyValuesReplacements[node.property][
146+
nodeValue
147+
];
148+
if (replacement) {
149+
context.report({
150+
loc: node.value.children[0].loc,
151+
messageId: "notLogicalValue",
152+
data: {
153+
value: nodeValue,
154+
replacement,
155+
},
156+
});
157+
}
158+
}
159+
}
160+
},
161+
Dimension(node) {
162+
if (unitReplacements[node.unit]) {
163+
context.report({
164+
loc: node.loc,
165+
messageId: "notLogicalUnit",
166+
data: {
167+
unit: node.unit,
168+
replacement: unitReplacements[node.unit],
169+
},
170+
});
171+
}
172+
},
173+
};
174+
},
175+
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//------------------------------------------------------------------------------
2+
// Imports
3+
//------------------------------------------------------------------------------
4+
5+
import rule from "../../src/rules/prefer-logical-properties.js";
6+
import css from "../../src/index.js";
7+
import { RuleTester } from "eslint";
8+
9+
//------------------------------------------------------------------------------
10+
// Tests
11+
//------------------------------------------------------------------------------
12+
13+
const ruleTester = new RuleTester({
14+
plugins: {
15+
css,
16+
},
17+
language: "css/css",
18+
});
19+
20+
ruleTester.run("prefer-logical-properties", rule, {
21+
valid: [
22+
"a { margin-block: 10px; }",
23+
"a { padding-inline: 20px; }",
24+
"a { margin: 10px; }",
25+
"a { padding: 20px; }",
26+
"a { text-align: start }",
27+
],
28+
invalid: [
29+
{
30+
code: "a { margin-top: 10px; }",
31+
errors: [
32+
{
33+
messageId: "notLogicalProperty",
34+
line: 1,
35+
column: 5,
36+
endLine: 1,
37+
endColumn: 21,
38+
data: {
39+
property: "margin-top",
40+
replacement: "margin-block-start",
41+
},
42+
},
43+
],
44+
},
45+
{
46+
code: "a { padding-top: 20px; }",
47+
errors: [
48+
{
49+
messageId: "notLogicalProperty",
50+
line: 1,
51+
column: 5,
52+
endLine: 1,
53+
endColumn: 22,
54+
data: {
55+
property: "padding-top",
56+
replacement: "padding-block-start",
57+
},
58+
},
59+
],
60+
},
61+
{
62+
code: "a { text-align: left }",
63+
errors: [
64+
{
65+
messageId: "notLogicalValue",
66+
line: 1,
67+
column: 17,
68+
endLine: 1,
69+
endColumn: 21,
70+
data: {
71+
value: "left",
72+
replacement: "start",
73+
},
74+
},
75+
],
76+
},
77+
{
78+
code: "a { block-size: 100vh }",
79+
errors: [
80+
{
81+
messageId: "notLogicalUnit",
82+
line: 1,
83+
column: 17,
84+
endLine: 1,
85+
endColumn: 22,
86+
data: {
87+
unit: "vh",
88+
replacement: "vb",
89+
},
90+
},
91+
],
92+
},
93+
],
94+
});

0 commit comments

Comments
 (0)