UNPKG

postcss-minify-selectors

Version:

Minify selectors with PostCSS.

348 lines (323 loc) • 11.9 kB
import cssnanoUtils from 'cssnano-utils'; import { dropHexEscapeTerminator, hexEscapeDigitCount, needsTerminatorAfterDigits, } from './hexEscape.js'; import { serializeNormalized } from './serializeArena.js'; import { isImportantCommentToken, skipTrivia, unquote } from './tokenUtils.js'; const { TokenType, tokenStart } = cssnanoUtils; /** @typedef {import('./arena.js').SelectorArena} SelectorArena */ /** @typedef {import('./arena.js').ArenaNode} ArenaNode */ /** @typedef {import('./arena.js').Specificity} Specificity */ /** @typedef {import('./outputOverlay.js').Emit} Emit */ /** @typedef {{emit?:Emit,id:number,length:number,text?:string,sourceNode?:number,changed?:boolean,hexDigits?:number,head?:number}} Output */ export class OutputPool { /** @param {SelectorArena} arena */ constructor(arena) { this.arena = arena; /** @type {Map<string,Output>} */ this.texts = new Map(); /** @type {Map<string,Map<number,Map<number,number>>>} */ this.identities = new Map(); /** @type {Map<string,number>} */ this.payloads = new Map(); /** @type {Map<number,Map<number,number>>} */ this.pairs = new Map(); this.nextId = 1; this.empty = this.text(''); } /** @param {string} value @return {Output} */ text(value) { const found = this.texts.get(value); if (found) return found; const output = { emit: /** @type {const} */ ({ kind: 'text', value }), id: this.nextId++, length: value.length, text: value, hexDigits: hexEscapeDigitCount(value, value.length), head: value.charCodeAt(0), changed: true, }; this.texts.set(value, output); return output; } /** @param {string} value */ payload(value) { let id = this.payloads.get(value); if (id === undefined) { id = this.nextId++; this.payloads.set(value, id); } return id; } /** * The structural ID is interned from normalized local text and ordered child * IDs as each output sequence is assembled. It therefore carries significant * raw bytes without retaining arena positions. * @param {string} kind * @param {number} payload * @param {number} structure */ identity(kind, payload, structure) { let byPayload = this.identities.get(kind); if (!byPayload) { byPayload = new Map(); this.identities.set(kind, byPayload); } let structures = byPayload.get(payload); if (!structures) { structures = new Map(); byPayload.set(payload, structures); } let id = structures.get(structure); if (id === undefined) { id = this.nextId++; structures.set(structure, id); } return id; } /** @param {number} left @param {number} right */ pairId(left, right) { let rights = this.pairs.get(left); if (!rights) { rights = new Map(); this.pairs.set(left, rights); } let id = rights.get(right); if (id === undefined) { id = this.nextId++; rights.set(right, id); } return id; } /** @param {readonly number[]} values */ sequenceId(values) { if (values.length === 0) return this.empty.id; if (values.length === 1) return values[0]; let id = 0; for (const value of values) id = this.pairId(id, value); return id; } /** @param {Output} output @return {Emit} */ emit(output) { if (output.emit) return output.emit; if (output.sourceNode !== undefined) return { kind: 'node', node: output.sourceNode }; throw new Error('normalized output has no emission'); } /** @param {readonly Output[]} values @return {Output} */ sequence(values) { const items = values.filter(({ length }) => length !== 0); if (items.length === 0) return this.empty; if (items.length === 1) return items[0]; let id = 0; let length = 0; let digits = 0; for (const item of items) { id = this.pairId(id, item.id); length += item.length; // serializeNormalized inserts a terminator space between these pieces. if (needsTerminatorAfterDigits(digits, this.headCode(item))) length++; digits = this.trailingHexDigits(item); } const emit = { kind: /** @type {const} */ ('sequence'), items: items.map((item) => this.emit(item)), }; return { emit, id, length, changed: true, hexDigits: digits, head: this.headCode(items[0]), }; } /** @param {Output} output */ headCode(output) { return output.sourceNode === undefined ? /** @type {number} */ (output.head) : this.arena.source.charCodeAt( offset(this.arena, this.arena.nodes[output.sourceNode].startToken) ); } /** @param {Output} output */ trailingHexDigits(output) { return output.sourceNode === undefined ? /** @type {number} */ (output.hexDigits) : hexEscapeDigitCount( this.arena.source, offset(this.arena, this.arena.nodes[output.sourceNode].endToken) ); } } /** @param {SelectorArena} arena @param {number} tokenIndex */ export function offset(arena, tokenIndex) { return tokenIndex === arena.tokens.length ? arena.source.length : tokenStart(arena.tokens[tokenIndex]); } /** @param {SelectorArena} arena @param {number} start @param {number} end */ export function sourceText(arena, start, end) { return arena.source.slice(offset(arena, start), offset(arena, end)); } /** @param {OutputPool} pool @param {Output} output */ export function outputText(pool, output) { if (output.text === undefined) { if (output.sourceNode !== undefined) { const node = pool.arena.nodes[output.sourceNode]; output.text = sourceText(pool.arena, node.startToken, node.endToken); } else output.text = serializeNormalized(pool.arena, pool.emit(output)); } return output.text; } /** @param {OutputPool} pool @param {Output} left @param {Output} right */ export function compareOutputs(pool, left, right) { const a = outputText(pool, left); const b = outputText(pool, right); if (a < b) return -1; if (a > b) return 1; return 0; } /** * Source spelling of a class (`.` and ident tokens) or ID (hash token). * @param {SelectorArena} arena @param {ArenaNode} node */ export function classOrIdSource(arena, node) { const { tokens } = arena; return node.kind === 'class' ? `${tokens[node.startToken][1]}${tokens[node.startToken + 1][1]}` : tokens[node.startToken][1]; } /** * Serialized class or ID: an ident's hex escape terminator is restored by * the joiners, so it is dropped here. * @param {SelectorArena} arena @param {ArenaNode} node */ export function classOrIdText(arena, node) { const { tokens } = arena; return node.kind === 'class' ? `${tokens[node.startToken][1]}${dropHexEscapeTerminator(tokens[node.startToken + 1][1])}` : dropHexEscapeTerminator(tokens[node.startToken][1]); } /** @param {SelectorArena} arena @param {OutputPool} pool @param {number} start @param {number} end */ export function importantTrivia(arena, pool, start, end) { /** @type {Output[]} */ const output = []; for (let index = start; index < end; index++) { const token = arena.tokens[index]; if (isImportantCommentToken(token)) output.push(pool.text(token[1])); } return pool.sequence(output); } /** @param {SelectorArena} arena @param {OutputPool} pool @param {ArenaNode} node */ export function descendantCombinator(arena, pool, node) { /** @type {Output[]} */ const output = []; let lastWasSpace = false; for (let index = node.startToken; index < node.endToken; index++) { const token = arena.tokens[index]; if (token[0] === TokenType.Whitespace) { if (!lastWasSpace) output.push(pool.text(' ')); lastWasSpace = true; } else if (isImportantCommentToken(token)) { output.push(pool.text(token[1])); lastWasSpace = false; } } if (!lastWasSpace) output.push(pool.text(' ')); return pool.sequence(output); } /** @param {SelectorArena} arena @param {number} nodeIndex */ export function childrenOf(arena, nodeIndex) { /** @type {number[]} */ const children = []; arena.forEachChild(nodeIndex, (child) => children.push(child)); return children; } /** @template {Output} T @param {(T | undefined)[]} normalized @param {number} nodeIndex @return {T} */ export function normalizedAt(normalized, nodeIndex) { const output = normalized[nodeIndex]; if (!output) throw new Error('child node was released before its parent'); return output; } /** @param {SelectorArena} arena @param {OutputPool} pool @param {ArenaNode} node */ export function rawOutput(arena, pool, node) { return pool.text(sourceText(arena, node.startToken, node.endToken)); } /** @param {SelectorArena} arena @param {OutputPool} pool @param {ArenaNode} node @param {boolean} removable */ export function qualifiedNameOutput(arena, pool, node, removable) { const payload = arena.payloads.qualifiedNames[node.payload]; if ( removable && payload.namespace.kind === 'absent' && payload.subject.kind === 'universal' ) return pool.empty; if ( payload.namespace.kind === 'absent' && node.endToken > payload.subject.token + 1 ) return pool.text(sourceText(arena, node.startToken, node.endToken)); let prefix = ''; if (payload.namespace.kind === 'empty') prefix = '|'; else if (payload.namespace.kind === 'wildcard') prefix = '*|'; else if (payload.namespace.kind === 'named') prefix = `${dropHexEscapeTerminator( arena.tokens[payload.namespace.token][1] )}|`; return pool.text( `${prefix}${dropHexEscapeTerminator(arena.tokens[payload.subject.token][1])}` ); } /** @param {SelectorArena} arena @param {OutputPool} pool @param {ArenaNode} node */ export function attributeOutput(arena, pool, node) { if (node.status !== 'valid') return rawOutput(arena, pool, node); const payload = arena.payloads.attributes[node.payload]; const close = node.endToken - 1; let nameStart = payload.nameToken; let name = dropHexEscapeTerminator(arena.tokens[payload.nameToken][1]); if (payload.namespace.kind === 'empty') { nameStart--; name = `|${name}`; } else if (payload.namespace.kind === 'wildcard') { nameStart -= 2; name = `*|${name}`; } else if (payload.namespace.kind === 'named') { nameStart = payload.namespace.token; name = `${dropHexEscapeTerminator( arena.tokens[payload.namespace.token][1] )}|${name}`; } /** @type {Output[]} */ const values = [pool.text('[')]; values.push(importantTrivia(arena, pool, node.startToken + 1, nameStart)); values.push(pool.text(name)); let cursor = payload.nameToken + 1; if (!payload.matcher) { values.push(importantTrivia(arena, pool, cursor, close)); values.push(pool.text(']')); return pool.sequence(values); } const matcherStart = skipTrivia(arena.tokens, cursor, close); const matcherEnd = matcherStart + payload.matcher.length; values.push(importantTrivia(arena, pool, cursor, matcherStart)); values.push(pool.text(payload.matcher)); if (payload.valueToken === undefined) return rawOutput(arena, pool, node); const valueToken = arena.tokens[payload.valueToken]; let value = valueToken[1]; if (valueToken[0] === TokenType.String) value = unquote(value).replaceAll('\\\n', ''); else value = dropHexEscapeTerminator(value); values.push( importantTrivia(arena, pool, matcherEnd, payload.valueToken), pool.text(value) ); cursor = payload.valueToken + 1; if (payload.modifierToken !== undefined) { values.push( importantTrivia(arena, pool, cursor, payload.modifierToken), pool.text( ` ${dropHexEscapeTerminator(arena.tokens[payload.modifierToken][1])}` ) ); cursor = payload.modifierToken + 1; } values.push(importantTrivia(arena, pool, cursor, close), pool.text(']')); return pool.sequence(values); }