cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
203 lines (187 loc) • 6.91 kB
JavaScript
import DeclarationScan from './declarationScan.js';
import DroppableBlocks from './droppableBlocks.js';
import SlotIndex from './slotIndex.js';
/** @import {ChildNode, Container, Root, Rule} from 'postcss' */
/** @import {Slot} from './slotIndex.js' */
/**
* A doubly linked list lets the scan replace a merged pair in constant time.
*
* @typedef {{rule: Rule, slot: Slot, prev: RuleLink | null, next: RuleLink | null}} RuleLink
*/
/**
* The rules that stand in the slots of a pair after it is merged.
*
* @typedef {object} Placement
* @property {Rule[]} first replaces the earlier rule, in its slot
* @property {Rule[]} second replaces the later rule, in its slot
* @property {Rule[][]} [following] replaces each rule after the later one that
* joined the merge, in its slot
*/
/**
* Chains a link for each rule, in order, after `previous`.
*
* @param {RuleLink} previous
* @param {Rule[]} rules
* @param {Slot} slot
* @return {RuleLink} the last link, or `previous` when there are no rules
*/
function linkAfter(previous, rules, slot) {
let tail = previous;
for (const rule of rules) {
/** @type {RuleLink} */
const link = { rule, slot, prev: tail, next: null };
tail.next = link;
tail = link;
}
return tail;
}
/**
* Every rule of a stylesheet in stylesheet order, including rules nested in
* other rules, rewritten in place and written back once.
*/
export default class RuleSequence {
/** @type {RuleLink} a sentinel that precedes the first rule */
head = /** @type {RuleLink} */ ({ prev: null, next: null });
#slots = new SlotIndex();
#blocks = new DroppableBlocks();
#scan = new DeclarationScan(this.#slots, this.#blocks);
/** @type {Set<Container>} containers whose rules differ from the stylesheet */
#changed = new Set();
/** @param {Root} root */
constructor(root) {
let tail = this.head;
/** @param {Container} container */
const collect = (container) => {
for (const [index, node] of /** @type {ChildNode[]} */ (
container.nodes
).entries()) {
if (node.type === 'rule') {
/** @type {RuleLink} */
const link = {
rule: node,
slot: this.#slots.register(container, index, node),
prev: tail,
next: null,
};
tail.next = link;
tail = link;
}
if ('nodes' in node && node.nodes) {
this.#blocks.register(node);
this.#slots.recordPosition(node, index);
collect(node);
}
}
};
collect(root);
}
/**
* @param {RuleLink} first
* @param {RuleLink} second
* @param {WeakMap<Container, boolean>} outsideDeclarations see
* `DeclarationScan#declarationBetween`
* @return {boolean} whether a declaration outside both rules lies between them
*/
declarationBetween(first, second, outsideDeclarations) {
return this.#scan.declarationBetween(first, second, outsideDeclarations);
}
/**
* Moves the rule of `second` to the end of the parent of `first`. It follows
* `first` and holds no nested rules, so passing the rest of that parent
* leaves the order of the rules unchanged.
*
* @param {RuleLink} first
* @param {RuleLink} second
* @return {boolean} whether the parents differed
*/
moveIntoParent(first, second) {
const target = first.slot.parent;
const source = second.slot.parent;
if (target === source) return false;
this.#blocks.moved(source, target);
second.slot = this.#slots.moveRule(second.rule, second.slot, target);
this.#changed.add(target).add(source);
return true;
}
/**
* Replaces the pair `first` and the link after it, and the links after
* those that joined, with the `placement` rules.
*
* @param {RuleLink} first
* @param {Placement} placement
* @return {RuleLink} the link of the first replacement
*/
replacePair(first, placement) {
const second = /** @type {RuleLink} */ (first.next);
const before = /** @type {RuleLink} */ (first.prev);
const after = second.next;
const firstSlot = first.slot;
const secondSlot = second.slot;
// Both rules may share a slot after an earlier partial merge, so each is
// replaced where it stands.
this.#blocks.changed(firstSlot.parent, placement.first.length - 1);
this.#blocks.changed(secondSlot.parent, placement.second.length - 1);
this.#slots.substitute(firstSlot, first.rule, placement.first);
this.#slots.substitute(secondSlot, second.rule, placement.second);
this.#changed.add(firstSlot.parent).add(secondSlot.parent);
let tail = linkAfter(
linkAfter(before, placement.first, firstSlot),
placement.second,
secondSlot
);
let following = after;
for (const rules of placement.following ?? []) {
const link = /** @type {RuleLink} */ (following);
following = link.next;
this.#blocks.changed(link.slot.parent, rules.length - 1);
this.#slots.substitute(link.slot, link.rule, rules);
tail = linkAfter(tail, rules, link.slot);
}
tail.next = following;
if (following) following.prev = tail;
return /** @type {RuleLink} */ (before.next);
}
/**
* Writes the sequence into the stylesheet, rebuilding each changed container
* once from its children and the rules now in its slots.
*
* @return {void}
*/
write() {
// A block which has become empty can be safely ignored, but keeping it would
// make every later pair walk across it. Rebuild its parent without it,
// which avoids searching the children of the parent once per removed
// block.
const emptied = this.#blocks.emptiedBlocks();
const changed = new Set(this.#changed);
for (const container of emptied) {
const parent = /** @type {Container} */ (container.parent);
if (!emptied.has(parent)) changed.add(parent);
changed.delete(container);
}
// Every container is emptied before any is refilled, so a rule that moves
// between two of them keeps the parent the refill gives it.
const rebuilt = Array.from(changed, (container) => ({
container,
children: this.#slots.childrenOf(
container,
(node) => !emptied.has(/** @type {Container} */ (node))
),
head: /** @type {ChildNode[]} */ (container.nodes)[0],
}));
for (const { container } of rebuilt) container.removeAll();
for (const { container, children, head } of rebuilt) {
// A rule kept from the source still points at its former container.
for (const node of children) node.parent = undefined;
container.append(children);
// Removing the first child of a root hands its whitespace to the next.
if (
container.type === 'root' &&
children.length &&
children[0] !== head
) {
children[0].raws.before = head.raws.before;
}
}
}
}