cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
203 lines (190 loc) • 7.13 kB
JavaScript
import cssnanoUtils from '#utils';
import {
aspectRatio,
aspectRatioFeatures,
calcProductEndTypes,
calcProductStartTypes,
closingTypes,
decoded,
featureValueIndex,
mathFunctions,
openingTypes,
significantIndex,
TokenType,
tokenEnd,
whitespaceInsensitiveFunctions,
} from './tokenUtils.js';
const { numeric } = cssnanoUtils;
/** @typedef {Exclude<ReturnType<typeof cssnanoUtils.balancedTokens>, undefined>['tokens']} Tokens */
/** @param {ReturnType<typeof cssnanoUtils.balancedTokens>} structure @return {(number|undefined)[]} */
export function parentIndexes(structure) {
if (!structure) return [];
const parents = [];
const stack = [];
for (let i = 0; i < structure.tokens.length; i++) {
const type = structure.tokens[i][0];
parents[i] = stack.at(-1);
if (openingTypes.has(type)) stack.push(i);
else if (closingTypes.has(type)) stack.pop();
}
return parents;
}
/** @param {Tokens} input @param {number} index @return {boolean} */
function isTightWhitespace(input, index) {
const previous = input[index - 1];
const next = input[index + 1];
return [
previous?.[0] === TokenType.Function,
previous?.[0] === TokenType.OpenParen,
next?.[0] === TokenType.CloseParen,
previous?.[0] === TokenType.Comma,
next?.[0] === TokenType.Comma,
previous?.[0] === TokenType.Colon,
next?.[0] === TokenType.Colon,
previous?.[0] === TokenType.Delim,
next?.[0] === TokenType.Delim,
].some(Boolean);
}
/** @param {unknown} token @return {boolean} */
function isAdditiveOperator(token) {
return (
Array.isArray(token) &&
token[0] === TokenType.Delim &&
(token[1] === '+' || token[1] === '-')
);
}
/** @param {Tokens} input @param {number} index @param {(number|undefined)[]} parents @return {boolean} */
function isRequiredMathOperatorWhitespace(input, index, parents) {
const previous = significantIndex(input, index - 1, -1);
const next = significantIndex(input, index + 1, 1);
const before = input[previous];
const after = input[next];
let operator;
if (isAdditiveOperator(before)) operator = previous;
else if (isAdditiveOperator(after)) operator = next;
if (operator === undefined) return false;
let parent = parents[index];
while (parent !== undefined) {
const token = input[parent];
if (token?.[0] === TokenType.Function) {
const name = String(decoded(token)).toLowerCase();
if (whitespaceInsensitiveFunctions.has(name)) return false;
if (!mathFunctions.has(name)) return true;
break;
}
parent = parents[parent];
}
const left = input[significantIndex(input, operator - 1, -1)];
const right = input[significantIndex(input, operator + 1, 1)];
return (
calcProductEndTypes.has(left?.[0]) && calcProductStartTypes.has(right?.[0])
);
}
/** @param {Tokens} input @param {number} name @param {number} left @param {number} slash @param {number} right @param {number} after @param {number} close @return {boolean} */
function isAspectRatioFeature(input, name, left, slash, right, after, close) {
const a = input[left] ? numeric(input[left]) : false;
const b = input[right] ? numeric(input[right]) : false;
const lowerName = String(decoded(input[name])).toLowerCase();
return (
aspectRatioFeatures.has(lowerName) &&
input[slash]?.[0] === TokenType.Delim &&
input[slash][1] === '/' &&
input[left]?.[0] === TokenType.Number &&
input[right]?.[0] === TokenType.Number &&
a &&
b &&
!a.unit &&
!b.unit &&
after === close
);
}
/** @param {Exclude<ReturnType<typeof cssnanoUtils.balancedTokens>, undefined>} structure @param {number} index @param {(number|undefined)[]} parents @return {{start:number,end:number,text:string}} */
function whitespaceEdit(structure, index, parents) {
const input = structure.tokens;
const token = input[index];
const next = input[index + 1];
const tight = isTightWhitespace(input, index);
const open = parents[index];
const emptyCustomProperty =
next?.[0] === TokenType.CloseParen &&
open !== undefined &&
featureValueIndex(input, open) === structure.endForOpening(open) &&
decoded(input[significantIndex(input, open + 1, 1)]).startsWith('--');
return {
start: token[2],
end: tokenEnd(token),
text:
tight &&
!emptyCustomProperty &&
!isRequiredMathOperatorWhitespace(input, index, parents)
? ''
: ' ',
};
}
/** @param {Exclude<ReturnType<typeof cssnanoUtils.balancedTokens>, undefined>} structure @param {(number|undefined)[]} parents @param {{start:number,end:number,text:string}[]} changes @return {void} */
export function minifyWhitespace(structure, parents, changes) {
const { tokens: input } = structure;
const existingRanges = new Set(
changes.map((change) => `${change.start}:${change.end}`)
);
for (let i = 0; i < input.length; i++) {
if (input[i][0] === TokenType.Whitespace) {
const edit = whitespaceEdit(structure, i, parents);
const range = `${edit.start}:${edit.end}`;
if (!existingRanges.has(range)) {
changes.push(edit);
existingRanges.add(range);
}
}
}
}
/** @param {Tokens} input @param {(number|undefined)[]} parents @param {number} index @return {boolean} */
function isGroupingParent(input, parents, index) {
let parent = parents[index];
while (parent !== undefined) {
if (input[parent][0] !== TokenType.OpenParen) return false;
parent = parents[parent];
}
return true;
}
/** @param {boolean} supports @param {Exclude<ReturnType<typeof cssnanoUtils.balancedTokens>, undefined>} structure @param {(number|undefined)[]} parents @param {{start:number,end:number,text:string}[]} changes @return {void} */
export function minifyAspectRatios(supports, structure, parents, changes) {
const { tokens: input } = structure;
for (let open = 0; open < input.length; open++) {
if (
input[open][0] !== TokenType.OpenParen ||
(supports
? !isGroupingParent(input, parents, open)
: parents[open] !== undefined)
)
continue;
const close = /** @type {number} */ (structure.endForOpening(open));
const left = featureValueIndex(input, open);
if (left < 0) continue;
const name = significantIndex(input, open + 1, 1);
const slash = significantIndex(input, left + 1, 1);
const right = significantIndex(input, slash + 1, 1);
const after = significantIndex(input, right + 1, 1);
if (!isAspectRatioFeature(input, name, left, slash, right, after, close))
continue;
const a = numeric(input[left]);
const b = numeric(input[right]);
if (
!a ||
!b ||
!Number.isFinite(a.number) ||
a.number < 0 ||
!Number.isFinite(b.number) ||
b.number < 0 ||
(a.number === 0 && b.number === 0)
)
continue;
const ratio = aspectRatio(input[left][1], input[right][1]);
if (!ratio) continue;
const [x, y] = ratio;
changes.push(
{ start: input[left][2], end: tokenEnd(input[left]), text: String(x) },
{ start: input[right][2], end: tokenEnd(input[right]), text: String(y) }
);
}
}