114 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			114 lines
		
	
	
		
			2.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| exports.type = 'visitor';
 | |
| exports.name = 'sortAttrs';
 | |
| exports.active = false;
 | |
| exports.description = 'Sort element attributes for better compression';
 | |
| 
 | |
| /**
 | |
|  * Sort element attributes for better compression
 | |
|  *
 | |
|  * @author Nikolay Frantsev
 | |
|  *
 | |
|  * @type {import('../lib/types').Plugin<{
 | |
|  *   order?: Array<string>
 | |
|  *   xmlnsOrder?: 'front' | 'alphabetical'
 | |
|  * }>}
 | |
|  */
 | |
| exports.fn = (_root, params) => {
 | |
|   const {
 | |
|     order = [
 | |
|       'id',
 | |
|       'width',
 | |
|       'height',
 | |
|       'x',
 | |
|       'x1',
 | |
|       'x2',
 | |
|       'y',
 | |
|       'y1',
 | |
|       'y2',
 | |
|       'cx',
 | |
|       'cy',
 | |
|       'r',
 | |
|       'fill',
 | |
|       'stroke',
 | |
|       'marker',
 | |
|       'd',
 | |
|       'points',
 | |
|     ],
 | |
|     xmlnsOrder = 'front',
 | |
|   } = params;
 | |
| 
 | |
|   /**
 | |
|    * @type {(name: string) => number}
 | |
|    */
 | |
|   const getNsPriority = (name) => {
 | |
|     if (xmlnsOrder === 'front') {
 | |
|       // put xmlns first
 | |
|       if (name === 'xmlns') {
 | |
|         return 3;
 | |
|       }
 | |
|       // xmlns:* attributes second
 | |
|       if (name.startsWith('xmlns:')) {
 | |
|         return 2;
 | |
|       }
 | |
|     }
 | |
|     // other namespaces after and sort them alphabetically
 | |
|     if (name.includes(':')) {
 | |
|       return 1;
 | |
|     }
 | |
|     // other attributes
 | |
|     return 0;
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * @type {(a: [string, string], b: [string, string]) => number}
 | |
|    */
 | |
|   const compareAttrs = ([aName], [bName]) => {
 | |
|     // sort namespaces
 | |
|     const aPriority = getNsPriority(aName);
 | |
|     const bPriority = getNsPriority(bName);
 | |
|     const priorityNs = bPriority - aPriority;
 | |
|     if (priorityNs !== 0) {
 | |
|       return priorityNs;
 | |
|     }
 | |
|     // extract the first part from attributes
 | |
|     // for example "fill" from "fill" and "fill-opacity"
 | |
|     const [aPart] = aName.split('-');
 | |
|     const [bPart] = bName.split('-');
 | |
|     // rely on alphabetical sort when the first part is the same
 | |
|     if (aPart !== bPart) {
 | |
|       const aInOrderFlag = order.includes(aPart) ? 1 : 0;
 | |
|       const bInOrderFlag = order.includes(bPart) ? 1 : 0;
 | |
|       // sort by position in order param
 | |
|       if (aInOrderFlag === 1 && bInOrderFlag === 1) {
 | |
|         return order.indexOf(aPart) - order.indexOf(bPart);
 | |
|       }
 | |
|       // put attributes from order param before others
 | |
|       const priorityOrder = bInOrderFlag - aInOrderFlag;
 | |
|       if (priorityOrder !== 0) {
 | |
|         return priorityOrder;
 | |
|       }
 | |
|     }
 | |
|     // sort alphabetically
 | |
|     return aName < bName ? -1 : 1;
 | |
|   };
 | |
| 
 | |
|   return {
 | |
|     element: {
 | |
|       enter: (node) => {
 | |
|         const attrs = Object.entries(node.attributes);
 | |
|         attrs.sort(compareAttrs);
 | |
|         /**
 | |
|          * @type {Record<string, string>}
 | |
|          */
 | |
|         const sortedAttributes = {};
 | |
|         for (const [name, value] of attrs) {
 | |
|           sortedAttributes[name] = value;
 | |
|         }
 | |
|         node.attributes = sortedAttributes;
 | |
|       },
 | |
|     },
 | |
|   };
 | |
| };
 |