@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 CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  [![version](https://img.shields.io/npm/v/@mrhenry/stylelint-mrhenry-nesting.svg)](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
- /* valid, the nested is a filter or a condition */
62
+ /* invalid, the nested selector is not a "filter" on the elements matched by the parent */
41
63
  .foo {
42
- &:is(.bar) {
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
- + :focus {
52
- color: green;
53
- }
105
+ @custom-selector :--bar .bar;
54
106
  }
107
+ ```
55
108
 
56
- /* valid, the nested is a filter or a condition */
109
+ ```css
57
110
  .foo {
58
- &:focus {
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
- @media (prefers-color-scheme: dark) {
68
- color: green;
69
- }
117
+ @mixins bar;
70
118
  }
71
119
  ```
72
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrhenry/stylelint-mrhenry-nesting",
3
- "version": "1.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Mr. Henry's preferred way of writing nested CSS",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,7 +1,7 @@
1
1
  const stylelint = require("stylelint");
2
2
  const selectorParser = require('postcss-selector-parser');
3
3
 
4
- const ruleName = "plugin/stylelint-mrhenry-nesting";
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
- if (nestingCounter > 1) {
109
- stylelint.utils.report({
110
- message: messages.rejectedMustContainOnlyOneAmpersand(),
111
- node: rule,
112
- index: 0,
113
- endIndex: rule.selector.length,
114
- result: postcssResult,
115
- ruleName,
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
- return;
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?.[0]?.type !== 'nesting') {
123
- if (context.fix && nestingCounter === 0) {
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?.length !== 2) {
176
+ if (selectorAST.nodes.length !== 2) {
141
177
  if (context.fix) {
142
- selectorAST.nodes?.[0]?.remove();
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?.[1]?.type !== 'pseudo') {
195
+ if (selectorAST.nodes[1]?.type !== 'pseudo') {
160
196
  if (context.fix) {
161
- selectorAST.nodes?.[0]?.remove();
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?.length === 1 &&
185
- selectorAST.nodes?.[0].type === 'pseudo'
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;