You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
132 lines
4.5 KiB
132 lines
4.5 KiB
2 years ago
|
import { parse } from 'css-tree';
|
||
|
|
||
|
function ensureSelectorList(node) {
|
||
|
if (node.type === 'Raw') {
|
||
|
return parse(node.value, { context: 'selectorList' });
|
||
|
}
|
||
|
|
||
|
return node;
|
||
|
}
|
||
|
|
||
|
function maxSpecificity(a, b) {
|
||
|
for (let i = 0; i < 3; i++) {
|
||
|
if (a[i] !== b[i]) {
|
||
|
return a[i] > b[i] ? a : b;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return a;
|
||
|
}
|
||
|
|
||
|
function maxSelectorListSpecificity(selectorList) {
|
||
|
return ensureSelectorList(selectorList).children.reduce(
|
||
|
(result, node) => maxSpecificity(specificity(node), result),
|
||
|
[0, 0, 0]
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// §16. Calculating a selector’s specificity
|
||
|
// https://www.w3.org/TR/selectors-4/#specificity-rules
|
||
|
function specificity(simpleSelector) {
|
||
|
let A = 0;
|
||
|
let B = 0;
|
||
|
let C = 0;
|
||
|
|
||
|
// A selector’s specificity is calculated for a given element as follows:
|
||
|
simpleSelector.children.forEach((node) => {
|
||
|
switch (node.type) {
|
||
|
// count the number of ID selectors in the selector (= A)
|
||
|
case 'IdSelector':
|
||
|
A++;
|
||
|
break;
|
||
|
|
||
|
// count the number of class selectors, attributes selectors, ...
|
||
|
case 'ClassSelector':
|
||
|
case 'AttributeSelector':
|
||
|
B++;
|
||
|
break;
|
||
|
|
||
|
// ... and pseudo-classes in the selector (= B)
|
||
|
case 'PseudoClassSelector':
|
||
|
switch (node.name.toLowerCase()) {
|
||
|
// The specificity of an :is(), :not(), or :has() pseudo-class is replaced
|
||
|
// by the specificity of the most specific complex selector in its selector list argument.
|
||
|
case 'not':
|
||
|
case 'has':
|
||
|
case 'is':
|
||
|
// :matches() is used before it was renamed to :is()
|
||
|
// https://github.com/w3c/csswg-drafts/issues/3258
|
||
|
case 'matches':
|
||
|
// Older browsers support :is() functionality as prefixed pseudo-class :any()
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/:is
|
||
|
case '-webkit-any':
|
||
|
case '-moz-any': {
|
||
|
const [a, b, c] = maxSelectorListSpecificity(node.children.first);
|
||
|
|
||
|
A += a;
|
||
|
B += b;
|
||
|
C += c;
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Analogously, the specificity of an :nth-child() or :nth-last-child() selector
|
||
|
// is the specificity of the pseudo class itself (counting as one pseudo-class selector)
|
||
|
// plus the specificity of the most specific complex selector in its selector list argument (if any).
|
||
|
case 'nth-child':
|
||
|
case 'nth-last-child': {
|
||
|
const arg = node.children.first;
|
||
|
|
||
|
if (arg.type === 'Nth' && arg.selector) {
|
||
|
const [a, b, c] = maxSelectorListSpecificity(arg.selector);
|
||
|
|
||
|
A += a;
|
||
|
B += b + 1;
|
||
|
C += c;
|
||
|
} else {
|
||
|
B++;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// The specificity of a :where() pseudo-class is replaced by zero.
|
||
|
case 'where':
|
||
|
break;
|
||
|
|
||
|
// The four Level 2 pseudo-elements (::before, ::after, ::first-line, and ::first-letter) may,
|
||
|
// for legacy reasons, be represented using the <pseudo-class-selector> grammar,
|
||
|
// with only a single ":" character at their start.
|
||
|
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
|
||
|
case 'before':
|
||
|
case 'after':
|
||
|
case 'first-line':
|
||
|
case 'first-letter':
|
||
|
C++;
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
B++;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
// count the number of type selectors ...
|
||
|
case 'TypeSelector':
|
||
|
// ignore the universal selector
|
||
|
if (!node.name.endsWith('*')) {
|
||
|
C++;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
// ... and pseudo-elements in the selector (= C)
|
||
|
case 'PseudoElementSelector':
|
||
|
C++;
|
||
|
break;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return [A, B, C];
|
||
|
};
|
||
|
|
||
|
export default specificity;
|