htmlnano
Version:
Modular HTML minifier, built on top of the PostHTML
159 lines (156 loc) • 5.46 kB
JavaScript
import { optionalImport, isStyleNode, isCssStyleType, extractCssFromStyleNode, stripCssCdata, wrapCssCdata } from '../helpers.mjs';
// These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
const uncssOptions = {
ignoreSheets: [
/\s*/
],
stylesheets: []
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- uncss has no types
function processStyleNodeUnCSS(html, styleNode, uncssOptions, uncss) {
const css = extractCssFromStyleNode(styleNode);
const { strippedCss, isCdataWrapped } = stripCssCdata(css);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- uncss no types
return runUncss(html, strippedCss, uncssOptions, uncss).then((css)=>{
// uncss may have left some style tags empty
if (css.trim().length === 0) {
// @ts-expect-error -- explicitly remove the tag
styleNode.tag = false;
styleNode.content = [];
return;
}
styleNode.content = [
wrapCssCdata(css, isCdataWrapped)
];
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- uncss callback uses untyped args
function runUncss(html, css, userOptions, uncss) {
if (typeof userOptions !== 'object') {
userOptions = {};
}
const options = {
...userOptions,
...uncssOptions
};
return new Promise((resolve, reject)=>{
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- we dont have uncss types
options.raw = css;
uncss(html, options, (error, output)=>{
if (error) {
reject(error);
return;
}
resolve(output);
});
});
}
const purgeFromHtml = function(tree) {
// content is not used as we can directly used the parsed HTML,
// making the process faster
const selectors = [];
tree.walk((node)=>{
const classes = getSelectorTokens(node.attrs && node.attrs.class);
const ids = getSelectorTokens(node.attrs && node.attrs.id);
selectors.push(...classes, ...ids);
if (node.tag) {
selectors.push(node.tag);
}
return node;
});
return ()=>selectors;
};
function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions, purgecss, extractor) {
const css = extractCssFromStyleNode(styleNode);
const { strippedCss, isCdataWrapped } = stripCssCdata(css);
return runPurgecss(tree, strippedCss, purgecssOptions, purgecss, extractor).then((css)=>{
if (css.trim().length === 0) {
// @ts-expect-error -- explicitly remove the tag
styleNode.tag = false;
styleNode.content = [];
return;
}
styleNode.content = [
wrapCssCdata(css, isCdataWrapped)
];
});
}
function runPurgecss(tree, css, userOptions, purgecss, extractor) {
if (typeof userOptions !== 'object') {
userOptions = {};
}
const options = {
...userOptions,
content: [
{
raw: tree.render(),
extension: 'html'
}
],
css: [
{
raw: css,
// @ts-expect-error -- old purgecss options
extension: 'css'
}
],
extractors: [
{
extractor,
extensions: [
'html'
]
}
]
};
return new purgecss.PurgeCSS().purge(options).then((result)=>{
return result[0].css;
});
}
/** Remove unused CSS */ const mod = {
async default (tree, options, userOptions) {
var _resolvedOptions_tool;
const promises = [];
let html;
let extractor;
const purgecss = await optionalImport('purgecss');
const uncss = await optionalImport('uncss');
const resolvedOptions = resolveUserOptions(userOptions);
const tool = (_resolvedOptions_tool = resolvedOptions.tool) != null ? _resolvedOptions_tool : 'purgeCSS';
const toolOptions = stripToolOption(resolvedOptions);
tree.walk((node)=>{
if (isStyleNode(node) && isCssStyleType(node)) {
if (tool === 'purgeCSS') {
if (purgecss) {
extractor != null ? extractor : extractor = purgeFromHtml(tree);
promises.push(processStyleNodePurgeCSS(tree, node, toolOptions, purgecss, extractor));
}
} else if (tool === 'uncss') {
if (uncss) {
html != null ? html : html = tree.render(tree);
promises.push(processStyleNodeUnCSS(html, node, toolOptions, uncss));
}
}
}
return node;
});
return Promise.all(promises).then(()=>tree);
}
};
function getSelectorTokens(value) {
if (typeof value !== 'string') {
return [];
}
return value.split(/\s+/).filter(Boolean);
}
function resolveUserOptions(userOptions) {
if (userOptions && typeof userOptions === 'object') {
return userOptions;
}
return {};
}
function stripToolOption(options) {
const { tool: _tool, ...rest } = options;
return rest;
}
export { mod as default };