blob: 9d8e8444ef0fba12b44f4f99e68bd153fa03b0f8 [file]
import valueParser from 'postcss-value-parser';
const atrule = 'atrule';
const decl = 'decl';
const rule = 'rule';
const variableFunctions = new Set(['var', 'env', 'constant']);
const ieHackRegex = /\s*(\\9)\s*/;
const whitespaceRegex = /\s/g;
/**
* Reports whether a value ends in a backslash that begins an escape
* sequence, as opposed to a backslash that is itself escaped by a
* preceding backslash.
* @param {string} value
* @return {boolean}
*/
function endsWithEscapingBackslash(value) {
let backslashes = 0;
for (let i = value.length - 1; i >= 0 && value[i] === '\\'; i--) {
backslashes++;
}
return backslashes % 2 === 1;
}
/**
* @param {valueParser.Node} node
* @return {void}
*/
function reduceCalcWhitespaces(node) {
if (node.type === 'space') {
node.value = ' ';
} else if (node.type === 'function') {
if (!variableFunctions.has(node.value.toLowerCase())) {
node.before = node.after = '';
}
}
}
/**
* @param {valueParser.Node} node
* @return {void | false}
*/
function reduceWhitespaces(node) {
if (node.type === 'space') {
node.value = ' ';
} else if (node.type === 'div') {
node.before = node.after = '';
} else if (node.type === 'function') {
if (!variableFunctions.has(node.value.toLowerCase())) {
node.before = node.after = '';
}
if (node.value.toLowerCase() === 'calc') {
valueParser.walk(node.nodes, reduceCalcWhitespaces);
return false;
}
}
}
/**
*
* @param {import('postcss').Declaration} node
* @param {Map<string, string>} cache
* @return {void}
*/
function trimDeclaration(node, cache) {
// Ensure that !important values do not have any excess whitespace
if (node.important) {
node.raws.important = '!important';
}
// Remove whitespaces around ie 9 hack
node.value = node.value.replace(ieHackRegex, '$1');
const value = node.value;
if (cache.has(value)) {
node.value = /** @type {string} **/ (cache.get(value));
} else {
const parsed = valueParser(node.value);
const result = parsed.walk(reduceWhitespaces).toString();
// Trim whitespace inside functions & dividers
node.value = result;
cache.set(value, result);
}
// Remove extra semicolons and whitespace before the declaration
if (node.raws.before) {
const prev = node.prev();
if (prev && prev.type !== rule) {
node.raws.before = node.raws.before.replace(/;/g, '');
}
}
node.raws.between = ':';
node.raws.semicolon = false;
}
/**
* @return {import('postcss').Plugin}
*/
function pluginCreator() {
return {
postcssPlugin: 'postcss-normalize-whitespace',
/**
* @param {import('postcss').Root} css
*/
OnceExit(css) {
const declarationCache = new Map();
css.walk((node) => {
const { type } = node;
if ([decl, rule, atrule].includes(type) && node.raws.before) {
node.raws.before = node.raws.before.replace(whitespaceRegex, '');
}
if (type === decl && !node.prop.startsWith('--')) {
trimDeclaration(node, declarationCache);
} else if (type === rule || type === atrule) {
// When the last declaration has no trailing semicolon and its
// value ends in an escape sequence consuming whitespace (e.g.
// `\9` written as `\` followed by a literal tab), the parser
// attributes the escaped code point to the rule's trailing
// raw instead of the declaration's value. Reattach the single
// character the backslash escapes before discarding the rest
// of that raw, or the escape is left dangling and becomes a
// valid escape of whatever follows it in the output (`}`, or
// even a `;` inserted as a terminator, since only a newline or
// end of input is not a valid escape target).
const last = node.last;
if (
last &&
last.type === decl &&
endsWithEscapingBackslash(last.value) &&
node.raws.after
) {
last.value += node.raws.after[0];
}
node.raws.between = node.raws.after = '';
node.raws.semicolon = false;
}
});
// Remove final newline
css.raws.after = '';
},
};
}
/** @type {true} */
pluginCreator.postcss = true;
const moduleExports = pluginCreator;
export { moduleExports as default, moduleExports as 'module.exports' };