UNPKG

cssnano

Version:

A modular CSS minifier, built on top of the PostCSS ecosystem.

227 lines (207 loc) • 6.92 kB
import getBrowsersList from './lib/computeBrowsersToSupport.js'; import { tokenize, TokenType } from '@csstools/css-tokenizer'; import cssnanoUtils from '#utils'; /** @import browserslist from 'browserslist' */ /** @import { TokenUnicodeRange } from '@csstools/css-tokenizer' */ const { asciiLowerCase } = cssnanoUtils; const unicodeRangePropertyRegex = /^[uU][nN][iI][cC][oO][dD][eE]-[rR][aA][nN][gG][eE]$/v; const maxCodePoint = 0x10ffff; /** * @param {number} start * @param {number} end * @return {number} number of trailing "?" wildcards when [start, end] can be * written as a wildcard range (a power-of-16 size starting at a multiple of * that size), otherwise 0 */ function wildcardMarks(start, end) { const size = end - start + 1; let marks = 0; let block = 1; while (block < size) { block *= 16; marks++; } return block === size && marks > 0 && start % size === 0 ? marks : 0; } /** * @param {number} start * @param {number} end * @param {string} prefix * @return {string} */ function formatRange(start, end, prefix) { const marks = wildcardMarks(start, end); if (marks > 0) { const size = 16 ** marks; const digits = start === 0 ? '' : (start / size).toString(16); return `${prefix}+${digits}${'?'.repeat(marks)}`; } if (start === end) return `${prefix}+${start.toString(16)}`; return `${prefix}+${start.toString(16)}-${end.toString(16)}`; } /** * @param {import('@csstools/css-tokenizer').CSSToken} token * @return {token is TokenUnicodeRange} */ function isUnicodeRangeDescriptorListToken(token) { return token[0] === TokenType.UnicodeRange; } /** * @param {string} value * @return {{ ranges: TokenUnicodeRange[], canonicalize: boolean } | null} null * when the value is not a comma-separated list of unicode-range tokens; * canonicalize is false when a comment or an invalid range forbids rewriting * the list as a union of its ranges */ function parseDescriptorList(value) { let expectsRange = true; let canonicalize = true; /** @type {TokenUnicodeRange[]} */ const ranges = []; for (const token of tokenize({ css: value, unicodeRangesAllowed: true })) { if (token[0] === TokenType.EOF) continue; if (token[0] === TokenType.Whitespace) continue; if (token[0] === TokenType.Comment) { if (!token[1].endsWith('*/')) return null; canonicalize = false; continue; } if (expectsRange && isUnicodeRangeDescriptorListToken(token)) { expectsRange = false; ranges.push(token); if (!isValidRange(token)) canonicalize = false; } else if (!expectsRange && token[0] === TokenType.Comma) { expectsRange = true; } else { return null; } } if (expectsRange || ranges.length === 0) return null; return { ranges, canonicalize }; } /** * A range whose start is after its end, or whose end exceeds U+10FFFF, is a * syntax error that browsers ignore, so it must not be rewritten. * * @param {TokenUnicodeRange} token * @return {boolean} */ function isValidRange(token) { const { startOfRange, endOfRange } = token[4]; return startOfRange <= endOfRange && endOfRange <= maxCodePoint; } /** * @param {string} value * @param {boolean} isLegacy * @return {string} */ function transform(value, isLegacy = false) { const list = parseDescriptorList(value); if (!list) return value; const { ranges, canonicalize } = list; const prefix = isLegacy ? 'U' : 'u'; if (canonicalize) { // The descriptor is the union of its ranges, so sorting and joining // overlapping or touching ranges preserves the code point set. ranges.sort((a, b) => a[4].startOfRange - b[4].startOfRange); const formatted = []; let { startOfRange: start, endOfRange: end } = ranges[0][4]; for (let i = 1; i < ranges.length; i++) { const bounds = ranges[i][4]; if (bounds.startOfRange <= end + 1) { end = Math.max(end, bounds.endOfRange); } else { formatted.push(formatRange(start, end, prefix)); ({ startOfRange: start, endOfRange: end } = bounds); } } formatted.push(formatRange(start, end, prefix)); return formatted.join(','); } const chunks = []; let cursor = 0; for (const token of ranges) { const { startOfRange, endOfRange } = token[4]; const transformed = isValidRange(token) ? formatRange(startOfRange, endOfRange, prefix) : prefix + asciiLowerCase(token[1].slice(1)); if (transformed !== token[1]) { chunks.push(value.slice(cursor, token[2]), transformed); cursor = token[3] + 1; } } if (cursor === 0) return value; chunks.push(value.slice(cursor)); return chunks.join(''); } /** * @param {import('postcss').Declaration} decl * @param {string} value */ function assignValue(decl, value) { decl.value = value; if (decl.raws.value?.raw) { decl.raws.value = { raw: value, value }; } } /** * @typedef {{ overrideBrowserslist?: string | string[] }} AutoprefixerOptions * @typedef {Pick<browserslist.Options, 'stats' | 'path' | 'env'>} BrowserslistOptions * @typedef {AutoprefixerOptions & BrowserslistOptions} Options */ /** * @type {import('postcss').PluginCreator<Options>} * @param {Options} opts * @return {import('postcss').Plugin} */ function pluginCreator(/** @type {Options} */ opts = {}) { return { postcssPlugin: 'postcss-normalize-unicode', /** * @param {import('postcss').Result & {opts: BrowserslistOptions & {file?: string}}} result */ prepare(result) { const { stats, env, from, file } = result.opts || {}; const browsers = getBrowsersList(null, opts, stats, from, file, env); const cache = new Map(); /** * IE and Edge before 16 version ignore the unicode-range if the 'U' is * lowercase * * https://caniuse.com/#search=unicode-range */ const lowerCaseUPrefixBugBrowsers = new Set( getBrowsersList('ie <=11, edge <= 15') ); const isLegacy = !new Set(browsers).isDisjointFrom( lowerCaseUPrefixBugBrowsers ); return { /** * @param {import('postcss').Root} css */ OnceExit(css) { css.walkDecls(unicodeRangePropertyRegex, (decl) => { const value = decl.raws.value?.value === decl.value ? (decl.raws.value.raw ?? decl.value) : decl.value; if (cache.has(value)) { const newValue = cache.get(value); assignValue(decl, newValue); return; } const newValue = transform(value, isLegacy); assignValue(decl, newValue); cache.set(value, newValue); }); }, }; }, }; } /** @type {true} */ pluginCreator.postcss = true; const moduleExports = pluginCreator; export { moduleExports as default, moduleExports as 'module.exports' };