cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
251 lines (230 loc) • 7.86 kB
JavaScript
import { elementKeyOf } from './elementKeys.js';
import { VendorPrefixSummary } from './vendorPrefixSummary.js';
/** @import {Declaration, Rule} from 'postcss' */
/**
* @typedef {object} RuleDeclarationKeys
* @property {number[]} keys
* @property {Set<number>} keySet
*/
/**
* @typedef {object} RuleElementKeys
* @property {string[]} list the distinct keys, in order of first appearance
* @property {Set<string>} seen the same keys, for membership tests
*/
/** @typedef {(selector: string) => import('./ensureCompatibility.js').SelectorInfo} SelectorLookup */
/** @type {readonly string[]} */
const noElementKeys = Object.freeze([]);
/**
* @param {import('postcss').ChildNode} node
* @return {node is Declaration}
*/
function isDeclaration(node) {
return node.type === 'decl';
}
/**
* The selector list of a rule with what is derived from it. Every field is
* set up front so all instances share one object layout.
*/
export class RuleMeta {
/** @type {string[]} */
#selectors;
/** @type {Declaration[]} */
#declarations;
/** @type {VendorPrefixSummary | undefined} derived from the selector list */
#vendor;
/** @type {string | undefined} the selector list joined by commas, once known */
#selectorText;
/** @type {RuleDeclarationKeys | undefined} derived from the declarations */
#declarationKeys;
/** @type {RuleElementKeys | null | undefined} derived from the selector list; null when a selector has no element key */
#elementKeys;
/**
* @param {string[]} selectors
* @param {Declaration[]} declarations
*/
constructor(selectors, declarations) {
this.#selectors = selectors;
this.#declarations = declarations;
this.#vendor = undefined;
this.#selectorText = undefined;
this.#declarationKeys = undefined;
this.#elementKeys = undefined;
}
/** @return {string[]} change it only through the methods of this class */
get selectors() {
return this.#selectors;
}
/** @return {Declaration[]} */
get declarations() {
return this.#declarations;
}
/** @param {Declaration[]} declarations */
setDeclarations(declarations) {
this.#declarations = declarations;
this.#declarationKeys = undefined;
}
/**
* Numbers the declarations so equal ones share a key. The numbering is
* the caller's, and the keys last until the declarations change; adding
* selectors leaves them valid.
*
* @param {(declaration: Declaration) => number} keyOf
* @return {RuleDeclarationKeys}
*/
declarationKeys(keyOf) {
if (!this.#declarationKeys) {
const keys = this.#declarations.map(keyOf);
this.#declarationKeys = { keys, keySet: new Set(keys) };
}
return this.#declarationKeys;
}
/**
* @param {SelectorLookup} lookup
* @return {VendorPrefixSummary}
*/
vendorProfile(lookup) {
this.#vendor ??= VendorPrefixSummary.of(this.#selectors.map(lookup));
return this.#vendor;
}
/**
* The elements the selector list can match, as `elementKeyOf` in
* `elementKeys.js` describes them, or null when any selector has no key or
* there are none, which must not read as a rule that writes to no element.
* A caller must not hold the list across a change to the rule.
*
* @param {(selector: string) => string | null} [keyOf] the key of one selector
* @return {string[] | null}
*/
elementKeys(keyOf = elementKeyOf) {
if (this.#elementKeys === undefined) {
this.#elementKeys = { list: [], seen: new Set() };
this.#addElementKeys(this.#selectors, keyOf);
}
return this.#elementKeys?.list.length ? this.#elementKeys.list : null;
}
/**
* @param {string[]} selectors
* @param {(selector: string) => string | null} keyOf
*/
#addElementKeys(selectors, keyOf) {
const keys = this.#elementKeys;
if (!keys) return;
for (const selector of selectors) {
const key = keyOf(selector);
if (key === null) {
this.#elementKeys = null;
return;
}
if (!keys.seen.has(key)) {
keys.seen.add(key);
keys.list.push(key);
}
}
}
/**
* @param {string[]} selectors
* @param {(selector: string) => string | null} keyOf
* @return {readonly string[] | null}
*/
#gainElementKeys(selectors, keyOf) {
const listed = this.#elementKeys?.list.length;
this.#addElementKeys(selectors, keyOf);
const list = this.#elementKeys?.list;
if (listed === undefined || list === undefined) return null;
return list.length === listed ? noElementKeys : list.slice(listed);
}
/** @return {string} */
selectorText() {
this.#selectorText ??= this.#selectors.join(',');
return this.#selectorText;
}
/**
* Lists of different lengths differ in text too, since a selector holds no
* top-level comma, so the text of long lists is joined only when needed.
*
* @param {RuleMeta} other
* @return {boolean}
*/
hasSameSelectors(other) {
return (
this.#selectors.length === other.#selectors.length &&
this.selectorText() === other.selectorText()
);
}
/**
* @param {string[]} selectors appended to the rule's selector list
* @param {SelectorLookup} lookup
* @param {(selector: string) => string | null} [keyOf]
* @return {readonly string[] | null} the element keys the selectors add to
* those listed, or null when the rule's elements are not listed
*/
addSelectors(selectors, lookup, keyOf = elementKeyOf) {
this.#selectorText = undefined;
for (const selector of selectors) {
this.#selectors.push(selector);
this.#vendor?.add(lookup(selector));
}
return this.#gainElementKeys(selectors, keyOf);
}
/**
* @param {string[]} selectors replaces the rule's selector list
* @param {VendorPrefixSummary} [vendor] the profile of `selectors` when the caller
* already knows it; otherwise it is derived on the next `vendorProfile`
*/
setSelectors(selectors, vendor) {
this.#selectors = selectors;
this.#selectorText = undefined;
this.#vendor = vendor;
this.#elementKeys = undefined;
}
/**
* Takes the place of a rule that is dropped and stood before this one, so
* its selectors come first. The dropped rule's list grows in place and
* passes to this one, and the joined text is concatenated from the two
* texts: copying or re-joining a long list on every merge would take
* quadratic time.
*
* @param {RuleMeta} earlier left unusable
* @param {SelectorLookup} lookup
* @param {(selector: string) => string | null} [keyOf]
*/
absorbEarlier(earlier, lookup, keyOf = elementKeyOf) {
const text = `${earlier.selectorText()},${this.selectorText()}`;
const vendor = earlier
.vendorProfile(lookup)
.concat(this.vendorProfile(lookup));
const selectors = earlier.#selectors;
const own = this.#selectors;
for (const selector of own) selectors.push(selector);
this.#selectors = selectors;
this.#selectorText = text;
this.#vendor = vendor;
// Extends the earlier rule's keys only if they are known: deriving them
// from a long list would repeat on every merge.
this.#elementKeys = earlier.#elementKeys;
this.#addElementKeys(own, keyOf);
}
}
/**
* @param {Rule} rule
* @param {WeakMap<Rule, RuleMeta>} [ruleMeta]
* @return {RuleMeta}
*/
export function getMeta(rule, ruleMeta) {
if (ruleMeta && rule) {
let meta = ruleMeta.get(rule);
if (!meta && rule.nodes) {
meta = new RuleMeta(rule.selectors, rule.nodes.filter(isDeclaration));
ruleMeta.set(rule, meta);
}
return meta ?? new RuleMeta([], []);
}
return new RuleMeta(
rule?.selectors ?? [],
rule?.nodes?.filter(isDeclaration) ?? []
);
}
/** @param {Rule} rule @return {Declaration[]} */
export function getDecls(rule) {
return rule.nodes.filter(isDeclaration);
}