postcss-discard-comments
Version:
Discard comments in your CSS files with PostCSS.
196 lines (172 loc) • 5.26 kB
JavaScript
import { TokenType } from '@csstools/css-tokenizer';
import CommentRemover from './lib/commentRemover.js';
import { getTokens } from './lib/tokenUtils.js';
import {
replaceComments,
replaceCommentsInSelector,
} from './lib/replaceComments.js';
/** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
/** @typedef {object} Options
* @property {boolean=} removeAll
* @property {boolean=} removeAllButFirst
* @property {(s: string) => boolean=} remove
*/
/**
* @param {import('postcss').Declaration} node
* @param {CommentRemover} remover
* @param {Map<string, CSSToken[]>} parserCache
*/
function processDeclaration(node, remover, parserCache) {
const preserveWhitespace = node.prop.startsWith('--');
// Raw value metadata is authoritative only while it still mirrors the
// value; a previous plugin may have changed the value underneath it.
const rawValue = node.raws.value?.raw ? node.raws.value : null;
const rawMirrorsValue = rawValue !== null && rawValue.value === node.value;
if (rawValue && rawValue.raw.includes('/*')) {
node.value = replaceComments(
rawMirrorsValue ? rawValue.raw : node.value,
remover,
parserCache,
' ',
preserveWhitespace
);
/** @type {null | {value: string, raw: string}} */ (node.raws.value) = null;
} else if (node.value.includes('/*')) {
node.value = replaceComments(
node.value,
remover,
parserCache,
' ',
preserveWhitespace
);
if (rawValue) {
// The raw captured an earlier value and must not outlive it.
/** @type {null | {value: string, raw: string}} */ (node.raws.value) =
null;
}
}
if (node.raws.important && node.raws.important.includes('/*')) {
node.raws.important = replaceComments(
node.raws.important,
remover,
parserCache
);
const hasComment = getTokens(
/** @type {string} */ (node.raws.important),
parserCache
).some(([type]) => type === TokenType.Comment);
if (!hasComment) {
node.raws.important = '!important';
}
}
}
/**
* @param {import('postcss').Rule} node
* @param {CommentRemover} remover
* @param {Map<string, CSSToken[]>} parserCache
*/
function processRule(node, remover, parserCache) {
if (node.raws.selector && node.raws.selector.raw) {
if (node.raws.selector.raw.includes('/*')) {
node.raws.selector.raw = replaceCommentsInSelector(
node.raws.selector.raw,
remover,
parserCache
);
}
} else if (node.selector && node.selector.includes('/*')) {
node.selector = replaceCommentsInSelector(
node.selector,
remover,
parserCache
);
}
}
/**
* @param {import('postcss').AtRule} node
* @param {CommentRemover} remover
* @param {Map<string, CSSToken[]>} parserCache
*/
function processAtRule(node, remover, parserCache) {
if (node.raws.afterName && node.raws.afterName.includes('/*')) {
const commentsReplaced = replaceComments(
node.raws.afterName,
remover,
parserCache
);
if (!commentsReplaced.length) {
node.raws.afterName = commentsReplaced + ' ';
} else {
node.raws.afterName = ' ' + commentsReplaced + ' ';
}
}
if (node.raws.params && node.raws.params.raw) {
if (node.raws.params.raw.includes('/*')) {
node.raws.params.raw = replaceComments(
node.raws.params.raw,
remover,
parserCache
);
}
} else if (node.params && node.params.includes('/*')) {
node.params = replaceComments(node.params, remover, parserCache);
}
}
/**
* @param {import('postcss').ChildNode} node
* @param {CommentRemover} remover
* @param {Map<string, CSSToken[]>} parserCache
*/
function processNode(node, remover, parserCache) {
if (node.type === 'comment' && remover.canRemove(node.text)) {
node.remove();
return;
}
if (
typeof node.raws.between === 'string' &&
node.raws.between.includes('/*')
) {
const preserveWhitespace =
node.type === 'decl' && node.prop.startsWith('--');
node.raws.between = replaceComments(
node.raws.between,
remover,
parserCache,
' ',
preserveWhitespace
);
if (preserveWhitespace && !node.raws.between.includes('/*')) {
node.raws.between = ': ';
}
}
if (node.type === 'decl') {
processDeclaration(node, remover, parserCache);
} else if (node.type === 'rule') {
processRule(node, remover, parserCache);
} else if (node.type === 'atrule') {
processAtRule(node, remover, parserCache);
}
}
/**
* @param {Options} [opts]
* @return {import('postcss').Plugin}
*/
function pluginCreator(opts = {}) {
return {
postcssPlugin: 'postcss-discard-comments',
/**
* @param {import('postcss').Root} css
*/
OnceExit(css) {
// Removal decisions are stateful and parsing is pure, so neither is
// cached across traversals.
const remover = new CommentRemover(opts);
const parserCache = new Map();
css.walk((node) => processNode(node, remover, parserCache));
},
};
}
/** @type {true} */
pluginCreator.postcss = true;
const moduleExports = pluginCreator;
export { moduleExports as default, moduleExports as 'module.exports' };