postcss-minify-selectors
Version:
Minify selectors with PostCSS.
230 lines (210 loc) • 7.95 kB
JavaScript
import { hasSemanticFact, semanticFacts } from './arena.js';
/** @typedef {import('./arena.js').SelectorArena} SelectorArena */
/** @typedef {import('./arena.js').Specificity} Specificity */
/** @typedef {import('./normalizeOutput.js').Normalized} Normalized */
/** @param {{pairs:Map<number,Map<number,number>>,next:number}} state @param {number} left @param {number} right */
export function consId(state, left, right) {
let rights = state.pairs.get(left);
if (!rights) {
rights = new Map();
state.pairs.set(left, rights);
}
let id = rights.get(right);
if (id === undefined) {
id = state.next++;
rights.set(right, id);
}
return id;
}
/** @typedef {{selector:ActiveSelector,position:number,middle:Normalized,group:FoldGroup,text:string}} FoldOccurrence */
/** @typedef {{activeOccurrences:Set<FoldOccurrence>,orderHeap:FoldOccurrence[],lexHeap:FoldOccurrence[],middleCounts:Map<number,{count:number,middle:Normalized}>,specificity:Specificity,specificityId:number,activeCount:number,selectorLength:number,middleLength:number,version:number,sequence:number}} FoldGroup */
/** @typedef {{group:FoldGroup,version:number,savings:number,count:number,first:FoldOccurrence,lex:string}} FoldCandidate */
/** @typedef {Normalized & {active:boolean,activeId:number,order:number,memberships:FoldOccurrence[],previousId?:number,nextId?:number}} ActiveSelector */
/** @param {Normalized} selector @param {SelectorArena} arena */
function arenaUnsafeForFold(selector, arena) {
const facts =
selector.node >= 0 ? (arena.nodes[selector.node]?.facts ?? 0) : 0;
return (
hasSemanticFact(facts, semanticFacts.vendorPseudo) ||
hasSemanticFact(facts, semanticFacts.commentDescendant) ||
selector.parts?.some((part) => 'kind' in part && part.text === '||')
);
}
/** @param {Normalized} selector @param {SelectorArena} arena */
export function selectorCanFold(selector, arena) {
if (!selector.parts || arenaUnsafeForFold(selector, arena)) return false;
for (let position = 0; position < selector.parts.length; position += 2) {
const middle = selector.parts[position];
if (!('kind' in middle) && middle.foldEligible && middle.specificity)
return true;
}
return false;
}
/** @param {Map<number,Map<number,Map<number,FoldGroup>>>} groups @param {{value:number}} sequence @param {number} prefix @param {number} suffix @param {Normalized} middle */
export function foldGroup(groups, sequence, prefix, suffix, middle) {
const specificityId = /** @type {number} */ (middle.specificityId);
let bySuffix = groups.get(prefix);
if (!bySuffix) {
bySuffix = new Map();
groups.set(prefix, bySuffix);
}
let bySpec = bySuffix.get(suffix);
if (!bySpec) {
bySpec = new Map();
bySuffix.set(suffix, bySpec);
}
let group = bySpec.get(specificityId);
if (!group) {
group = {
activeOccurrences: new Set(),
orderHeap: [],
lexHeap: [],
middleCounts: new Map(),
specificity: /** @type {Specificity} */ (middle.specificity),
specificityId,
activeCount: 0,
selectorLength: 0,
middleLength: 0,
version: 0,
sequence: sequence.value++,
};
bySpec.set(specificityId, group);
}
return group;
}
/** @param {FoldGroup} group */
export function activeOccurrences(group) {
return [...group.activeOccurrences];
}
/**
* Sift the element at `index` up to restore a binary-heap invariant.
* `before` is a strict higher-priority relation. No-op when `index` is 0.
* @template T @param {T[]} values @param {number} index @param {(left:T,right:T)=>boolean} before
*/
function siftUp(values, index, before) {
const value = values[index];
let current = index;
while (current > 0) {
const parent = Math.floor((current - 1) / 2);
if (!before(value, values[parent])) break;
values[current] = values[parent];
current = parent;
}
values[current] = value;
}
/**
* Remove the root of `values` and restore the binary-heap invariant by
* sifting the former last element down from the top.
* @template T @param {T[]} values @param {(left:T,right:T)=>boolean} before
*/
function siftDown(values, before) {
const last = values.pop();
if (!last || values.length === 0) return;
let index = 0;
while (true) {
const left = index * 2 + 1;
if (left >= values.length) break;
const right = left + 1;
const child =
right < values.length && before(values[right], values[left])
? right
: left;
if (!before(values[child], last)) break;
values[index] = values[child];
index = child;
}
values[index] = last;
}
/** @param {FoldOccurrence[]} values @param {FoldOccurrence} value @param {(left:FoldOccurrence,right:FoldOccurrence)=>boolean} before */
export function pushOccurrence(values, value, before) {
values.push(value);
siftUp(values, values.length - 1, before);
}
/** @param {FoldOccurrence} left @param {FoldOccurrence} right */
export function occurrenceOrder(left, right) {
return (
left.selector.order < right.selector.order ||
(left.selector.order === right.selector.order &&
left.position < right.position)
);
}
/** @param {FoldOccurrence} left @param {FoldOccurrence} right */
export function occurrenceText(left, right) {
return left.text < right.text;
}
/** @param {FoldOccurrence[]} values @param {(left:FoldOccurrence,right:FoldOccurrence)=>boolean} before */
function activeOccurrence(values, before) {
while (values[0] && !values[0].selector.active) siftDown(values, before);
return values[0];
}
/** @param {FoldGroup} group @return {FoldCandidate | undefined} */
export function foldCandidate(group) {
if (group.activeCount < 2 || group.middleCounts.size < 2) return;
const first = activeOccurrence(group.orderHeap, occurrenceOrder);
const lexical = activeOccurrence(group.lexHeap, occurrenceText);
if (!first || !lexical) return;
const foldedLength =
first.selector.length -
first.middle.length +
5 +
group.middleLength +
group.middleCounts.size -
1;
const originalLength = group.selectorLength + group.activeCount - 1;
const savings = originalLength - foldedLength;
if (savings <= 0) return;
return {
group,
version: group.version,
savings,
count: group.activeCount,
first,
lex: lexical.text,
};
}
export class CandidateHeap {
/** @param {boolean} sort */
constructor(sort) {
/** @type {FoldCandidate[]} */ this.values = [];
this.sort = sort;
}
/** @param {FoldCandidate} left @param {FoldCandidate} right */
before(left, right) {
return foldCandidateBefore(this.sort, left, right);
}
/** @param {FoldCandidate} value */
push(value) {
this.values.push(value);
siftUp(this.values, this.values.length - 1, (left, right) =>
this.before(left, right)
);
}
pop() {
const root = this.values[0];
siftDown(this.values, (left, right) => this.before(left, right));
return root;
}
}
/** @param {boolean} sort @param {FoldCandidate} left @param {FoldCandidate} right */
export function foldCandidateBefore(sort, left, right) {
if (!sort)
return (
left.first.selector.order < right.first.selector.order ||
(left.first.selector.order === right.first.selector.order &&
(left.first.position < right.first.position ||
(left.first.position === right.first.position &&
left.group.sequence < right.group.sequence)))
);
return (
left.savings > right.savings ||
(left.savings === right.savings &&
(left.count > right.count ||
(left.count === right.count &&
(left.lex < right.lex ||
(left.lex === right.lex &&
(left.first.position < right.first.position ||
(left.first.position === right.first.position &&
left.group.sequence < right.group.sequence)))))))
);
}
/** @param {FoldGroup} group @param {FoldOccurrence} occurrence */