postcss-discard-empty
Version:
Discard empty rules and values with PostCSS.
130 lines (116 loc) • 3.16 kB
JavaScript
;
const plugin = 'postcss-discard-empty';
/**
* @param {string} layerName
* @return {string[]}
*/
function getLayerPath(layerName) {
const layerPath = [];
let component = '';
let escaped = false;
for (const character of layerName) {
if (character === '\\' && !escaped) {
escaped = true;
component += character;
} else if (character === '.' && !escaped) {
layerPath.push(component.trim());
component = '';
} else {
component += character;
escaped = false;
}
}
layerPath.push(component.trim());
return layerPath;
}
/**
* @param {import('postcss').AnyNode} node
* @param {import('postcss').Container['nodes']} sub
* @param {boolean} isLayer
* @param {boolean} isEmptyLayer
* @param {string} layerKey
* @param {Set<string>} nonEmptyLayers
* @return {boolean}
*/
function shouldDiscard(
node,
sub,
isLayer,
isEmptyLayer,
layerKey,
nonEmptyLayers
) {
const { type } = node;
return Boolean(
(type === 'decl' && !node.value && !node.prop.startsWith('--')) ||
(type === 'rule' && !node.selector) ||
(sub && !sub.length && !isLayer) ||
(isEmptyLayer && nonEmptyLayers.has(layerKey)) ||
(type === 'atrule' &&
((!sub && !node.params) ||
(!node.params &&
!(/** @type {import('postcss').ChildNode[]} */ (sub).length))))
);
}
/**
* @param {import('postcss').Root} css
* @param {import('postcss').Result} result
* @return {void}
*/
function discardAndReport(css, result) {
const nonEmptyLayers = new Set();
/**
* @param {import('postcss').AnyNode} node
* @param {string[]} [layerPath=[]]
* @return {void}
*/
function discardEmpty(node, layerPath = []) {
const { type } = node;
/** @type {import('postcss').ChildNode[] | undefined} */
const sub = /** @type {import('postcss').Container} */ (node).nodes;
const isLayer = type === 'atrule' && node.name === 'layer';
const layerName = isLayer ? node.params.trim() : '';
const currentLayerPath = layerName
? [...layerPath, ...getLayerPath(layerName)]
: layerPath;
if (sub) {
/** @type {import('postcss').Container} */ (node).each((child) =>
discardEmpty(child, currentLayerPath)
);
}
const isEmptyLayer = Boolean(isLayer && layerName && sub && !sub.length);
const layerKey = currentLayerPath.join('\0');
if (
shouldDiscard(node, sub, isLayer, isEmptyLayer, layerKey, nonEmptyLayers)
) {
node.remove();
result.messages.push({
type: 'removal',
plugin,
node,
});
} else if (isLayer && sub && sub.length) {
nonEmptyLayers.add(layerKey);
}
}
css.each((node) => discardEmpty(node));
}
/**
* @return {import('postcss').Plugin}
*/
function pluginCreator() {
return {
postcssPlugin: plugin,
/**
* @param {import('postcss').Root} css
* @param {import('postcss').Helpers} helpers
*/
OnceExit(css, { result }) {
discardAndReport(css, result);
},
};
}
pluginCreator.postcss = true;
module.exports = /** @type {import('postcss').PluginCreator<void>}*/ (
pluginCreator
);