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];
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      },
 | 
						|
    },
 | 
						|
  };
 | 
						|
};
 |