Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
node-version: "17"

- name: Cache node_modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Expand Down Expand Up @@ -52,7 +52,7 @@ jobs:
node-version: "17"

- name: Cache node_modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Expand All @@ -78,7 +78,7 @@ jobs:
node-version: "17"

- name: Cache node_modules
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
Expand Down
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type SpecificityObject = { a: number; b: number; c: number };

export default class Specificity {
static calculate(selector: string | CSSTreeAST): Array<Specificity>;
static calculateForAST(selectorAST: CSSTreeAST): Array<Specificity>;
static compare(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): number;
static equals(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
static lessThan(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
Expand Down Expand Up @@ -41,6 +42,7 @@ 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;
Expand Down
76 changes: 42 additions & 34 deletions src/core/calculate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ import parse from 'css-tree/selector-parser';
import Specificity from '../index.js';
import { max } from './../util/index.js';

const calculateSpecificityOfSelectorObject = (selectorObj) => {
/** @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
const specificity = {
a: 0 /* ID Selectors */,
b: 0 /* Class selectors, Attributes selectors, and Pseudo-classes */,
c: 0 /* Type selectors and Pseudo-elements */,
};
let a = 0; /* ID Selectors */
let b = 0; /* Class selectors, Attributes selectors, and Pseudo-classes */
let c = 0; /* Type selectors and Pseudo-elements */

selectorObj.children.forEach((child) => {
selectorAST.children.forEach((child) => {
switch (child.type) {
case 'IdSelector':
specificity.a += 1;
a += 1;
break;

case 'AttributeSelector':
case 'ClassSelector':
specificity.b += 1;
b += 1;
break;

case 'PseudoClassSelector':
Expand All @@ -31,7 +35,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
case '-webkit-any':
case 'any':
if (child.children) {
specificity.b += 1;
b += 1;
}
break;

Expand All @@ -46,34 +50,34 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
const max1 = max(...calculate(child.children.first));

// Adjust orig specificity
specificity.a += max1.a;
specificity.b += max1.b;
specificity.c += max1.c;
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':
specificity.b += 1;
b += 1;

if (child.children && child.children.first.selector) {
// Calculate Specificity from SelectorList
const max2 = max(...calculate(child.children.first.selector));

// Adjust orig specificity
specificity.a += max2.a;
specificity.b += max2.b;
specificity.c += max2.c;
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':
specificity.b += 1;
b += 1;

if (child.children) {
// Workaround to a css-tree bug in which it allows complex selectors instead of only compound selectors
Expand All @@ -93,9 +97,9 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
const childSpecificity = calculate(childAST)[0];

// Adjust orig specificity
specificity.a += childSpecificity.a;
specificity.b += childSpecificity.b;
specificity.c += childSpecificity.c;
a += childSpecificity.a;
b += childSpecificity.b;
c += childSpecificity.c;
}
break;

Expand All @@ -105,11 +109,11 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
case 'before':
case 'first-letter':
case 'first-line':
specificity.c += 1;
c += 1;
break;

default:
specificity.b += 1;
b += 1;
break;
}
break;
Expand All @@ -118,7 +122,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
switch (child.name) {
// “The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument.”
case 'slotted':
specificity.c += 1;
c += 1;

if (child.children) {
// Workaround to a css-tree bug in which it allows complex selectors instead of only compound selectors
Expand All @@ -138,9 +142,9 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
const childSpecificity = calculate(childAST)[0];

// Adjust orig specificity
specificity.a += childSpecificity.a;
specificity.b += childSpecificity.b;
specificity.c += childSpecificity.c;
a += childSpecificity.a;
b += childSpecificity.b;
c += childSpecificity.c;
}
break;

Expand All @@ -154,11 +158,11 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
}
// 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.
specificity.c += 1;
c += 1;
break;

default:
specificity.c += 1;
c += 1;
break;
}
break;
Expand All @@ -172,7 +176,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {

// “Ignore the universal selector”
if (typeSelector !== '*') {
specificity.c += 1;
c += 1;
}
break;

Expand All @@ -182,7 +186,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
}
});

return new Specificity(specificity, selectorObj);
return new Specificity({ a, b, c }, selectorAST);
};

const convertToAST = (source) => {
Expand Down Expand Up @@ -222,6 +226,10 @@ const convertToAST = (source) => {
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) {
Expand All @@ -234,19 +242,19 @@ const calculate = (selector) => {

// Selector?
if (ast.type === 'Selector') {
return [calculateSpecificityOfSelectorObject(selector)];
return [calculateForAST(selector)];
}

// SelectorList?
// ~> Calculate Specificity for each contained Selector
if (ast.type === 'SelectorList') {
const specificities = [];
ast.children.forEach((selector) => {
const specificity = calculateSpecificityOfSelectorObject(selector);
const specificity = calculateForAST(selector);
specificities.push(specificity);
});
return specificities;
}
};

export { calculate };
export { calculate, calculateForAST };
2 changes: 1 addition & 1 deletion src/core/index.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { calculate } from './calculate.js';
export { calculate, calculateForAST } from './calculate.js';
7 changes: 6 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import generate from 'css-tree/generator';
import { calculate } from './core/index.js';

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';
Expand Down Expand Up @@ -94,6 +95,10 @@ class Specificity {
return calculate(selector);
}

static calculateForAST(selector) {
return calculateForAST(selector);
}

static compare(s1, s2) {
return compare(s1, s2);
}
Expand Down
36 changes: 36 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { deepEqual } from 'assert';
import Specificity from '../dist/index.js';
import * as csstree from 'css-tree';

describe('CALCULATE', () => {
describe('Examples from the spec', () => {
Expand Down Expand Up @@ -279,6 +280,41 @@ describe('CALCULATE', () => {
});
});

describe('CALCULATE_FOR_SELECTOR_AST', () => {
describe('Examples from the spec', () => {
it('* = (0,0,0)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('*', { context: 'selector' })).toObject(), { a: 0, b: 0, c: 0 });
});
it('li = (0,0,1)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('li', { context: 'selector' })).toObject(), { a: 0, b: 0, c: 1 });
});
it('ul li = (0,0,2)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('ul li', { context: 'selector' })).toObject(), { a: 0, b: 0, c: 2 });
});
it('UL OL+LI = (0,0,3)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('UL OL+LI ', { context: 'selector' })).toObject(), { a: 0, b: 0, c: 3 });
});
it('H1 + *[REL=up] = (0,1,1)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('H1 + *[REL=up]', { context: 'selector' })).toObject(), { a: 0, b: 1, c: 1 });
});
it('UL OL LI.red = (0,1,3)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('UL OL LI.red', { context: 'selector' })).toObject(), { a: 0, b: 1, c: 3 });
});
it('LI.red.level = (0,2,1)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('LI.red.level', { context: 'selector' })).toObject(), { a: 0, b: 2, c: 1 });
});
it('#x34y = (1,0,0)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('#x34y', { context: 'selector' })).toObject(), { a: 1, b: 0, c: 0 });
});
it('#s12:not(FOO) = (1,0,1)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('#s12:not(FOO)', { context: 'selector' })).toObject(), { a: 1, b: 0, c: 1 });
});
it('.foo :is(.bar, #baz) = (1,1,0)', () => {
deepEqual(Specificity.calculateForAST(csstree.parse('.foo :is(.bar, #baz)', { context: 'selector' })).toObject(), { a: 1, b: 1, c: 0 });
});
});
});

describe('COMPARE', () => {
const sHigh = { a: 1, b: 0, c: 0 };
const sMed = { a: 0, b: 1, c: 0 };
Expand Down
31 changes: 30 additions & 1 deletion test/standalone.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { deepEqual } from 'assert';
import * as csstree from 'css-tree';

import { calculate } from './../src/core/index.js';
import { calculate, calculateForAST } from './../src/core/index.js';
import { compare, equals, greaterThan, lessThan } from './../src/util/compare.js';
import { min, max } from './../src/util/filter.js';
import { sortAsc, sortDesc } from './../src/util/sort.js';
Expand Down Expand Up @@ -49,6 +49,35 @@ describe('STANDALONE CACULATE WITH PREPARSED AST', () => {
});
});

describe('STANDALONE CACULATE_FOR_SELECTOR_AST', () => {
it('trows an error if called without a selector', () => {
try {
calculateForAST();
} catch (error) {
deepEqual(error.message, 'Passed in source is not a Selector AST');
}
});

it('calculates specificity', () => {
const css = `
html #test,
.class[cool] {
color: red;
}
foo {
background: lime;
}
`;

const ast = csstree.parse(css);
const selectors = csstree.findAll(ast, (node) => node.type === 'Selector');

deepEqual(calculateForAST(selectors[0]).toObject(), { a: 1, b: 0, c: 1 });
deepEqual(calculateForAST(selectors[1]).toObject(), { a: 0, b: 2, c: 0 });
deepEqual(calculateForAST(selectors[2]).toObject(), { a: 0, b: 0, c: 1 });
});
});

describe('STANDALONE COMPARE', () => {
const sHigh = { a: 1, b: 0, c: 0 };
const sMed = { a: 0, b: 1, c: 0 };
Expand Down
Loading