cssnano
Version:
A modular CSS minifier, built on top of the PostCSS ecosystem.
189 lines (177 loc) • 5.53 kB
JavaScript
import cssnanoUtils from '#utils';
const { TokenType } = cssnanoUtils;
const decoded = /** @type {typeof cssnanoUtils.decoded} */ (
cssnanoUtils.decoded
);
const tokenEnd = /** @type {typeof cssnanoUtils.tokenEnd} */ (
cssnanoUtils.tokenEnd
);
/** @typedef {Exclude<ReturnType<typeof cssnanoUtils.balancedTokens>, undefined>['tokens']} Tokens */
export { TokenType, decoded, tokenEnd };
export const openingTypes = new Set([
TokenType.Function,
TokenType.OpenParen,
TokenType.OpenSquare,
TokenType.OpenCurly,
]);
export const closingTypes = new Set([
TokenType.CloseParen,
TokenType.CloseSquare,
TokenType.CloseCurly,
]);
export const calcProductEndTypes = new Set([
TokenType.Number,
TokenType.Percentage,
TokenType.Dimension,
TokenType.Ident,
TokenType.CloseParen,
]);
export const calcProductStartTypes = new Set([
TokenType.Number,
TokenType.Percentage,
TokenType.Dimension,
TokenType.Ident,
TokenType.Function,
TokenType.OpenParen,
]);
export const mathFunctions = cssnanoUtils.mathFunctions;
export const whitespaceInsensitiveFunctions = new Set(['selector']);
export const aspectRatioFeatures = new Set([
'aspect-ratio',
'min-aspect-ratio',
'max-aspect-ratio',
'device-aspect-ratio',
'min-device-aspect-ratio',
'max-device-aspect-ratio',
]);
/**
* Convert a CSS number spelling to an exact rational number.
*
* @param {string} source
* @return {[bigint, bigint] | undefined}
*/
export function rational(source) {
const match = source.match(
/^[+]?((?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[eE]([+\-]?\d+))?$/v
);
if (!match) return undefined;
const [, coefficient, exponent = '0'] = match;
const [whole, fraction = ''] = coefficient.split('.');
const digits = `${whole}${fraction}`;
let numerator = BigInt(digits);
let denominator = 10n ** BigInt(fraction.length);
if (numerator === 0n) return [0n, 1n];
const power = BigInt(exponent);
if (power > 10000n || power < -10000n) return undefined;
if (power >= 0n) numerator *= 10n ** power;
else denominator *= 10n ** -power;
return [numerator, denominator];
}
/** @param {bigint} a @param {bigint} b @return {bigint} */
function gcd(a, b) {
return b ? gcd(b, a % b) : a;
}
/**
* @param {string} left
* @param {string} right
* @return {[string, string] | undefined}
*/
export function aspectRatio(left, right) {
const a = rational(left);
const b = rational(right);
if (!a || !b) return undefined;
const numerator = a[0] * b[1];
const denominator = b[0] * a[1];
if (numerator === 0n && denominator === 0n) return undefined;
const divisor = gcd(numerator, denominator);
const result = [String(numerator / divisor), String(denominator / divisor)];
return result.join('/').length < left.length + right.length + 1
? /** @type {[string, string]} */ (result)
: undefined;
}
/**
* Whether the token at `index` is whitespace or a comment, which a query may
* place between any two significant tokens.
*
* @param {Tokens} input
* @param {number} index
* @return {boolean}
*/
export function isWhitespaceOrComment(input, index) {
const type = input[index]?.[0];
return type === TokenType.Whitespace || type === TokenType.Comment;
}
/** @param {Tokens} input @param {number} index @param {number} step @return {number} */
export function significantIndex(input, index, step) {
let cursor = index;
while (isWhitespaceOrComment(input, cursor)) cursor += step;
return cursor;
}
/**
* Where the value of a `(name: value)` feature starts, for the `(` at `open`;
* the name is the first significant token after it. Returns -1 unless an
* identifier and a colon open the feature.
*
* @param {Tokens} input
* @param {number} open
* @return {number} the index of the first significant token after the colon
*/
export function featureValueIndex(input, open) {
const name = significantIndex(input, open + 1, 1);
if (input[name]?.[0] !== TokenType.Ident) return -1;
const colon = significantIndex(input, name + 1, 1);
if (input[colon]?.[0] !== TokenType.Colon) return -1;
return significantIndex(input, colon + 1, 1);
}
/**
* @param {string} source
* @param {{start:number,end:number}[]} segments
* @param {{start:number,end:number,text:string}[]} edits
* @return {string[]}
*/
export function serializeSegments(source, segments, edits) {
let segmentIndex = 0;
let previousEnd = -1;
let previousEdit;
for (const edit of edits) {
if (
previousEdit &&
edit.start === previousEdit.start &&
edit.end === previousEdit.end &&
edit.text === previousEdit.text
) {
continue;
}
while (
segmentIndex < segments.length &&
edit.start >= segments[segmentIndex].end
) {
segmentIndex++;
}
const segment = segments[segmentIndex];
if (
!segment ||
edit.start < segment.start ||
edit.start < previousEnd ||
edit.end < edit.start ||
edit.end > segment.end
) {
return segments.map(({ start, end }) => source.slice(start, end));
}
previousEnd = edit.end;
previousEdit = edit;
}
let editIndex = 0;
return segments.map(({ start, end }) => {
let cursor = start;
let result = '';
while (editIndex < edits.length && edits[editIndex].start < end) {
const edit = edits[editIndex++];
if (edit.start < start || edit.start < cursor) continue;
result += source.slice(cursor, edit.start) + edit.text;
cursor = edit.end;
}
result += source.slice(cursor, end);
return result;
});
}