UNPKG

postcss-minify-selectors

Version:

Minify selectors with PostCSS.

255 lines (243 loc) • 8.06 kB
import cssnanoUtils from 'cssnano-utils'; import { isIdentifierContinuationToken, mergeStatus, } from './parseArenaStructure.js'; import { addAttribute, qualifiedNameAt } from './parseArenaAttribute.js'; import { openPseudo, pseudoDetails, pseudoEndAt } from './parseArenaPseudo.js'; const { TokenType } = cssnanoUtils; /** @typedef {import('./arena.js').ListMode} ListMode */ /** @typedef {import('./arena.js').ParseStatus} ParseStatus */ /** @typedef {{work: ParseWork, end: number, status?: ParseStatus, hasQualifiedName?: boolean}} CompoundChild */ /** @typedef {NonNullable<ReturnType<typeof cssnanoUtils.balancedTokens>>} Structure */ /** @typedef {import('./parseArenaCore.js').ParseWork} ParseWork */ /** @typedef {import('./parseArenaCore.js').RawWork} RawWork */ /** @typedef {Parameters<Parameters<typeof import('./arena.js').buildSelectorArena>[2]>[0]} Builder */ /** @param {readonly import('./tokenUtils.js').CSSToken[]} input @param {number} index */ function opaqueNumericClass(input, index) { const token = input[index]; const nextType = input[index + 1]?.[0]; if ( token[0] === TokenType.Delim && token[1] === '.' && (nextType === TokenType.Number || nextType === TokenType.Dimension) ) return index + 2; if (token[0] === TokenType.Dimension && token[1].startsWith('.')) return index + 1; } /** @param {number} start @param {number} end @param {ParseStatus} status @return {CompoundChild} */ function rawChild(start, end, status) { return { work: { kind: 'raw', start, end, status }, end, status }; } /** * A hash that is not a valid <ident-token> (`#1a`) is no ID selector. * * @param {import('./tokenUtils.js').CSSToken} token * @param {number} index * @return {CompoundChild} */ function hashChildAt(token, index) { if (/** @type {{type?: string}} */ (token[4])?.type === 'id') return { work: /** @type {ParseWork} */ ({ kind: 'id', start: index, end: index + 1, }), end: index + 1, }; return rawChild(index, index + 1, 'invalid'); } /** @param {Structure} structure @param {number} index @param {number} end @param {boolean} hasQualifiedName @return {CompoundChild} */ function compoundChildAt( structure, index, end, hasQualifiedName, keyframe = false ) { const input = structure.tokens; const token = input[index]; if (token[0] === TokenType.OpenSquare) return { work: /** @type {ParseWork} */ ({ kind: 'attribute', start: index }), end: (structure.endForOpening(index) ?? index) + 1, }; if (token[0] === TokenType.Colon) { const pseudoEnd = pseudoEndAt(structure, index, end); return { work: /** @type {ParseWork} */ ({ kind: 'pseudo', start: index, end: pseudoEnd, }), end: pseudoEnd, }; } if ( token[0] === TokenType.Delim && token[1] === '.' && input[index + 1]?.[0] === TokenType.Ident ) return { work: /** @type {ParseWork} */ ({ kind: 'class', start: index, end: index + 2, }), end: index + 2, }; const opaqueClassEnd = opaqueNumericClass(input, index); if (opaqueClassEnd !== undefined) return rawChild(index, opaqueClassEnd, 'opaque'); if (token[0] === TokenType.Hash) return hashChildAt(token, index); if (keyframe && token[0] === TokenType.Percentage) return rawChild(index, index + 1, 'valid'); if (token[0] === TokenType.Delim && token[1] === '&') return { work: /** @type {ParseWork} */ ({ kind: 'nesting', start: index, end: index + 1, }), end: index + 1, status: /** @type {ParseStatus} */ ('opaque'), }; const qualified = qualifiedNameAt(input, index, end); if (qualified?.end && !hasQualifiedName) return { work: /** @type {ParseWork} */ ({ kind: 'qualified-name', start: index, end: qualified.end, payload: qualified.payload, }), end: qualified.end, hasQualifiedName: true, }; return rawChild(index, index + 1, 'invalid'); } /** @param {string} kind */ const isSubclassAfterPseudo = (kind) => kind === 'class' || kind === 'id' || kind === 'attribute' || kind === 'qualified-name'; /** * @param {readonly import('./tokenUtils.js').CSSToken[]} input * @param {number} index * @param {number} end * @return {RawWork} */ function readRawContinuation(input, index, end) { let rawEnd = index + 1; while (rawEnd < end && isIdentifierContinuationToken(input[rawEnd])) rawEnd++; return { kind: 'raw', start: index, end: rawEnd, status: 'opaque', }; } /** * @param {{ work: ParseWork, end: number, status?: ParseStatus, hasQualifiedName?: boolean }} child * @param {Structure} structure * @param {number} end */ function extendQualifiedNameType(child, structure, end) { if ( child.work.kind === 'qualified-name' && child.work.payload.namespace.kind === 'absent' && child.work.payload.subject.kind === 'type' ) { while ( child.end < end && isIdentifierContinuationToken(structure.tokens[child.end]) ) child.end++; child.work.end = child.end; } } /** @param {Structure} structure @param {number} start @param {number} end @param {ListMode} mode @param {boolean} insideHas */ export function compoundChildren( structure, start, end, mode, insideHas, keyframe = false ) { const input = structure.tokens; /** @type {ParseWork[]} */ const children = []; let status = /** @type {ParseStatus} */ ('valid'); let index = start; let hasQualifiedName = false; let sawPseudoElement = false; while (index < end) { if ( (hasQualifiedName || index !== start) && input[index]?.[0] === TokenType.Ident && isIdentifierContinuationToken(input[index + 1]) ) { const rawWork = readRawContinuation(input, index, end); children.push(rawWork); status = mergeStatus(status, 'opaque'); index = rawWork.end; continue; } // A comment is not a combinator, so it neither splits nor invalidates the // compound; its bytes are kept as written. if (input[index]?.[0] === TokenType.Comment) { children.push({ kind: 'raw', start: index, end: index + 1, status: 'valid', }); index++; continue; } const child = compoundChildAt( structure, index, end, hasQualifiedName || index !== start, keyframe ); if (sawPseudoElement && isSubclassAfterPseudo(child.work.kind)) { status = 'invalid'; child.status = 'invalid'; } if (child.work.kind === 'pseudo') { const details = pseudoDetails(structure, child.work.start); if (details.isElement) sawPseudoElement = true; child.work.mode = mode; child.work.insideHas = insideHas; } extendQualifiedNameType(child, structure, end); children.push(child.work); status = mergeStatus(status, child.status ?? 'valid'); hasQualifiedName ||= child.hasQualifiedName ?? false; index = child.end; } return { status, children }; } /** @param {Builder} builder @param {ParseWork} item @param {Structure} structure @param {unknown[]} work */ export function addLeafWork(builder, item, structure, work) { if (item.kind === 'attribute') addAttribute(builder, structure, item.start); else if (item.kind === 'pseudo') openPseudo(builder, structure, item, work); else if (item.kind === 'class') builder.leaf('class', item.start, item.end); else if (item.kind === 'id') builder.leaf('id', item.start, item.end); else if (item.kind === 'nesting') builder.leaf('nesting', item.start, item.end, { status: 'opaque' }); else if (item.kind === 'qualified-name') builder.leaf('qualified-name', item.start, item.end, { payload: builder.payload('qualifiedNames', item.payload), }); else if (item.kind === 'raw') builder.leaf('raw', item.start, item.end, { status: item.status, }); } /** @param {string} source @param {ParseContext} [context] */