Skip to content

Commit b415635

Browse files
authored
fix #1625 (#1630)
1 parent f9bf520 commit b415635

19 files changed

+247
-14
lines changed

plugins/postcss-is-pseudo-class/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to PostCSS Is Pseudo Class
22

3+
### Unreleased (patch)
4+
5+
- Add support for more complex selector patterns. `.a > :is(.b > .c)` -> `.a.b > .c`
6+
37
### 5.0.1
48

59
_October 23, 2024_

plugins/postcss-is-pseudo-class/dist/index.cjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

plugins/postcss-is-pseudo-class/dist/index.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

plugins/postcss-is-pseudo-class/src/split-selectors/complex.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { childAdjacentChild } from './complex/child-adjacent-child';
44
import { isInCompoundWithOneOtherElement } from './complex/is-in-compound';
55
import type { Container } from 'postcss-selector-parser';
66
import { isPseudoInFirstCompound } from './complex/is-pseudo-in-first-compound';
7+
import { samePrecedingElement } from './complex/same-preceding-element';
78

89
export default function complexSelectors(selectors: Array<string>, pluginOptions: { onComplexSelector?: 'warning' }, warnOnComplexSelector: () => void, warnOnPseudoElements: () => void): Array<string> {
910
return selectors.flatMap((selector) => {
@@ -82,7 +83,8 @@ export default function complexSelectors(selectors: Array<string>, pluginOptions
8283
if (
8384
childAdjacentChild(pseudo.parent) ||
8485
isInCompoundWithOneOtherElement(pseudo.parent) ||
85-
isPseudoInFirstCompound(pseudo.parent)
86+
isPseudoInFirstCompound(pseudo.parent) ||
87+
samePrecedingElement(pseudo.parent)
8688
) {
8789
return;
8890
}

plugins/postcss-is-pseudo-class/src/split-selectors/complex/is-in-compound.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export function isInCompoundWithOneOtherElement(selector: parser.Container): boo
4646
return false;
4747
}
4848

49+
if (parser.isPseudoElement(simpleSelector)) {
50+
return false;
51+
}
52+
4953
isPseudo.nodes[0].append(simpleSelector.clone());
5054
isPseudo.replaceWith(...isPseudo.nodes[0].nodes);
5155
simpleSelector.remove();

plugins/postcss-is-pseudo-class/src/split-selectors/complex/is-pseudo-in-first-compound.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// equivalent to
33
// .a > .b > .c
44

5+
import type { Pseudo } from 'postcss-selector-parser';
56
import parser from 'postcss-selector-parser';
67

78
// because `:is()` is in the left-most compound selector
@@ -21,16 +22,28 @@ export function isPseudoInFirstCompound(selector: parser.Container): boolean {
2122
return false;
2223
}
2324

24-
if (parser.isPseudoClass(node) && node.value === ':-csstools-matches') {
25-
if (!node.nodes || node.nodes.length !== 1) {
26-
return false;
27-
}
25+
if (parser.isPseudoElement(node)) {
26+
return false;
27+
}
28+
29+
if (parser.isPseudoClass(node)) {
30+
const nn = node as Pseudo; // because `isPseudoElement` is incorrectly typed
31+
32+
if (nn.value === ':-csstools-matches') {
33+
if (!nn.nodes || nn.nodes.length !== 1) {
34+
return false;
35+
}
2836

29-
isPseudoIndex = i;
30-
break;
37+
isPseudoIndex = i;
38+
break;
39+
}
3140
}
3241
}
3342

43+
if (isPseudoIndex === -1) {
44+
return false;
45+
}
46+
3447
const isPseudo = selector.nodes[isPseudoIndex];
3548
if (!isPseudo || !parser.isPseudoClass(isPseudo)) {
3649
return false;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import parser from 'postcss-selector-parser';
2+
3+
// .a > :-csstools-matches(.b > .c)
4+
// equivalent to
5+
// .a.b > .c
6+
//
7+
// and
8+
//
9+
// .a + :-csstools-matches(.b + .c)
10+
// equivalent to
11+
// .a.b + .c
12+
//
13+
// because adjacent elements have the same parent element.
14+
export function samePrecedingElement(selector: parser.Container): boolean {
15+
if (!selector || !selector.nodes) {
16+
return false;
17+
}
18+
if (selector.type !== 'selector') {
19+
return false;
20+
}
21+
22+
let combinatorIndex = -1;
23+
for (let i = 0; i < selector.nodes.length; i++) {
24+
const node = selector.nodes[i];
25+
if (parser.isCombinator(node)) {
26+
combinatorIndex = i;
27+
break;
28+
}
29+
30+
if (parser.isPseudoElement(node)) {
31+
return false;
32+
}
33+
}
34+
35+
if (combinatorIndex === -1) {
36+
return false;
37+
}
38+
39+
const isPseudoIndex = combinatorIndex + 1;
40+
41+
// immediate sibling/child combinator
42+
if (!selector.nodes[combinatorIndex] || selector.nodes[combinatorIndex].type !== 'combinator' || (selector.nodes[combinatorIndex].value !== '>' && selector.nodes[combinatorIndex].value !== '+')) {
43+
return false;
44+
}
45+
46+
const combinator = selector.nodes[combinatorIndex].value;
47+
48+
if (!selector.nodes[isPseudoIndex] || selector.nodes[isPseudoIndex].type !== 'pseudo' || selector.nodes[isPseudoIndex].value !== ':-csstools-matches') {
49+
return false;
50+
}
51+
52+
// second `:-csstools-matches`
53+
{
54+
if (!selector.nodes[isPseudoIndex].nodes || selector.nodes[isPseudoIndex].nodes.length !== 1) {
55+
return false;
56+
}
57+
58+
if (selector.nodes[isPseudoIndex].nodes[0].type !== 'selector') {
59+
return false;
60+
}
61+
62+
if (!selector.nodes[isPseudoIndex].nodes[0].nodes || selector.nodes[isPseudoIndex].nodes[0].nodes.length !== 3) {
63+
return false;
64+
}
65+
66+
// same combinator
67+
if (!selector.nodes[isPseudoIndex].nodes[0].nodes || selector.nodes[isPseudoIndex].nodes[0].nodes[1].type !== 'combinator' || selector.nodes[isPseudoIndex].nodes[0].nodes[1].value !== combinator) {
68+
return false;
69+
}
70+
}
71+
72+
const isPseudo = selector.nodes[isPseudoIndex];
73+
if (!isPseudo || !parser.isPseudoClass(isPseudo)) {
74+
return false;
75+
}
76+
77+
const before = selector.nodes.slice(0, combinatorIndex);
78+
79+
selector.each((node) => {
80+
node.remove();
81+
});
82+
83+
before.forEach((node) => {
84+
selector.append(node);
85+
});
86+
87+
isPseudo.nodes[0].nodes.forEach((node) => {
88+
selector.append(node);
89+
});
90+
91+
return true;
92+
}

plugins/postcss-is-pseudo-class/test/_tape.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ postcssTape(plugin)({
1919
},
2020
'basic:oncomplex:warning': {
2121
message: 'warns on complex selectors',
22-
warnings: 9,
22+
warnings: 10,
2323
options: {
2424
onComplexSelector: 'warning',
2525
},

plugins/postcss-is-pseudo-class/test/basic.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,7 @@ header:is(.a .b) {
181181
div:is(.a > .b) > .c {
182182
color: green;
183183
}
184+
185+
::before:is(.a > .b) {
186+
color: green;
187+
}

plugins/postcss-is-pseudo-class/test/basic.does-not-exist.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,7 @@ input:hover, input:focus, button:hover, button:focus {
285285
.a > div.b > .c {
286286
color: green;
287287
}
288+
289+
::before:is(.a > .b) {
290+
color: green;
291+
}

plugins/postcss-is-pseudo-class/test/basic.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,7 @@ input:hover, input:focus, button:hover, button:focus {
285285
.a > div.b > .c {
286286
color: green;
287287
}
288+
289+
::before:is(.a > .b) {
290+
color: green;
291+
}

plugins/postcss-is-pseudo-class/test/basic.oncomplex.no-warning.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,7 @@ input:hover, input:focus, button:hover, button:focus {
285285
.a > div.b > .c {
286286
color: green;
287287
}
288+
289+
::before:is(.a > .b) {
290+
color: green;
291+
}

plugins/postcss-is-pseudo-class/test/basic.oncomplex.warning.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,7 @@ input:hover, input:focus, button:hover, button:focus {
285285
.a > div.b > .c {
286286
color: green;
287287
}
288+
289+
::before:is(.a > .b) {
290+
color: green;
291+
}

plugins/postcss-is-pseudo-class/test/basic.preserve.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,7 @@ header:is(.a .b) {
437437
div:is(.a > .b) > .c {
438438
color: green;
439439
}
440+
441+
::before:is(.a > .b) {
442+
color: green;
443+
}

plugins/postcss-is-pseudo-class/test/basic.with-cloned-declarations.expect.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,7 @@ header:is(.a .b) {
439439
div:is(.a > .b) > .c {
440440
color: green;
441441
}
442+
443+
::before:is(.a > .b) {
444+
color: green;
445+
}

plugins/postcss-is-pseudo-class/test/complex.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,48 @@ cartesianProduct([' ', '+', '>', '~'], [' ', '+', '>', '~'], [' ', '+', '>', '~'
279279
280280
console.log(out.join(''));
281281
*/
282+
283+
.ignore :is(.b > .c) {
284+
order: 065;
285+
}
286+
287+
.ignore > :is(.b + .c) {
288+
order: 066;
289+
}
290+
291+
.ignore ~ :is(.b + .c) {
292+
order: 067;
293+
}
294+
295+
/* https: //github.com/csstools/postcss-plugins/issues/1625 */
296+
.a > :is(.b > .d, .c > .d) {
297+
order: 65;
298+
}
299+
300+
.a:hover > :is(.b > .d, .c > .d) {
301+
order: 66;
302+
}
303+
304+
.ignore + .b > :is(.b > .c) {
305+
order: 67;
306+
}
307+
308+
.ignore::before > :is(.b > .c) {
309+
order: 68;
310+
}
311+
312+
.a + :is(.b + .d, .c + .d) {
313+
order: 75;
314+
}
315+
316+
.a:hover + :is(.b + .d, .c + .d) {
317+
order: 76;
318+
}
319+
320+
.ignore + .b + :is(.b + .c) {
321+
order: 77;
322+
}
323+
324+
.ignore::before + :is(.b + .c) {
325+
order: 78;
326+
}

plugins/postcss-is-pseudo-class/test/complex.expect.css

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,48 @@ cartesianProduct([' ', '+', '>', '~'], [' ', '+', '>', '~'], [' ', '+', '>', '~'
279279
280280
console.log(out.join(''));
281281
*/
282+
283+
.ignore :is(.b > .c) {
284+
order: 065;
285+
}
286+
287+
.ignore > :is(.b + .c) {
288+
order: 066;
289+
}
290+
291+
.ignore ~ :is(.b + .c) {
292+
order: 067;
293+
}
294+
295+
/* https: //github.com/csstools/postcss-plugins/issues/1625 */
296+
.a.b > .d, .a.c > .d {
297+
order: 65;
298+
}
299+
300+
.a.b:hover > .d, .a.c:hover > .d {
301+
order: 66;
302+
}
303+
304+
.ignore + .b > :is(.b > .c) {
305+
order: 67;
306+
}
307+
308+
.ignore::before > :is(.b > .c) {
309+
order: 68;
310+
}
311+
312+
.a.b + .d, .a.c + .d {
313+
order: 75;
314+
}
315+
316+
.a.b:hover + .d, .a.c:hover + .d {
317+
order: 76;
318+
}
319+
320+
.ignore + .b + :is(.b + .c) {
321+
order: 77;
322+
}
323+
324+
.ignore::before + :is(.b + .c) {
325+
order: 78;
326+
}

plugins/postcss-is-pseudo-class/test/compound-after-complex-is.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
order: 2.1;
2727
}
2828

29-
.ignore > :is(.a > .b) > .c {
29+
::before > :is(.a > .b) > .c {
3030
order: 2.2;
3131
}
3232

@@ -38,7 +38,7 @@
3838
order: 3.1;
3939
}
4040

41-
.ignore + :is(.a + .b) + .c {
41+
::before + :is(.a + .b) + .c {
4242
order: 3.2;
4343
}
4444

plugins/postcss-is-pseudo-class/test/compound-after-complex-is.expect.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
order: 2.1;
2727
}
2828

29-
.ignore > :is(.a > .b) > .c {
29+
::before > :is(.a > .b) > .c {
3030
order: 2.2;
3131
}
3232

@@ -38,7 +38,7 @@
3838
order: 3.1;
3939
}
4040

41-
.ignore + :is(.a + .b) + .c {
41+
::before + :is(.a + .b) + .c {
4242
order: 3.2;
4343
}
4444

0 commit comments

Comments
 (0)