UNPKG

postcss-merge-longhand

Version:

Merge longhand properties into shorthand with PostCSS.

353 lines (339 loc) • 10.6 kB
import cssnanoUtils from 'cssnano-utils'; import minifyTrbl from '../minifyTrbl.js'; import minifyWidthStyleColor from '../minifyWsc.js'; import { toLower } from '../parseWsc.js'; import { mergeBlockingSupport, needsUnmetSupport } from '../isFallback.js'; import { allBorderShorthands, cellProperties, componentCells, sideCells, sides, widthStyleColor as components, } from './borderData.js'; import { declCost } from './slotVector.js'; const { asciiLowerCase } = cssnanoUtils; /** @import {Declaration} from 'postcss'; */ /* Builds the reset shorthands, component shorthands, side shorthands and * leaves a border cover can choose from, together with the availability and * support analysis those shapes depend on. Selection and application live in * borderCandidates.js. */ /** @param {{prop: string, value: string}[]} decls @param {boolean} [important] */ function declSize(decls, important) { let sum = 0; for (const d of decls) sum += declCost(d.prop, d.value, important); return sum; } /** @param {string} prop @param {string} value */ const decl = (prop, value) => ({ prop, value }); /** @param {number} s @param {number} c @param {string} value */ const leaf = (s, c, value) => ({ prop: cellProperties[s * 3 + c], value }); /** @param {number} s @param {string} value */ const side = (s, value) => ({ prop: `border-${sides[s]}`, value }); /** @param {number} c @param {string} value */ const comp = (c, value) => ({ prop: `border-${components[c]}`, value }); /* Priority classes for a candidate's structure, cheapest first. On equal byte * cost a reset shorthand wins over component shorthands (`border-width`…), * those over side shorthands (`border-top`…), and leaves come last. */ const RESET_CANDIDATE = 1; const COMPONENT_SHORTHAND_CANDIDATE = 2; const SIDE_SHORTHAND_CANDIDATE = 3; const LEAF_CANDIDATE = 4; /** @param {string[]} cells @param {boolean} lane @param {{decls: {prop: string, value: string}[], rank: number}[]} rawCandidates */ function addResetCandidates(cells, lane, rawCandidates) { /** @type {string[][]} */ const sideTriples = []; for (let s = 0; s < 4; s++) sideTriples.push(cells.slice(s * 3, s * 3 + 3)); const seen = new Set(), uniqueTriples = []; for (const t of sideTriples) { const key = t.join('|'); if (!seen.has(key)) { seen.add(key); uniqueTriples.push(t); } } for (const base of uniqueTriples) { const borderDecl = decl('border', minifyWidthStyleColor(base.join(' '))); const corrections = []; for (let s = 0; s < 4; s++) { const t = sideTriples[s]; if (t[0] === base[0] && t[1] === base[1] && t[2] === base[2]) continue; const asSide = [side(s, minifyWidthStyleColor(t.join(' ')))]; const asLeaves = []; for (let c = 0; c < 3; c++) { if (t[c] !== base[c]) { const val = asciiLowerCase(t[c]) === 'currentcolor' ? 'currentcolor' : t[c]; asLeaves.push(leaf(s, c, val)); } } if ( asLeaves.length === 1 && asLeaves[0].prop.endsWith('-color') && asciiLowerCase(asLeaves[0].value) === 'currentcolor' ) { corrections.push(...asLeaves); } else { const isSideSmaller = declSize(asSide, lane) < declSize(asLeaves, lane); corrections.push(...(isSideSmaller ? asSide : asLeaves)); } } rawCandidates.push({ decls: [borderDecl, ...corrections], rank: RESET_CANDIDATE, }); } for (const c of [2, 1, 0]) { const [o1, o2] = [0, 1, 2].filter((x) => x !== c); const t0 = sideTriples[0]; if (sideTriples.every((t) => t[o1] === t0[o1] && t[o2] === t0[o2])) { const bTriple = ['', '', '']; bTriple[o1] = t0[o1] || ''; bTriple[o2] = t0[o2] || ''; const bVal = minifyWidthStyleColor(bTriple.join(' ')); const rawComp = `${cells[c]} ${cells[3 + c]} ${cells[6 + c]} ${cells[9 + c]}`; rawCandidates.push({ decls: [decl('border', bVal), comp(c, minifyTrbl(toLower(rawComp)))], rank: RESET_CANDIDATE, }); } } } /** * A shorthand fills several cells, so a declaration appears once per cell it * set; collecting into a Set makes each one count once. * * @param {Set<Declaration>[]} cellHistory * @param {Iterable<number>} cellIndexes * @return {Set<Declaration>} */ function declarationsOfCells(cellHistory, cellIndexes) { /** @type {Set<Declaration>} */ const decls = new Set(); for (const index of cellIndexes) { for (const d of cellHistory[index]) decls.add(d); } return decls; } /** * @param {Set<Declaration>[]} cellHistory * @param {Set<Declaration>} fallbacks * @param {number} s * @return {boolean} */ function isSynthesizableSide(cellHistory, fallbacks, s) { const decls = declarationsOfCells(cellHistory, sideCells[s]); if (decls.size === 1) { const [only] = decls; const p = asciiLowerCase(only.prop); if (p === `border-${sides[s]}` || p === 'border') return true; } for (const d of decls) { if (fallbacks.has(d)) return false; } return true; } /** * @param {Set<Declaration>} decls * @return {boolean} */ function hasConsistentSupport(decls) { let s0; for (const d of decls) { const support = mergeBlockingSupport(d); s0 ??= support; if (support !== s0 && s0.symmetricDifference(support).size !== 0) { return false; } } return true; } /** * @param {Set<Declaration>[]} cellHistory * @param {Set<number>} touched * @param {Set<number>} barrierCells * @param {Set<Declaration>} fallbacks * @return {number[]} */ function getAvailableSides(cellHistory, touched, barrierCells, fallbacks) { return [0, 1, 2, 3] .filter( (s) => sideCells[s].every((i) => touched.has(i) && !barrierCells.has(i)) && isSynthesizableSide(cellHistory, fallbacks, s) ) .filter((s) => hasConsistentSupport(declarationsOfCells(cellHistory, sideCells[s])) ); } /** * @param {Set<Declaration>[]} cellHistory * @param {Set<number>} touched * @param {Set<number>} barrierCells * @return {number[]} */ function getAvailableComponents(cellHistory, touched, barrierCells) { return [2, 1, 0] .filter((c) => componentCells[c].every((i) => touched.has(i) && !barrierCells.has(i)) ) .filter((c) => hasConsistentSupport(declarationsOfCells(cellHistory, componentCells[c])) ); } /** * Gives every touched cell that is neither blocked nor covered its own leaf, * and marks it covered. * * @param {string[]} cells * @param {Set<number>} touched * @param {Set<number>} blockedCells * @param {Set<number>} coveredCells * @return {{ prop: string, value: string }[]} */ function leavesOfFreeCells(cells, touched, blockedCells, coveredCells) { /** @type {{ prop: string, value: string }[]} */ const leafDecls = []; for (let i = 0; i < 12; i++) { if (touched.has(i) && !blockedCells.has(i) && !coveredCells.has(i)) { coveredCells.add(i); leafDecls.push(leaf(Math.floor(i / 3), i % 3, cells[i])); } } return leafDecls; } /** * @param {number[][]} groups * @param {string[]} cells * @param {Set<number>} touched * @param {Set<number>} blockedCells * @param {boolean} componentGroups * @return {{ decls: { prop: string, value: string }[], rank: number, mask: number, coveredCells: Set<number> }} */ function createGroupCandidate( groups, cells, touched, blockedCells, componentGroups ) { /** @type {Set<number>} */ const coveredCells = new Set(); const sortedGroups = [...groups].toSorted((a, b) => componentGroups ? (b[0] % 3) - (a[0] % 3) : a[0] - b[0] ); const groupDecls = sortedGroups.map((group) => { for (const i of group) coveredCells.add(i); const groupValues = group.map((i) => cells[i]); if (componentGroups) { const c = group[0] % 3; return comp( c, minifyTrbl(groups.length === 3 ? groupValues.map(toLower) : groupValues) ); } return side( Math.floor(group[0] / 3), minifyWidthStyleColor(groupValues.join(' ')) ); }); /** @type {{ prop: string, value: string }[]} */ const leafDecls = leavesOfFreeCells( cells, touched, blockedCells, coveredCells ); const mask = sortedGroups.reduce( (acc, group) => acc + Math.pow( 2, componentGroups ? group[0] % 3 : 3 - Math.floor(group[0] / 3) ), 0 ); return { decls: [...groupDecls, ...leafDecls], rank: componentGroups ? COMPONENT_SHORTHAND_CANDIDATE : SIDE_SHORTHAND_CANDIDATE, mask, coveredCells, }; } /** * @param {string[]} cells * @param {Set<number>} touched * @param {Set<number>} blockedCells * @return {{ decls: { prop: string, value: string }[], rank: number, mask: number, coveredCells: Set<number> }} */ function createLeafCandidate(cells, touched, blockedCells) { /** @type {Set<number>} */ const coveredCells = new Set(); return { decls: leavesOfFreeCells(cells, touched, blockedCells, coveredCells), rank: LEAF_CANDIDATE, mask: 0, coveredCells, }; } /** * @param {boolean} hasReset * @param {Set<number>} touched * @param {Set<number>} barrierCells * @param {Set<Declaration>[]} cellHistory * @param {string[]} cells * @param {boolean} lane * @param {Set<Declaration>} fallbacks * @return {{ decls: { prop: string, value: string }[], rank: number, mask: number, resetIndex: number, coveredCells: Set<number> }[]} */ function createResetCandidates( hasReset, touched, barrierCells, cellHistory, cells, lane, fallbacks ) { if (!hasReset || touched.size !== 12 || barrierCells.size !== 0) return []; const allDecls = declarationsOfCells(cellHistory, touched); for (const d of allDecls) { if ( !allBorderShorthands.has(asciiLowerCase(d.prop)) && needsUnmetSupport(d) ) { return []; } } for (const d of fallbacks) { if (asciiLowerCase(d.prop) !== 'border') return []; } if (!hasConsistentSupport(allDecls)) return []; /** @type {{decls: {prop: string, value: string}[], rank: number}[]} */ const rawReset = []; addResetCandidates(cells, lane, rawReset); const coveredCells = new Set(touched); return rawReset.map((item, resetIndex) => ({ decls: item.decls, rank: item.rank, mask: 0, resetIndex, coveredCells, })); } export { COMPONENT_SHORTHAND_CANDIDATE, LEAF_CANDIDATE, RESET_CANDIDATE, SIDE_SHORTHAND_CANDIDATE, createGroupCandidate, createLeafCandidate, createResetCandidates, declSize, declarationsOfCells, getAvailableComponents, getAvailableSides, };