removeHiddenElems.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. 'use strict';
  2. const {
  3. querySelector,
  4. closestByName,
  5. detachNodeFromParent,
  6. } = require('../lib/xast.js');
  7. const { collectStylesheet, computeStyle } = require('../lib/style.js');
  8. const { parsePathData } = require('../lib/path.js');
  9. exports.name = 'removeHiddenElems';
  10. exports.type = 'visitor';
  11. exports.active = true;
  12. exports.description =
  13. 'removes hidden elements (zero sized, with absent attributes)';
  14. /**
  15. * Remove hidden elements with disabled rendering:
  16. * - display="none"
  17. * - opacity="0"
  18. * - circle with zero radius
  19. * - ellipse with zero x-axis or y-axis radius
  20. * - rectangle with zero width or height
  21. * - pattern with zero width or height
  22. * - image with zero width or height
  23. * - path with empty data
  24. * - polyline with empty points
  25. * - polygon with empty points
  26. *
  27. * @author Kir Belevich
  28. *
  29. * @type {import('../lib/types').Plugin<{
  30. * isHidden: boolean,
  31. * displayNone: boolean,
  32. * opacity0: boolean,
  33. * circleR0: boolean,
  34. * ellipseRX0: boolean,
  35. * ellipseRY0: boolean,
  36. * rectWidth0: boolean,
  37. * rectHeight0: boolean,
  38. * patternWidth0: boolean,
  39. * patternHeight0: boolean,
  40. * imageWidth0: boolean,
  41. * imageHeight0: boolean,
  42. * pathEmptyD: boolean,
  43. * polylineEmptyPoints: boolean,
  44. * polygonEmptyPoints: boolean,
  45. * }>}
  46. */
  47. exports.fn = (root, params) => {
  48. const {
  49. isHidden = true,
  50. displayNone = true,
  51. opacity0 = true,
  52. circleR0 = true,
  53. ellipseRX0 = true,
  54. ellipseRY0 = true,
  55. rectWidth0 = true,
  56. rectHeight0 = true,
  57. patternWidth0 = true,
  58. patternHeight0 = true,
  59. imageWidth0 = true,
  60. imageHeight0 = true,
  61. pathEmptyD = true,
  62. polylineEmptyPoints = true,
  63. polygonEmptyPoints = true,
  64. } = params;
  65. const stylesheet = collectStylesheet(root);
  66. return {
  67. element: {
  68. enter: (node, parentNode) => {
  69. // Removes hidden elements
  70. // https://www.w3schools.com/cssref/pr_class_visibility.asp
  71. const computedStyle = computeStyle(stylesheet, node);
  72. if (
  73. isHidden &&
  74. computedStyle.visibility &&
  75. computedStyle.visibility.type === 'static' &&
  76. computedStyle.visibility.value === 'hidden' &&
  77. // keep if any descendant enables visibility
  78. querySelector(node, '[visibility=visible]') == null
  79. ) {
  80. detachNodeFromParent(node, parentNode);
  81. return;
  82. }
  83. // display="none"
  84. //
  85. // https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
  86. // "A value of display: none indicates that the given element
  87. // and its children shall not be rendered directly"
  88. if (
  89. displayNone &&
  90. computedStyle.display &&
  91. computedStyle.display.type === 'static' &&
  92. computedStyle.display.value === 'none' &&
  93. // markers with display: none still rendered
  94. node.name !== 'marker'
  95. ) {
  96. detachNodeFromParent(node, parentNode);
  97. return;
  98. }
  99. // opacity="0"
  100. //
  101. // https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
  102. if (
  103. opacity0 &&
  104. computedStyle.opacity &&
  105. computedStyle.opacity.type === 'static' &&
  106. computedStyle.opacity.value === '0' &&
  107. // transparent element inside clipPath still affect clipped elements
  108. closestByName(node, 'clipPath') == null
  109. ) {
  110. detachNodeFromParent(node, parentNode);
  111. return;
  112. }
  113. // Circles with zero radius
  114. //
  115. // https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
  116. // "A value of zero disables rendering of the element"
  117. //
  118. // <circle r="0">
  119. if (
  120. circleR0 &&
  121. node.name === 'circle' &&
  122. node.children.length === 0 &&
  123. node.attributes.r === '0'
  124. ) {
  125. detachNodeFromParent(node, parentNode);
  126. return;
  127. }
  128. // Ellipse with zero x-axis radius
  129. //
  130. // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
  131. // "A value of zero disables rendering of the element"
  132. //
  133. // <ellipse rx="0">
  134. if (
  135. ellipseRX0 &&
  136. node.name === 'ellipse' &&
  137. node.children.length === 0 &&
  138. node.attributes.rx === '0'
  139. ) {
  140. detachNodeFromParent(node, parentNode);
  141. return;
  142. }
  143. // Ellipse with zero y-axis radius
  144. //
  145. // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
  146. // "A value of zero disables rendering of the element"
  147. //
  148. // <ellipse ry="0">
  149. if (
  150. ellipseRY0 &&
  151. node.name === 'ellipse' &&
  152. node.children.length === 0 &&
  153. node.attributes.ry === '0'
  154. ) {
  155. detachNodeFromParent(node, parentNode);
  156. return;
  157. }
  158. // Rectangle with zero width
  159. //
  160. // https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
  161. // "A value of zero disables rendering of the element"
  162. //
  163. // <rect width="0">
  164. if (
  165. rectWidth0 &&
  166. node.name === 'rect' &&
  167. node.children.length === 0 &&
  168. node.attributes.width === '0'
  169. ) {
  170. detachNodeFromParent(node, parentNode);
  171. return;
  172. }
  173. // Rectangle with zero height
  174. //
  175. // https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
  176. // "A value of zero disables rendering of the element"
  177. //
  178. // <rect height="0">
  179. if (
  180. rectHeight0 &&
  181. rectWidth0 &&
  182. node.name === 'rect' &&
  183. node.children.length === 0 &&
  184. node.attributes.height === '0'
  185. ) {
  186. detachNodeFromParent(node, parentNode);
  187. return;
  188. }
  189. // Pattern with zero width
  190. //
  191. // https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
  192. // "A value of zero disables rendering of the element (i.e., no paint is applied)"
  193. //
  194. // <pattern width="0">
  195. if (
  196. patternWidth0 &&
  197. node.name === 'pattern' &&
  198. node.attributes.width === '0'
  199. ) {
  200. detachNodeFromParent(node, parentNode);
  201. return;
  202. }
  203. // Pattern with zero height
  204. //
  205. // https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
  206. // "A value of zero disables rendering of the element (i.e., no paint is applied)"
  207. //
  208. // <pattern height="0">
  209. if (
  210. patternHeight0 &&
  211. node.name === 'pattern' &&
  212. node.attributes.height === '0'
  213. ) {
  214. detachNodeFromParent(node, parentNode);
  215. return;
  216. }
  217. // Image with zero width
  218. //
  219. // https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
  220. // "A value of zero disables rendering of the element"
  221. //
  222. // <image width="0">
  223. if (
  224. imageWidth0 &&
  225. node.name === 'image' &&
  226. node.attributes.width === '0'
  227. ) {
  228. detachNodeFromParent(node, parentNode);
  229. return;
  230. }
  231. // Image with zero height
  232. //
  233. // https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
  234. // "A value of zero disables rendering of the element"
  235. //
  236. // <image height="0">
  237. if (
  238. imageHeight0 &&
  239. node.name === 'image' &&
  240. node.attributes.height === '0'
  241. ) {
  242. detachNodeFromParent(node, parentNode);
  243. return;
  244. }
  245. // Path with empty data
  246. //
  247. // https://www.w3.org/TR/SVG11/paths.html#DAttribute
  248. //
  249. // <path d=""/>
  250. if (pathEmptyD && node.name === 'path') {
  251. if (node.attributes.d == null) {
  252. detachNodeFromParent(node, parentNode);
  253. return;
  254. }
  255. const pathData = parsePathData(node.attributes.d);
  256. if (pathData.length === 0) {
  257. detachNodeFromParent(node, parentNode);
  258. return;
  259. }
  260. // keep single point paths for markers
  261. if (
  262. pathData.length === 1 &&
  263. computedStyle['marker-start'] == null &&
  264. computedStyle['marker-end'] == null
  265. ) {
  266. detachNodeFromParent(node, parentNode);
  267. return;
  268. }
  269. return;
  270. }
  271. // Polyline with empty points
  272. //
  273. // https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
  274. //
  275. // <polyline points="">
  276. if (
  277. polylineEmptyPoints &&
  278. node.name === 'polyline' &&
  279. node.attributes.points == null
  280. ) {
  281. detachNodeFromParent(node, parentNode);
  282. return;
  283. }
  284. // Polygon with empty points
  285. //
  286. // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
  287. //
  288. // <polygon points="">
  289. if (
  290. polygonEmptyPoints &&
  291. node.name === 'polygon' &&
  292. node.attributes.points == null
  293. ) {
  294. detachNodeFromParent(node, parentNode);
  295. return;
  296. }
  297. },
  298. },
  299. };
  300. };