postcss-merge-longhand
Version:
Merge longhand properties into shorthand with PostCSS.
90 lines (79 loc) • 2.63 kB
JavaScript
import cssnanoUtils from 'cssnano-utils';
const { TokenType, closeForOpening, tokenEnd, tokenStart, tokens } =
cssnanoUtils;
export const closingTokens = new Set([
TokenType.CloseParen,
TokenType.CloseSquare,
TokenType.CloseCurly,
]);
/**
* @typedef {{raw: string, tokens: import('@csstools/css-tokenizer').CSSToken[]}}
* Component
*/
/**
* Split raw CSS into top-level components and comma-separated parts. This is
* deliberately lexical: property grammar is applied by the caller after
* brackets and source ranges have been preserved.
*
* @param {string} value
* @param {',' | '/'} [separator] - the top-level token that divides the value
* into parts; a comma is otherwise a failure, and a slash an ordinary token
* @return {{components: Component[], raw: string}[] | null}
*/
export function splitValue(value, separator) {
/** @type {{components: Component[], start: number, end: number}[]} */
const parts = [{ components: [], start: 0, end: value.length }];
/** @type {import('@csstools/css-tokenizer').CSSToken[]} */
let current = [];
/** @type {import('@csstools/css-tokenizer').TokenType[]} */
const stack = [];
const push = () => {
if (!current.length) return;
const first = current[0];
const last = current.at(-1);
const part = parts.at(-1);
if (!first || !last || !part) return;
part.components.push({
raw: value.slice(tokenStart(first), tokenEnd(last)),
tokens: current,
});
current = [];
};
for (const token of tokens(value)) {
const type = token[0];
if (type === TokenType.Comment) return null;
if (!stack.length && type === TokenType.Whitespace) {
push();
continue;
}
if (!stack.length && type === TokenType.Comma && separator !== ',') {
return null;
}
if (
!stack.length &&
(type === TokenType.Comma ||
(separator === '/' && type === TokenType.Delim && token[1] === '/'))
) {
push();
const part = parts.at(-1);
if (!part) return null;
part.end = tokenStart(token);
parts.push({ components: [], start: tokenEnd(token), end: value.length });
continue;
}
current.push(token);
const expected = closeForOpening(type);
if (expected !== undefined) {
stack.push(expected);
} else if (closingTokens.has(type) && stack.pop() !== type) {
return null;
}
}
if (stack.length) return null;
push();
if (parts.some((part) => !part.components.length)) return null;
return parts.map((part) => ({
components: part.components,
raw: value.slice(part.start, part.end).trim(),
}));
}