removeUnknownsAndDefaults.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. 'use strict';
  2. const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
  3. const { collectStylesheet, computeStyle } = require('../lib/style.js');
  4. const {
  5. elems,
  6. attrsGroups,
  7. elemsGroups,
  8. attrsGroupsDefaults,
  9. presentationNonInheritableGroupAttrs,
  10. } = require('./_collections');
  11. exports.type = 'visitor';
  12. exports.name = 'removeUnknownsAndDefaults';
  13. exports.active = true;
  14. exports.description =
  15. 'removes unknown elements content and attributes, removes attrs with default values';
  16. // resolve all groups references
  17. /**
  18. * @type {Map<string, Set<string>>}
  19. */
  20. const allowedChildrenPerElement = new Map();
  21. /**
  22. * @type {Map<string, Set<string>>}
  23. */
  24. const allowedAttributesPerElement = new Map();
  25. /**
  26. * @type {Map<string, Map<string, string>>}
  27. */
  28. const attributesDefaultsPerElement = new Map();
  29. for (const [name, config] of Object.entries(elems)) {
  30. /**
  31. * @type {Set<string>}
  32. */
  33. const allowedChildren = new Set();
  34. if (config.content) {
  35. for (const elementName of config.content) {
  36. allowedChildren.add(elementName);
  37. }
  38. }
  39. if (config.contentGroups) {
  40. for (const contentGroupName of config.contentGroups) {
  41. const elemsGroup = elemsGroups[contentGroupName];
  42. if (elemsGroup) {
  43. for (const elementName of elemsGroup) {
  44. allowedChildren.add(elementName);
  45. }
  46. }
  47. }
  48. }
  49. /**
  50. * @type {Set<string>}
  51. */
  52. const allowedAttributes = new Set();
  53. if (config.attrs) {
  54. for (const attrName of config.attrs) {
  55. allowedAttributes.add(attrName);
  56. }
  57. }
  58. /**
  59. * @type {Map<string, string>}
  60. */
  61. const attributesDefaults = new Map();
  62. if (config.defaults) {
  63. for (const [attrName, defaultValue] of Object.entries(config.defaults)) {
  64. attributesDefaults.set(attrName, defaultValue);
  65. }
  66. }
  67. for (const attrsGroupName of config.attrsGroups) {
  68. const attrsGroup = attrsGroups[attrsGroupName];
  69. if (attrsGroup) {
  70. for (const attrName of attrsGroup) {
  71. allowedAttributes.add(attrName);
  72. }
  73. }
  74. const groupDefaults = attrsGroupsDefaults[attrsGroupName];
  75. if (groupDefaults) {
  76. for (const [attrName, defaultValue] of Object.entries(groupDefaults)) {
  77. attributesDefaults.set(attrName, defaultValue);
  78. }
  79. }
  80. }
  81. allowedChildrenPerElement.set(name, allowedChildren);
  82. allowedAttributesPerElement.set(name, allowedAttributes);
  83. attributesDefaultsPerElement.set(name, attributesDefaults);
  84. }
  85. /**
  86. * Remove unknown elements content and attributes,
  87. * remove attributes with default values.
  88. *
  89. * @author Kir Belevich
  90. *
  91. * @type {import('../lib/types').Plugin<{
  92. * unknownContent?: boolean,
  93. * unknownAttrs?: boolean,
  94. * defaultAttrs?: boolean,
  95. * uselessOverrides?: boolean,
  96. * keepDataAttrs?: boolean,
  97. * keepAriaAttrs?: boolean,
  98. * keepRoleAttr?: boolean,
  99. * }>}
  100. */
  101. exports.fn = (root, params) => {
  102. const {
  103. unknownContent = true,
  104. unknownAttrs = true,
  105. defaultAttrs = true,
  106. uselessOverrides = true,
  107. keepDataAttrs = true,
  108. keepAriaAttrs = true,
  109. keepRoleAttr = false,
  110. } = params;
  111. const stylesheet = collectStylesheet(root);
  112. return {
  113. element: {
  114. enter: (node, parentNode) => {
  115. // skip namespaced elements
  116. if (node.name.includes(':')) {
  117. return;
  118. }
  119. // skip visiting foreignObject subtree
  120. if (node.name === 'foreignObject') {
  121. return visitSkip;
  122. }
  123. // remove unknown element's content
  124. if (unknownContent && parentNode.type === 'element') {
  125. const allowedChildren = allowedChildrenPerElement.get(
  126. parentNode.name
  127. );
  128. if (allowedChildren == null || allowedChildren.size === 0) {
  129. // remove unknown elements
  130. if (allowedChildrenPerElement.get(node.name) == null) {
  131. detachNodeFromParent(node, parentNode);
  132. return;
  133. }
  134. } else {
  135. // remove not allowed children
  136. if (allowedChildren.has(node.name) === false) {
  137. detachNodeFromParent(node, parentNode);
  138. return;
  139. }
  140. }
  141. }
  142. const allowedAttributes = allowedAttributesPerElement.get(node.name);
  143. const attributesDefaults = attributesDefaultsPerElement.get(node.name);
  144. const computedParentStyle =
  145. parentNode.type === 'element'
  146. ? computeStyle(stylesheet, parentNode)
  147. : null;
  148. // remove element's unknown attrs and attrs with default values
  149. for (const [name, value] of Object.entries(node.attributes)) {
  150. if (keepDataAttrs && name.startsWith('data-')) {
  151. continue;
  152. }
  153. if (keepAriaAttrs && name.startsWith('aria-')) {
  154. continue;
  155. }
  156. if (keepRoleAttr && name === 'role') {
  157. continue;
  158. }
  159. // skip xmlns attribute
  160. if (name === 'xmlns') {
  161. continue;
  162. }
  163. // skip namespaced attributes except xml:* and xlink:*
  164. if (name.includes(':')) {
  165. const [prefix] = name.split(':');
  166. if (prefix !== 'xml' && prefix !== 'xlink') {
  167. continue;
  168. }
  169. }
  170. if (
  171. unknownAttrs &&
  172. allowedAttributes &&
  173. allowedAttributes.has(name) === false
  174. ) {
  175. delete node.attributes[name];
  176. }
  177. if (
  178. defaultAttrs &&
  179. node.attributes.id == null &&
  180. attributesDefaults &&
  181. attributesDefaults.get(name) === value
  182. ) {
  183. // keep defaults if parent has own or inherited style
  184. if (
  185. computedParentStyle == null ||
  186. computedParentStyle[name] == null
  187. ) {
  188. delete node.attributes[name];
  189. }
  190. }
  191. if (uselessOverrides && node.attributes.id == null) {
  192. const style =
  193. computedParentStyle == null ? null : computedParentStyle[name];
  194. if (
  195. presentationNonInheritableGroupAttrs.includes(name) === false &&
  196. style != null &&
  197. style.type === 'static' &&
  198. style.value === value
  199. ) {
  200. delete node.attributes[name];
  201. }
  202. }
  203. }
  204. },
  205. },
  206. };
  207. };