postcss-convert-values
Version:
Convert values with PostCSS (e.g. ms -> s)
281 lines (253 loc) • 7.76 kB
JavaScript
import cssnanoUtils from 'cssnano-utils';
import { syntaxAllowsPercentage } from './parseSyntax.js';
import { findReplacements, stripVendorPrefix } from './findReplacements.js';
const { applyEdits } = cssnanoUtils;
const flexBasisProperties = new Set([
'flex',
'flex-basis',
'flex-basic',
'flex-preferred-size',
]);
const alphaProperties = new Set([
'opacity',
'shape-image-threshold',
'fill-opacity',
'stroke-opacity',
'stop-opacity',
'flood-opacity',
]);
const zeroPercentRetention = new Set([
'descent-override',
'ascent-override',
'font-stretch',
'font-width',
'size-adjust',
'line-gap-override',
'text-size-adjust',
'subscript-position-override',
'superscript-position-override',
'subscript-size-override',
'superscript-size-override',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-width',
]);
const zeroUnitRetention = {
ie11Percent: new Set(['max-height', 'height', 'min-width']),
keyframePercent: new Set([
'border-image-width',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-width',
]),
};
/** @typedef {import('./findReplacements.js').Options} Options */
/** @type {WeakMap<import('postcss').AtRule, boolean>} */
const atPropertySyntaxCache = new WeakMap();
/** @param {import('postcss').AtRule} parent @return {boolean} */
function atPropertyAllowsPercentage(parent) {
const cached = atPropertySyntaxCache.get(parent);
if (cached !== undefined) return cached;
const syntaxDecl = /** @type {import('postcss').Declaration | undefined} */ (
parent.nodes?.find(
(node) => node.type === 'decl' && node.prop.toLowerCase() === 'syntax'
)
);
const result = syntaxDecl ? syntaxAllowsPercentage(syntaxDecl.value) : true;
atPropertySyntaxCache.set(parent, result);
return result;
}
/** @param {import('postcss').Declaration} decl @return {boolean} */
function isInsideKeyframes(decl) {
let current = decl.parent;
while (current && current.type !== 'root') {
if (
current.type === 'atrule' &&
/** @type {import('postcss').AtRule} */ (current).name
.toLowerCase()
.endsWith('keyframes')
) {
return true;
}
current = current.parent;
}
return false;
}
/** @param {string} property @param {Options} opts @return {boolean} */
function skipsTransformation(property, opts) {
return (
property === 'unicode-range' ||
(property.startsWith('--') && !opts.transformCustomProperties)
);
}
/** @param {boolean | string[]} supportsIE @return {boolean} */
function hasIESupportTarget(supportsIE) {
if (typeof supportsIE === 'boolean') {
return supportsIE;
}
return supportsIE.some((b) => b.startsWith('ie '));
}
/** @param {import('postcss').Declaration} decl @param {string} lowerCasedProp @return {boolean} */
function isAtPropertyInitialValue(decl, lowerCasedProp) {
if (lowerCasedProp !== 'initial-value') return false;
const parent = decl.parent;
return (
parent?.type === 'atrule' &&
/** @type {import('postcss').AtRule} */ (parent).name.toLowerCase() ===
'property'
);
}
/**
* @param {string} strippedProp
* @param {boolean} isLineHeight
* @param {boolean} isFlexBasis
* @param {boolean} hasIESupport
* @param {boolean} inKeyframes
* @param {boolean} isKeyframeProp
* @param {boolean} allowsAtPropertyPercentage
* @return {boolean}
*/
function shouldKeepZeroPercent(
strippedProp,
isLineHeight,
isFlexBasis,
hasIESupport,
inKeyframes,
isKeyframeProp,
allowsAtPropertyPercentage
) {
if (isLineHeight || isFlexBasis || zeroPercentRetention.has(strippedProp)) {
return true;
}
if (hasIESupport && zeroUnitRetention.ie11Percent.has(strippedProp)) {
return true;
}
if (inKeyframes && isKeyframeProp) {
return true;
}
return allowsAtPropertyPercentage;
}
/**
* @param {string} strippedProp
* @param {boolean} isLineHeight
* @param {boolean} isFlexBasis
* @param {boolean} isAtProperty
* @return {boolean}
*/
function shouldKeepZeroLength(
strippedProp,
isLineHeight,
isFlexBasis,
isAtProperty
) {
return (
isLineHeight ||
isFlexBasis ||
strippedProp === 'columns' ||
strippedProp === 'flex-order' ||
isAtProperty
);
}
/**
* @param {import('postcss').Declaration} decl
* @param {string} lowerCasedProp
* @param {string} strippedProp
* @param {boolean | string[]} supportsIE
* @return {import('./findReplacements.js').ReplacementFlags & { hasParentContext: boolean }}
*/
function getDeclarationFlags(decl, lowerCasedProp, strippedProp, supportsIE) {
const isLineHeight = strippedProp === 'line-height';
const isFlexBasis = flexBasisProperties.has(strippedProp);
const isAtProperty = isAtPropertyInitialValue(decl, lowerCasedProp);
const isAlpha = alphaProperties.has(strippedProp);
const isKeyframeProp = zeroUnitRetention.keyframePercent.has(strippedProp);
const inKeyframes = (isKeyframeProp || isAlpha) && isInsideKeyframes(decl);
const hasParentContext = lowerCasedProp === 'initial-value' || inKeyframes;
const allowsAtPropertyPercentage =
isAtProperty &&
atPropertyAllowsPercentage(
/** @type {import('postcss').AtRule} */ (decl.parent)
);
const hasIESupport = hasIESupportTarget(supportsIE);
const keepZeroPercent = shouldKeepZeroPercent(
strippedProp,
isLineHeight,
isFlexBasis,
hasIESupport,
inKeyframes,
isKeyframeProp,
allowsAtPropertyPercentage
);
const keepZeroLength = shouldKeepZeroLength(
strippedProp,
isLineHeight,
isFlexBasis,
isAtProperty
);
const clampAlpha = !inKeyframes;
const isFont = strippedProp === 'font';
return {
keepZeroPercent,
keepZeroLength,
clampAlpha,
isAlpha,
isFont,
hasParentContext,
};
}
/** @param {import('postcss').Declaration} decl @param {string} newValue @return {void} */
function updateDeclValue(decl, newValue) {
decl.value = newValue;
if (decl.raws?.value?.raw) {
decl.raws.value = { raw: newValue, value: newValue };
} else if (decl.raws?.value) {
delete decl.raws.value;
}
}
/** @param {import('postcss').Declaration} decl @return {string} */
function getRawOrDeclarationValue(decl) {
const raw = decl.raws?.value;
return raw?.raw && raw.value === decl.value ? raw.raw : decl.value;
}
/**
* @param {import('postcss').Declaration} decl
* @param {string} newValue
* @param {string} originalValue
* @return {void}
*/
function syncDeclValue(decl, newValue, originalValue) {
const isStale = Boolean(
decl.raws?.value && decl.raws.value.value !== decl.value
);
if (newValue !== originalValue || isStale) {
updateDeclValue(decl, newValue);
}
}
/** @param {Options} opts @param {boolean | string[]} supportsIE @param {import('postcss').Declaration} decl @param {Map<string, string>} [cache] @return {void} */
export default function transform(opts, supportsIE, decl, cache) {
const lowerCasedProp = decl.prop.toLowerCase();
const strippedProp = stripVendorPrefix(lowerCasedProp);
if (skipsTransformation(strippedProp, opts)) return;
const value = getRawOrDeclarationValue(decl);
if (!/[0-9]/v.test(value)) return;
const flags = getDeclarationFlags(
decl,
lowerCasedProp,
strippedProp,
supportsIE
);
const cacheKey = flags.hasParentContext
? undefined
: `${lowerCasedProp}:${value}`;
if (cacheKey !== undefined && cache?.has(cacheKey)) {
const cached = /** @type {string} */ (cache.get(cacheKey));
syncDeclValue(decl, cached, value);
return;
}
const replacements = findReplacements(value, flags, opts);
const result = replacements.length ? applyEdits(value, replacements) : value;
if (cacheKey !== undefined) {
cache?.set(cacheKey, result);
}
syncDeclValue(decl, result, value);
}