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 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;