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.
131 lines
4.5 KiB
131 lines
4.5 KiB
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;
|
|
|