Skip to content

Commit

Permalink
feat: add prefer-logical-properties rule (#63)
Browse files Browse the repository at this point in the history
* feat: add prefer-logical-properties rule

* perf: move to maps in prefer-logical-properties rule

* feat: add allowes properties and units option in prefer-logical-properties

* refactor: rename options in prefer-logical-properties rule

* docs: add options in prefer-logical-properties

* refactor: define fixable value as const

* docs: improve prefer-logical-property docs

* fix: prevent testing in supports declaration
  • Loading branch information
azat-io authored Mar 3, 2025
1 parent 3ea5047 commit 2a440ce
Show file tree
Hide file tree
Showing 5 changed files with 403 additions and 8 deletions.
17 changes: 9 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ export default [

<!-- Rule Table Start -->

| **Rule Name** | **Description** | **Recommended** |
| :--------------------------------------------------------------- | :----------------------------------- | :-------------: |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
| [`require-baseline`](./docs/rules/require-baseline.md) | Enforce the use of baseline features | yes |
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
| **Rule Name** | **Description** | **Recommended** |
| :------------------------------------------------------------------------- | :------------------------------------ | :-------------: |
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
| [`prefer-logical-properties`](./docs//rules//prefer-logical-properties.md) | Enforce the use of logical properties | no |
| [`require-baseline`](./docs/rules/require-baseline.md) | Enforce the use of baseline features | yes |
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |

<!-- Rule Table End -->

Expand Down
43 changes: 43 additions & 0 deletions docs/rules/prefer-logical-properties.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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;
}
```

### Options

This rule accepts an option object with the following properties:

- `allowProperties` (default: `[]`) - Specify an array of physical properties that are allowed to be used.
- `allowUnits` (default: `[]`) - Specify an array of physical units that are allowed to be used.

## When Not to Use It

If you aren't concerned with the use of logical properties, then you can safely disable this rule.

## Prior Art

- [stylelint-use-logical](https://github.com/csstools/stylelint-use-logical)
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
237 changes: 237 additions & 0 deletions src/rules/prefer-logical-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------

const propertiesReplacements = new Map([
["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 = new Map([
[
"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 = new Map([
["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: /** @type {const} */ ("code"),

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

schema: [
{
type: "object",
properties: {
allowProperties: {
type: "array",
items: {
type: "string",
},
},
allowUnits: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
],

defaultOptions: [
{
allowProperties: [],
allowUnits: [],
},
],

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) {
const parent = context.sourceCode.getParent(node);
if (parent.type === "SupportsDeclaration") {
return;
}

const allowProperties = context.options[0].allowProperties;
if (
propertiesReplacements.get(node.property) &&
!allowProperties.includes(node.property)
) {
context.report({
loc: node.loc,
messageId: "notLogicalProperty",
data: {
property: node.property,
replacement: propertiesReplacements.get(
node.property,
),
},
});
}

if (
propertyValuesReplacements.get(node.property) &&
node.value.children[0].type === "Identifier"
) {
const nodeValue = node.value.children[0].name;
if (
propertyValuesReplacements.get(node.property)[nodeValue]
) {
const replacement = propertyValuesReplacements.get(
node.property,
)[nodeValue];
if (replacement) {
context.report({
loc: node.value.children[0].loc,
messageId: "notLogicalValue",
data: {
value: nodeValue,
replacement,
},
});
}
}
}
},
Dimension(node) {
const allowUnits = context.options[0].allowUnits;
if (
unitReplacements.get(node.unit) &&
!allowUnits.includes(node.unit)
) {
context.report({
loc: node.loc,
messageId: "notLogicalUnit",
data: {
unit: node.unit,
replacement: unitReplacements.get(node.unit),
},
});
}
},
};
},
};
Loading

0 comments on commit 2a440ce

Please sign in to comment.