You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
|
|
'use strict';
/** * @typedef {import('../lib/types').XastElement} XastElement */
const csso = require('csso');
exports.type = 'visitor'; exports.name = 'minifyStyles'; exports.active = true; exports.description = 'minifies styles and removes unused styles based on usage data';
/** * Minifies styles (<style> element + style attribute) using CSSO * * @author strarsis <strarsis@gmail.com> * * @type {import('../lib/types').Plugin<csso.MinifyOptions & Omit<csso.CompressOptions, 'usage'> & { * usage?: boolean | { * force?: boolean, * ids?: boolean, * classes?: boolean, * tags?: boolean * } * }>} */ exports.fn = (_root, { usage, ...params }) => { let enableTagsUsage = true; let enableIdsUsage = true; let enableClassesUsage = true; // force to use usage data even if it unsafe (document contains <script> or on* attributes)
let forceUsageDeoptimized = false; if (typeof usage === 'boolean') { enableTagsUsage = usage; enableIdsUsage = usage; enableClassesUsage = usage; } else if (usage) { enableTagsUsage = usage.tags == null ? true : usage.tags; enableIdsUsage = usage.ids == null ? true : usage.ids; enableClassesUsage = usage.classes == null ? true : usage.classes; forceUsageDeoptimized = usage.force == null ? false : usage.force; } /** * @type {Array<XastElement>} */ const styleElements = []; /** * @type {Array<XastElement>} */ const elementsWithStyleAttributes = []; let deoptimized = false; /** * @type {Set<string>} */ const tagsUsage = new Set(); /** * @type {Set<string>} */ const idsUsage = new Set(); /** * @type {Set<string>} */ const classesUsage = new Set();
return { element: { enter: (node) => { // detect deoptimisations
if (node.name === 'script') { deoptimized = true; } for (const name of Object.keys(node.attributes)) { if (name.startsWith('on')) { deoptimized = true; } } // collect tags, ids and classes usage
tagsUsage.add(node.name); if (node.attributes.id != null) { idsUsage.add(node.attributes.id); } if (node.attributes.class != null) { for (const className of node.attributes.class.split(/\s+/)) { classesUsage.add(className); } } // collect style elements or elements with style attribute
if (node.name === 'style' && node.children.length !== 0) { styleElements.push(node); } else if (node.attributes.style != null) { elementsWithStyleAttributes.push(node); } }, },
root: { exit: () => { /** * @type {csso.Usage} */ const cssoUsage = {}; if (deoptimized === false || forceUsageDeoptimized === true) { if (enableTagsUsage && tagsUsage.size !== 0) { cssoUsage.tags = Array.from(tagsUsage); } if (enableIdsUsage && idsUsage.size !== 0) { cssoUsage.ids = Array.from(idsUsage); } if (enableClassesUsage && classesUsage.size !== 0) { cssoUsage.classes = Array.from(classesUsage); } } // minify style elements
for (const node of styleElements) { if ( node.children[0].type === 'text' || node.children[0].type === 'cdata' ) { const cssText = node.children[0].value; const minified = csso.minify(cssText, { ...params, usage: cssoUsage, }).css; // preserve cdata if necessary
// TODO split cdata -> text optimisation into separate plugin
if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) { node.children[0].type = 'cdata'; node.children[0].value = minified; } else { node.children[0].type = 'text'; node.children[0].value = minified; } } } // minify style attributes
for (const node of elementsWithStyleAttributes) { // style attribute
const elemStyle = node.attributes.style; node.attributes.style = csso.minifyBlock(elemStyle, { ...params, }).css; } }, }, }; };
|