UNPKG

cssnano

Version:

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

176 lines (161 loc) • 5.46 kB
import { createSelectorLookup, selectorsCompatible, } from './ensureCompatibility.js'; import { createElementKeyLookup } from './elementKeys.js'; import { propertyNameKey } from './declarations.js'; import { getMeta } from './ruleMeta.js'; import { isKeyframes, sameContainer } from './sameContainer.js'; /** @import {Container, Declaration, Rule} from 'postcss' */ /** @import {RuleMeta} from './ruleMeta.js' */ /** @import {RuleDeclarationKeys} from './ruleMeta.js' */ /** @param {import('postcss').ChildNode} node @return {boolean} */ function isRuleOrAtRule(node) { return node.type === 'rule' || node.type === 'atrule'; } /** * The state shared by every merge of one stylesheet. */ export default class MergeState { /** @type {import('./ruleMeta.js').SelectorLookup} */ #lookup; /** @type {WeakSet<Rule>} */ #ruleCache; /** @type {WeakMap<Rule, RuleMeta>} */ #ruleMeta; /** @type {Map<string, number>} */ #declarationKeys = new Map(); /** @type {(selector: string) => string | null} */ #elementKeyOf = createElementKeyLookup(); /** * @param {string[]} browsers * @param {Map<string, import('./ensureCompatibility.js').SelectorInfo>} compatibilityCache * @param {WeakSet<Rule>} ruleCache rules whose selectors are known to be compatible * @param {WeakMap<Rule, RuleMeta>} ruleMeta */ constructor(browsers, compatibilityCache, ruleCache, ruleMeta) { this.#lookup = createSelectorLookup(browsers, compatibilityCache); this.#ruleCache = ruleCache; this.#ruleMeta = ruleMeta; } /** * @param {Rule} rule * @return {RuleMeta} */ meta(rule) { return getMeta(rule, this.#ruleMeta); } /** * Drops the description of a rule whose selectors or declarations changed * or that left the stylesheet. It is rebuilt from the rule when next * needed, so a rule that was never described stays so. * * @param {Rule} rule */ forget(rule) { this.#ruleMeta.delete(rule); } /** * The elements the rule can match. An arrow function, so it passes as a * callback without a closure per call. * * @param {Rule} rule * @return {string[] | null} */ elementKeysOf = (rule) => this.meta(rule).elementKeys(this.#elementKeyOf); /** * Numbers declarations so equal ones, with the same property, value and * importance, share a key for the whole stylesheet. An arrow function, so * it passes as a callback without a closure per call. * * @param {Declaration} declaration * @return {number} */ declarationKey = (declaration) => { const canonical = `${propertyNameKey(declaration.prop)}:${declaration.value}:${declaration.important}`; let key = this.#declarationKeys.get(canonical); if (key === undefined) { key = this.#declarationKeys.size; this.#declarationKeys.set(canonical, key); } return key; }; /** * @param {Rule} rule * @return {RuleDeclarationKeys} */ declarationKeysOf(rule) { return this.meta(rule).declarationKeys(this.declarationKey); } /** * Records that every selector of the rule is compatible with the target * browsers, so `canMerge` does not check them again. The caller must have * established it: a rule built from selectors that passed `canMerge` * qualifies, since a list is compatible exactly when each selector is. * * @param {Rule} rule */ markCompatible(rule) { this.#ruleCache.add(rule); } /** * Appends selectors to the rule's selector list, keeping its vendor prefix * profile in step. * * @param {Rule} rule * @param {string[]} selectors * @return {readonly string[] | null} the element keys the selectors add, or * null when the rule's elements are not listed */ addSelectors(rule, selectors) { return this.meta(rule).addSelectors( selectors, this.#lookup, this.#elementKeyOf ); } /** * Gives `later` the selectors of `earlier`, which stands before it and is * dropped by the caller. * * @param {Rule} later * @param {Rule} earlier */ absorbEarlier(later, earlier) { this.meta(later).absorbEarlier( this.meta(earlier), this.#lookup, this.#elementKeyOf ); } /** * Whether the rules can be merged. `parentA` and `parentB` are where the * rules stand, which differs from their `parent` for a rule that moved * during the sweep. * * @param {Rule} ruleA * @param {Rule} ruleB * @param {Container | undefined} [parentA] * @param {Container | undefined} [parentB] * @return {boolean} */ canMerge(ruleA, ruleB, parentA = ruleA.parent, parentB = ruleB.parent) { if (!sameContainer(parentA, parentB)) return false; if (isKeyframes(parentA)) return false; if (ruleA.some(isRuleOrAtRule) || ruleB.some(isRuleOrAtRule)) return false; const metaA = this.meta(ruleA); const metaB = this.meta(ruleB); // Compatibility holds for a concatenated list exactly when it holds for // each side, so a long absorbing list is never rescanned. The vendor check // follows it, because an incompatible selector has no known prefix. return ( (this.#ruleCache.has(ruleA) || selectorsCompatible(metaA.selectors, this.#lookup)) && (this.#ruleCache.has(ruleB) || selectorsCompatible(metaB.selectors, this.#lookup)) && metaA .vendorProfile(this.#lookup) .allowsMerge(metaB.vendorProfile(this.#lookup)) ); } }