149 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			149 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 | |
|         }
 | |
|       },
 | |
|     },
 | |
|   };
 | |
| };
 |