postcss-minify-selectors
Advanced tools
| 'use strict'; | ||
| /** | ||
| * @typedef {object} Token | ||
| * @property {'compound'|'combinator'} kind | ||
| * @property {string} str | ||
| * @property {import('postcss-selector-parser').Node[]} [nodes] | ||
| */ | ||
| /** @typedef {[number, number, number]} Specificity */ | ||
| /** | ||
| * @param {import('postcss-selector-parser').Selector} selector | ||
| * @return {Token[]} | ||
| */ | ||
| function tokenize(selector) { | ||
| /** @type {Token[]} */ | ||
| const tokens = []; | ||
| /** @type {import('postcss-selector-parser').Node[]} */ | ||
| let bucket = []; | ||
| const flush = () => { | ||
| if (bucket.length) { | ||
| tokens.push({ | ||
| kind: 'compound', | ||
| str: bucket.map((n) => String(n)).join(''), | ||
| nodes: bucket, | ||
| }); | ||
| bucket = []; | ||
| } | ||
| }; | ||
| for (const node of selector.nodes) { | ||
| if (node.type === 'combinator') { | ||
| flush(); | ||
| tokens.push({ kind: 'combinator', str: String(node) }); | ||
| } else { | ||
| bucket.push(node); | ||
| } | ||
| } | ||
| flush(); | ||
| return tokens; | ||
| } | ||
| /** | ||
| * @param {Token} token | ||
| * @return {boolean} | ||
| */ | ||
| function hasPseudoElementOrNesting(token) { | ||
| if (token.kind !== 'compound' || !token.nodes) { | ||
| return false; | ||
| } | ||
| for (const n of token.nodes) { | ||
| if (n.type === 'nesting') { | ||
| return true; | ||
| } | ||
| if (n.type === 'pseudo') { | ||
| const v = n.value; | ||
| if (v.startsWith('::')) { | ||
| return true; | ||
| } | ||
| if ( | ||
| v === ':before' || | ||
| v === ':after' || | ||
| v === ':first-letter' || | ||
| v === ':first-line' | ||
| ) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * @param {Token} token | ||
| * @return {boolean} | ||
| */ | ||
| function hasNthChildOfClause(token) { | ||
| if (token.kind !== 'compound' || !token.nodes) { | ||
| return false; | ||
| } | ||
| for (const n of token.nodes) { | ||
| if (n.type !== 'pseudo') { | ||
| continue; | ||
| } | ||
| if (n.value !== ':nth-child' && n.value !== ':nth-last-child') { | ||
| continue; | ||
| } | ||
| for (const child of n.nodes) { | ||
| for (const inner of child.nodes) { | ||
| if (inner.type === 'tag' && inner.value === 'of') { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * @param {import('postcss-selector-parser').Node[]} nodes | ||
| * @return {Specificity} | ||
| */ | ||
| function specificityOf(nodes) { | ||
| let id = 0; | ||
| let cls = 0; | ||
| let type = 0; | ||
| for (const n of nodes) { | ||
| if (n.type === 'id') { | ||
| id++; | ||
| } else if (n.type === 'class' || n.type === 'attribute') { | ||
| cls++; | ||
| } else if (n.type === 'pseudo') { | ||
| const v = n.value; | ||
| if (v.startsWith('::')) { | ||
| type++; | ||
| continue; | ||
| } | ||
| if (v === ':where') { | ||
| continue; | ||
| } | ||
| if (v === ':is' || v === ':matches' || v === ':not' || v === ':has') { | ||
| const s = maxChildSpecificity(n); | ||
| id += s[0]; | ||
| cls += s[1]; | ||
| type += s[2]; | ||
| continue; | ||
| } | ||
| if ( | ||
| v === ':before' || | ||
| v === ':after' || | ||
| v === ':first-letter' || | ||
| v === ':first-line' | ||
| ) { | ||
| type++; | ||
| } else { | ||
| cls++; | ||
| } | ||
| } else if (n.type === 'tag') { | ||
| type++; | ||
| } | ||
| } | ||
| return [id, cls, type]; | ||
| } | ||
| /** | ||
| * @param {import('postcss-selector-parser').Pseudo} pseudo | ||
| * @return {Specificity} | ||
| */ | ||
| function maxChildSpecificity(pseudo) { | ||
| /** @type {Specificity} */ | ||
| let best = [0, 0, 0]; | ||
| for (const child of pseudo.nodes) { | ||
| if (child.type !== 'selector') { | ||
| continue; | ||
| } | ||
| const s = specificityOf(child.nodes); | ||
| if (compareSpecificity(s, best) > 0) { | ||
| best = s; | ||
| } | ||
| } | ||
| return best; | ||
| } | ||
| /** | ||
| * Sums the specificity of compound tokens in a fold middle — the divergent | ||
| * portion of a selector list, between the shared prefix and shared suffix. | ||
| * | ||
| * @param {Token[]} middle | ||
| * @return {Specificity} | ||
| */ | ||
| function specificityOfMiddle(middle) { | ||
| let id = 0; | ||
| let cls = 0; | ||
| let type = 0; | ||
| for (const token of middle) { | ||
| if (token.kind !== 'compound' || !token.nodes) { | ||
| continue; | ||
| } | ||
| const s = specificityOf(token.nodes); | ||
| id += s[0]; | ||
| cls += s[1]; | ||
| type += s[2]; | ||
| } | ||
| return [id, cls, type]; | ||
| } | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {number} | ||
| */ | ||
| function compareSpecificity(a, b) { | ||
| return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]; | ||
| } | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {boolean} | ||
| */ | ||
| function equalSpecificity(a, b) { | ||
| return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; | ||
| } | ||
| /** | ||
| * @param {Token[]} tokens | ||
| * @return {string} | ||
| */ | ||
| function joinTokens(tokens) { | ||
| return tokens.map((t) => t.str).join(''); | ||
| } | ||
| module.exports = { | ||
| tokenize, | ||
| hasPseudoElementOrNesting, | ||
| hasNthChildOfClause, | ||
| specificityOf, | ||
| specificityOfMiddle, | ||
| maxChildSpecificity, | ||
| compareSpecificity, | ||
| equalSpecificity, | ||
| joinTokens, | ||
| }; |
| export type Token = { | ||
| kind: "compound" | "combinator"; | ||
| str: string; | ||
| nodes?: import("postcss-selector-parser").Node[] | undefined; | ||
| }; | ||
| export type Specificity = [number, number, number]; | ||
| /** | ||
| * @typedef {object} Token | ||
| * @property {'compound'|'combinator'} kind | ||
| * @property {string} str | ||
| * @property {import('postcss-selector-parser').Node[]} [nodes] | ||
| */ | ||
| /** @typedef {[number, number, number]} Specificity */ | ||
| /** | ||
| * @param {import('postcss-selector-parser').Selector} selector | ||
| * @return {Token[]} | ||
| */ | ||
| export function tokenize(selector: import("postcss-selector-parser").Selector): Token[]; | ||
| /** | ||
| * @param {Token} token | ||
| * @return {boolean} | ||
| */ | ||
| export function hasPseudoElementOrNesting(token: Token): boolean; | ||
| /** | ||
| * @param {Token} token | ||
| * @return {boolean} | ||
| */ | ||
| export function hasNthChildOfClause(token: Token): boolean; | ||
| /** | ||
| * @param {import('postcss-selector-parser').Node[]} nodes | ||
| * @return {Specificity} | ||
| */ | ||
| export function specificityOf(nodes: import("postcss-selector-parser").Node[]): Specificity; | ||
| /** | ||
| * Sums the specificity of compound tokens in a fold middle — the divergent | ||
| * portion of a selector list, between the shared prefix and shared suffix. | ||
| * | ||
| * @param {Token[]} middle | ||
| * @return {Specificity} | ||
| */ | ||
| export function specificityOfMiddle(middle: Token[]): Specificity; | ||
| /** | ||
| * @param {import('postcss-selector-parser').Pseudo} pseudo | ||
| * @return {Specificity} | ||
| */ | ||
| export function maxChildSpecificity(pseudo: import("postcss-selector-parser").Pseudo): Specificity; | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {number} | ||
| */ | ||
| export function compareSpecificity(a: Specificity, b: Specificity): number; | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {boolean} | ||
| */ | ||
| export function equalSpecificity(a: Specificity, b: Specificity): boolean; | ||
| /** | ||
| * @param {Token[]} tokens | ||
| * @return {string} | ||
| */ | ||
| export function joinTokens(tokens: Token[]): string; | ||
| //# sourceMappingURL=foldToIsHelpers.d.ts.map |
| {"version":3,"file":"foldToIsHelpers.d.ts","sourceRoot":"","sources":["../../src/lib/foldToIsHelpers.js"],"names":[],"mappings":";UAIc,UAAU,GAAC,YAAY;SACvB,MAAM;;;0BAIN,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;AAPtC;;;;;GAKG;AAEH,sDAAsD;AAEtD;;;GAGG;AACH,mCAHW,OAAO,yBAAyB,EAAE,QAAQ,GACzC,KAAK,EAAE,CA6BlB;AAED;;;GAGG;AACH,iDAHW,KAAK,GACJ,OAAO,CA0BlB;AAED;;;GAGG;AACH,2CAHW,KAAK,GACJ,OAAO,CAsBlB;AAED;;;GAGG;AACH,qCAHW,OAAO,yBAAyB,EAAE,IAAI,EAAE,GACvC,WAAW,CA0CtB;AAqBD;;;;;;GAMG;AACH,4CAHW,KAAK,EAAE,GACN,WAAW,CAgBtB;AAxCD;;;GAGG;AACH,4CAHW,OAAO,yBAAyB,EAAE,MAAM,GACvC,WAAW,CAetB;AAyBD;;;;GAIG;AACH,sCAJW,WAAW,KACX,WAAW,GACV,MAAM,CAIjB;AAED;;;;GAIG;AACH,oCAJW,WAAW,KACX,WAAW,GACV,OAAO,CAIlB;AAED;;;GAGG;AACH,mCAHW,KAAK,EAAE,GACN,MAAM,CAIjB"} |
+3
-3
| { | ||
| "name": "postcss-minify-selectors", | ||
| "version": "7.1.0", | ||
| "version": "7.1.1", | ||
| "description": "Minify selectors with PostCSS.", | ||
@@ -43,7 +43,7 @@ "main": "src/index.js", | ||
| "@types/cssesc": "^3.0.2", | ||
| "postcss": "^8.5.10" | ||
| "postcss": "^8.5.13" | ||
| }, | ||
| "peerDependencies": { | ||
| "postcss": "^8.5.10" | ||
| "postcss": "^8.5.13" | ||
| } | ||
| } |
+12
-203
| 'use strict'; | ||
| const { | ||
| tokenize, | ||
| hasPseudoElementOrNesting, | ||
| hasNthChildOfClause, | ||
| specificityOfMiddle, | ||
| equalSpecificity, | ||
| joinTokens, | ||
| } = require('./foldToIsHelpers.js'); | ||
| // Folds sibling selectors in a comma-separated list by factoring out a common | ||
| // prefix and/or suffix into a shared `:is()` group. | ||
| // | ||
| // The specificity guard below is what keeps the rewrite safe w.r.t. the | ||
| // cascade: `:is(a, b)` takes the max specificity of its arguments, so folding | ||
| // selectors whose divergent parts have different specificities would silently | ||
| // change which rule wins at match time. | ||
| /** | ||
| * @typedef {object} Token | ||
| * @property {'compound'|'combinator'} kind | ||
| * @property {string} str | ||
| * @property {import('postcss-selector-parser').Node[]} [nodes] | ||
| */ | ||
| /** | ||
| * @param {import('postcss-selector-parser').Selector} selector | ||
| * @return {Token[]} | ||
| */ | ||
| function tokenize(selector) { | ||
| /** @type {Token[]} */ | ||
| const tokens = []; | ||
| /** @type {import('postcss-selector-parser').Node[]} */ | ||
| let bucket = []; | ||
| const flush = () => { | ||
| if (bucket.length) { | ||
| tokens.push({ | ||
| kind: 'compound', | ||
| str: bucket.map((n) => String(n)).join(''), | ||
| nodes: bucket, | ||
| }); | ||
| bucket = []; | ||
| } | ||
| }; | ||
| for (const node of selector.nodes) { | ||
| if (node.type === 'combinator') { | ||
| flush(); | ||
| tokens.push({ kind: 'combinator', str: String(node) }); | ||
| } else { | ||
| bucket.push(node); | ||
| } | ||
| } | ||
| flush(); | ||
| return tokens; | ||
| } | ||
| /** | ||
| * Pseudo-elements cannot live inside `:is()`; nesting `&` would change meaning. | ||
| * | ||
| * @param {Token} token | ||
| * @return {boolean} | ||
| */ | ||
| function hasPseudoElementOrNesting(token) { | ||
| if (token.kind !== 'compound' || !token.nodes) { | ||
| return false; | ||
| } | ||
| for (const n of token.nodes) { | ||
| if (n.type === 'nesting') { | ||
| return true; | ||
| } | ||
| if (n.type === 'pseudo') { | ||
| const v = n.value; | ||
| if (v.startsWith('::')) { | ||
| return true; | ||
| } | ||
| if ( | ||
| v === ':before' || | ||
| v === ':after' || | ||
| v === ':first-letter' || | ||
| v === ':first-line' | ||
| ) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** @typedef {[number, number, number]} Specificity */ | ||
| /** | ||
| * Specificity contribution of a list of simple-selector nodes, following the | ||
| * nested-specificity rules for `:is()`, `:not()`, `:has()`, and `:where()`. | ||
| * | ||
| * @param {import('postcss-selector-parser').Node[]} nodes | ||
| * @return {Specificity} | ||
| */ | ||
| function specificityOf(nodes) { | ||
| let id = 0; | ||
| let cls = 0; | ||
| let type = 0; | ||
| for (const n of nodes) { | ||
| if (n.type === 'id') { | ||
| id++; | ||
| } else if (n.type === 'class' || n.type === 'attribute') { | ||
| cls++; | ||
| } else if (n.type === 'pseudo') { | ||
| const v = n.value; | ||
| if (v.startsWith('::')) { | ||
| type++; | ||
| continue; | ||
| } | ||
| if (v === ':where') { | ||
| continue; | ||
| } | ||
| if (v === ':is' || v === ':matches' || v === ':not' || v === ':has') { | ||
| const s = maxChildSpecificity(n); | ||
| id += s[0]; | ||
| cls += s[1]; | ||
| type += s[2]; | ||
| continue; | ||
| } | ||
| // Legacy single-colon pseudo-elements count as type. | ||
| if ( | ||
| v === ':before' || | ||
| v === ':after' || | ||
| v === ':first-letter' || | ||
| v === ':first-line' | ||
| ) { | ||
| type++; | ||
| } else { | ||
| cls++; | ||
| } | ||
| } else if (n.type === 'tag') { | ||
| type++; | ||
| } | ||
| } | ||
| return [id, cls, type]; | ||
| } | ||
| /** | ||
| * @param {import('postcss-selector-parser').Pseudo} pseudo | ||
| * @return {Specificity} | ||
| */ | ||
| function maxChildSpecificity(pseudo) { | ||
| /** @type {Specificity} */ | ||
| let best = [0, 0, 0]; | ||
| for (const child of pseudo.nodes) { | ||
| if (child.type !== 'selector') { | ||
| continue; | ||
| } | ||
| const s = specificityOf(child.nodes); | ||
| if (compareSpecificity(s, best) > 0) { | ||
| best = s; | ||
| } | ||
| } | ||
| return best; | ||
| } | ||
| /** | ||
| * @param {Token[]} middle | ||
| * @return {Specificity} | ||
| */ | ||
| function specificityOfMiddle(middle) { | ||
| let id = 0; | ||
| let cls = 0; | ||
| let type = 0; | ||
| for (const token of middle) { | ||
| if (token.kind !== 'compound' || !token.nodes) { | ||
| continue; | ||
| } | ||
| const s = specificityOf(token.nodes); | ||
| id += s[0]; | ||
| cls += s[1]; | ||
| type += s[2]; | ||
| } | ||
| return [id, cls, type]; | ||
| } | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {number} | ||
| */ | ||
| function compareSpecificity(a, b) { | ||
| return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]; | ||
| } | ||
| /** | ||
| * @param {Specificity} a | ||
| * @param {Specificity} b | ||
| * @return {boolean} | ||
| */ | ||
| function sameSpecificity(a, b) { | ||
| return a[0] === b[0] && a[1] === b[1] && a[2] === b[2]; | ||
| } | ||
| /** | ||
| * @param {Token[]} tokens | ||
| * @return {string} | ||
| */ | ||
| function joinTokens(tokens) { | ||
| return tokens.map((t) => t.str).join(''); | ||
| } | ||
| /** | ||
| * Returns the folded selector list string if beneficial, otherwise null. | ||
| * | ||
| * @param {import('postcss-selector-parser').Root} root | ||
@@ -247,5 +56,2 @@ * @return {string | null} | ||
| // Each prefix/suffix boundary must land on a combinator — otherwise | ||
| // splicing `:is(...)` in would place it adjacent to a type/class token | ||
| // in the prefix/suffix and silently change what the selector matches. | ||
| const firstTokens = tokenLists[0]; | ||
@@ -274,2 +80,5 @@ while (prefix > 0 && firstTokens[prefix - 1].kind !== 'combinator') { | ||
| } | ||
| if (hasNthChildOfClause(token)) { | ||
| return null; | ||
| } | ||
| } | ||
@@ -280,3 +89,3 @@ } | ||
| for (let i = 1; i < middles.length; i++) { | ||
| if (!sameSpecificity(firstSpec, specificityOfMiddle(middles[i]))) { | ||
| if (!equalSpecificity(firstSpec, specificityOfMiddle(middles[i]))) { | ||
| return null; | ||
@@ -283,0 +92,0 @@ } |
| export = tryFold; | ||
| /** | ||
| * Returns the folded selector list string if beneficial, otherwise null. | ||
| * | ||
| * @param {import('postcss-selector-parser').Root} root | ||
@@ -9,11 +7,2 @@ * @return {string | null} | ||
| declare function tryFold(root: import("postcss-selector-parser").Root): string | null; | ||
| declare namespace tryFold { | ||
| export { Token, Specificity }; | ||
| } | ||
| type Token = { | ||
| kind: "compound" | "combinator"; | ||
| str: string; | ||
| nodes?: import("postcss-selector-parser").Node[] | undefined; | ||
| }; | ||
| type Specificity = [number, number, number]; | ||
| //# sourceMappingURL=foldToIs.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"foldToIs.d.ts","sourceRoot":"","sources":["../../src/lib/foldToIs.js"],"names":[],"mappings":";AAuMA;;;;;GAKG;AACH,+BAHW,OAAO,yBAAyB,EAAE,IAAI,GACrC,MAAM,GAAG,IAAI,CAsGxB;;;;;UArSa,UAAU,GAAC,YAAY;SACvB,MAAM;;;mBAqEN,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC"} | ||
| {"version":3,"file":"foldToIs.d.ts","sourceRoot":"","sources":["../../src/lib/foldToIs.js"],"names":[],"mappings":";AAUA;;;GAGG;AACH,+BAHW,OAAO,yBAAyB,EAAE,IAAI,GACrC,MAAM,GAAG,IAAI,CAsGxB"} |
25690
10.51%15
25%719
13.41%