summaryrefslogtreecommitdiff
path: root/node_modules/svgo/plugins/moveElemsAttrsToGroup.js
blob: 633f44396e5dde819aff13659c97485235f393d5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
'use strict';

const { visit } = require('../lib/xast.js');
const { inheritableAttrs, pathElems } = require('./_collections.js');

exports.name = 'moveElemsAttrsToGroup';
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('./plugins-types').Plugin<'moveElemsAttrsToGroup'>}
 */
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.has(child.name)) {
              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.has(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];
            }
          }
        }
      },
    },
  };
};