feat: initial commit of Railtrack Pro prototype with complete test suite
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2022 Bramus Van Damme - https://www.bram.us/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
[](https://codepen.io/bramus/pen/WNXyoYm)
|
||||
|
||||
# Specificity
|
||||
|
||||
`@bramus/specificity` is a package to calculate the specificity of CSS Selectors. It also includes some convenience functions to compare, sort, and filter an array of specificity values.
|
||||
|
||||
Supports [Selectors Level 4](https://www.w3.org/TR/selectors-4/), including those special cases `:is()`, `:where()`, `:not()`, etc.
|
||||
|
||||
Demo: [https://codepen.io/bramus/pen/WNXyoYm](https://codepen.io/bramus/pen/WNXyoYm)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i @bramus/specificity
|
||||
```
|
||||
|
||||
## Usage / Example
|
||||
|
||||
At its core, `@bramus/specificity` exposes a `Specificity` class. Its static `calculate` method can be used to calculate the specificity of a given CSS [Selector List](https://www.w3.org/TR/selectors-4/#grouping) string.
|
||||
|
||||
```js
|
||||
import Specificity from '@bramus/specificity';
|
||||
|
||||
const specificities = Specificity.calculate('header:where(#top) nav li:nth-child(2n), #doormat');
|
||||
```
|
||||
|
||||
Because `calculate` accepts a [Selector List](https://www.w3.org/TR/selectors-4/#grouping) — which can contain more than 1 [Selector](https://www.w3.org/TR/selectors-4/#selector) — it will always return an array, with each entry being a `Specificity` instance — one per found selector.
|
||||
|
||||
```js
|
||||
const specificities = Specificity.calculate('header:where(#top) nav li:nth-child(2n), #doormat');
|
||||
specificities.map((s) => s.toString()); // ~> ["(0,1,3)","(1,0,0)"]
|
||||
```
|
||||
|
||||
💡 If you know you’re passing only a single Selector into `calculate()`, you can use JavaScript’s built-in destructuring to keep your variable names clean.
|
||||
|
||||
```js
|
||||
const [s] = Specificity.calculate('header:where(#top) nav li:nth-child(2n)');
|
||||
s.toString(); // ~> "(0,1,3)"
|
||||
```
|
||||
|
||||
💡 Under the hood, `@bramus/specificity` uses [CSSTree](https://github.com/csstree/csstree) to do the parsing of strings to Selectors. As a result, the `calculate` method also accepts a [CSSTree AST](https://github.com/csstree/csstree/blob/master/docs/ast.md) of the types `Selector` and `SelectorList`.
|
||||
|
||||
If you have a pre-parsed CSSTree AST of the type `Selector` you can pass it into `Specificity.calculateForAST()`. It [performs slightly better](#benchmark) than `Specificity.calculate()` as it needs to check fewer things. It differs from `Specificity.calculate()` in that it does not return an array of `Specificity` instances but only a single value.
|
||||
|
||||
## The Return Format
|
||||
|
||||
A calculated specificity is represented as an instance of the `Specificity` class. The `Specificity` class includes methods to get the specificity value in a certain format, along with some convenience methods to compare it against other instances.
|
||||
|
||||
```js
|
||||
// 🚀 Thunderbirds are go!
|
||||
import Specificity from '@bramus/specificity';
|
||||
|
||||
// ✨ Calculate specificity for each Selector in the given Selector List
|
||||
const specificities = Specificity.calculate('header:where(#top) nav li:nth-child(2n), #doormat');
|
||||
|
||||
// 🚚 The values in the array are instances of the Specificity class
|
||||
const s = specificities[0]; // Instance of Specificity
|
||||
|
||||
// 👀 Read the specificity value using one of its accessors
|
||||
s.value; // { a: 0, b: 1, c: 3 }
|
||||
s.a; // 0
|
||||
s.b; // 1
|
||||
s.c; // 3
|
||||
|
||||
// 🛠 Convert the calculated value to various formats using one of the toXXX() instance methods
|
||||
s.toString(); // "(0,1,3)"
|
||||
s.toArray(); // [0, 1, 3]
|
||||
s.toObject(); // { a: 0, b: 1, c: 3 }
|
||||
|
||||
// 💡 Extract the matched selector string
|
||||
s.selectorString(); // "header:where(#top) nav li:nth-child(2n)"
|
||||
|
||||
// 🔀 Use one of its instance comparison methods to compare it to another Specificity instance
|
||||
s.isEqualTo(specificities[1]); // false
|
||||
s.isGreaterThan(specificities[1]); // false
|
||||
s.isLessThan(specificities[1]); // true
|
||||
|
||||
// 💻 Don’t worry about JSON.stringify()
|
||||
JSON.stringify(s);
|
||||
// {
|
||||
// "selector": 'header:where(#top) nav li:nth-child(2n)',
|
||||
// "asObject": { "a": 0, "b": 1, "c": 3 },
|
||||
// "asArray": [0, 1, 3],
|
||||
// "asString": "(0,1,3)",
|
||||
// }
|
||||
```
|
||||
|
||||
## Utility Functions (Static Methods)
|
||||
|
||||
This package also exposes some utility functions to work with specificities. These utility functions are all exposed as static methods on the `Specificity` class.
|
||||
|
||||
- Comparing:
|
||||
|
||||
- `Specificity.compare(s1, s2)`: Compares s1 to s2. Returns a value that can be:
|
||||
- `> 0` = Sort s2 before s1 _(i.e. s1 is more specific than s2)_
|
||||
- `0` = Keep original order of s1 and s2 _(i.e. s1 and s2 are equally specific)_
|
||||
- `< 0` = Sort s1 before s2 _(i.e. s1 is less specific than s2)_
|
||||
- `Specificity.equals(s1, s2)`: Returns `true` if s1 and s2 have the same specificity. If not, `false` is returned.
|
||||
- `Specificity.greaterThan(s1, s2)`: Returns `true` if s1 has a higher specificity than s2. If not, `false` is returned.
|
||||
- `Specificity.lessThan(s1, s2)`: Returns `true` if s1 has a lower specificity than s2. If not, `false` is returned.
|
||||
|
||||
- Sorting:
|
||||
|
||||
- `Specificity.sortAsc(s1, s2, …, sN)`: Sorts the given specificities in ascending order _(low specificity to high specificity)_
|
||||
- `Specificity.sortDesc(s1, s2, …, sN)`: Sorts the given specificities in descending order _(high specificity to low specificity)_
|
||||
|
||||
- Filtering:
|
||||
- `Specificity.min(s1, s2, …, sN)`: Filters out the value with the lowest specificity
|
||||
- `Specificity.max(s1, s2, …, sN)`: Filters out the value with the highest specificity
|
||||
|
||||
A specificity passed into any of these utility functions can be any of:
|
||||
|
||||
- An instance of the included `Specificity` class
|
||||
- A simple Object such as `{'a': 1, 'b': 0, 'c': 2}`
|
||||
|
||||
## Utility Functions (Standalone)
|
||||
|
||||
All static methods the `Specificity` class exposes are also exported as standalone functions using [Subpath Exports](https://nodejs.org/api/packages.html#subpath-exports).
|
||||
|
||||
If you're only interested in including some of these functions into your project you can import them from their Subpath. As a result, your bundle size will be reduced greatly _(except for including the standalone `calculate`, as it returns an array of `Specificity` instances that relies on the whole lot)_
|
||||
|
||||
```js
|
||||
import { calculate, calculateForAST } from '@bramus/specificity/core';
|
||||
import { compare, equals, greaterThan, lessThan } from '@bramus/specificity/compare';
|
||||
import { min, max } from '@bramus/specificity/filter';
|
||||
import { sortAsc, sortDesc } from '@bramus/specificity/sort';
|
||||
```
|
||||
|
||||
## Type Definitions
|
||||
|
||||
Although `@bramus/specificity` is written in Vanilla JavaScript, it does include [Type Definitions](https://www.typescriptlang.org/docs/handbook/2/type-declarations.html) which are exposed via its `package.json`.
|
||||
|
||||
## Binary/CLI
|
||||
|
||||
`@bramus/specificity` exposes a binary named `specificity` to calculate the specificity of a given selector list on the CLI. For each selector that it finds, it'll print out the calculated specificity as a string on a new line.
|
||||
|
||||
```bash
|
||||
$ specificity "header:where(#top) nav li:nth-child(2n), #doormat"
|
||||
(0,1,3)
|
||||
(1,0,0)
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
A benchmark is included, which you can invoke using `npm run benchmark`.
|
||||
|
||||
Sample results (tested on a MacBook Air M3):
|
||||
|
||||
```
|
||||
Specificity.calculate(string) x 420,682 ops/sec ±0.34% (98 runs sampled)
|
||||
Specificity.calculate(ast) - using SelectorList x 8,994,080 ops/sec ±0.25% (98 runs sampled)
|
||||
Specificity.calculate(ast) - using Selector x 11,054,856 ops/sec ±0.39% (91 runs sampled)
|
||||
Specificity.calculateForAST(ast) x 12,652,322 ops/sec ±0.35% (96 runs sampled)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
`@bramus/specificity` is released under the MIT public license. See the enclosed `LICENSE` for details.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
The idea to create this package was sparked by [the wonderful Specificity Calculator created by Kilian Valkhof / Polypane](https://polypane.app/css-specificity-calculator/), a highly educational tool that not only calculates the specificity, but also explains which parts are responsible for it.
|
||||
|
||||
The heavy lifting of doing the actual parsing of Selectors is done by [CSSTree](https://github.com/csstree/csstree).
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import Specificity from '../dist/index.js';
|
||||
|
||||
if (!process.argv[2]) {
|
||||
console.error('❌ Missing selector argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const specificities = Specificity.calculate(process.argv[2]);
|
||||
console.log(specificities.map((specificity) => `${specificity}`).join('\n'));
|
||||
} catch (e) {
|
||||
console.error(`❌ ${e.message}`);
|
||||
}
|
||||
+8
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+8
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+59
@@ -0,0 +1,59 @@
|
||||
// Types & Classes
|
||||
export type SpecificityArray = [number, number, number];
|
||||
export type SpecificityObject = { a: number; b: number; c: number };
|
||||
|
||||
export default class Specificity {
|
||||
static calculate(selector: string | CSSTreeAST): Array<Specificity>;
|
||||
static calculateForAST(selectorAST: CSSTreeAST): Specificity;
|
||||
static compare(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): number;
|
||||
static equals(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
static lessThan(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
static greaterThan(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
static min(...specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
static max(...specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
static sortAsc(...specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
static sortDesc(...specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
constructor(value: SpecificityObject, selector?: any);
|
||||
value: SpecificityObject;
|
||||
selector: string | CSSTreeAST;
|
||||
set a(arg: number);
|
||||
get a(): number;
|
||||
set b(arg: number);
|
||||
get b(): number;
|
||||
set c(arg: number);
|
||||
get c(): number;
|
||||
selectorString(): string;
|
||||
toObject(): SpecificityObject;
|
||||
toArray(): SpecificityArray;
|
||||
toString(): string;
|
||||
toJSON(): {
|
||||
selector: string;
|
||||
asObject: SpecificityObject;
|
||||
asArray: SpecificityArray;
|
||||
asString: string;
|
||||
};
|
||||
isEqualTo(otherSpecificity: SpecificityInstanceOrObject): boolean;
|
||||
isGreaterThan(otherSpecificity: SpecificityInstanceOrObject): boolean;
|
||||
isLessThan(otherSpecificity: SpecificityInstanceOrObject): boolean;
|
||||
}
|
||||
|
||||
type SpecificityInstanceOrObject = Specificity | SpecificityObject;
|
||||
type CSSTreeAST = Object; // @TODO: Define shape
|
||||
|
||||
// CORE
|
||||
export function calculate(selector: string | CSSTreeAST): Array<Specificity>;
|
||||
export function calculateForAST(selectorAST: CSSTreeAST): Specificity;
|
||||
|
||||
// UTIL: COMPARE
|
||||
export function equals(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
export function greaterThan(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
export function lessThan(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
|
||||
export function compare(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): number;
|
||||
|
||||
// UTIL: FILTER
|
||||
export function min(specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
export function max(specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject;
|
||||
|
||||
// UTIL: SORT
|
||||
export function sortAsc(specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject[];
|
||||
export function sortDesc(specificities: SpecificityInstanceOrObject[]): SpecificityInstanceOrObject[];
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "@bramus/specificity",
|
||||
"version": "2.4.2",
|
||||
"description": "Calculate specificity of a CSS Selector",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"browser": "./dist/index.js",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./core": {
|
||||
"import": "./src/core/index.js"
|
||||
},
|
||||
"./util": {
|
||||
"import": "./src/util/index.js"
|
||||
},
|
||||
"./compare": {
|
||||
"import": "./src/util/compare.js"
|
||||
},
|
||||
"./filter": {
|
||||
"import": "./src/util/filter.js"
|
||||
},
|
||||
"./sort": {
|
||||
"import": "./src/util/sort.js"
|
||||
}
|
||||
},
|
||||
"unpkg": "./dist/index.js",
|
||||
"jsdelivr": "./dist/index.js",
|
||||
"files": [
|
||||
"bin",
|
||||
"src",
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"types": "./index.d.ts",
|
||||
"bin": {
|
||||
"specificity": "./bin/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build-esm": "esbuild --bundle ./src/index.js --outfile=./dist/index.js --format=esm --sourcemap --minify",
|
||||
"build-cjs": "esbuild --bundle ./src/index.js --outfile=./dist/index.cjs --format=cjs --sourcemap --minify",
|
||||
"lint": "prettier --check '{src,test}/**/*.{ts,tsx,js,jsx}'",
|
||||
"format": "prettier --write '{src,test}/**/*.{ts,tsx,js,jsx}'",
|
||||
"build": "npm run build-esm && npm run build-cjs",
|
||||
"prepack": "npm run prevent-dirty-tree && npm run test",
|
||||
"prepublish": "npm run build",
|
||||
"pretest": "npm run build",
|
||||
"test": "mocha",
|
||||
"prebenchmark": "npm run build",
|
||||
"benchmark": "node ./benchmark/bench.cjs",
|
||||
"beta-version-patch": "npm version $(semver $npm_package_version -i prerelease --preid beta)",
|
||||
"beta-version-minor": "npm version $(semver $npm_package_version -i preminor --preid beta)",
|
||||
"beta-version-major": "npm version $(semver $npm_package_version -i premajor --preid beta)",
|
||||
"rc-version": "npm version $(semver $npm_package_version -i prerelease --preid rc)",
|
||||
"final-release": "npm version $(semver $npm_package_version -i)",
|
||||
"preversion": "npm run prevent-dirty-tree && npm run test",
|
||||
"prevent-dirty-tree": "exit $(git status --porcelain | wc -l)"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bramus/specificity.git"
|
||||
},
|
||||
"keywords": [
|
||||
"css",
|
||||
"specificity"
|
||||
],
|
||||
"author": {
|
||||
"name": "Bramus Van Damme",
|
||||
"email": "bramus@bram.us",
|
||||
"twitter": "@bramus",
|
||||
"web": "https://www.bram.us/"
|
||||
},
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/bramus/specificity/issues"
|
||||
},
|
||||
"homepage": "https://github.com/bramus/specificity#readme",
|
||||
"devDependencies": {
|
||||
"benchmark": "^2.1.4",
|
||||
"esbuild": "^0.25.0",
|
||||
"microtime": "^3.1.1",
|
||||
"mocha": "^11.1.0",
|
||||
"prettier": "^3.5.1",
|
||||
"semver": "^7.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
}
|
||||
}
|
||||
+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