Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

expose calculateSelectorNode #23

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type CSSTreeAST = Object; // @TODO: Define shape

// CORE
export function calculate(selector: string | CSSTreeAST): Array<Specificity>;
export function calculateSelectorNode(node: CSSTreeAST): { a: number; b: number; c: number; };

// UTIL: COMPARE
export function equals(s1: SpecificityInstanceOrObject, s2: SpecificityInstanceOrObject): boolean;
Expand Down
71 changes: 37 additions & 34 deletions src/core/calculate.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@ 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} selectorNode */
const calculateSelectorNode = (selectorNode) => {
// 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) => {
selectorNode.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 +30,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {
case '-webkit-any':
case 'any':
if (child.children) {
specificity.b += 1;
b += 1;
}
break;

Expand All @@ -46,34 +45,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.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 +92,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 +104,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 +117,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 +137,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 +153,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 +171,7 @@ const calculateSpecificityOfSelectorObject = (selectorObj) => {

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

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

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

const convertToAST = (source) => {
Expand Down Expand Up @@ -222,6 +221,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 +237,19 @@ const calculate = (selector) => {

// Selector?
if (ast.type === 'Selector') {
return [calculateSpecificityOfSelectorObject(selector)];
return [new Specificity(calculateSelectorNode(selector), selector)];
}

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

export { calculate };
export { calculate, calculateSelectorNode };
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, calculateSelectorNode } from './calculate.js';
6 changes: 5 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import generate from 'css-tree/generator';
import { calculate } from './core/index.js';
import { calculate, calculateSelectorNode } 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 +94,10 @@ class Specificity {
return calculate(selector);
}

static calculateSelectorNode(selector) {
return calculateSelectorNode(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 @@ -264,6 +265,41 @@ describe('CALCULATE', () => {
});
});

describe('CALCULATE_SELECTOR_NODE', () => {
describe('Examples from the spec', () => {
it('* = (0,0,0)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('*', { context: 'selector' })), { a: 0, b: 0, c: 0 });
});
it('li = (0,0,1)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('li', { context: 'selector' })), { a: 0, b: 0, c: 1 });
});
it('ul li = (0,0,2)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('ul li', { context: 'selector' })), { a: 0, b: 0, c: 2 });
});
it('UL OL+LI = (0,0,3)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('UL OL+LI ', { context: 'selector' })), { a: 0, b: 0, c: 3 });
});
it('H1 + *[REL=up] = (0,1,1)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('H1 + *[REL=up]', { context: 'selector' })), { a: 0, b: 1, c: 1 });
});
it('UL OL LI.red = (0,1,3)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('UL OL LI.red', { context: 'selector' })), { a: 0, b: 1, c: 3 });
});
it('LI.red.level = (0,2,1)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('LI.red.level', { context: 'selector' })), { a: 0, b: 2, c: 1 });
});
it('#x34y = (1,0,0)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('#x34y', { context: 'selector' })), { a: 1, b: 0, c: 0 });
});
it('#s12:not(FOO) = (1,0,1)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('#s12:not(FOO)', { context: 'selector' })), { a: 1, b: 0, c: 1 });
});
it('.foo :is(.bar, #baz) = (1,1,0)', () => {
deepEqual(Specificity.calculateSelectorNode(csstree.parse('.foo :is(.bar, #baz)', { context: 'selector' })), { 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
20 changes: 19 additions & 1 deletion test/standalone.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { deepEqual } from 'assert';
import * as csstree from 'css-tree';
import Specificity from '../dist/index.js';

import { calculate } from './../src/core/index.js';
import { calculate, calculateSelectorNode } 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 @@ -50,6 +50,24 @@ describe('STANDALONE CACULATE WITH PREPARSED AST', () => {
});
});

describe('STANDALONE CACULATE_SELECTOR_NODE', () => {
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(calculateSelectorNode(selectors[0]), { a: 1, b: 0, c: 1 });
deepEqual(calculateSelectorNode(selectors[1]), { a: 0, b: 2, c: 0 });
deepEqual(calculateSelectorNode(selectors[2]), { 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