Skip to content

Commit 74f109c

Browse files
committed
fix #1625
1 parent 06aa3af commit 74f109c

19 files changed

Lines changed: 249 additions & 14 deletions

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

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

0 commit comments

Comments
 (0)