Skip to content

Commit 22a6ef7

Browse files
authored
make specificity calculation customizable (#1392)
1 parent 6af68b6 commit 22a6ef7

16 files changed

+535
-31
lines changed

packages/selector-specificity/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changes to Selector Specificity
22

3+
### Unreleased (minor)
4+
5+
- Add an option to `selectorSpecificity` and `specificityOfMostSpecificListItem` to customize the calculation
6+
- Add `specificityOfMostSpecificListItem` as an exported function
7+
38
### 3.0.3
49

510
_March 31, 2024_
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"use strict";var e=require("postcss-selector-parser");function selectorSpecificity(s){if(!s)return{a:0,b:0,c:0};let t=0,i=0,c=0;if("universal"==s.type)return{a:0,b:0,c:0};if("id"===s.type)t+=1;else if("tag"===s.type)c+=1;else if("class"===s.type)i+=1;else if("attribute"===s.type)i+=1;else if(isPseudoElement(s))switch(s.value.toLowerCase()){case"::slotted":if(c+=1,s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case"::view-transition-group":case"::view-transition-image-pair":case"::view-transition-old":case"::view-transition-new":return s.nodes&&1===s.nodes.length&&"selector"===s.nodes[0].type&&selectorNodeContainsNothingOrOnlyUniversal(s.nodes[0])?{a:0,b:0,c:0}:{a:0,b:0,c:1};default:c+=1}else if(e.isPseudoClass(s))switch(s.value.toLowerCase()){case":-webkit-any":case":any":default:i+=1;break;case":-moz-any":case":has":case":is":case":matches":case":not":if(s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case":where":break;case":nth-child":case":nth-last-child":if(i+=1,s.nodes&&s.nodes.length>0){const n=s.nodes[0].nodes.findIndex((e=>"tag"===e.type&&"of"===e.value.toLowerCase()));if(n>-1){const o=[e.selector({nodes:s.nodes[0].nodes.slice(n+1),value:""})];s.nodes.length>1&&o.push(...s.nodes.slice(1));const a=specificityOfMostSpecificListItem(o);t+=a.a,i+=a.b,c+=a.c}}break;case":local":case":global":s.nodes&&s.nodes.length>0&&s.nodes.forEach((e=>{const s=selectorSpecificity(e);t+=s.a,i+=s.b,c+=s.c}));break;case":host":case":host-context":if(i+=1,s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case":active-view-transition":case":active-view-transition-type":return{a:0,b:1,c:0}}else e.isContainer(s)&&s.nodes.length>0&&s.nodes.forEach((e=>{const s=selectorSpecificity(e);t+=s.a,i+=s.b,c+=s.c}));return{a:t,b:i,c:c}}function specificityOfMostSpecificListItem(e){let s={a:0,b:0,c:0};return e.forEach((e=>{const t=selectorSpecificity(e);t.a>s.a?s=t:t.a<s.a||(t.b>s.b?s=t:t.b<s.b||t.c>s.c&&(s=t))})),s}function isPseudoElement(s){return e.isPseudoElement(s)}function selectorNodeContainsNothingOrOnlyUniversal(e){if(!e)return!1;if(!e.nodes)return!1;const s=e.nodes.filter((e=>"comment"!==e.type));return 0===s.length||1===s.length&&"universal"===s[0].type}exports.compare=function compare(e,s){return e.a===s.a?e.b===s.b?e.c-s.c:e.b-s.b:e.a-s.a},exports.selectorSpecificity=selectorSpecificity;
1+
"use strict";var e=require("postcss-selector-parser");function selectorSpecificity(t,s){const i=s?.customSpecificity?.(t);if(i)return i;if(!t)return{a:0,b:0,c:0};let c=0,n=0,o=0;if("universal"==t.type)return{a:0,b:0,c:0};if("id"===t.type)c+=1;else if("tag"===t.type)o+=1;else if("class"===t.type)n+=1;else if("attribute"===t.type)n+=1;else if(isPseudoElement(t))switch(t.value.toLowerCase()){case"::slotted":if(o+=1,t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case"::view-transition-group":case"::view-transition-image-pair":case"::view-transition-old":case"::view-transition-new":return t.nodes&&1===t.nodes.length&&"selector"===t.nodes[0].type&&selectorNodeContainsNothingOrOnlyUniversal(t.nodes[0])?{a:0,b:0,c:0}:{a:0,b:0,c:1};default:o+=1}else if(e.isPseudoClass(t))switch(t.value.toLowerCase()){case":-webkit-any":case":any":default:n+=1;break;case":-moz-any":case":has":case":is":case":matches":case":not":if(t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case":where":break;case":nth-child":case":nth-last-child":if(n+=1,t.nodes&&t.nodes.length>0){const i=t.nodes[0].nodes.findIndex((e=>"tag"===e.type&&"of"===e.value.toLowerCase()));if(i>-1){const a=e.selector({nodes:[],value:""});a.parent=t;t.nodes[0].nodes.slice(i+1).forEach((e=>{e.remove(),a.append(e)}));const r=[a];t.nodes.length>1&&r.push(...t.nodes.slice(1));const f=specificityOfMostSpecificListItem(r,s);c+=f.a,n+=f.b,o+=f.c}}break;case":local":case":global":t.nodes&&t.nodes.length>0&&t.nodes.forEach((e=>{const t=selectorSpecificity(e,s);c+=t.a,n+=t.b,o+=t.c}));break;case":host":case":host-context":if(n+=1,t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case":active-view-transition":case":active-view-transition-type":return{a:0,b:1,c:0}}else e.isContainer(t)&&t.nodes.length>0&&t.nodes.forEach((e=>{const t=selectorSpecificity(e,s);c+=t.a,n+=t.b,o+=t.c}));return{a:c,b:n,c:o}}function specificityOfMostSpecificListItem(e,t){let s={a:0,b:0,c:0};return e.forEach((e=>{const i=selectorSpecificity(e,t);i.a>s.a?s=i:i.a<s.a||(i.b>s.b?s=i:i.b<s.b||i.c>s.c&&(s=i))})),s}function isPseudoElement(t){return e.isPseudoElement(t)}function selectorNodeContainsNothingOrOnlyUniversal(e){if(!e)return!1;if(!e.nodes)return!1;const t=e.nodes.filter((e=>"comment"!==e.type));return 0===t.length||1===t.length&&"universal"===t[0].type}exports.compare=function compare(e,t){return e.a===t.a?e.b===t.b?e.c-t.c:e.b-t.b:e.a-t.a},exports.selectorSpecificity=selectorSpecificity,exports.specificityOfMostSpecificListItem=specificityOfMostSpecificListItem;

packages/selector-specificity/dist/index.d.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,55 @@
11
import type { Node } from 'postcss-selector-parser';
22

3+
/**
4+
* Options for the calculation of the specificity of a selector
5+
*/
6+
export declare type CalculationOptions = {
7+
/**
8+
* The callback to calculate a custom specificity for a node
9+
*/
10+
customSpecificity?: CustomSpecificityCallback;
11+
};
12+
13+
/**
14+
* Compare two specificities
15+
* @param s1 The first specificity
16+
* @param s2 The second specificity
17+
* @returns A value smaller than `0` if `s1` is less specific than `s2`, `0` if `s1` is equally specific as `s2`, a value larger than `0` if `s1` is more specific than `s2`
18+
*/
319
export declare function compare(s1: Specificity, s2: Specificity): number;
420

5-
export declare function selectorSpecificity(node: Node): Specificity;
21+
/**
22+
* Calculate a custom specificity for a node
23+
*/
24+
export declare type CustomSpecificityCallback = (node: Node) => Specificity | void | false | null | undefined;
625

26+
/**
27+
* Calculate the specificity for a selector
28+
*/
29+
export declare function selectorSpecificity(node: Node, options?: CalculationOptions): Specificity;
30+
31+
/**
32+
* The specificity of a selector
33+
*/
734
export declare type Specificity = {
35+
/**
36+
* The number of ID selectors in the selector
37+
*/
38+
a: number;
39+
/**
40+
* The number of class selectors, attribute selectors, and pseudo-classes in the selector
41+
*/
42+
b: number;
43+
/**
44+
* The number of type selectors and pseudo-elements in the selector
45+
*/
46+
c: number;
47+
};
48+
49+
/**
50+
* Calculate the most specific selector in a list
51+
*/
52+
export declare function specificityOfMostSpecificListItem(nodes: Array<Node>, options?: CalculationOptions): {
853
a: number;
954
b: number;
1055
c: number;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
import e from"postcss-selector-parser";function compare(e,s){return e.a===s.a?e.b===s.b?e.c-s.c:e.b-s.b:e.a-s.a}function selectorSpecificity(s){if(!s)return{a:0,b:0,c:0};let t=0,i=0,c=0;if("universal"==s.type)return{a:0,b:0,c:0};if("id"===s.type)t+=1;else if("tag"===s.type)c+=1;else if("class"===s.type)i+=1;else if("attribute"===s.type)i+=1;else if(isPseudoElement(s))switch(s.value.toLowerCase()){case"::slotted":if(c+=1,s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case"::view-transition-group":case"::view-transition-image-pair":case"::view-transition-old":case"::view-transition-new":return s.nodes&&1===s.nodes.length&&"selector"===s.nodes[0].type&&selectorNodeContainsNothingOrOnlyUniversal(s.nodes[0])?{a:0,b:0,c:0}:{a:0,b:0,c:1};default:c+=1}else if(e.isPseudoClass(s))switch(s.value.toLowerCase()){case":-webkit-any":case":any":default:i+=1;break;case":-moz-any":case":has":case":is":case":matches":case":not":if(s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case":where":break;case":nth-child":case":nth-last-child":if(i+=1,s.nodes&&s.nodes.length>0){const n=s.nodes[0].nodes.findIndex((e=>"tag"===e.type&&"of"===e.value.toLowerCase()));if(n>-1){const o=[e.selector({nodes:s.nodes[0].nodes.slice(n+1),value:""})];s.nodes.length>1&&o.push(...s.nodes.slice(1));const a=specificityOfMostSpecificListItem(o);t+=a.a,i+=a.b,c+=a.c}}break;case":local":case":global":s.nodes&&s.nodes.length>0&&s.nodes.forEach((e=>{const s=selectorSpecificity(e);t+=s.a,i+=s.b,c+=s.c}));break;case":host":case":host-context":if(i+=1,s.nodes&&s.nodes.length>0){const e=specificityOfMostSpecificListItem(s.nodes);t+=e.a,i+=e.b,c+=e.c}break;case":active-view-transition":case":active-view-transition-type":return{a:0,b:1,c:0}}else e.isContainer(s)&&s.nodes.length>0&&s.nodes.forEach((e=>{const s=selectorSpecificity(e);t+=s.a,i+=s.b,c+=s.c}));return{a:t,b:i,c:c}}function specificityOfMostSpecificListItem(e){let s={a:0,b:0,c:0};return e.forEach((e=>{const t=selectorSpecificity(e);t.a>s.a?s=t:t.a<s.a||(t.b>s.b?s=t:t.b<s.b||t.c>s.c&&(s=t))})),s}function isPseudoElement(s){return e.isPseudoElement(s)}function selectorNodeContainsNothingOrOnlyUniversal(e){if(!e)return!1;if(!e.nodes)return!1;const s=e.nodes.filter((e=>"comment"!==e.type));return 0===s.length||1===s.length&&"universal"===s[0].type}export{compare,selectorSpecificity};
1+
import e from"postcss-selector-parser";function compare(e,t){return e.a===t.a?e.b===t.b?e.c-t.c:e.b-t.b:e.a-t.a}function selectorSpecificity(t,s){const i=s?.customSpecificity?.(t);if(i)return i;if(!t)return{a:0,b:0,c:0};let c=0,n=0,o=0;if("universal"==t.type)return{a:0,b:0,c:0};if("id"===t.type)c+=1;else if("tag"===t.type)o+=1;else if("class"===t.type)n+=1;else if("attribute"===t.type)n+=1;else if(isPseudoElement(t))switch(t.value.toLowerCase()){case"::slotted":if(o+=1,t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case"::view-transition-group":case"::view-transition-image-pair":case"::view-transition-old":case"::view-transition-new":return t.nodes&&1===t.nodes.length&&"selector"===t.nodes[0].type&&selectorNodeContainsNothingOrOnlyUniversal(t.nodes[0])?{a:0,b:0,c:0}:{a:0,b:0,c:1};default:o+=1}else if(e.isPseudoClass(t))switch(t.value.toLowerCase()){case":-webkit-any":case":any":default:n+=1;break;case":-moz-any":case":has":case":is":case":matches":case":not":if(t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case":where":break;case":nth-child":case":nth-last-child":if(n+=1,t.nodes&&t.nodes.length>0){const i=t.nodes[0].nodes.findIndex((e=>"tag"===e.type&&"of"===e.value.toLowerCase()));if(i>-1){const a=e.selector({nodes:[],value:""});a.parent=t;t.nodes[0].nodes.slice(i+1).forEach((e=>{e.remove(),a.append(e)}));const r=[a];t.nodes.length>1&&r.push(...t.nodes.slice(1));const l=specificityOfMostSpecificListItem(r,s);c+=l.a,n+=l.b,o+=l.c}}break;case":local":case":global":t.nodes&&t.nodes.length>0&&t.nodes.forEach((e=>{const t=selectorSpecificity(e,s);c+=t.a,n+=t.b,o+=t.c}));break;case":host":case":host-context":if(n+=1,t.nodes&&t.nodes.length>0){const e=specificityOfMostSpecificListItem(t.nodes,s);c+=e.a,n+=e.b,o+=e.c}break;case":active-view-transition":case":active-view-transition-type":return{a:0,b:1,c:0}}else e.isContainer(t)&&t.nodes.length>0&&t.nodes.forEach((e=>{const t=selectorSpecificity(e,s);c+=t.a,n+=t.b,o+=t.c}));return{a:c,b:n,c:o}}function specificityOfMostSpecificListItem(e,t){let s={a:0,b:0,c:0};return e.forEach((e=>{const i=selectorSpecificity(e,t);i.a>s.a?s=i:i.a<s.a||(i.b>s.b?s=i:i.b<s.b||i.c>s.c&&(s=i))})),s}function isPseudoElement(t){return e.isPseudoElement(t)}function selectorNodeContainsNothingOrOnlyUniversal(e){if(!e)return!1;if(!e.nodes)return!1;const t=e.nodes.filter((e=>"comment"!==e.type));return 0===t.length||1===t.length&&"universal"===t[0].type}export{compare,selectorSpecificity,specificityOfMostSpecificListItem};

packages/selector-specificity/docs/selector-specificity.api.json

Lines changed: 177 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,45 @@
171171
"name": "",
172172
"preserveMemberOrder": false,
173173
"members": [
174+
{
175+
"kind": "TypeAlias",
176+
"canonicalReference": "@csstools/selector-specificity!CalculationOptions:type",
177+
"docComment": "/**\n * Options for the calculation of the specificity of a selector\n */\n",
178+
"excerptTokens": [
179+
{
180+
"kind": "Content",
181+
"text": "export type CalculationOptions = "
182+
},
183+
{
184+
"kind": "Content",
185+
"text": "{\n customSpecificity?: "
186+
},
187+
{
188+
"kind": "Reference",
189+
"text": "CustomSpecificityCallback",
190+
"canonicalReference": "@csstools/selector-specificity!CustomSpecificityCallback:type"
191+
},
192+
{
193+
"kind": "Content",
194+
"text": ";\n}"
195+
},
196+
{
197+
"kind": "Content",
198+
"text": ";"
199+
}
200+
],
201+
"fileUrlPath": "dist/_types/index.d.ts",
202+
"releaseTag": "Public",
203+
"name": "CalculationOptions",
204+
"typeTokenRange": {
205+
"startIndex": 1,
206+
"endIndex": 4
207+
}
208+
},
174209
{
175210
"kind": "Function",
176211
"canonicalReference": "@csstools/selector-specificity!compare:function(1)",
177-
"docComment": "",
212+
"docComment": "/**\n * Compare two specificities\n *\n * @param s1 - The first specificity\n *\n * @param s2 - The second specificity\n *\n * @returns A value smaller than `0` if `s1` is less specific than `s2`, `0` if `s1` is equally specific as `s2`, a value larger than `0` if `s1` is more specific than `s2`\n */\n",
178213
"excerptTokens": [
179214
{
180215
"kind": "Content",
@@ -234,10 +269,54 @@
234269
],
235270
"name": "compare"
236271
},
272+
{
273+
"kind": "TypeAlias",
274+
"canonicalReference": "@csstools/selector-specificity!CustomSpecificityCallback:type",
275+
"docComment": "/**\n * Calculate a custom specificity for a node\n */\n",
276+
"excerptTokens": [
277+
{
278+
"kind": "Content",
279+
"text": "export type CustomSpecificityCallback = "
280+
},
281+
{
282+
"kind": "Content",
283+
"text": "(node: "
284+
},
285+
{
286+
"kind": "Reference",
287+
"text": "Node",
288+
"canonicalReference": "postcss-selector-parser!parser.Node:type"
289+
},
290+
{
291+
"kind": "Content",
292+
"text": ") => "
293+
},
294+
{
295+
"kind": "Reference",
296+
"text": "Specificity",
297+
"canonicalReference": "@csstools/selector-specificity!Specificity:type"
298+
},
299+
{
300+
"kind": "Content",
301+
"text": " | void | false | null | undefined"
302+
},
303+
{
304+
"kind": "Content",
305+
"text": ";"
306+
}
307+
],
308+
"fileUrlPath": "dist/_types/index.d.ts",
309+
"releaseTag": "Public",
310+
"name": "CustomSpecificityCallback",
311+
"typeTokenRange": {
312+
"startIndex": 1,
313+
"endIndex": 6
314+
}
315+
},
237316
{
238317
"kind": "Function",
239318
"canonicalReference": "@csstools/selector-specificity!selectorSpecificity:function(1)",
240-
"docComment": "",
319+
"docComment": "/**\n * Calculate the specificity for a selector\n */\n",
241320
"excerptTokens": [
242321
{
243322
"kind": "Content",
@@ -248,6 +327,15 @@
248327
"text": "Node",
249328
"canonicalReference": "postcss-selector-parser!parser.Node:type"
250329
},
330+
{
331+
"kind": "Content",
332+
"text": ", options?: "
333+
},
334+
{
335+
"kind": "Reference",
336+
"text": "CalculationOptions",
337+
"canonicalReference": "@csstools/selector-specificity!CalculationOptions:type"
338+
},
251339
{
252340
"kind": "Content",
253341
"text": "): "
@@ -264,8 +352,8 @@
264352
],
265353
"fileUrlPath": "dist/_types/index.d.ts",
266354
"returnTypeTokenRange": {
267-
"startIndex": 3,
268-
"endIndex": 4
355+
"startIndex": 5,
356+
"endIndex": 6
269357
},
270358
"releaseTag": "Public",
271359
"overloadIndex": 1,
@@ -277,14 +365,22 @@
277365
"endIndex": 2
278366
},
279367
"isOptional": false
368+
},
369+
{
370+
"parameterName": "options",
371+
"parameterTypeTokenRange": {
372+
"startIndex": 3,
373+
"endIndex": 4
374+
},
375+
"isOptional": true
280376
}
281377
],
282378
"name": "selectorSpecificity"
283379
},
284380
{
285381
"kind": "TypeAlias",
286382
"canonicalReference": "@csstools/selector-specificity!Specificity:type",
287-
"docComment": "",
383+
"docComment": "/**\n * The specificity of a selector\n */\n",
288384
"excerptTokens": [
289385
{
290386
"kind": "Content",
@@ -306,6 +402,82 @@
306402
"startIndex": 1,
307403
"endIndex": 2
308404
}
405+
},
406+
{
407+
"kind": "Function",
408+
"canonicalReference": "@csstools/selector-specificity!specificityOfMostSpecificListItem:function(1)",
409+
"docComment": "/**\n * Calculate the most specific selector in a list\n */\n",
410+
"excerptTokens": [
411+
{
412+
"kind": "Content",
413+
"text": "export declare function specificityOfMostSpecificListItem(nodes: "
414+
},
415+
{
416+
"kind": "Reference",
417+
"text": "Array",
418+
"canonicalReference": "!Array:interface"
419+
},
420+
{
421+
"kind": "Content",
422+
"text": "<"
423+
},
424+
{
425+
"kind": "Reference",
426+
"text": "Node",
427+
"canonicalReference": "postcss-selector-parser!parser.Node:type"
428+
},
429+
{
430+
"kind": "Content",
431+
"text": ">"
432+
},
433+
{
434+
"kind": "Content",
435+
"text": ", options?: "
436+
},
437+
{
438+
"kind": "Reference",
439+
"text": "CalculationOptions",
440+
"canonicalReference": "@csstools/selector-specificity!CalculationOptions:type"
441+
},
442+
{
443+
"kind": "Content",
444+
"text": "): "
445+
},
446+
{
447+
"kind": "Content",
448+
"text": "{\n a: number;\n b: number;\n c: number;\n}"
449+
},
450+
{
451+
"kind": "Content",
452+
"text": ";"
453+
}
454+
],
455+
"fileUrlPath": "dist/_types/index.d.ts",
456+
"returnTypeTokenRange": {
457+
"startIndex": 8,
458+
"endIndex": 9
459+
},
460+
"releaseTag": "Public",
461+
"overloadIndex": 1,
462+
"parameters": [
463+
{
464+
"parameterName": "nodes",
465+
"parameterTypeTokenRange": {
466+
"startIndex": 1,
467+
"endIndex": 5
468+
},
469+
"isOptional": false
470+
},
471+
{
472+
"parameterName": "options",
473+
"parameterTypeTokenRange": {
474+
"startIndex": 6,
475+
"endIndex": 7
476+
},
477+
"isOptional": true
478+
}
479+
],
480+
"name": "specificityOfMostSpecificListItem"
309481
}
310482
]
311483
}

0 commit comments

Comments
 (0)