UNPKG

cssnano

Version:

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

206 lines (194 loc) • 7.5 kB
import { mergeMatchingDeclarations, mergeMatchingSelectors, mergeSharedDeclarations, } from './merge.js'; import { earliestPairReaching } from './sharedRuleGroup.js'; import RuleSequence from './ruleSequence.js'; import { sameContainer } from './sameContainer.js'; const NO_BENEFIT = -1; /** * @param {Placement} placement * @return {number} the rules the placement puts in place of the pair */ function linkCount(placement) { let count = placement.first.length + placement.second.length; for (const rules of placement.following ?? []) count += rules.length; return count; } /** @import {Container, Root, Rule} from 'postcss' */ /** @import MergeState from './mergeState.js' */ /** @import {Placement, RuleLink} from './ruleSequence.js' */ /** * Merges neighboring rules in left-to-right sweeps, trying the pair that * shares the most declarations first and the earlier pair on a tie. That * approximates merging the best pair of the whole stylesheet first. The two * orders give the same output on the framework corpus but not on every input. * * Termination: a rewrite either writes fewer declarations, writes as many in * fewer rules, or moves a rule into the parent of an earlier rule with an * equivalent parent, and replacements stay where they are. Neither the * counts nor the positions can recur, so the sweeps stop once one moves no * rule. The length is no measure: sharing declarations is judged by the * minified length, which source whitespace can contradict. * * @param {Root} root * @param {MergeState} mergeState * @return {void} */ export default function runScan(root, mergeState) { /** * Orders pairs for merging. For equal selectors it is the number of * declarations in both rules, since the selector is written once. Otherwise * it is the number of declarations the rules share, or 0 when only a move * into an equivalent parent or the removal of empty rules applies. * * @param {RuleLink} first * @param {RuleLink} second * @return {number} NO_BENEFIT when merging the pair cannot rewrite anything */ const mergeBenefit = (first, second) => { const a = mergeState.declarationKeysOf(first.rule); const b = mergeState.declarationKeysOf(second.rule); if ( mergeState.meta(first.rule).hasSameSelectors(mergeState.meta(second.rule)) ) return a.keys.length + b.keys.length; const [smaller, larger] = a.keys.length <= b.keys.length ? [a, b.keySet] : [b, a.keySet]; let shared = 0; for (const key of smaller.keySet) { if (larger.has(key)) shared++; } if (shared > 0) return shared; const rewritesStructure = (a.keys.length === 0 && b.keys.length === 0) || (first.slot.parent !== second.slot.parent && sameContainer(first.slot.parent, second.slot.parent)); return rewritesStructure ? 0 : NO_BENEFIT; }; // Pairs that passed `canMerge` as the best candidate but rewrote nothing; // they are skipped until another rewrite can change the outcome. /** @type {Map<Rule, Rule>} */ const unmergeablePairs = new Map(); /** @param {RuleLink} first @param {RuleLink} second */ const pairBenefit = (first, second) => unmergeablePairs.get(first.rule) === second.rule ? NO_BENEFIT : mergeBenefit(first, second); /** @type {WeakMap<Container, boolean>} */ const outsideDeclarations = new WeakMap(); /** * @param {RuleSequence} sequence * @param {RuleLink} first * @param {RuleLink} second * @return {{placement: Placement, moved: boolean} | null} the rules that * now stand in place of the pair, or null when nothing changed */ const mergePair = (sequence, first, second) => { if ( !mergeState.canMerge( first.rule, second.rule, first.slot.parent, second.slot.parent ) || sequence.declarationBetween(first, second, outsideDeclarations) ) { return null; } const moved = sequence.moveIntoParent(first, second); const placement = mergeMatchingDeclarations(first.rule, second.rule, mergeState) ?? mergeMatchingSelectors(first.rule, second.rule, mergeState) ?? mergeSharedDeclarations( first.rule, second.rule, mergeState, second, sequence, outsideDeclarations ); if (placement) return { placement, moved }; return moved ? { placement: { first: [first.rule], second: [second.rule] }, moved } : null; }; /** @type {number[]} */ const scratch = []; /** @type {{link: RuleLink | null, distance: number}} */ const earliest = { link: null, distance: 0 }; /** * The link where the sweep resumes after a rewrite. The normal resume point * is the lookahead, the rule before the new rules, or the first new rule. * The rewrite can also change a pair before it, up to the reach of a group * (see `earliestPairReaching`), so the sweep resumes at the earliest such pair. * * @param {RuleLink} replacement the first link of the new rules * @param {Placement} placement * @param {RuleLink | null} lookaheadStart a link before the replaced pair, * since the lookahead moves past it * @return {RuleLink} */ const resumeAfter = (replacement, placement, lookaheadStart) => { const before = /** @type {RuleLink} */ (replacement.prev); let resume = replacement; if (lookaheadStart) { resume = lookaheadStart; } else if (before.rule) { resume = before; } earliest.link = null; let link = replacement; const count = linkCount(placement); for (let index = 0; index < count; index++) { // The walk from the `index`th new rule starts `index` links after the // first one, so its distance is measured from the first. earliestPairReaching(link, resume, index, scratch, mergeState, earliest); link = /** @type {RuleLink} */ (link.next); } return earliest.link ?? resume; }; let moved; do { moved = false; const sequence = new RuleSequence(root); const list = sequence.head; let current = list.next ?? list; /** @type {RuleLink | null} */ let lookaheadStart = null; for (let second = current.next; second; second = current.next) { let benefit = pairBenefit(current, second); // Move on to the next pair while it shares strictly more declarations, // then come back to `lookaheadStart` so the skipped pairs are tried. for (let third = second.next; third; third = second.next) { const ahead = pairBenefit(second, third); if (ahead <= benefit) break; lookaheadStart ??= current; current = second; second = third; benefit = ahead; } const merged = benefit === NO_BENEFIT ? null : mergePair(sequence, current, second); if (!merged) { if (lookaheadStart && benefit !== NO_BENEFIT) { unmergeablePairs.set(current.rule, second.rule); current = lookaheadStart; } else { current = second; } lookaheadStart = null; continue; } moved ||= merged.moved; unmergeablePairs.clear(); const replacement = sequence.replacePair(current, merged.placement); current = resumeAfter(replacement, merged.placement, lookaheadStart); lookaheadStart = null; } sequence.write(); // A move changes which rules share a parent, so rules the sweep already // passed may now merge. } while (moved); }