postcss-merge-longhand
Version:
Merge longhand properties into shorthand with PostCSS.
115 lines (101 loc) • 3.5 kB
JavaScript
import { withoutVendorPrefix } from './vendorPrefix.js';
import cssnanoUtils from 'cssnano-utils';
import { tokenize, TokenType } from '@csstools/css-tokenizer';
const { asciiLowerCase, decoded } = cssnanoUtils;
/* Substitution functions from CSS Values. If a user agent does not support
* one, the entire declaration becomes invalid, so we cannot infer what value
* substitutes at runtime. */
const substitutionFunctions = ['var', 'env', 'constant'];
/* Math functions that can produce computed lengths (unlike sin(), pow(),
* sqrt() which produce only numbers). Inside other functions, their type is
* determined by context. */
const mathFunctions = [
'calc',
'min',
'max',
'clamp',
'hypot',
'round',
'mod',
'rem',
'abs',
'sign',
];
/* Unresolved functions: the math functions above, plus attr() and if(),
* which adopt their context's type requirement. */
const trustedFunctions = new Set([
...substitutionFunctions,
...mathFunctions,
'attr',
'if',
]);
/* Math functions, attr(), and if() have a fixed type from their own syntax
* or context—calc() produces a length or number, never a keyword or color.
* Unlike substitutions, they can only fill a component if its grammar accepts
* that type. */
const trustedSupportFunctions = trustedFunctions.difference(
new Set(substitutionFunctions)
);
/**
* @param {string} token
* @return {string|undefined} the function name a token opens with, unprefixed
*/
function leadingFunctionName(token) {
if (!token.includes('(')) return undefined;
let firstToken;
for (const candidate of tokenize({ css: token })) {
if (
candidate[0] !== TokenType.Whitespace &&
candidate[0] !== TokenType.Comment &&
candidate[0] !== TokenType.EOF
) {
firstToken = candidate;
break;
}
}
if (!firstToken || firstToken[0] !== TokenType.Function) return undefined;
return withoutVendorPrefix(asciiLowerCase(decoded(firstToken)));
}
/**
* Whether a token represents an unresolved value this plugin cannot compute.
*
* Only the leading function determines this: var(--x, rgba(0,0,0,.5)) will
* substitute a user-agent-computed value, so we cannot infer its type from the
* fallback. Accepting all bracketed tokens instead would allow any function to
* represent any type—padding-top: url(x) becomes a length, border-top-width:
* rgb(0 0 0) a width—and user agents ignore both invalid declarations, so
* merging them produces invalid shorthands.
*
* @param {string} token
* @return {boolean}
*/
function isUnresolved(token) {
const name = leadingFunctionName(token);
return name !== undefined && trustedFunctions.has(name);
}
/**
* @param {string} name - a function name as written, possibly vendor-prefixed
* @return {boolean} whether the function is one this plugin trusts
*/
function isTrustedFunction(name) {
return trustedFunctions.has(withoutVendorPrefix(asciiLowerCase(name)));
}
/**
* Whether a token is a substitution function specifically — the one class of
* trusted function whose type stays unknowable after substitution, so it can
* fill any border component rather than only the ones its own type matches.
*
* @param {string} token
* @return {boolean}
*/
function isSubstitution(token) {
const name = leadingFunctionName(token);
return name !== undefined && substitutionFunctions.includes(name);
}
export {
isSubstitution,
isTrustedFunction,
isUnresolved,
substitutionFunctions,
trustedSupportFunctions,
};