UNPKG

htmlnano

Version:

Modular HTML minifier, built on top of the PostHTML

142 lines (139 loc) 5.25 kB
import { optionalImport, isStyleNode, isCssStyleType, extractCssFromStyleNode, stripCssCdata, wrapCssCdata } from '../helpers.mjs'; const postcssOptions = { // Prevent the following warning from being shown: // > Without `from` option PostCSS could generate wrong source map and will not find Browserslist config. // > Set it to CSS file path or to `undefined` to prevent this warning. from: undefined }; const inlineCssExcludedPlugins = { mergeRules: false, minifySelectors: false, minifyParams: false, normalizeCharset: false, uniqueSelectors: false, normalizeUnicode: false }; /** Minify CSS with cssnano */ const mod = { async default (tree, _, cssnanoOptions) { const cssnano = await optionalImport('cssnano'); const postcss = await optionalImport('postcss'); if (!cssnano || !postcss) { return tree; } const processor = createCssProcessor(postcss, cssnano, cssnanoOptions); const inlineStyleProcessor = createCssProcessor(postcss, cssnano, getInlineCssnanoOptions(cssnanoOptions)); const minifiedCssCache = new Map(); const promises = []; let p; tree.walk((node)=>{ // Skip SRI, reasons are documented in "minifyJs" module if (node.attrs && 'integrity' in node.attrs) { return node; } if (isStyleNode(node) && isCssStyleType(node)) { p = processStyleNode(node, processor, minifiedCssCache); if (p) { promises.push(p); } } else if (node.attrs && node.attrs.style) { p = processStyleAttr(node, inlineStyleProcessor, minifiedCssCache); if (p) { promises.push(p); } } return node; }); return Promise.all(promises).then(()=>tree); } }; function createCssProcessor(postcss, cssnano, cssnanoOptions) { return postcss([ cssnano(cssnanoOptions) ]); } function getInlineCssnanoOptions(cssnanoOptions) { if (!cssnanoOptions || typeof cssnanoOptions !== 'object') { return { preset: [ 'default', inlineCssExcludedPlugins ] }; } if (Array.isArray(cssnanoOptions.plugins)) { return cssnanoOptions; } if (!('preset' in cssnanoOptions) || cssnanoOptions.preset === undefined) { return { ...cssnanoOptions, preset: [ 'default', inlineCssExcludedPlugins ] }; } if (cssnanoOptions.preset === 'default') { return { ...cssnanoOptions, preset: [ 'default', inlineCssExcludedPlugins ] }; } if (Array.isArray(cssnanoOptions.preset) && cssnanoOptions.preset[0] === 'default') { const presetOptions = cssnanoOptions.preset[1]; return { ...cssnanoOptions, preset: [ 'default', { ...inlineCssExcludedPlugins, ...presetOptions && typeof presetOptions === 'object' ? presetOptions : {} } ] }; } return cssnanoOptions; } function processStyleNode(styleNode, processor, minifiedCssCache) { let css = extractCssFromStyleNode(styleNode); if (!css || css.trim() === '') return; // Improve performance by avoiding calling stripCssCdata again and again const { strippedCss, isCdataWrapped } = stripCssCdata(css); css = strippedCss; return processCss(processor, minifiedCssCache, `${isCdataWrapped ? 'style-cdata:' : 'style:'}${css}`, css, isCdataWrapped).then((minifiedCss)=>{ styleNode.content = [ wrapCssCdata(minifiedCss, isCdataWrapped) ]; }); } function processStyleAttr(node, processor, minifiedCssCache) { // CSS "color: red;" is invalid. Therefore it should be wrapped inside some selector: // a{color: red;} const wrapperStart = 'a{'; const wrapperEnd = '}'; if (!node.attrs || !node.attrs.style || typeof node.attrs.style !== 'string') { return; } if (node.attrs.style.trim() === '') { return; } const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd; return processCss(processor, minifiedCssCache, `attr:${wrappedStyle}`, wrappedStyle).then((minifiedCss)=>{ // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length); }); } function processCss(processor, minifiedCssCache, cacheKey, css, useToString = false) { let minifiedCss = minifiedCssCache.get(cacheKey); if (!minifiedCss) { minifiedCss = processor.process(css, postcssOptions).then((result)=>useToString ? result.toString() : result.css).catch((error)=>{ minifiedCssCache.delete(cacheKey); throw error; }); minifiedCssCache.set(cacheKey, minifiedCss); } return minifiedCss; } export { mod as default, getInlineCssnanoOptions };