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