postcss-minify-selectors
Version:
Minify selectors with PostCSS.
258 lines (227 loc) • 6.79 kB
JavaScript
import cssnanoUtils from 'cssnano-utils';
import { dropHexEscapeTerminator } from './hexEscape.js';
import { consumeTrivia, unquote } from './tokenUtils.js';
const { TokenType } = cssnanoUtils;
/** @typedef {import('./tokenUtils.js').CSSToken} CSSToken */
/** @typedef {import('./arena.js').Specificity} Specificity */
/** @param {string} kind @return {string | undefined} */
export function firstPseudoReplacement(kind) {
if (kind === 'child') return ':first-child';
if (kind === 'of-type') return ':first-of-type';
if (kind === 'last-child' || kind === 'last-of-type') return `:${kind}`;
return undefined;
}
export { parseAnPlusB } from './anPlusB.js';
/** @param {CSSToken} token */
function isPtNameStart(token) {
return (
token[0] === TokenType.Ident ||
(token[0] === TokenType.Delim && token[1] === '*')
);
}
/** @param {CSSToken} token */
function isPtClassSeparator(token) {
return token[0] === TokenType.Delim && token[1] === '.';
}
/**
* @param {readonly CSSToken[]} tokens
* @param {number} start
* @param {number} end
* @return {{ pieces?: string[], specificity?: Specificity, valid: boolean }}
*/
function normalizePtNameArgument(tokens, start, end) {
/** @type {string[]} */
const pieces = [];
let foundName = false;
let expectsClass = false;
let hasClass = false;
let isWildcard = false;
let sawTrailingWhitespace = false;
for (let i = start; i < end; i++) {
const token = tokens[i];
const type = token[0];
if (type === TokenType.Whitespace) {
if (expectsClass) return { valid: false };
sawTrailingWhitespace = sawTrailingWhitespace || foundName;
continue;
}
if (sawTrailingWhitespace && type !== TokenType.Comment) {
return { valid: false };
}
if (consumeTrivia(token, pieces)) continue;
if (!foundName && isPtNameStart(token)) {
foundName = true;
isWildcard = token[0] === TokenType.Delim && token[1] === '*';
pieces.push(dropHexEscapeTerminator(token[1]));
continue;
}
if (!expectsClass && isPtClassSeparator(token)) {
foundName = true;
expectsClass = true;
pieces.push('.');
continue;
}
if (expectsClass && type === TokenType.Ident) {
pieces.push(dropHexEscapeTerminator(token[1]));
expectsClass = false;
hasClass = true;
continue;
}
return { valid: false };
}
if (!foundName || expectsClass) {
return { valid: false };
}
return {
pieces,
valid: true,
specificity: isWildcard && !hasClass ? [0, 0, 0] : [0, 0, 1],
};
}
/**
* @param {readonly CSSToken[]} tokens
* @param {number} start
* @param {number} end
* @return {{ pieces?: string[], valid: boolean }}
*/
function normalizeIdentListArgument(tokens, start, end) {
/** @type {string[]} */
const pieces = [];
let count = 0;
for (let i = start; i < end; i++) {
const token = tokens[i];
const type = token[0];
if (consumeTrivia(token, pieces)) {
continue;
}
if (type === TokenType.Ident) {
if (count > 0) {
pieces.push(' ');
}
pieces.push(dropHexEscapeTerminator(token[1]));
count++;
continue;
}
return { valid: false };
}
if (count === 0) {
return { valid: false };
}
return { pieces, valid: true };
}
/**
* @param {readonly CSSToken[]} tokens
* @param {number} start
* @param {number} end
* @return {{ pieces?: string[], valid: boolean }}
*/
function normalizeIdentArgument(tokens, start, end) {
/** @type {string[]} */
const pieces = [];
let foundIdent = false;
for (let i = start; i < end; i++) {
const token = tokens[i];
const type = token[0];
if (consumeTrivia(token, pieces)) {
continue;
}
if (type === TokenType.Ident && !foundIdent) {
foundIdent = true;
pieces.push(dropHexEscapeTerminator(token[1]));
continue;
}
return { valid: false };
}
if (!foundIdent) {
return { valid: false };
}
return { pieces, valid: true };
}
/** @param {CSSToken} token */
function isLanguageValue(token) {
return token[0] === TokenType.Ident || token[0] === TokenType.String;
}
/**
* @param {{ value: string, trivia: string[], trailingTrivia: string[] }[]} items
* @param {Set<string>} seen
* @param {string} value
* @param {string[]} trivia
*/
function recordLanguageItem(items, seen, value, trivia) {
if (!seen.has(value)) {
seen.add(value);
items.push({ value, trivia, trailingTrivia: [] });
return;
}
const previousItem = items.at(-1);
if (trivia.length > 0 && previousItem) {
previousItem.trailingTrivia.push(...trivia);
}
}
/**
* @param {readonly CSSToken[]} tokens
* @param {number} start
* @param {number} end
* @return {{ pieces?: string[], valid: boolean }}
*/
function normalizeIdentOrStringList(tokens, start, end) {
/** @type {{ value: string, trivia: string[], trailingTrivia: string[] }[]} */
const items = [];
/** @type {string[]} */
let currentTrivia = [];
/** @type {Set<string>} */
const seen = new Set();
let expectItem = true;
let hasTokenInItem = false;
for (let i = start; i < end; i++) {
const token = tokens[i];
const type = token[0];
if (consumeTrivia(token, currentTrivia)) {
continue;
}
if (type === TokenType.Comma) {
if (expectItem || !hasTokenInItem) {
return { valid: false };
}
expectItem = true;
hasTokenInItem = false;
continue;
}
if (expectItem && isLanguageValue(token)) {
const val =
type === TokenType.String
? unquote(token[1])
: dropHexEscapeTerminator(token[1]);
recordLanguageItem(items, seen, val, currentTrivia);
currentTrivia = [];
expectItem = false;
hasTokenInItem = true;
continue;
}
return { valid: false };
}
if (expectItem || items.length === 0) {
return { valid: false };
}
/** @type {string[]} */
const pieces = [];
for (let i = 0; i < items.length; i++) {
if (i > 0) pieces.push(',');
pieces.push(...items[i].trivia, items[i].value, ...items[i].trailingTrivia);
}
if (currentTrivia.length > 0) {
pieces.push(...currentTrivia);
}
return { pieces, valid: true };
}
/**
* Parsers of the pseudo-class and pseudo-element arguments that are
* normalized as token pieces, keyed by the grammar name in `selectorGrammar`.
* @type {ReadonlyMap<string, (tokens: readonly CSSToken[], start: number, end: number) => { pieces?: string[], specificity?: Specificity, valid: boolean }>}
*/
export const pieceArgumentParsers = new Map([
['ident', normalizeIdentArgument],
['ident-list', normalizeIdentListArgument],
['ident-or-string-list', normalizeIdentOrStringList],
['pt-name-selector', normalizePtNameArgument],
]);