@mrhenry/stylelint-mrhenry-nesting 1.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -17
- package/package.json +1 -1
- package/stylelint-mrhenry-nesting.js +77 -24
package/README.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@mrhenry/stylelint-mrhenry-nesting)
|
|
4
4
|
|
|
5
|
+
Specify a strict coding style for CSS nesting.
|
|
6
|
+
|
|
7
|
+
```css
|
|
8
|
+
/* valid, the nested selector is a "filter" for "focus" */
|
|
9
|
+
.element {
|
|
10
|
+
&:focus {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* invalid, the nested selector is not a "filter" on the elements matched by the parent */
|
|
14
|
+
.foo {
|
|
15
|
+
> .bar {}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
5
19
|
Use CSS nesting only for conditional styling.
|
|
6
20
|
This style of CSS aims to be expressive and readable.
|
|
7
21
|
|
|
@@ -9,6 +23,7 @@ This style of CSS aims to be expressive and readable.
|
|
|
9
23
|
- every nested selector must start with `&`
|
|
10
24
|
- only a single pseudo after `&`
|
|
11
25
|
|
|
26
|
+
**Valid CSS :**
|
|
12
27
|
|
|
13
28
|
```css
|
|
14
29
|
.element {
|
|
@@ -20,14 +35,21 @@ This style of CSS aims to be expressive and readable.
|
|
|
20
35
|
/* when ".element" has an attribute "data-theme" with value "red" */
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
&:is(body.some-modifier *) {
|
|
39
|
+
/* when ".element" is a child of <body class="some-modifier"> */
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
&:has(img) {
|
|
24
43
|
/* when ".element" has a child element of type "img" */
|
|
25
44
|
}
|
|
45
|
+
|
|
46
|
+
@media (prefers-color-scheme: dark) {
|
|
47
|
+
/* when ".element" is shown in a dark mode context */
|
|
48
|
+
}
|
|
26
49
|
}
|
|
27
50
|
```
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
**Invalid CSS :**
|
|
31
53
|
|
|
32
54
|
```css
|
|
33
55
|
/* invalid, the nested selector is not a "filter" on the elements matched by the parent */
|
|
@@ -37,36 +59,61 @@ This style of CSS aims to be expressive and readable.
|
|
|
37
59
|
}
|
|
38
60
|
}
|
|
39
61
|
|
|
40
|
-
/*
|
|
62
|
+
/* invalid, the nested selector is not a "filter" on the elements matched by the parent */
|
|
41
63
|
.foo {
|
|
42
|
-
|
|
64
|
+
+ :focus {
|
|
43
65
|
color: green;
|
|
44
66
|
}
|
|
45
67
|
}
|
|
68
|
+
|
|
69
|
+
/* invalid, "@custom-selector" is not a conditional at-rule */
|
|
70
|
+
.foo {
|
|
71
|
+
@custom-selector :--foo .bar;
|
|
72
|
+
}
|
|
46
73
|
```
|
|
47
74
|
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
`npm install --save-dev @mrhenry/stylelint-mrhenry-nesting`
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
// stylelint.config.js
|
|
81
|
+
module.exports = {
|
|
82
|
+
plugins: [
|
|
83
|
+
"@mrhenry/stylelint-mrhenry-nesting",
|
|
84
|
+
],
|
|
85
|
+
rules: {
|
|
86
|
+
"@mrhenry/stylelint-mrhenry-nesting": true,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Optional secondary options
|
|
92
|
+
|
|
93
|
+
### `ignoreAtRules: [/regex/, "string"]`
|
|
94
|
+
|
|
95
|
+
Given:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
["/^custom-/i", "mixins"]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The following patterns are _not_ considered problems:
|
|
102
|
+
|
|
48
103
|
```css
|
|
49
|
-
/* invalid, the nested selector is not a "filter" on the elements matched by the parent */
|
|
50
104
|
.foo {
|
|
51
|
-
|
|
52
|
-
color: green;
|
|
53
|
-
}
|
|
105
|
+
@custom-selector :--bar .bar;
|
|
54
106
|
}
|
|
107
|
+
```
|
|
55
108
|
|
|
56
|
-
|
|
109
|
+
```css
|
|
57
110
|
.foo {
|
|
58
|
-
|
|
59
|
-
color: green;
|
|
60
|
-
}
|
|
111
|
+
@CUSTOM-MEDIA --bar (min-width: 320px);
|
|
61
112
|
}
|
|
62
113
|
```
|
|
63
114
|
|
|
64
115
|
```css
|
|
65
|
-
/* valid, the nested is a filter or a condition */
|
|
66
116
|
.foo {
|
|
67
|
-
@
|
|
68
|
-
color: green;
|
|
69
|
-
}
|
|
117
|
+
@mixins bar;
|
|
70
118
|
}
|
|
71
119
|
```
|
|
72
|
-
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const stylelint = require("stylelint");
|
|
2
2
|
const selectorParser = require('postcss-selector-parser');
|
|
3
3
|
|
|
4
|
-
const ruleName = "
|
|
4
|
+
const ruleName = "@mrhenry/stylelint-mrhenry-nesting";
|
|
5
5
|
const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
6
6
|
rejectedAtRule: (name) => {
|
|
7
7
|
return `Nested at-rules with name "${name}" is not allowed.`;
|
|
@@ -12,9 +12,6 @@ const messages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
12
12
|
rejectedMustEndWithPseudo: () => {
|
|
13
13
|
return `Nested selectors must end with a pseudo selectors.`;
|
|
14
14
|
},
|
|
15
|
-
rejectedMustContainOnlyOneAmpersand: () => {
|
|
16
|
-
return `Nested selectors must only contain a single "&".`;
|
|
17
|
-
},
|
|
18
15
|
rejectedNestingDepth: () => {
|
|
19
16
|
return `Nested rules must be limited to 1 level deep.`;
|
|
20
17
|
},
|
|
@@ -29,6 +26,12 @@ const meta = {
|
|
|
29
26
|
|
|
30
27
|
const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
31
28
|
return (postcssRoot, postcssResult) => {
|
|
29
|
+
if (!primaryOption) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ignoreAtRulesOptions = secondaryOptionObject?.ignoreAtRules ?? [];
|
|
34
|
+
|
|
32
35
|
postcssRoot.walkAtRules((atrule) => {
|
|
33
36
|
let name = atrule.name.toLowerCase();
|
|
34
37
|
if (
|
|
@@ -41,6 +44,20 @@ const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
for (const ignoreAtRulesOption of ignoreAtRulesOptions) {
|
|
48
|
+
if (ignoreAtRulesOption instanceof RegExp) {
|
|
49
|
+
if (ignoreAtRulesOption.test(name)) {
|
|
50
|
+
// ignored by regexp match
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
if (name === ignoreAtRulesOption) {
|
|
55
|
+
// ignored by direct match
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
44
61
|
let rulesDepth = 0;
|
|
45
62
|
let parent = atrule.parent;
|
|
46
63
|
while (parent) {
|
|
@@ -97,30 +114,49 @@ const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
|
97
114
|
|
|
98
115
|
for (let i = 0; i < selectorsAST.nodes.length; i++) {
|
|
99
116
|
const selectorAST = selectorsAST.nodes[i];
|
|
117
|
+
if (!selectorAST.nodes || !selectorAST.nodes.length) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
100
120
|
|
|
101
121
|
let nestingCounter = 0;
|
|
102
122
|
{
|
|
103
|
-
let nestingCounter = 0;
|
|
104
123
|
selectorAST.walkNesting(() => {
|
|
105
124
|
nestingCounter++;
|
|
106
125
|
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
context.fix &&
|
|
130
|
+
nestingCounter === 1 &&
|
|
131
|
+
selectorAST.nodes[selectorAST.nodes.length - 1]?.type === 'nesting' &&
|
|
132
|
+
selectorAST.nodes[selectorAST.nodes.length - 2]?.type === 'combinator'
|
|
133
|
+
) {
|
|
134
|
+
fixSelector_AncestorPattern(rule, selectorsAST, selectorAST);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
if (
|
|
139
|
+
context.fix &&
|
|
140
|
+
selectorAST.nodes.length === 2 &&
|
|
141
|
+
selectorAST.nodes[0]?.type === 'pseudo' &&
|
|
142
|
+
selectorParser.isPseudoClass(selectorAST.nodes[0]) &&
|
|
143
|
+
selectorAST.nodes[1]?.type === 'nesting'
|
|
144
|
+
) {
|
|
145
|
+
const a = selectorAST.nodes[0];
|
|
146
|
+
const b = selectorAST.nodes[1];
|
|
117
147
|
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
selectorAST.replaceWith(selectorParser.selector({
|
|
149
|
+
nodes: [
|
|
150
|
+
b,
|
|
151
|
+
a,
|
|
152
|
+
]
|
|
153
|
+
}))
|
|
154
|
+
rule.selector = selectorsAST.toString();
|
|
155
|
+
return;
|
|
120
156
|
}
|
|
121
157
|
|
|
122
|
-
if (selectorAST.nodes
|
|
123
|
-
if (context.fix
|
|
158
|
+
if (selectorAST.nodes[0]?.type !== 'nesting') {
|
|
159
|
+
if (context.fix) {
|
|
124
160
|
fixSelector(rule, selectorsAST, selectorAST);
|
|
125
161
|
return;
|
|
126
162
|
}
|
|
@@ -137,9 +173,9 @@ const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
|
137
173
|
return;
|
|
138
174
|
}
|
|
139
175
|
|
|
140
|
-
if (selectorAST.nodes
|
|
176
|
+
if (selectorAST.nodes.length !== 2) {
|
|
141
177
|
if (context.fix) {
|
|
142
|
-
selectorAST.nodes
|
|
178
|
+
selectorAST.nodes[0]?.remove();
|
|
143
179
|
fixSelector(rule, selectorsAST, selectorAST);
|
|
144
180
|
return;
|
|
145
181
|
}
|
|
@@ -156,9 +192,9 @@ const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
|
156
192
|
return;
|
|
157
193
|
}
|
|
158
194
|
|
|
159
|
-
if (selectorAST.nodes
|
|
195
|
+
if (selectorAST.nodes[1]?.type !== 'pseudo') {
|
|
160
196
|
if (context.fix) {
|
|
161
|
-
selectorAST.nodes
|
|
197
|
+
selectorAST.nodes[0]?.remove();
|
|
162
198
|
fixSelector(rule, selectorsAST, selectorAST);
|
|
163
199
|
return;
|
|
164
200
|
}
|
|
@@ -181,8 +217,8 @@ const ruleFunction = (primaryOption, secondaryOptionObject, context) => {
|
|
|
181
217
|
|
|
182
218
|
function fixSelector(rule, selectorsAST, selectorAST) {
|
|
183
219
|
if (
|
|
184
|
-
selectorAST.nodes
|
|
185
|
-
selectorAST.nodes
|
|
220
|
+
selectorAST.nodes.length === 1 &&
|
|
221
|
+
selectorAST.nodes[0].type === 'pseudo'
|
|
186
222
|
) {
|
|
187
223
|
selectorAST.replaceWith(selectorParser.selector({
|
|
188
224
|
nodes: [
|
|
@@ -210,6 +246,23 @@ function fixSelector(rule, selectorsAST, selectorAST) {
|
|
|
210
246
|
rule.selector = selectorsAST.toString();
|
|
211
247
|
}
|
|
212
248
|
|
|
249
|
+
function fixSelector_AncestorPattern(rule, selectorsAST, selectorAST) {
|
|
250
|
+
selectorAST.nodes[selectorAST.nodes.length - 1].replaceWith(selectorParser.universal());
|
|
251
|
+
selectorAST.replaceWith(selectorParser.selector({
|
|
252
|
+
nodes: [
|
|
253
|
+
selectorParser.nesting(),
|
|
254
|
+
selectorParser.pseudo({
|
|
255
|
+
value: ':is',
|
|
256
|
+
nodes: [
|
|
257
|
+
selectorAST
|
|
258
|
+
]
|
|
259
|
+
}),
|
|
260
|
+
]
|
|
261
|
+
}))
|
|
262
|
+
|
|
263
|
+
rule.selector = selectorsAST.toString();
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
ruleFunction.ruleName = ruleName;
|
|
214
267
|
ruleFunction.messages = messages;
|
|
215
268
|
ruleFunction.meta = meta;
|