Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add prefer-logical-properties rule #63

Merged
merged 8 commits into from
Mar 3, 2025
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
35 changes: 35 additions & 0 deletions docs/rules/prefer-logical-properties.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to finish up the docs, we typically include two additional sections:

  1. When Not to Use It - gives guidance on when it's okay to not use the rule
  2. Prior Art - links to other tools that do the same thing

Here's an example:
https://github.com/eslint/css/blob/main/docs/rules/no-invalid-properties.md

For prior art, we have:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ✅

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# 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.
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
232 changes: 232 additions & 0 deletions src/rules/prefer-logical-properties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//-----------------------------------------------------------------------------
// 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: "code",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fix the type error.

Suggested change
fixable: "code",
fixable: /** @type {const} */ ("code"),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed ✅


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 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