UNPKG

postcss-minify-selectors

Version:

Minify selectors with PostCSS.

316 lines (296 loc) 6.54 kB
'use strict'; /** @typedef {import('postcss-selector-parser').Node} Node */ /** @typedef {import('postcss-selector-parser').Selector} Selector */ /** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */ /** * @typedef {object} Token * @property {'compound'|'combinator'} kind * @property {string} str * @property {Node[]} [nodes] */ /** @typedef {[number, number, number]} Specificity */ // Pseudo-classes accepted inside a fold middle. Limited to user-action // pseudos. Anything outside this set risks the rule-list-strict vs `:is()` const SAFE_PSEUDO_CLASSES = new Set([ ':hover', ':focus', ':active', ':visited', ':link', ]); /** * @param {Selector} selector * @return {Token[]} */ function tokenize(selector) { /** @type {Token[]} */ const tokens = []; /** @type {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; } return nodesContainNthChildOfClause(token.nodes); } /** * @param {Node[]} nodes * @return {boolean} */ function nodesContainNthChildOfClause(nodes) { for (const n of nodes) { if (n.type !== 'pseudo') { continue; } if (n.value === ':nth-child' || n.value === ':nth-last-child') { for (const child of n.nodes) { for (const inner of child.nodes) { if (inner.type === 'tag' && inner.value === 'of') { return true; } } } } for (const child of n.nodes) { if ( child.type === 'selector' && nodesContainNthChildOfClause(child.nodes) ) { return true; } } } return false; } /** * @param {Token} token * @return {boolean} */ function hasUnsafeForFold(token) { if (token.kind !== 'compound' || !token.nodes) { return false; } return nodesContainUnsafeForFold(token.nodes); } /** * @param {Node[]} nodes * @return {boolean} */ function nodesContainUnsafeForFold(nodes) { for (const n of nodes) { const t = n.type; if (t === 'class' || t === 'id') { continue; } if (t === 'tag') { if (n.namespace !== undefined && n.namespace !== null) { return true; } continue; } if (t === 'attribute') { if (n.namespace !== undefined && n.namespace !== null) { return true; } if (n.insensitive || (n.raws && 'insensitiveFlag' in n.raws)) { return true; } continue; } if (t === 'pseudo') { const v = n.value; if ( v.startsWith('::') || v === ':before' || v === ':after' || v === ':first-letter' || v === ':first-line' ) { return true; } if (!SAFE_PSEUDO_CLASSES.has(v)) { return true; } if (n.nodes && n.nodes.length > 0) { return true; } continue; } return true; } return false; } /** * @param {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 {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, hasUnsafeForFold, specificityOf, specificityOfMiddle, maxChildSpecificity, compareSpecificity, equalSpecificity, joinTokens, };