This commit is contained in:
Your Name
2026-07-23 17:56:25 +08:00
parent a05dae8412
commit 4970d8f8d3
4262 changed files with 735221 additions and 0 deletions
+260
View File
@@ -0,0 +1,260 @@
import parse from 'css-tree/selector-parser';
import Specificity from '../index.js';
import { max } from './../util/index.js';
/** @param {import('css-tree').Selector} selectorAST */
const calculateForAST = (selectorAST) => {
// Quit while you're ahead
if (!selectorAST || selectorAST.type !== 'Selector') {
throw new TypeError(`Passed in source is not a Selector AST`);
}
// https://www.w3.org/TR/selectors-4/#specificity-rules
let a = 0; /* ID Selectors */
let b = 0; /* Class selectors, Attributes selectors, and Pseudo-classes */
let c = 0; /* Type selectors and Pseudo-elements */
selectorAST.children.forEach((child) => {
switch (child.type) {
case 'IdSelector':
a += 1;
break;
case 'AttributeSelector':
case 'ClassSelector':
b += 1;
break;
case 'PseudoClassSelector':
switch (child.name.toLowerCase()) {
// “The specificity of a :where() pseudo-class is replaced by zero.”
case 'where':
// Noop :)
break;
case '-webkit-any':
case 'any':
if (child.children?.first) {
b += 1;
}
break;
// “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 '-moz-any':
case 'is':
case 'matches':
case 'not':
case 'has':
if (child.children?.first) {
// Calculate Specificity from nested SelectorList
const max1 = max(...calculate(child.children.first));
// Adjust orig specificity
a += max1.a;
b += max1.b;
c += max1.c;
}
break;
// “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”
case 'nth-child':
case 'nth-last-child':
b += 1;
if (child.children?.first?.selector) {
// Calculate Specificity from SelectorList
const max2 = max(...calculate(child.children.first.selector));
// Adjust orig specificity
a += max2.a;
b += max2.b;
c += max2.c;
}
break;
// “The specificity of :host is that of a pseudo-class. The specificity of :host() is that of a pseudo-class, plus the specificity of its argument.”
// “The specificity of :host-context() is that of a pseudo-class, plus the specificity of its argument.”
case 'host-context':
case 'host':
b += 1;
if (child.children?.first?.children) {
// Workaround to a css-tree bug in which it allows complex selectors instead of only compound selectors
// We work around it by filtering out any Combinator and successive Selectors
const childAST = { type: 'Selector', children: [] };
let foundCombinator = false;
child.children.first.children.forEach((entry) => {
if (foundCombinator) return false;
if (entry.type === 'Combinator') {
foundCombinator = true;
return false;
}
childAST.children.push(entry);
});
// Calculate Specificity from Selector
const childSpecificity = calculate(childAST)[0];
// Adjust orig specificity
a += childSpecificity.a;
b += childSpecificity.b;
c += childSpecificity.c;
}
break;
// Improper use of Pseudo-Class Selectors instead of a Pseudo-Element
// @ref https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements#index
case 'after':
case 'before':
case 'first-letter':
case 'first-line':
c += 1;
break;
default:
b += 1;
break;
}
break;
case 'PseudoElementSelector':
switch (child.name) {
// “The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument.”
case 'slotted':
c += 1;
if (child.children?.first?.children) {
// Workaround to a css-tree bug in which it allows complex selectors instead of only compound selectors
// We work around it by filtering out any Combinator and successive Selectors
const childAST = { type: 'Selector', children: [] };
let foundCombinator = false;
child.children.first.children.forEach((entry) => {
if (foundCombinator) return false;
if (entry.type === 'Combinator') {
foundCombinator = true;
return false;
}
childAST.children.push(entry);
});
// Calculate Specificity from Selector
const childSpecificity = calculate(childAST)[0];
// Adjust orig specificity
a += childSpecificity.a;
b += childSpecificity.b;
c += childSpecificity.c;
}
break;
case 'view-transition-group':
case 'view-transition-image-pair':
case 'view-transition-old':
case 'view-transition-new':
// The specificity of a view-transition selector with a * argument is zero.
if (child.children?.first?.value === '*') {
break;
}
// The specificity of a view-transition selector with an argument is the same
// as for other pseudo - elements, and is equivalent to a type selector.
c += 1;
break;
default:
c += 1;
break;
}
break;
case 'TypeSelector':
// Omit namespace
let typeSelector = child.name;
if (typeSelector.includes('|')) {
typeSelector = typeSelector.split('|')[1];
}
// “Ignore the universal selector”
if (typeSelector !== '*') {
c += 1;
}
break;
default:
// NOOP
break;
}
});
return new Specificity({ a, b, c }, selectorAST);
};
const convertToAST = (source) => {
// The passed in argument was a String.
// ~> Let's try and parse to an AST
if (typeof source === 'string' || source instanceof String) {
try {
return parse(source, {
context: 'selectorList',
});
} catch (e) {
throw new TypeError(`Could not convert passed in source '${source}' to SelectorList: ${e.message}`);
}
}
// The passed in argument was an Object.
// ~> Let's verify if it's a AST of the type Selector or SelectorList
if (source instanceof Object) {
if (source.type && ['Selector', 'SelectorList'].includes(source.type)) {
return source;
}
// Manually parsing subtree when the child is of the type Raw, most likely due to https://github.com/csstree/csstree/issues/151
if (source.type && source.type === 'Raw') {
try {
return parse(source.value, {
context: 'selectorList',
});
} catch (e) {
throw new TypeError(`Could not convert passed in source to SelectorList: ${e.message}`);
}
}
throw new TypeError(`Passed in source is an Object but no AST / AST of the type Selector or SelectorList`);
}
throw new TypeError(`Passed in source is not a String nor an Object. I don't know what to do with it.`);
};
/**
* @param {string} selector
* @returns {Specificity[]}
*/
const calculate = (selector) => {
// Quit while you're ahead
if (!selector) {
return [];
}
// Make sure we have a SelectorList AST
// If not, an exception will be thrown
const ast = convertToAST(selector);
// Selector?
if (ast.type === 'Selector') {
return [calculateForAST(selector)];
}
// SelectorList?
// ~> Calculate Specificity for each contained Selector
if (ast.type === 'SelectorList') {
const specificities = [];
ast.children.forEach((childAST) => {
const specificity = calculateForAST(childAST);
specificities.push(specificity);
});
return specificities;
}
};
export { calculate, calculateForAST };
+1
View File
@@ -0,0 +1 @@
export { calculate, calculateForAST } from './calculate.js';
+135
View File
@@ -0,0 +1,135 @@
import generate from 'css-tree/generator';
import { calculate, calculateForAST } from './core/index.js';
import { compare, equals, greaterThan, lessThan } from './util/compare.js';
import { min, max } from './util/filter.js';
import { sortAsc, sortDesc } from './util/sort.js';
class NotAllowedError extends Error {
constructor() {
super('Manipulating a Specificity instance is not allowed. Instead, create a new Specificity()');
}
}
class Specificity {
constructor(value, selector = null) {
this.value = value;
this.selector = selector;
}
get a() {
return this.value.a;
}
set a(val) {
throw new NotAllowedError();
}
get b() {
return this.value.b;
}
set b(val) {
throw new NotAllowedError();
}
get c() {
return this.value.c;
}
set c(val) {
throw new NotAllowedError();
}
selectorString() {
// this.selector already is a String
if (typeof this.selector === 'string' || this.selector instanceof String) {
return this.selector;
}
// this.selector is a Selector as parsed by CSSTree
if (this.selector instanceof Object) {
if (this.selector.type === 'Selector') {
return generate(this.selector);
}
}
// this.selector is something else …
return '';
}
toObject() {
return this.value;
}
toArray() {
return [this.value.a, this.value.b, this.value.c];
}
toString() {
return `(${this.value.a},${this.value.b},${this.value.c})`;
}
toJSON() {
return {
selector: this.selectorString(),
asObject: this.toObject(),
asArray: this.toArray(),
asString: this.toString(),
};
}
isEqualTo(otherSpecificity) {
return equals(this, otherSpecificity);
}
isGreaterThan(otherSpecificity) {
return greaterThan(this, otherSpecificity);
}
isLessThan(otherSpecificity) {
return lessThan(this, otherSpecificity);
}
static calculate(selector) {
return calculate(selector);
}
static calculateForAST(selector) {
return calculateForAST(selector);
}
static compare(s1, s2) {
return compare(s1, s2);
}
static equals(s1, s2) {
return equals(s1, s2);
}
static lessThan(s1, s2) {
return lessThan(s1, s2);
}
static greaterThan(s1, s2) {
return greaterThan(s1, s2);
}
static min(...specificities) {
return min(...specificities);
}
static max(...specificities) {
return max(...specificities);
}
static sortAsc(...specificities) {
return sortAsc(...specificities);
}
static sortDesc(...specificities) {
return sortDesc(...specificities);
}
}
export default Specificity;
+23
View File
@@ -0,0 +1,23 @@
const compare = (s1, s2) => {
if (s1.a === s2.a) {
if (s1.b === s2.b) {
return s1.c - s2.c;
}
return s1.b - s2.b;
}
return s1.a - s2.a;
};
const equals = (s1, s2) => {
return compare(s1, s2) === 0;
};
const greaterThan = (s1, s2) => {
return compare(s1, s2) > 0;
};
const lessThan = (s1, s2) => {
return compare(s1, s2) < 0;
};
export { compare, equals, greaterThan, lessThan };
+13
View File
@@ -0,0 +1,13 @@
import { sortAsc, sortDesc } from './sort.js';
const max = (...specificities) => {
const sorted = sortDesc(...specificities);
return sorted[0];
};
const min = (...specificities) => {
const sorted = sortAsc(...specificities);
return sorted[0];
};
export { max, min };
+3
View File
@@ -0,0 +1,3 @@
export * from './compare.js';
export * from './sort.js';
export * from './filter.js';
+21
View File
@@ -0,0 +1,21 @@
import { compare } from './compare.js';
const sort = (specificities, order = 'ASC') => {
const sorted = specificities.sort(compare);
if (order === 'DESC') {
return sorted.reverse();
}
return sorted;
};
const sortAsc = (...specificities) => {
return sort(specificities, 'ASC');
};
const sortDesc = (...specificities) => {
return sort(specificities, 'DESC');
};
export { sortAsc, sortDesc };