moveElemsAttrsToGroup.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. 'use strict';
  2. const { visit } = require('../lib/xast.js');
  3. const { inheritableAttrs, pathElems } = require('./_collections.js');
  4. exports.type = 'visitor';
  5. exports.name = 'moveElemsAttrsToGroup';
  6. exports.active = true;
  7. exports.description = 'Move common attributes of group children to the group';
  8. /**
  9. * Move common attributes of group children to the group
  10. *
  11. * @example
  12. * <g attr1="val1">
  13. * <g attr2="val2">
  14. * text
  15. * </g>
  16. * <circle attr2="val2" attr3="val3"/>
  17. * </g>
  18. * ⬇
  19. * <g attr1="val1" attr2="val2">
  20. * <g>
  21. * text
  22. * </g>
  23. * <circle attr3="val3"/>
  24. * </g>
  25. *
  26. * @author Kir Belevich
  27. *
  28. * @type {import('../lib/types').Plugin<void>}
  29. */
  30. exports.fn = (root) => {
  31. // find if any style element is present
  32. let deoptimizedWithStyles = false;
  33. visit(root, {
  34. element: {
  35. enter: (node) => {
  36. if (node.name === 'style') {
  37. deoptimizedWithStyles = true;
  38. }
  39. },
  40. },
  41. });
  42. return {
  43. element: {
  44. exit: (node) => {
  45. // process only groups with more than 1 children
  46. if (node.name !== 'g' || node.children.length <= 1) {
  47. return;
  48. }
  49. // deoptimize the plugin when style elements are present
  50. // selectors may rely on id, classes or tag names
  51. if (deoptimizedWithStyles) {
  52. return;
  53. }
  54. /**
  55. * find common attributes in group children
  56. * @type {Map<string, string>}
  57. */
  58. const commonAttributes = new Map();
  59. let initial = true;
  60. let everyChildIsPath = true;
  61. for (const child of node.children) {
  62. if (child.type === 'element') {
  63. if (pathElems.includes(child.name) === false) {
  64. everyChildIsPath = false;
  65. }
  66. if (initial) {
  67. initial = false;
  68. // collect all inheritable attributes from first child element
  69. for (const [name, value] of Object.entries(child.attributes)) {
  70. // consider only inheritable attributes
  71. if (inheritableAttrs.includes(name)) {
  72. commonAttributes.set(name, value);
  73. }
  74. }
  75. } else {
  76. // exclude uncommon attributes from initial list
  77. for (const [name, value] of commonAttributes) {
  78. if (child.attributes[name] !== value) {
  79. commonAttributes.delete(name);
  80. }
  81. }
  82. }
  83. }
  84. }
  85. // preserve transform on children when group has clip-path or mask
  86. if (
  87. node.attributes['clip-path'] != null ||
  88. node.attributes.mask != null
  89. ) {
  90. commonAttributes.delete('transform');
  91. }
  92. // preserve transform when all children are paths
  93. // so the transform could be applied to path data by other plugins
  94. if (everyChildIsPath) {
  95. commonAttributes.delete('transform');
  96. }
  97. // add common children attributes to group
  98. for (const [name, value] of commonAttributes) {
  99. if (name === 'transform') {
  100. if (node.attributes.transform != null) {
  101. node.attributes.transform = `${node.attributes.transform} ${value}`;
  102. } else {
  103. node.attributes.transform = value;
  104. }
  105. } else {
  106. node.attributes[name] = value;
  107. }
  108. }
  109. // delete common attributes from children
  110. for (const child of node.children) {
  111. if (child.type === 'element') {
  112. for (const [name] of commonAttributes) {
  113. delete child.attributes[name];
  114. }
  115. }
  116. }
  117. },
  118. },
  119. };
  120. };