| import { |
| tokenize, |
| hasPseudoElementOrNesting, |
| hasNthChildOfClause, |
| hasUnsafeForFold, |
| specificityOfMiddle, |
| equalSpecificity, |
| joinTokens, |
| } from './foldToIsHelpers.js'; |
| |
| /** |
| * @param {import('postcss-selector-parser').Root} root |
| * @return {string | null} |
| */ |
| function tryFold(root) { |
| const selectors = getSelectors(root); |
| if (selectors.length < 2) { |
| return null; |
| } |
| |
| const tokenLists = selectors.map(tokenize); |
| |
| if (!haveNonEmptyTokenLists(tokenLists)) { |
| return null; |
| } |
| |
| const { prefix, suffix } = findSharedEdges(tokenLists); |
| |
| if (prefix === 0 && suffix === 0) { |
| return null; |
| } |
| |
| const middles = getMiddles(tokenLists, prefix, suffix); |
| if (!middles || !areCompoundMiddles(middles)) { |
| return null; |
| } |
| |
| if (!areSafeMiddles(middles) || !haveEqualSpecificity(middles)) { |
| return null; |
| } |
| |
| const middleStrs = uniqueMiddleStrings(middles); |
| if (middleStrs.length < 2) { |
| return null; |
| } |
| |
| const firstTokens = tokenLists[0]; |
| 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; |
| } |
| |
| /** |
| * @param {import('postcss-selector-parser').Root} root |
| * @return {import('postcss-selector-parser').Selector[]} |
| */ |
| function getSelectors(root) { |
| return /** @type {import('postcss-selector-parser').Selector[]} */ ( |
| root.nodes.filter((n) => n.type === 'selector') |
| ); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @return {boolean} |
| */ |
| function haveNonEmptyTokenLists(tokenLists) { |
| return !tokenLists.some((tokens) => tokens.length === 0); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @return {{prefix: number, suffix: number}} |
| */ |
| function findSharedEdges(tokenLists) { |
| const minLen = Math.min(...tokenLists.map((tokens) => tokens.length)); |
| const prefix = findPrefix(tokenLists, minLen); |
| const suffix = findSuffix(tokenLists, minLen, prefix); |
| return { |
| prefix: moveToCompoundBoundary(tokenLists[0], prefix, -1), |
| suffix: moveToCompoundBoundary(tokenLists[0], suffix, 1), |
| }; |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @param {number} minLen |
| * @return {number} |
| */ |
| function findPrefix(tokenLists, minLen) { |
| let prefix = 0; |
| while (prefix < minLen && tokensMatchAt(tokenLists, prefix)) { |
| prefix++; |
| } |
| return prefix; |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @param {number} minLen |
| * @param {number} prefix |
| * @return {number} |
| */ |
| function findSuffix(tokenLists, minLen, prefix) { |
| let suffix = 0; |
| while ( |
| suffix < minLen - prefix && |
| suffixTokensMatch(tokenLists, suffix, prefix) |
| ) { |
| suffix++; |
| } |
| return suffix; |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @param {number} index |
| * @return {boolean} |
| */ |
| function tokensMatchAt(tokenLists, index) { |
| const ref = tokenLists[0][index]; |
| return tokenLists.every( |
| (tokens) => tokens[index].kind === ref.kind && tokens[index].str === ref.str |
| ); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @param {number} suffix |
| * @param {number} prefix |
| * @return {boolean} |
| */ |
| function suffixTokensMatch(tokenLists, suffix, prefix) { |
| const refIndex = tokenLists[0].length - 1 - suffix; |
| const ref = tokenLists[0][refIndex]; |
| return tokenLists.every((tokens) => { |
| const index = tokens.length - 1 - suffix; |
| return ( |
| index >= prefix && |
| tokens[index].kind === ref.kind && |
| tokens[index].str === ref.str |
| ); |
| }); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[]} tokens |
| * @param {number} count |
| * @param {number} direction |
| * @return {number} |
| */ |
| function moveToCompoundBoundary(tokens, count, direction) { |
| let remaining = count; |
| while ( |
| remaining > 0 && |
| tokens[direction === -1 ? remaining - 1 : tokens.length - remaining] |
| .kind !== 'combinator' |
| ) { |
| remaining--; |
| } |
| return remaining; |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} tokenLists |
| * @param {number} prefix |
| * @param {number} suffix |
| * @return {import('./foldToIsHelpers.js').Token[][] | null} |
| */ |
| function getMiddles(tokenLists, prefix, suffix) { |
| const middles = tokenLists.map((tokens) => |
| tokens.slice(prefix, tokens.length - suffix) |
| ); |
| return middles.some((middle) => middle.length === 0) ? null : middles; |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} middles |
| * @return {boolean} |
| */ |
| function areCompoundMiddles(middles) { |
| // Each middle must be a single compound. Combinators inside change the |
| // matched element under `:is()`. See cssnano/cssnano#1786. |
| return !middles.some((middle) => |
| middle.some((token) => token.kind === 'combinator') |
| ); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} middles |
| * @return {boolean} |
| */ |
| function areSafeMiddles(middles) { |
| return !middles.some((middle) => |
| middle.some( |
| (token) => |
| hasPseudoElementOrNesting(token) || |
| hasNthChildOfClause(token) || |
| hasUnsafeForFold(token) |
| ) |
| ); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} middles |
| * @return {boolean} |
| */ |
| function haveEqualSpecificity(middles) { |
| const firstSpec = specificityOfMiddle(middles[0]); |
| return middles |
| .slice(1) |
| .every((middle) => |
| equalSpecificity(firstSpec, specificityOfMiddle(middle)) |
| ); |
| } |
| |
| /** |
| * @param {import('./foldToIsHelpers.js').Token[][]} middles |
| * @return {string[]} |
| */ |
| function uniqueMiddleStrings(middles) { |
| const middleStrs = []; |
| const seen = new Set(); |
| for (const middle of middles) { |
| const string = joinTokens(middle); |
| if (!seen.has(string)) { |
| seen.add(string); |
| middleStrs.push(string); |
| } |
| } |
| return middleStrs; |
| } |
| export default tryFold; |