UNPKG

cssnano

Version:

A modular CSS minifier, built on top of the PostCSS ecosystem.

185 lines (179 loc) • 6.21 kB
import { declarationIsEqual } from './declarations.js'; import { PropertyWrites } from './lastWriteIndex.js'; import { getDecls } from './ruleMeta.js'; import { findGroup, selectorsLength } from './sharedRuleGroup.js'; /** @import {Container, Declaration, Rule} from 'postcss' */ /** @import RuleSequence, {Placement, RuleLink} from './ruleSequence.js' */ /** * Appends the content of `incoming` to `receiving`. A declaration of * `incoming` is redundant only when the last declaration of `receiving` that * can set the same property already has an identical value. Otherwise it * revives an overridden value or overrides intermediate declarations, so * appending it is required to preserve the cascade result. * * @param {Rule} receiving * @param {Rule} incoming * @return {void} */ export function appendDeclarations(receiving, incoming) { const kept = getDecls(receiving); // Rewrites compare whole rules and never ask about one element, so a plain // property table is enough. const index = new PropertyWrites(); for (const [position, declaration] of kept.entries()) { index.record(declaration, position); } for (const node of incoming.nodes.slice()) { if (node.type === 'decl') { const last = index.lastConflict(node); if (last !== -1 && declarationIsEqual(kept[last], node)) { node.remove(); continue; } index.record(node, kept.length); kept.push(node); } receiving.append(node); } } /** * The bytes a declaration takes in minified output, with the separator after * it. A rule adds its braces and drops the last separator, so it takes the * length of its selector list, one byte, and its declarations. * * @param {Declaration} declaration * @return {number} */ function declarationLength(declaration) { return ( declaration.prop.length + declaration.value.length + (declaration.important ? 10 : 0) + 2 ); } /** * Splits the declarations the rules set out of them into a rule with all * their selectors, which takes the place of the later rule of the pair, * between a leftover of each rule. A rule left without declarations is * dropped. Rules after the pair that repeat the shared declarations join * when that makes the output shorter. * * The lengths are those of minified output: raws would count the whitespace * of the source, which a minifier removes, so a merge that pays only in * source bytes would grow the output. * * @param {Rule} first * @param {Rule} second * @param {Set<number>} claimedEarlierIndices * @param {Set<number>} claimedIndices * @param {import('./mergeState.js').default} mergeState * @param {RuleLink} secondLink where `second` stands in the sweep * @param {RuleSequence} sequence * @param {WeakMap<Container, boolean>} outsideDeclarations * @return {Placement | null} null when the output would not get shorter */ export function buildMergedRule( first, second, claimedEarlierIndices, claimedIndices, mergeState, secondLink, sequence, outsideDeclarations ) { const firstSelectors = mergeState.meta(first).selectors; const secondSelectors = mergeState.meta(second).selectors; const firstDeclarations = mergeState.meta(first).declarations; let sharedBytes = 0; // A later plugin, such as Autoprefixer, removes an outdated prefixed // declaration from the rule that sets it, which a shared rule leaves alone. let prefixed = 0; for (const index of claimedEarlierIndices) { const declaration = firstDeclarations[index]; const length = declarationLength(declaration); sharedBytes += length; const { prop } = declaration; if (prop.charCodeAt(0) === 45 && prop.charCodeAt(1) !== 45) { prefixed += length; } } // The shared rule repeats both selector lists; a rule left empty is dropped. let delta = selectorsLength(firstSelectors) + selectorsLength(secondSelectors) + 2 - sharedBytes; if (first.nodes.length === claimedEarlierIndices.size) { delta -= selectorsLength(firstSelectors) + 1; } if (second.nodes.length === claimedIndices.size) { delta -= selectorsLength(secondSelectors) + 1; } // A pair that shortens the output merges alone: the rules after it may // join the shared rule later, when it is known which rule it can join. const group = delta < 0 ? null : findGroup( first, secondLink, claimedEarlierIndices, claimedIndices, sharedBytes, prefixed, delta, mergeState, sequence, outsideDeclarations ); if (delta >= 0 && !group) return null; const receivingBlock = second.clone(); receivingBlock.selector = [ ...firstSelectors, ...secondSelectors, ...(group?.added ?? []), ].join(); receivingBlock.nodes = []; const firstClone = first.clone({ selectors: firstSelectors }); const secondClone = second.clone({ selectors: secondSelectors }); // Index-based removal matches duplicates positionally, symmetric to how // `claimedIndices` selects declarations in the later rule. let earlierIndex = 0; firstClone.walkDecls((decl) => { if (claimedEarlierIndices.has(earlierIndex++)) { decl.remove(); receivingBlock.append(decl); } }); let laterIndex = 0; secondClone.walkDecls((decl) => { if (claimedIndices.has(laterIndex++)) { decl.remove(); } }); mergeState.markCompatible(receivingBlock); if (secondClone.nodes.length) mergeState.markCompatible(secondClone); /** @type {Placement} */ const placement = { first: [firstClone].filter((rule) => rule.nodes.length), second: [receivingBlock, secondClone].filter((rule) => rule.nodes.length), }; if (group) { placement.following = group.followers.map(({ rule, claimed }) => { const leftover = rule.clone({ selectors: mergeState.meta(rule).selectors, }); mergeState.forget(rule); let index = 0; leftover.walkDecls((decl) => { if (claimed.has(index++)) decl.remove(); }); if (!leftover.nodes.length) return []; mergeState.markCompatible(leftover); return [leftover]; }); } return placement; }