更新
This commit is contained in:
+260
@@ -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
@@ -0,0 +1 @@
|
||||
export { calculate, calculateForAST } from './calculate.js';
|
||||
+135
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
export * from './compare.js';
|
||||
export * from './sort.js';
|
||||
export * from './filter.js';
|
||||
+21
@@ -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 };
|
||||
Reference in New Issue
Block a user