UNPKG

postcss-merge-longhand

Version:

Merge longhand properties into shorthand with PostCSS.

237 lines (218 loc) • 7.55 kB
import placeMinimums from '../data/placeSupport.json' with { type: 'json' }; import pairMinimums from '../data/pairSupport.json' with { type: 'json' }; import boxSupport from '../data/boxPropertySupport.json' with { type: 'json' }; import featureMinimums from '../data/featureSupport.json' with { type: 'json' }; import { compareVersions } from './compareVersions.js'; import { boxProperties } from './decl/boxGroups.js'; import { pairFamilies } from './decl/pairForms.js'; import { longstandingFeatures } from './syntaxFeatures.js'; const pairMinimumsByShorthand = /** @type {Record<string, Record<string, string>>} */ (pairMinimums); /** * @param {string} entry - a browserslist entry such as "safari 10.1" * @param {Record<string, string>} minimums - first supporting version by * browserslist name * @return {boolean} whether the target reaches its engine's minimum; a target * without compatibility data counts as lacking support */ function reaches(entry, minimums) { const [name, version] = entry.split(' '); const minimum = minimums[name]; if (minimum === undefined) return false; if (version === 'TP') return true; const lowest = version.split('-')[0]; return ( /^\d+(?:\.\d+)*$/v.test(lowest) && compareVersions(lowest, minimum) >= 0 ); } /** * Whether every target supports all place-* shorthands. An engine without * them drops the whole declaration, losing both axes that separate longhands * would have kept. * * @param {string[]} browsers - browserslist entries such as "safari 10.1" * @return {boolean} */ export function supportsPlaceShorthands(browsers) { return browsers.every((entry) => reaches(entry, placeMinimums)); } /** * The pair families whose shorthand every target parses, and the families * that need no support check. An engine without a gated shorthand drops the * whole declaration, losing both axes that separate longhands would have kept. * A gated family without minimums is taken as unsupported. * * @param {string[]} browsers - browserslist entries such as "safari 10.1" * @param {Record<string, Record<string, string>>} [minimumsByShorthand] - first * supporting version by shorthand and browserslist name * @return {ReadonlySet<string>} the shorthand names */ export function supportedPairShorthands( browsers, minimumsByShorthand = pairMinimumsByShorthand ) { return new Set( pairFamilies .filter(({ supportKey }) => { if (supportKey === null) return true; const minimums = minimumsByShorthand[supportKey] ?? {}; return browsers.every((entry) => reaches(entry, minimums)); }) .map(({ shorthand }) => shorthand) ); } /** * The oldest release of each engine that parses every longstanding unit and * function `isFallback` lists, by browserslist name. An engine left out, such * as Opera Mini, is assumed to reach it; its gaps are tracked as features. * * @type {Readonly<Record<string, string>>} */ export const longstandingFloor = { android: '4.4.3', chrome: '49', edge: '12', firefox: '52', ie: '11', ios_saf: '9', op_mob: '36', opera: '36', safari: '9', samsung: '4', }; /** * @param {string} entry - a browserslist entry such as "ie 8" * @return {boolean} */ function reachesLongstandingFloor(entry) { return ( !Object.hasOwn(longstandingFloor, entry.split(' ')[0]) || reaches(entry, longstandingFloor) ); } /** * The newer syntax every target parses, so an earlier declaration is no * fallback for it. Features without compatibility data are never included, * and longstanding syntax only when every target reaches the floor. * * @param {string[]} browsers - browserslist entries such as "safari 10.1" * @return {ReadonlySet<string>} */ export function featuresSupportedByAll(browsers) { /** @type {Set<string>} */ const supported = new Set( browsers.every(reachesLongstandingFloor) ? longstandingFeatures : [] ); for (const [feature, minimums] of Object.entries(featureMinimums)) { if (browsers.every((entry) => reaches(entry, minimums))) { supported.add(feature); } } return supported; } /** @import {BoxProperty} from './decl/boxGroups.js' */ const unknown = 0; const no = 1; const yes = 2; const boxMinimums = /** @type {Record<string, string | Record<string, string>>} */ ( boxSupport.properties ); const coveredEngines = new Set(boxSupport.engines); /** * @param {string} entry - a browserslist entry such as "safari 10.1" * @param {string} property - a property of the box groups * @return {boolean} */ function reachesBoxProperty(entry, property) { const minimums = boxMinimums[property]; if (minimums === undefined) return false; return typeof minimums === 'string' || reaches(entry, minimums); } /** * The compatibility data says nothing about some engines, such as UC Browser, * so they may understand any property. * * @param {string} entry - a browserslist entry such as "safari 10.1" * @param {string} property - a property of the box groups * @return {boolean} */ function mayUnderstandBoxProperty(entry, property) { return ( !coveredEngines.has(entry.split(' ')[0]) || reachesBoxProperty(entry, property) ); } /** * What the targets support of the box groups. The newer shorthands arrived * long after their longhands, so which of two declarations applies in a * target depends on which of them it understands. */ export class BoxSupport { /** @type {string[]} */ #browsers; #targetCount; /** @type {Map<string, ReadonlySet<string>>} */ #supporting = new Map(); /** @type {Map<string, ReadonlySet<string>>} */ #understanding = new Map(); /* Answers by pair of properties, found when first asked: a rule asks about * the same few pairs for every side it sets. */ #answers = new Uint8Array(boxProperties.size ** 2); /** @param {string[]} browsers */ constructor(browsers) { this.#browsers = browsers; this.#targetCount = new Set(browsers).size; } /** * @param {Map<string, ReadonlySet<string>>} cache * @param {(entry: string, property: string) => boolean} test * @param {string} property * @return {ReadonlySet<string>} */ #targetsWhere(cache, test, property) { let targets = cache.get(property); if (targets === undefined) { targets = new Set( this.#browsers.filter((entry) => test(entry, property)) ); cache.set(property, targets); } return targets; } /** @param {string} property */ #targetsSupporting(property) { return this.#targetsWhere(this.#supporting, reachesBoxProperty, property); } /** * @param {string} property * @return {boolean} whether every target understands the property */ supportsAll(property) { return this.#targetsSupporting(property).size === this.#targetCount; } /** * @param {BoxProperty} earlier * @param {BoxProperty} later * @return {boolean} whether every target that may understand `earlier` * also understands `later`, so that `later` leaves nothing of `earlier` * showing */ understandsWherever(earlier, later) { const slot = earlier.index * boxProperties.size + later.index; let answer = this.#answers[slot]; if (answer === unknown) { answer = earlier === later || this.#targetsWhere( this.#understanding, mayUnderstandBoxProperty, earlier.name ).isSubsetOf(this.#targetsSupporting(later.name)) ? yes : no; this.#answers[slot] = answer; } return answer === yes; } }