UNPKG

postcss-minify-selectors

Version:

Minify selectors with PostCSS.

250 lines (240 loc) • 8.31 kB
import { classOrIdSource, classOrIdText, normalizedAt, offset, sourceText, } from './normalizePool.js'; import { formatPseudoPrefixAndName } from './normalizePseudo.js'; /** @typedef {import('./normalizePool.js').Output} Output */ /** @typedef {import('./normalizePool.js').OutputPool} OutputPool */ /** @typedef {import('./outputOverlay.js').Emit} Emit */ /** @typedef {import('./arena.js').SelectorArena} SelectorArena */ /** @typedef {import('./arena.js').ArenaNode} ArenaNode */ /** @typedef {import('./arena.js').Specificity} Specificity */ /** @typedef {Output & {node:number,parts?:Part[],foldEligible?:boolean,specificity?:Specificity,specificityId?:number,facts:number,entries?:Normalized[],valid:boolean,hasPseudoElement:boolean,trailing?:Output}} Normalized */ /** @typedef {Normalized | {kind:'combinator',id:number,emit?:import('./outputOverlay.js').Emit,text:string,length:number}} Part */ /** @param {number} nodeIndex @param {Output} output */ export function sourceNodeOutput(nodeIndex, output) { return { ...output, emit: undefined, sourceNode: nodeIndex, text: undefined, changed: false, }; } /** @param {SelectorArena} arena @param {number} nodeIndex @param {number} id */ function unchangedOutput(arena, nodeIndex, id) { const node = arena.nodes[nodeIndex]; return { id, length: offset(arena, node.endToken) - offset(arena, node.startToken), sourceNode: nodeIndex, changed: false, }; } /** @param {SelectorArena} arena @param {OutputPool} pool @param {number} nodeIndex @param {(Normalized | undefined)[]} normalized */ function unchangedPseudoOutput(arena, pool, nodeIndex, normalized) { const node = arena.nodes[nodeIndex]; const payload = arena.payloads.pseudos[node.payload]; const argument = payload.argumentNode; const { name, prefix } = formatPseudoPrefixAndName(arena, payload); if (argument === undefined) { const text = `${prefix}${name}`; if (text === sourceText(arena, node.startToken, node.endToken)) return unchangedOutput(arena, nodeIndex, pool.text(text).id); return; } const argumentOutput = normalized[argument]; if ( !argumentOutput || argumentOutput.valid === false || !canReusePseudo(arena, node, normalized) ) return; const opening = `${prefix}${name}(`; if ( opening !== sourceText(arena, node.startToken, arena.nodes[argument].startToken) || sourceText(arena, arena.nodes[argument].endToken, node.endToken) !== ')' ) return; return unchangedOutput( arena, nodeIndex, pool.sequenceId([ pool.text(opening).id, argumentOutput.id, pool.text(')').id, ]) ); } /** @param {SelectorArena} arena @param {OutputPool} pool @param {number} nodeIndex @param {(Normalized | undefined)[]} normalized @param {readonly number[]} children */ export function unchangedNodeOutput( arena, pool, nodeIndex, normalized, children ) { const node = arena.nodes[nodeIndex]; if (node.kind === 'raw') return unchangedOutput( arena, nodeIndex, pool.text(sourceText(arena, node.startToken, node.endToken)).id ); if (node.kind === 'class' || node.kind === 'id') { const text = classOrIdText(arena, node); if (text === classOrIdSource(arena, node)) return unchangedOutput(arena, nodeIndex, pool.text(text).id); return; } if (node.kind === 'compound') { if (!canReuseContainer(arena, nodeIndex, normalized, children)) return; if ( children.length > 1 && children.some((childIndex) => { const child = arena.nodes[childIndex]; if (child.kind !== 'qualified-name') return false; const payload = arena.payloads.qualifiedNames[child.payload]; return ( payload.namespace.kind === 'absent' && payload.subject.kind === 'universal' ); }) ) return; if (children.length === 1) return unchangedOutput( arena, nodeIndex, normalizedAt(normalized, children[0]).id ); return unchangedOutput( arena, nodeIndex, pool.sequenceId( children.map((child) => normalizedAt(normalized, child).id) ) ); } if (node.kind === 'complex') { if (!canReuseContainer(arena, nodeIndex, normalized, children)) return; return unchangedOutput( arena, nodeIndex, normalizedAt(normalized, children[0]).id ); } if (node.kind === 'list') { if ( nodeIndex === 0 || !canReuseContainer(arena, nodeIndex, normalized, children) ) return; return unchangedOutput( arena, nodeIndex, normalizedAt(normalized, children[0]).id ); } if (node.kind === 'pseudo') return unchangedPseudoOutput(arena, pool, nodeIndex, normalized); } /** @param {SelectorArena} arena @param {number} nodeIndex @param {(Normalized | undefined)[]} normalized @param {readonly number[]} children */ function canReuseContainer(arena, nodeIndex, normalized, children) { const node = arena.nodes[nodeIndex]; if (node.kind === 'list' || node.kind === 'complex') return ( children.length === 1 && arena.nodes[children[0]].startToken === node.startToken && arena.nodes[children[0]].endToken === node.endToken && normalized[children[0]]?.sourceNode === children[0] ); if (node.kind !== 'compound') return false; let cursor = node.startToken; for (const child of children) { const childNode = arena.nodes[child]; if ( childNode.startToken !== cursor || normalized[child]?.sourceNode !== child ) return false; cursor = childNode.endToken; } return cursor === node.endToken; } /** @param {SelectorArena} arena @param {ArenaNode} node @param {(Normalized | undefined)[]} normalized */ function canReusePseudo(arena, node, normalized) { const payload = arena.payloads.pseudos[node.payload]; const argument = payload.argumentNode; return ( argument !== undefined && normalized[argument]?.sourceNode === argument && arena.nodes[argument].startToken === payload.nameToken + 1 && arena.nodes[argument].endToken === node.endToken - 1 ); } /** @param {SelectorArena} arena @param {number} nodeIndex @param {(Normalized | undefined)[]} normalized @param {readonly number[]} children @param {Output} output */ export function canReuseSourceNode( arena, nodeIndex, normalized, children, output ) { const node = arena.nodes[nodeIndex]; if ( output.length !== offset(arena, node.endToken) - offset(arena, node.startToken) ) return false; if (node.kind === 'class' || node.kind === 'id') return output.text === classOrIdSource(arena, node); if (node.kind === 'raw') return true; if (node.kind === 'qualified-name') return output.text === sourceText(arena, node.startToken, node.endToken); if (node.kind === 'combinator') return output.text === sourceText(arena, node.startToken, node.endToken); if ( node.kind === 'list' || node.kind === 'complex' || node.kind === 'compound' ) return canReuseContainer(arena, nodeIndex, normalized, children); if (node.kind === 'pseudo') return canReusePseudo(arena, node, normalized); return false; } /** @param {SelectorArena} arena @param {ArenaNode} node @param {OutputPool} pool @param {Output} output */ export function normalizedPayload(arena, node, pool, output) { let status = 2; if (node.status === 'valid') status = 0; else if (node.status === 'invalid') status = 1; const summary = ((node.specificityId ?? -1) + 1) * 3 * 1024 + status * 1024 + (node.facts ?? 0); if (node.kind === 'list') { const payload = arena.payloads.lists[node.payload]; return pool.payload( `${summary};${payload.mode};${Boolean(payload.keyframe)};${Boolean(payload.hasDefaultNamespace)}` ); } if (node.kind === 'qualified-name') return summary; if (node.kind === 'pseudo') { const payload = arena.payloads.pseudos[node.payload]; return pool.payload( `${summary};${[ payload.name, payload.colonCount, payload.pseudoKind, payload.argumentGrammar ?? '', payload.specificityPolicy, payload.argumentNode === undefined ? output.id : 0, ].join(';')}` ); } return summary; }