postcss-minify-selectors
Version:
Minify selectors with PostCSS.
128 lines (110 loc) • 3.12 kB
JavaScript
;
const {
tokenize,
hasPseudoElementOrNesting,
hasNthChildOfClause,
hasUnsafeForFold,
specificityOfMiddle,
equalSpecificity,
joinTokens,
} = require('./foldToIsHelpers.js');
/**
* @param {import('postcss-selector-parser').Root} root
* @return {string | null}
*/
function tryFold(root) {
const selectors = /** @type {import('postcss-selector-parser').Selector[]} */ (
root.nodes.filter((n) => n.type === 'selector')
);
if (selectors.length < 2) {
return null;
}
const tokenLists = selectors.map(tokenize);
if (tokenLists.some((t) => t.length === 0)) {
return null;
}
let prefix = 0;
const minLen = Math.min(...tokenLists.map((t) => t.length));
while (prefix < minLen) {
const ref = tokenLists[0][prefix];
const allMatch = tokenLists.every(
(t) => t[prefix].kind === ref.kind && t[prefix].str === ref.str
);
if (!allMatch) {
break;
}
prefix++;
}
let suffix = 0;
while (suffix < minLen - prefix) {
const refIdx = tokenLists[0].length - 1 - suffix;
const ref = tokenLists[0][refIdx];
const allMatch = tokenLists.every((t) => {
const idx = t.length - 1 - suffix;
return idx >= prefix && t[idx].kind === ref.kind && t[idx].str === ref.str;
});
if (!allMatch) {
break;
}
suffix++;
}
const firstTokens = tokenLists[0];
while (prefix > 0 && firstTokens[prefix - 1].kind !== 'combinator') {
prefix--;
}
while (suffix > 0 && firstTokens[firstTokens.length - suffix].kind !== 'combinator') {
suffix--;
}
if (prefix === 0 && suffix === 0) {
return null;
}
const middles = tokenLists.map((t) => t.slice(prefix, t.length - suffix));
if (middles.some((m) => m.length === 0)) {
return null;
}
// Each middle must be a single compound. Combinators inside change the
// matched element under `:is()`. See cssnano/cssnano#1786.
if (middles.some((m) => m.some((t) => t.kind === 'combinator'))) {
return null;
}
for (const middle of middles) {
for (const token of middle) {
if (hasPseudoElementOrNesting(token)) {
return null;
}
if (hasNthChildOfClause(token)) {
return null;
}
if (hasUnsafeForFold(token)) {
return null;
}
}
}
const firstSpec = specificityOfMiddle(middles[0]);
for (let i = 1; i < middles.length; i++) {
if (!equalSpecificity(firstSpec, specificityOfMiddle(middles[i]))) {
return null;
}
}
const middleStrs = [];
const seen = new Set();
for (const m of middles) {
const s = joinTokens(m);
if (!seen.has(s)) {
seen.add(s);
middleStrs.push(s);
}
}
if (middleStrs.length < 2) {
return null;
}
const prefixStr = joinTokens(firstTokens.slice(0, prefix));
const suffixStr = joinTokens(firstTokens.slice(firstTokens.length - suffix));
const folded = `${prefixStr}:is(${middleStrs.join(',')})${suffixStr}`;
const original = selectors.map((s) => String(s)).join(',');
if (folded.length >= original.length) {
return null;
}
return folded;
}
module.exports = tryFold;