Skip to content

Commit bb5782b

Browse files
committed
feat(purgecss): add support for :where and :is #978
1 parent 7858b7a commit bb5782b

File tree

7 files changed

+141
-5
lines changed

7 files changed

+141
-5
lines changed

packages/purgecss-from-html/src/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const mergedExtractorResults = (
3535
};
3636

3737
const getSelectorsInElement = (
38-
element: htmlparser2.Htmlparser2TreeAdapterMap['element']
38+
element: htmlparser2.Htmlparser2TreeAdapterMap["element"]
3939
): ExtractorResultDetailed => {
4040
const result: ExtractorResultDetailed = {
4141
attributes: {
@@ -63,7 +63,9 @@ const getSelectorsInElement = (
6363
};
6464

6565
const getSelectorsInNodes = (
66-
node: htmlparser2.Htmlparser2TreeAdapterMap['document'] | htmlparser2.Htmlparser2TreeAdapterMap['element']
66+
node:
67+
| htmlparser2.Htmlparser2TreeAdapterMap["document"]
68+
| htmlparser2.Htmlparser2TreeAdapterMap["element"]
6769
): ExtractorResultDetailed => {
6870
let result: ExtractorResultDetailed = {
6971
attributes: {
@@ -103,7 +105,7 @@ const getSelectorsInNodes = (
103105
*/
104106
const purgecssFromHtml = (content: string): ExtractorResultDetailed => {
105107
const tree = parse5.parse(content, {
106-
treeAdapter: htmlparser2.adapter
108+
treeAdapter: htmlparser2.adapter,
107109
});
108110

109111
return getSelectorsInNodes(tree);

packages/purgecss/__tests__/pseudo-class.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,37 @@ describe("pseudo classes", () => {
100100
expect(purgedCSS.includes("row:after")).toBe(false);
101101
});
102102
});
103+
104+
describe(":where pseudo class", () => {
105+
let purgedCSS: string;
106+
beforeAll(async () => {
107+
const resultsPurge = await new PurgeCSS().purge({
108+
content: [`${ROOT_TEST_EXAMPLES}pseudo-class/where.html`],
109+
css: [`${ROOT_TEST_EXAMPLES}pseudo-class/where.css`],
110+
});
111+
purgedCSS = resultsPurge[0].css;
112+
});
113+
114+
it("removes unused selectors", () => {
115+
expect(purgedCSS.includes(".unused")).toBe(false);
116+
expect(purgedCSS.includes(".root :where(.a) .c {")).toBe(true);
117+
expect(purgedCSS.includes(".root:where(.a) .c {")).toBe(true);
118+
});
119+
});
120+
121+
describe(":is pseudo class", () => {
122+
let purgedCSS: string;
123+
beforeAll(async () => {
124+
const resultsPurge = await new PurgeCSS().purge({
125+
content: [`${ROOT_TEST_EXAMPLES}pseudo-class/is.html`],
126+
css: [`${ROOT_TEST_EXAMPLES}pseudo-class/is.css`],
127+
});
128+
purgedCSS = resultsPurge[0].css;
129+
});
130+
131+
it("removes unused selectors", () => {
132+
expect(purgedCSS.includes(".unused")).toBe(false);
133+
expect(purgedCSS.includes(".root :is(.a) .c {")).toBe(true);
134+
expect(purgedCSS.includes(".root:is(.a) .c {")).toBe(true);
135+
});
136+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.root :is(.a, .b) .unused {
2+
color: red;
3+
}
4+
5+
.root :is(.a, .unused) .c {
6+
color: blue;
7+
}
8+
9+
.root :is(.a, .b) .c :is(.unused, .unused2) {
10+
color: green;
11+
}
12+
13+
.root:is(.unused) .c {
14+
color: rebeccapurple;
15+
}
16+
17+
.root:is(.a) .c {
18+
color: cyan;
19+
}
20+
21+
.root :is(.unused) .c,
22+
.root :is(.a, .b) .c :is(.unused, .unused2) {
23+
display: flex;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="root">
2+
<div class="a">
3+
<div class="c"></div>
4+
</div>
5+
<div class="b">
6+
<div class="c"></div>
7+
</div>
8+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.root :where(.a, .b) .unused {
2+
color: red;
3+
}
4+
5+
.root :where(.a, .unused) .c {
6+
color: blue;
7+
}
8+
9+
.root :where(.a, .b) .c :where(.unused, .unused2) {
10+
color: green;
11+
}
12+
13+
.root:where(.unused) .c {
14+
color: rebeccapurple;
15+
}
16+
17+
.root:where(.a) .c {
18+
color: cyan;
19+
}
20+
21+
.root :where(.unused) .c,
22+
.root :where(.a, .b) .c :where(.unused, .unused2) {
23+
display: flex;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<div class="root">
2+
<div class="a">
3+
<div class="c"></div>
4+
</div>
5+
<div class="b">
6+
<div class="c"></div>
7+
</div>
8+
</div>

packages/purgecss/src/index.ts

+38-2
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,20 @@ function isInPseudoClass(selector: selectorParser.Node): boolean {
289289
);
290290
}
291291

292+
/**
293+
* Returns true if the selector is inside the pseudo classes :where() or :is()
294+
* @param selector - selector
295+
*/
296+
function isInPseudoClassWhereOrIs(selector: selectorParser.Node): boolean {
297+
return (
298+
(selector.parent &&
299+
selector.parent.type === "pseudo" &&
300+
(selector.parent.value === ":where" ||
301+
selector.parent.value === ":is")) ||
302+
false
303+
);
304+
}
305+
292306
function isPostCSSAtRule(node?: postcss.Node): node is postcss.AtRule {
293307
return node?.type === "atrule";
294308
}
@@ -511,6 +525,13 @@ class PurgeCSS {
511525

512526
let keepSelector = true;
513527
const selectorsRemovedFromRule: string[] = [];
528+
529+
// selector transformer, walk over the list of the parsed selectors twice.
530+
// First pass will remove the unused selectors. It goes through
531+
// pseudo-classes like :where() and :is() and remove the unused
532+
// selectors inside of them, but will not remove the pseudo-classes
533+
// themselves. Second pass will remove selectors containing empty
534+
// :where and :is.
514535
node.selector = selectorParser((selectorsParsed) => {
515536
selectorsParsed.walk((selector) => {
516537
if (selector.type !== "selector") {
@@ -529,6 +550,19 @@ class PurgeCSS {
529550
selector.remove();
530551
}
531552
});
553+
554+
selectorsParsed.walk((selector) => {
555+
if (selector.type !== "selector") {
556+
return;
557+
}
558+
559+
if (
560+
selector.toString() &&
561+
/(:where$)|(:is$)|(:where[^(])|(:is[^(])/.test(selector.toString())
562+
) {
563+
selector.remove();
564+
}
565+
});
532566
}).processSync(node.selector);
533567

534568
// declarations
@@ -815,8 +849,10 @@ class PurgeCSS {
815849
selector: selectorParser.Selector,
816850
selectorsFromExtractor: ExtractorResultSets
817851
): boolean {
818-
// ignore the selector if it is inside a pseudo class
819-
if (isInPseudoClass(selector)) return true;
852+
// selectors in pseudo classes are ignored except :where() and :is(). For those pseudo-classes, we are treating the selectors inside the same way as they would be outside.
853+
if (isInPseudoClass(selector) && !isInPseudoClassWhereOrIs(selector)) {
854+
return true;
855+
}
820856

821857
// if there is any greedy safelist pattern, run all the selector parts through them
822858
// if there is any match, return true

0 commit comments

Comments
 (0)