removeOffCanvasPaths.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. 'use strict';
  2. /**
  3. * @typedef {import('../lib/types').PathDataItem} PathDataItem
  4. */
  5. const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
  6. const { parsePathData } = require('../lib/path.js');
  7. const { intersects } = require('./_path.js');
  8. exports.type = 'visitor';
  9. exports.name = 'removeOffCanvasPaths';
  10. exports.active = false;
  11. exports.description =
  12. 'removes elements that are drawn outside of the viewbox (disabled by default)';
  13. /**
  14. * Remove elements that are drawn outside of the viewbox.
  15. *
  16. * @author JoshyPHP
  17. *
  18. * @type {import('../lib/types').Plugin<void>}
  19. */
  20. exports.fn = () => {
  21. /**
  22. * @type {null | {
  23. * top: number,
  24. * right: number,
  25. * bottom: number,
  26. * left: number,
  27. * width: number,
  28. * height: number
  29. * }}
  30. */
  31. let viewBoxData = null;
  32. return {
  33. element: {
  34. enter: (node, parentNode) => {
  35. if (node.name === 'svg' && parentNode.type === 'root') {
  36. let viewBox = '';
  37. // find viewbox
  38. if (node.attributes.viewBox != null) {
  39. // remove commas and plus signs, normalize and trim whitespace
  40. viewBox = node.attributes.viewBox;
  41. } else if (
  42. node.attributes.height != null &&
  43. node.attributes.width != null
  44. ) {
  45. viewBox = `0 0 ${node.attributes.width} ${node.attributes.height}`;
  46. }
  47. // parse viewbox
  48. // remove commas and plus signs, normalize and trim whitespace
  49. viewBox = viewBox
  50. .replace(/[,+]|px/g, ' ')
  51. .replace(/\s+/g, ' ')
  52. .replace(/^\s*|\s*$/g, '');
  53. // ensure that the dimensions are 4 values separated by space
  54. const m =
  55. /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec(
  56. viewBox
  57. );
  58. if (m == null) {
  59. return;
  60. }
  61. const left = Number.parseFloat(m[1]);
  62. const top = Number.parseFloat(m[2]);
  63. const width = Number.parseFloat(m[3]);
  64. const height = Number.parseFloat(m[4]);
  65. // store the viewBox boundaries
  66. viewBoxData = {
  67. left,
  68. top,
  69. right: left + width,
  70. bottom: top + height,
  71. width,
  72. height,
  73. };
  74. }
  75. // consider that any item with a transform attribute is visible
  76. if (node.attributes.transform != null) {
  77. return visitSkip;
  78. }
  79. if (
  80. node.name === 'path' &&
  81. node.attributes.d != null &&
  82. viewBoxData != null
  83. ) {
  84. const pathData = parsePathData(node.attributes.d);
  85. // consider that a M command within the viewBox is visible
  86. let visible = false;
  87. for (const pathDataItem of pathData) {
  88. if (pathDataItem.command === 'M') {
  89. const [x, y] = pathDataItem.args;
  90. if (
  91. x >= viewBoxData.left &&
  92. x <= viewBoxData.right &&
  93. y >= viewBoxData.top &&
  94. y <= viewBoxData.bottom
  95. ) {
  96. visible = true;
  97. }
  98. }
  99. }
  100. if (visible) {
  101. return;
  102. }
  103. if (pathData.length === 2) {
  104. // close the path too short for intersects()
  105. pathData.push({ command: 'z', args: [] });
  106. }
  107. const { left, top, width, height } = viewBoxData;
  108. /**
  109. * @type {Array<PathDataItem>}
  110. */
  111. const viewBoxPathData = [
  112. { command: 'M', args: [left, top] },
  113. { command: 'h', args: [width] },
  114. { command: 'v', args: [height] },
  115. { command: 'H', args: [left] },
  116. { command: 'z', args: [] },
  117. ];
  118. if (intersects(viewBoxPathData, pathData) === false) {
  119. detachNodeFromParent(node, parentNode);
  120. }
  121. }
  122. },
  123. },
  124. };
  125. };