131 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			131 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const { visit } = require('../lib/xast.js');
 | |
| const { inheritableAttrs, pathElems } = require('./_collections.js');
 | |
| 
 | |
| exports.type = 'visitor';
 | |
| exports.name = 'moveElemsAttrsToGroup';
 | |
| exports.active = true;
 | |
| exports.description = 'Move common attributes of group children to the group';
 | |
| 
 | |
| /**
 | |
|  * Move common attributes of group children to the group
 | |
|  *
 | |
|  * @example
 | |
|  * <g attr1="val1">
 | |
|  *     <g attr2="val2">
 | |
|  *         text
 | |
|  *     </g>
 | |
|  *     <circle attr2="val2" attr3="val3"/>
 | |
|  * </g>
 | |
|  *              ⬇
 | |
|  * <g attr1="val1" attr2="val2">
 | |
|  *     <g>
 | |
|  *         text
 | |
|  *     </g>
 | |
|  *    <circle attr3="val3"/>
 | |
|  * </g>
 | |
|  *
 | |
|  * @author Kir Belevich
 | |
|  *
 | |
|  * @type {import('../lib/types').Plugin<void>}
 | |
|  */
 | |
| exports.fn = (root) => {
 | |
|   // find if any style element is present
 | |
|   let deoptimizedWithStyles = false;
 | |
|   visit(root, {
 | |
|     element: {
 | |
|       enter: (node) => {
 | |
|         if (node.name === 'style') {
 | |
|           deoptimizedWithStyles = true;
 | |
|         }
 | |
|       },
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   return {
 | |
|     element: {
 | |
|       exit: (node) => {
 | |
|         // process only groups with more than 1 children
 | |
|         if (node.name !== 'g' || node.children.length <= 1) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         // deoptimize the plugin when style elements are present
 | |
|         // selectors may rely on id, classes or tag names
 | |
|         if (deoptimizedWithStyles) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         /**
 | |
|          * find common attributes in group children
 | |
|          * @type {Map<string, string>}
 | |
|          */
 | |
|         const commonAttributes = new Map();
 | |
|         let initial = true;
 | |
|         let everyChildIsPath = true;
 | |
|         for (const child of node.children) {
 | |
|           if (child.type === 'element') {
 | |
|             if (pathElems.includes(child.name) === false) {
 | |
|               everyChildIsPath = false;
 | |
|             }
 | |
|             if (initial) {
 | |
|               initial = false;
 | |
|               // collect all inheritable attributes from first child element
 | |
|               for (const [name, value] of Object.entries(child.attributes)) {
 | |
|                 // consider only inheritable attributes
 | |
|                 if (inheritableAttrs.includes(name)) {
 | |
|                   commonAttributes.set(name, value);
 | |
|                 }
 | |
|               }
 | |
|             } else {
 | |
|               // exclude uncommon attributes from initial list
 | |
|               for (const [name, value] of commonAttributes) {
 | |
|                 if (child.attributes[name] !== value) {
 | |
|                   commonAttributes.delete(name);
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // preserve transform on children when group has clip-path or mask
 | |
|         if (
 | |
|           node.attributes['clip-path'] != null ||
 | |
|           node.attributes.mask != null
 | |
|         ) {
 | |
|           commonAttributes.delete('transform');
 | |
|         }
 | |
| 
 | |
|         // preserve transform when all children are paths
 | |
|         // so the transform could be applied to path data by other plugins
 | |
|         if (everyChildIsPath) {
 | |
|           commonAttributes.delete('transform');
 | |
|         }
 | |
| 
 | |
|         // add common children attributes to group
 | |
|         for (const [name, value] of commonAttributes) {
 | |
|           if (name === 'transform') {
 | |
|             if (node.attributes.transform != null) {
 | |
|               node.attributes.transform = `${node.attributes.transform} ${value}`;
 | |
|             } else {
 | |
|               node.attributes.transform = value;
 | |
|             }
 | |
|           } else {
 | |
|             node.attributes[name] = value;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         // delete common attributes from children
 | |
|         for (const child of node.children) {
 | |
|           if (child.type === 'element') {
 | |
|             for (const [name] of commonAttributes) {
 | |
|               delete child.attributes[name];
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|     },
 | |
|   };
 | |
| };
 |