Skip to content

Commit 162f6e5

Browse files
authored
feat: add no-invalid-named-grid-areas rule (eslint#169)
1 parent 932cf62 commit 162f6e5

File tree

5 files changed

+580
-11
lines changed

5 files changed

+580
-11
lines changed

README.md

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,18 @@ export default defineConfig([
6565

6666
<!-- Rule Table Start -->
6767

68-
| **Rule Name** | **Description** | **Recommended** |
69-
| :----------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70-
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71-
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72-
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73-
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
74-
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
75-
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
76-
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
77-
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
78-
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
68+
| **Rule Name** | **Description** | **Recommended** |
69+
| :--------------------------------------------------------------------------- | :------------------------------------- | :-------------: |
70+
| [`no-duplicate-imports`](./docs/rules/no-duplicate-imports.md) | Disallow duplicate @import rules | yes |
71+
| [`no-empty-blocks`](./docs/rules/no-empty-blocks.md) | Disallow empty blocks | yes |
72+
| [`no-important`](./docs/rules/no-important.md) | Disallow !important flags | yes |
73+
| [`no-invalid-at-rules`](./docs/rules/no-invalid-at-rules.md) | Disallow invalid at-rules | yes |
74+
| [`no-invalid-named-grid-areas`](./docs/rules/no-invalid-named-grid-areas.md) | Disallow invalid named grid areas | yes |
75+
| [`no-invalid-properties`](./docs/rules/no-invalid-properties.md) | Disallow invalid properties | yes |
76+
| [`prefer-logical-properties`](./docs/rules/prefer-logical-properties.md) | Enforce the use of logical properties | no |
77+
| [`relative-font-units`](./docs/rules/relative-font-units.md) | Enforce the use of relative font units | no |
78+
| [`use-baseline`](./docs/rules/use-baseline.md) | Enforce the use of baseline features | yes |
79+
| [`use-layers`](./docs/rules/use-layers.md) | Require use of layers | no |
7980

8081
<!-- Rule Table End -->
8182

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# no-invalid-named-grid-areas
2+
3+
Disallow invalid named grid areas.
4+
5+
## Background
6+
7+
CSS Grid allows you to define named grid areas using the `grid-template-areas` property. Each string in the value creates a row, and each cell token in the string creates a column. Multiple cell tokens with the same name within and between rows create a single named grid area that spans the corresponding grid cells.
8+
9+
A named grid area is considered invalid if:
10+
11+
1. The strings in the value have different numbers of cell tokens
12+
2. No cell tokens are present
13+
3. Cell tokens with the same name do not form a rectangle
14+
15+
## Rule Details
16+
17+
This rule prevents invalid named grid areas in CSS grid templates.
18+
19+
Examples of **incorrect** code:
20+
21+
```css
22+
/* eslint css/no-invalid-named-grid-areas: "error" */
23+
24+
.grid {
25+
grid-template-areas: "";
26+
}
27+
```
28+
29+
```css
30+
/* eslint css/no-invalid-named-grid-areas: "error" */
31+
32+
.grid {
33+
grid-template-areas:
34+
"header header header"
35+
"nav main main main";
36+
}
37+
```
38+
39+
```css
40+
/* eslint css/no-invalid-named-grid-areas: "error" */
41+
42+
.grid {
43+
grid-template-areas:
44+
"header header header"
45+
"nav main main"
46+
"nav . main";
47+
}
48+
```
49+
50+
Examples of **correct** code:
51+
52+
```css
53+
/* eslint css/no-invalid-named-grid-areas: "error" */
54+
55+
.grid {
56+
grid-template-areas:
57+
"header"
58+
"nav"
59+
"main";
60+
}
61+
```
62+
63+
```css
64+
/* eslint css/no-invalid-named-grid-areas: "error" */
65+
66+
.grid {
67+
grid-template-areas:
68+
"header header header"
69+
"nav main main"
70+
"nav main main";
71+
}
72+
```
73+
74+
```css
75+
/* eslint css/no-invalid-named-grid-areas: "error" */
76+
77+
.grid {
78+
grid-template-areas:
79+
"header header header"
80+
"nav . main"
81+
"nav . main";
82+
}
83+
```
84+
85+
## When Not to Use It
86+
87+
If you aren't concerned with invalid grid area definitions, then you can safely disable this rule.
88+
89+
## Prior Art
90+
91+
- [`named-grid-areas-no-invalid`](https://stylelint.io/user-guide/rules/named-grid-areas-no-invalid)

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import noDuplicateImports from "./rules/no-duplicate-imports.js";
1414
import noImportant from "./rules/no-important.js";
1515
import noInvalidProperties from "./rules/no-invalid-properties.js";
1616
import noInvalidAtRules from "./rules/no-invalid-at-rules.js";
17+
import noInvalidNamedGridAreas from "./rules/no-invalid-named-grid-areas.js";
1718
import preferLogicalProperties from "./rules/prefer-logical-properties.js";
1819
import relativeFontUnits from "./rules/relative-font-units.js";
1920
import useLayers from "./rules/use-layers.js";
@@ -36,6 +37,7 @@ const plugin = {
3637
"no-duplicate-imports": noDuplicateImports,
3738
"no-important": noImportant,
3839
"no-invalid-at-rules": noInvalidAtRules,
40+
"no-invalid-named-grid-areas": noInvalidNamedGridAreas,
3941
"no-invalid-properties": noInvalidProperties,
4042
"prefer-logical-properties": preferLogicalProperties,
4143
"relative-font-units": relativeFontUnits,
@@ -50,6 +52,7 @@ const plugin = {
5052
"css/no-duplicate-imports": "error",
5153
"css/no-important": "error",
5254
"css/no-invalid-at-rules": "error",
55+
"css/no-invalid-named-grid-areas": "error",
5356
"css/no-invalid-properties": "error",
5457
"css/use-baseline": "warn",
5558
}),
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @fileoverview Rule to prevent invalid named grid areas in CSS grid templates.
3+
* @author xbinaryx
4+
*/
5+
6+
//-----------------------------------------------------------------------------
7+
// Type Definitions
8+
//-----------------------------------------------------------------------------
9+
10+
/**
11+
* @import { CSSRuleDefinition } from "../types.js"
12+
* @typedef {"emptyGridArea" | "unevenGridArea" | "nonRectangularGridArea"} NoInvalidNamedGridAreasMessageIds
13+
* @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: NoInvalidNamedGridAreasMessageIds }>} NoInvalidNamedGridAreasRuleDefinition
14+
*/
15+
16+
//-----------------------------------------------------------------------------
17+
// Helpers
18+
//-----------------------------------------------------------------------------
19+
20+
/**
21+
* Regular expression to match null cell tokens (sequences of one or more dots)
22+
*/
23+
const nullCellToken = /^\.+$/u;
24+
25+
/**
26+
* Finds non-rectangular grid areas in a 2D grid
27+
* @param {string[][]} grid 2D array representing the grid areas
28+
* @returns {Array<{name: string, row: number}>} Array of errors found
29+
*/
30+
function findNonRectangularAreas(grid) {
31+
const errors = [];
32+
const reported = new Set();
33+
const names = [...new Set(grid.flat())].filter(
34+
name => !nullCellToken.test(name),
35+
);
36+
37+
for (const name of names) {
38+
const indicesByRow = grid.map(row => {
39+
const indices = [];
40+
let idx = row.indexOf(name);
41+
42+
while (idx !== -1) {
43+
indices.push(idx);
44+
idx = row.indexOf(name, idx + 1);
45+
}
46+
47+
return indices;
48+
});
49+
50+
for (let i = 0; i < indicesByRow.length; i++) {
51+
for (let j = i + 1; j < indicesByRow.length; j++) {
52+
const row1 = indicesByRow[i];
53+
const row2 = indicesByRow[j];
54+
55+
if (row1.length === 0 || row2.length === 0) {
56+
continue;
57+
}
58+
59+
if (
60+
row1.length !== row2.length ||
61+
!row1.every((val, idx) => val === row2[idx])
62+
) {
63+
const key = `${name}|${j}`;
64+
if (!reported.has(key)) {
65+
errors.push({ name, row: j });
66+
reported.add(key);
67+
}
68+
}
69+
}
70+
}
71+
}
72+
73+
return errors;
74+
}
75+
76+
const validProps = new Set(["grid-template-areas", "grid-template", "grid"]);
77+
78+
//-----------------------------------------------------------------------------
79+
// Rule Definition
80+
//-----------------------------------------------------------------------------
81+
82+
/** @type {NoInvalidNamedGridAreasRuleDefinition} */
83+
export default {
84+
meta: {
85+
type: "problem",
86+
87+
docs: {
88+
description: "Disallow invalid named grid areas",
89+
recommended: true,
90+
url: "https://github.com/eslint/css/blob/main/docs/rules/no-invalid-named-grid-areas.md",
91+
},
92+
93+
messages: {
94+
emptyGridArea: "Grid area must contain at least one cell token.",
95+
unevenGridArea:
96+
"Grid area strings must have the same number of cell tokens.",
97+
nonRectangularGridArea:
98+
"Cell tokens with name '{{name}}' must form a rectangle.",
99+
},
100+
},
101+
102+
create(context) {
103+
return {
104+
Declaration(node) {
105+
const propName = node.property.toLowerCase();
106+
107+
if (
108+
validProps.has(propName) &&
109+
node.value.type === "Value" &&
110+
node.value.children.length > 0
111+
) {
112+
const stringNodes = node.value.children.filter(
113+
child => child.type === "String",
114+
);
115+
116+
if (stringNodes.length === 0) {
117+
return;
118+
}
119+
120+
const grid = [];
121+
const emptyNodes = [];
122+
const unevenNodes = [];
123+
let firstRowLen = null;
124+
125+
for (const stringNode of stringNodes) {
126+
const trimmedValue = stringNode.value.trim();
127+
128+
if (trimmedValue === "") {
129+
emptyNodes.push(stringNode);
130+
continue;
131+
}
132+
133+
const row = trimmedValue.split(" ").filter(Boolean);
134+
grid.push(row);
135+
136+
if (firstRowLen === null) {
137+
firstRowLen = row.length;
138+
} else if (row.length !== firstRowLen) {
139+
unevenNodes.push(stringNode);
140+
}
141+
}
142+
143+
if (emptyNodes.length > 0) {
144+
emptyNodes.forEach(emptyNode =>
145+
context.report({
146+
node: emptyNode,
147+
messageId: "emptyGridArea",
148+
}),
149+
);
150+
return;
151+
}
152+
153+
if (unevenNodes.length > 0) {
154+
unevenNodes.forEach(unevenNode =>
155+
context.report({
156+
node: unevenNode,
157+
messageId: "unevenGridArea",
158+
}),
159+
);
160+
return;
161+
}
162+
163+
const nonRectErrors = findNonRectangularAreas(grid);
164+
nonRectErrors.forEach(({ name, row }) => {
165+
const stringNode = stringNodes[row];
166+
context.report({
167+
node: stringNode,
168+
messageId: "nonRectangularGridArea",
169+
data: {
170+
name,
171+
},
172+
});
173+
});
174+
}
175+
},
176+
};
177+
},
178+
};

0 commit comments

Comments
 (0)