cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
225 lines (211 loc) • 6.35 kB
JavaScript
import { TokenType } from '@csstools/css-tokenizer';
import cssnanoUtils from '#utils';
const { asciiLowerCase, decoded, tokens } = cssnanoUtils;
/**
* Parses a @layer parameter list into decoded layer-name paths. A period is a
* separator only as a Delim token between idents; an escaped dot is part of
* the preceding identifier's value, so `a\.b` is one segment named "a.b".
* Whitespace and comments around separators are allowed, preserving the
* previous handling of `a . b`.
*
* @param {string} params
* @return {string[][] | null} one decoded segment path per declared name
*/
function parseLayerNameList(params) {
const tokenList = tokens(params).filter(
(t) => t[0] !== TokenType.Whitespace && t[0] !== TokenType.Comment
);
/** @type {string[][]} */
const names = [];
/** @type {string[] | null} */
let current = null;
let expectIdent = true;
for (const token of tokenList) {
const type = token[0];
if (type === TokenType.Comma) {
if (expectIdent || !current) {
return null;
}
names.push(current);
current = null;
expectIdent = true;
} else if (type === TokenType.Delim && token[1] === '.') {
if (expectIdent || !current) {
return null;
}
expectIdent = true;
} else if (type === TokenType.Ident && expectIdent) {
current ??= [];
current.push(decoded(token));
expectIdent = false;
} else {
return null;
}
}
if (expectIdent) {
return null;
}
if (current) {
names.push(current);
}
return names;
}
/**
* @param {import('postcss').Node} node
* @return {import('postcss').AtRule[]} enclosing @layer at-rules, outermost first
*/
function enclosingLayerRules(node) {
/** @type {import('postcss').AtRule[]} */
const chain = [];
let curr = node.parent;
while (curr && curr.type !== 'root') {
if (
curr.type === 'atrule' &&
asciiLowerCase(/** @type {import('postcss').AtRule} */ (curr).name) ===
'layer'
) {
chain.unshift(/** @type {import('postcss').AtRule} */ (curr));
}
curr = curr.parent;
}
return chain;
}
class LayerRegistry {
constructor() {
this.nextIndex = 0;
/** @type {Map<string, { index: number, children: LayerRegistry }>} */
this.layers = new Map();
/** @type {Map<import('postcss').AtRule, { index: number, children: LayerRegistry }>} */
this.anonymous = new Map();
}
/**
* @param {string} name
* @return {{ index: number, children: LayerRegistry }}
*/
getOrCreate(name) {
let entry = this.layers.get(name);
if (!entry) {
entry = { index: this.nextIndex++, children: new LayerRegistry() };
this.layers.set(name, entry);
}
return entry;
}
/**
* Entry for a layer with no name or an unparseable name: every occurrence
* is a unique cascade layer, but names nested inside still order beneath
* it through the child registry.
*
* @param {import('postcss').AtRule} layerRule
* @return {{ index: number, children: LayerRegistry }}
*/
getOrCreateAnonymous(layerRule) {
let entry = this.anonymous.get(layerRule);
if (!entry) {
entry = { index: this.nextIndex++, children: new LayerRegistry() };
this.anonymous.set(layerRule, entry);
}
return entry;
}
/**
* The registry entry a single layer at-rule occupies, creating missing
* segments as needed. Statement lists and unparseable names fall back to a
* unique anonymous entry so they order but never match another spelling.
*
* @param {import('postcss').AtRule} layerRule
* @return {{ index: number, children: LayerRegistry }}
*/
entryFor(layerRule) {
const names = layerRule.params.trim()
? parseLayerNameList(layerRule.params)
: null;
if (names === null || names.length !== 1) {
return this.getOrCreateAnonymous(layerRule);
}
let entry = this.getOrCreate(names[0][0]);
for (let i = 1; i < names[0].length; i++) {
entry = entry.children.getOrCreate(names[0][i]);
}
return entry;
}
/**
* Registers the layers a @layer at-rule declares, in document order and
* nested beneath any enclosing layers, so later lookups observe the real
* layer tree.
*
* @param {import('postcss').AtRule} layerRule
* @return {void}
*/
declareLayerAtRule(layerRule) {
/** @type {LayerRegistry} */
let current = this;
for (const parent of enclosingLayerRules(layerRule)) {
current = current.entryFor(parent).children;
}
if (!layerRule.params.trim()) {
// An unnamed block layer is itself a unique anonymous layer; register
// it so nested names order beneath it.
if (layerRule.nodes) {
current.getOrCreateAnonymous(layerRule);
}
return;
}
const names = parseLayerNameList(layerRule.params);
if (names === null) {
return;
}
for (const segments of names) {
let entry = current.getOrCreate(segments[0]);
for (let i = 1; i < segments.length; i++) {
entry = entry.children.getOrCreate(segments[i]);
}
}
}
/**
* @param {import('postcss').Node} node
* @return {number[]}
*/
getPriority(node) {
const chain = enclosingLayerRules(node);
if (chain.length === 0) {
return [Infinity];
}
/** @type {number[]} */
const priority = [];
/** @type {LayerRegistry} */
let current = this;
for (const layerRule of chain) {
const entry = current.entryFor(layerRule);
priority.push(entry.index);
current = entry.children;
}
priority.push(Infinity);
return priority;
}
}
/**
* @param {number[]} p1
* @param {number[]} p2
* @return {number}
*/
function compareLayerPriority(p1, p2) {
const len = Math.min(p1.length, p2.length);
for (let i = 0; i < len; i++) {
if (p1[i] !== p2[i]) {
return p1[i] - p2[i];
}
}
return p1.length - p2.length;
}
/**
* @param {{ layerPriority: number[], documentIndex: number }} a
* @param {{ layerPriority: number[], documentIndex: number }} b
* @return {number}
*/
function compareAtRulePriority(a, b) {
const layerCmp = compareLayerPriority(a.layerPriority, b.layerPriority);
if (layerCmp !== 0) {
return layerCmp;
}
return a.documentIndex - b.documentIndex;
}
export { LayerRegistry, compareAtRulePriority };