Skip to content

Commit a6064d8

Browse files
committed
Selectors: Add support for comma-separated selector lists in pseudo-classes.
1 parent e4377ea commit a6064d8

File tree

4 files changed

+73
-18
lines changed

4 files changed

+73
-18
lines changed

src/__tests__/selectors.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,18 @@ describe( 'Audit: Selectors', () => {
6363
const { value } = results.find( ( { id } ) => 'count' === id );
6464
expect( value ).toBe( 2 );
6565
} );
66+
67+
it( 'should handle modern CSS', () => {
68+
expect( () => {
69+
audit( [
70+
{
71+
name: 'a.css',
72+
content: `h1, h2 { color: green; }
73+
div:not([hidden]) { color: black; }
74+
body :is(h1, h2) { color: red; }
75+
body :where(h1, h2) { color: orange; }`,
76+
},
77+
] );
78+
} ).not.toThrow( SyntaxError );
79+
} );
6680
} );

src/audits/selectors.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* External dependencies
33
*/
4+
const csstree = require( 'css-tree' );
45
const { parse } = require( 'postcss' );
56

67
const { getSpecificityArray } = require( '../utils/get-specificity' );
@@ -12,10 +13,11 @@ module.exports = function ( files = [] ) {
1213
files.forEach( ( { name, content } ) => {
1314
const root = parse( content, { from: name } );
1415
root.walkRules( function ( { selector } ) {
15-
const selectorList = selector.split( ',' );
16-
selectorList.forEach( ( selectorName ) => {
17-
// Remove excess whitespace from selectors.
18-
selectorName = selectorName.replace( /\s+/g, ' ' ).trim();
16+
const selectorList = csstree.parse( selector, {
17+
context: 'selectorList',
18+
} );
19+
selectorList.children.forEach( ( _selector ) => {
20+
const selectorName = csstree.generate( _selector );
1921
const [ a, b, c ] = getSpecificityArray( selectorName );
2022
const sum = 100 * a + 10 * b + c; // eslint-disable-line no-mixed-operators
2123
selectors.push( {

src/utils/__tests__/get-specificity.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ describe( 'Calculate Specificity', () => {
1414
it( 'should calculate for pseudo-classes', () => {
1515
expect( getSpecificity( ':checked' ) ).toBe( 10 );
1616
expect( getSpecificity( 'a:link' ) ).toBe( 11 );
17+
expect( getSpecificity( 'body:lang(en)' ) ).toBe( 11 );
18+
expect( getSpecificity( 'body:lang(en,ja)' ) ).toBe( 11 );
1719
} );
1820

1921
it( 'should calculate for class selectors', () => {
@@ -36,4 +38,25 @@ describe( 'Calculate Specificity', () => {
3638
getSpecificity( 'li > a[href*="en-US"] > .inline-warning' )
3739
).toBe( 22 );
3840
} );
41+
42+
it( 'should calculate for :is selectors', () => {
43+
expect( getSpecificity( ':is(h1)' ) ).toBe( 1 );
44+
expect( getSpecificity( ':is(h1, .class)' ) ).toBe( 10 );
45+
expect( getSpecificity( ':is(h1, .class, #id)' ) ).toBe( 100 );
46+
expect( getSpecificity( 'span:is(h1)' ) ).toBe( 2 );
47+
} );
48+
49+
it( 'should calculate for :where selectors', () => {
50+
expect( getSpecificity( ':where(h1)' ) ).toBe( 0 );
51+
expect( getSpecificity( ':where(h1, .class)' ) ).toBe( 0 );
52+
expect( getSpecificity( ':where(h1, .class, #id)' ) ).toBe( 0 );
53+
expect( getSpecificity( 'span:where(h1)' ) ).toBe( 1 );
54+
} );
55+
56+
it( 'should calculate for :not selectors', () => {
57+
expect( getSpecificity( ':not(h1)' ) ).toBe( 1 );
58+
expect( getSpecificity( ':not(h1, .class)' ) ).toBe( 10 );
59+
expect( getSpecificity( ':not(h1, .class, #id)' ) ).toBe( 100 );
60+
expect( getSpecificity( 'span:not(h1)' ) ).toBe( 2 );
61+
} );
3962
} );

src/utils/get-specificity.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,28 @@ function calculateSpecificity( [ a, b, c ], selector ) {
1414
if ( ! selector.type ) {
1515
return;
1616
}
17-
if ( 'lang' !== selector.name && selector.children ) {
18-
return selector.children
19-
.toArray()
20-
.reduce( calculateSpecificity, [ a, b, c ] );
17+
18+
if ( 'PseudoClassSelector' === selector.type && 'lang' !== selector.name ) {
19+
if ( 'where' === selector.name ) {
20+
return [ a, b, c ];
21+
}
22+
if ( selector.children ) {
23+
let maxSpec = [ 0, 0, 0 ];
24+
let max = -1;
25+
selector.children.forEach( ( list ) => {
26+
if ( 'SelectorList' === list.type ) {
27+
list.children.forEach( ( s ) => {
28+
const _selector = csstree.generate( s );
29+
const result = getSpecificity( _selector );
30+
if ( result > max ) {
31+
maxSpec = getSpecificityArray( _selector );
32+
max = result;
33+
}
34+
} );
35+
}
36+
} );
37+
return [ a + maxSpec[ 0 ], b + maxSpec[ 1 ], c + maxSpec[ 2 ] ];
38+
}
2139
}
2240

2341
switch ( selector.type ) {
@@ -65,11 +83,10 @@ function calculateSpecificity( [ a, b, c ], selector ) {
6583
function getSpecificity( selector ) {
6684
const node = csstree.parse( selector, { context: 'selector' } );
6785
const selectorList = node.children.toArray();
68-
const [ a, b, c ] = selectorList.reduce( calculateSpecificity, [
69-
0,
70-
0,
71-
0,
72-
] );
86+
const [ a, b, c ] = selectorList.reduce(
87+
calculateSpecificity,
88+
[ 0, 0, 0 ]
89+
);
7390
return 100 * a + 10 * b + c;
7491
}
7592

@@ -82,11 +99,10 @@ function getSpecificity( selector ) {
8299
function getSpecificityArray( selector ) {
83100
const node = csstree.parse( selector, { context: 'selector' } );
84101
const selectorList = node.children.toArray();
85-
const [ a, b, c ] = selectorList.reduce( calculateSpecificity, [
86-
0,
87-
0,
88-
0,
89-
] );
102+
const [ a, b, c ] = selectorList.reduce(
103+
calculateSpecificity,
104+
[ 0, 0, 0 ]
105+
);
90106
return [ a, b, c ];
91107
}
92108

0 commit comments

Comments
 (0)