collapseGroups.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. 'use strict';
  2. /**
  3. * @typedef {import('../lib/types').XastNode} XastNode
  4. */
  5. const { inheritableAttrs, elemsGroups } = require('./_collections.js');
  6. exports.type = 'visitor';
  7. exports.name = 'collapseGroups';
  8. exports.active = true;
  9. exports.description = 'collapses useless groups';
  10. /**
  11. * @type {(node: XastNode, name: string) => boolean}
  12. */
  13. const hasAnimatedAttr = (node, name) => {
  14. if (node.type === 'element') {
  15. if (
  16. elemsGroups.animation.includes(node.name) &&
  17. node.attributes.attributeName === name
  18. ) {
  19. return true;
  20. }
  21. for (const child of node.children) {
  22. if (hasAnimatedAttr(child, name)) {
  23. return true;
  24. }
  25. }
  26. }
  27. return false;
  28. };
  29. /**
  30. * Collapse useless groups.
  31. *
  32. * @example
  33. * <g>
  34. * <g attr1="val1">
  35. * <path d="..."/>
  36. * </g>
  37. * </g>
  38. * ⬇
  39. * <g>
  40. * <g>
  41. * <path attr1="val1" d="..."/>
  42. * </g>
  43. * </g>
  44. * ⬇
  45. * <path attr1="val1" d="..."/>
  46. *
  47. * @author Kir Belevich
  48. *
  49. * @type {import('../lib/types').Plugin<void>}
  50. */
  51. exports.fn = () => {
  52. return {
  53. element: {
  54. exit: (node, parentNode) => {
  55. if (parentNode.type === 'root' || parentNode.name === 'switch') {
  56. return;
  57. }
  58. // non-empty groups
  59. if (node.name !== 'g' || node.children.length === 0) {
  60. return;
  61. }
  62. // move group attibutes to the single child element
  63. if (
  64. Object.keys(node.attributes).length !== 0 &&
  65. node.children.length === 1
  66. ) {
  67. const firstChild = node.children[0];
  68. // TODO untangle this mess
  69. if (
  70. firstChild.type === 'element' &&
  71. firstChild.attributes.id == null &&
  72. node.attributes.filter == null &&
  73. (node.attributes.class == null ||
  74. firstChild.attributes.class == null) &&
  75. ((node.attributes['clip-path'] == null &&
  76. node.attributes.mask == null) ||
  77. (firstChild.name === 'g' &&
  78. node.attributes.transform == null &&
  79. firstChild.attributes.transform == null))
  80. ) {
  81. for (const [name, value] of Object.entries(node.attributes)) {
  82. // avoid copying to not conflict with animated attribute
  83. if (hasAnimatedAttr(firstChild, name)) {
  84. return;
  85. }
  86. if (firstChild.attributes[name] == null) {
  87. firstChild.attributes[name] = value;
  88. } else if (name === 'transform') {
  89. firstChild.attributes[name] =
  90. value + ' ' + firstChild.attributes[name];
  91. } else if (firstChild.attributes[name] === 'inherit') {
  92. firstChild.attributes[name] = value;
  93. } else if (
  94. inheritableAttrs.includes(name) === false &&
  95. firstChild.attributes[name] !== value
  96. ) {
  97. return;
  98. }
  99. delete node.attributes[name];
  100. }
  101. }
  102. }
  103. // collapse groups without attributes
  104. if (Object.keys(node.attributes).length === 0) {
  105. // animation elements "add" attributes to group
  106. // group should be preserved
  107. for (const child of node.children) {
  108. if (
  109. child.type === 'element' &&
  110. elemsGroups.animation.includes(child.name)
  111. ) {
  112. return;
  113. }
  114. }
  115. // replace current node with all its children
  116. const index = parentNode.children.indexOf(node);
  117. parentNode.children.splice(index, 1, ...node.children);
  118. // TODO remove in v3
  119. for (const child of node.children) {
  120. // @ts-ignore parentNode is forbidden for public usage
  121. // and will be moved in v3
  122. child.parentNode = parentNode;
  123. }
  124. }
  125. },
  126. },
  127. };
  128. };