cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
178 lines (169 loc) • 5.86 kB
JavaScript
import { TokenType } from '@csstools/css-tokenizer';
import cssnanoUtils from '#utils';
import { classifyDeclaration } from './grammar.js';
import { trailingEscape } from './trailingEscape.js';
import { walkValue } from './valueWalk.js';
const { asciiLowerCase, decoded, tokens } = cssnanoUtils;
/**
* @param {import('@csstools/css-tokenizer').CSSToken} token
* @return {string | undefined} the name an ident or string token spells
*/
function spelledName(token) {
if (token[0] === TokenType.Ident) {
return decoded(token);
}
return token[0] === TokenType.String
? /** @type {{value: string}} */ (token[4]).value
: undefined;
}
/**
* Whether a value may spell one of the names. Without an escape or a NUL,
* which CSS replaces with U+FFFD, an ident or string spelling a name contains
* it literally, so most values are ruled out without tokenizing them.
*
* @param {string} value
* @param {Set<string>} names
* @return {boolean}
*/
function maySpell(value, names) {
if (value.includes('\\') || value.includes('\0')) {
return true;
}
for (const name of names) {
if (value.includes(name)) {
return true;
}
}
return false;
}
/**
* @param {string} value
* @param {Set<string>} candidates the names worth protecting
* @param {Set<string>} names
* @param {boolean} everywhere whether the whole value is substituted text
* @param {boolean} rewritesCounterSlots whether the rewriter respells the
* counter style arguments of counter functions in this value
* @return {void}
*/
function collectNames(
value,
candidates,
names,
everywhere,
rewritesCounterSlots
) {
if (!maySpell(value, candidates)) {
return;
}
const spelled = walkValue(tokens(value), (token, frame) => {
const isRewritten =
rewritesCounterSlots && frame?.expectedArgs?.includes(frame.argIndex);
return everywhere || (frame && !isRewritten)
? spelledName(token)
: undefined;
});
for (const name of spelled) {
if (candidates.has(name)) {
names.add(name);
}
}
}
/**
* @param {import('postcss').Declaration} decl
* @param {string | undefined} restored the result of `trailingEscape(decl)`
* @return {string[]} every text the declaration value may be read as; an
* unknown code point after a trailing backslash is read both as the end of
* the value and as an escaped space
*/
function spellingsOf(decl, restored) {
return restored === undefined ? [decl.value, `${decl.value} `] : [restored];
}
/**
* @typedef {{
* decl: import('postcss').Declaration,
* classification: NonNullable<ReturnType<typeof classifyDeclaration>>,
* value: string
* }} Reference
*/
/**
* One walk over the stylesheet's declarations that finds the names substituted
* text may spell and the declarations that reference keyframes or counter
* styles. Substituted text is custom property values, `@property` initial
* values, `@function` definitions, and references inside functions other than
* the known counter-function argument slots. Declaration walks do not rewrite
* such names, so they must protect their definitions.
*
* The invariant: every place a name is spelled is either rewritten when its
* definition is removed or keeps that definition. A spelling that no
* declaration rewrite reaches therefore has to be collected here.
*
* @param {import('postcss').Root} css
* @param {Set<string>} candidates the names that may be merged; only they
* need protection
* @param {import('postcss').AtRule[]} functionRules the `@function` rules
* @param {import('postcss').AtRule[]} parameterRules the `@mixin` and
* `@apply` rules, whose parameter defaults and arguments are substituted
* @param {import('postcss').AtRule[]} conditionRules the `@container`, `@when`
* and `@else` rules, whose preludes may test a property in a style query
* @return {{ protectedNames: Set<string>, references: Reference[] }}
*/
function scanDeclarations(
css,
candidates,
functionRules,
parameterRules,
conditionRules
) {
/** @type {Set<string>} */
const protectedNames = new Set();
/** @type {Reference[]} */
const references = [];
for (const rule of functionRules) {
collectNames(rule.params, candidates, protectedNames, true, false);
rule.walkDecls((decl) => {
for (const spelling of spellingsOf(decl, trailingEscape(decl))) {
collectNames(spelling, candidates, protectedNames, true, false);
}
});
}
for (const rule of parameterRules) {
collectNames(rule.params, candidates, protectedNames, true, false);
}
for (const rule of conditionRules) {
collectNames(rule.params, candidates, protectedNames, false, false);
}
css.walkDecls((decl) => {
const prop = asciiLowerCase(decl.prop);
const classification = classifyDeclaration(decl, prop);
const restored = trailingEscape(decl);
// A reference that cannot be rewritten is skipped, so it must protect
// every name it may spell.
const isUnrewritable = restored === undefined;
if (classification && !isUnrewritable) {
references.push({ decl, classification, value: restored });
}
const isInitialValue =
prop === 'initial-value' &&
decl.parent?.type === 'atrule' &&
asciiLowerCase(
/** @type {import('postcss').AtRule} */ (decl.parent).name
) === 'property';
const everywhere =
prop.startsWith('--') ||
isInitialValue ||
(classification !== null && isUnrewritable);
if (everywhere || decl.value.includes('(')) {
for (const spelling of spellingsOf(decl, restored)) {
collectNames(
spelling,
candidates,
protectedNames,
everywhere,
classification?.kind === 'counter-style-func'
);
}
}
});
return { protectedNames, references };
}
export { scanDeclarations };