219 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
 | |
| const { collectStylesheet, computeStyle } = require('../lib/style.js');
 | |
| const {
 | |
|   elems,
 | |
|   attrsGroups,
 | |
|   elemsGroups,
 | |
|   attrsGroupsDefaults,
 | |
|   presentationNonInheritableGroupAttrs,
 | |
| } = require('./_collections');
 | |
| 
 | |
| exports.type = 'visitor';
 | |
| exports.name = 'removeUnknownsAndDefaults';
 | |
| exports.active = true;
 | |
| exports.description =
 | |
|   'removes unknown elements content and attributes, removes attrs with default values';
 | |
| 
 | |
| // resolve all groups references
 | |
| 
 | |
| /**
 | |
|  * @type {Map<string, Set<string>>}
 | |
|  */
 | |
| const allowedChildrenPerElement = new Map();
 | |
| /**
 | |
|  * @type {Map<string, Set<string>>}
 | |
|  */
 | |
| const allowedAttributesPerElement = new Map();
 | |
| /**
 | |
|  * @type {Map<string, Map<string, string>>}
 | |
|  */
 | |
| const attributesDefaultsPerElement = new Map();
 | |
| 
 | |
| for (const [name, config] of Object.entries(elems)) {
 | |
|   /**
 | |
|    * @type {Set<string>}
 | |
|    */
 | |
|   const allowedChildren = new Set();
 | |
|   if (config.content) {
 | |
|     for (const elementName of config.content) {
 | |
|       allowedChildren.add(elementName);
 | |
|     }
 | |
|   }
 | |
|   if (config.contentGroups) {
 | |
|     for (const contentGroupName of config.contentGroups) {
 | |
|       const elemsGroup = elemsGroups[contentGroupName];
 | |
|       if (elemsGroup) {
 | |
|         for (const elementName of elemsGroup) {
 | |
|           allowedChildren.add(elementName);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   /**
 | |
|    * @type {Set<string>}
 | |
|    */
 | |
|   const allowedAttributes = new Set();
 | |
|   if (config.attrs) {
 | |
|     for (const attrName of config.attrs) {
 | |
|       allowedAttributes.add(attrName);
 | |
|     }
 | |
|   }
 | |
|   /**
 | |
|    * @type {Map<string, string>}
 | |
|    */
 | |
|   const attributesDefaults = new Map();
 | |
|   if (config.defaults) {
 | |
|     for (const [attrName, defaultValue] of Object.entries(config.defaults)) {
 | |
|       attributesDefaults.set(attrName, defaultValue);
 | |
|     }
 | |
|   }
 | |
|   for (const attrsGroupName of config.attrsGroups) {
 | |
|     const attrsGroup = attrsGroups[attrsGroupName];
 | |
|     if (attrsGroup) {
 | |
|       for (const attrName of attrsGroup) {
 | |
|         allowedAttributes.add(attrName);
 | |
|       }
 | |
|     }
 | |
|     const groupDefaults = attrsGroupsDefaults[attrsGroupName];
 | |
|     if (groupDefaults) {
 | |
|       for (const [attrName, defaultValue] of Object.entries(groupDefaults)) {
 | |
|         attributesDefaults.set(attrName, defaultValue);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   allowedChildrenPerElement.set(name, allowedChildren);
 | |
|   allowedAttributesPerElement.set(name, allowedAttributes);
 | |
|   attributesDefaultsPerElement.set(name, attributesDefaults);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Remove unknown elements content and attributes,
 | |
|  * remove attributes with default values.
 | |
|  *
 | |
|  * @author Kir Belevich
 | |
|  *
 | |
|  * @type {import('../lib/types').Plugin<{
 | |
|  *   unknownContent?: boolean,
 | |
|  *   unknownAttrs?: boolean,
 | |
|  *   defaultAttrs?: boolean,
 | |
|  *   uselessOverrides?: boolean,
 | |
|  *   keepDataAttrs?: boolean,
 | |
|  *   keepAriaAttrs?: boolean,
 | |
|  *   keepRoleAttr?: boolean,
 | |
|  * }>}
 | |
|  */
 | |
| exports.fn = (root, params) => {
 | |
|   const {
 | |
|     unknownContent = true,
 | |
|     unknownAttrs = true,
 | |
|     defaultAttrs = true,
 | |
|     uselessOverrides = true,
 | |
|     keepDataAttrs = true,
 | |
|     keepAriaAttrs = true,
 | |
|     keepRoleAttr = false,
 | |
|   } = params;
 | |
|   const stylesheet = collectStylesheet(root);
 | |
| 
 | |
|   return {
 | |
|     element: {
 | |
|       enter: (node, parentNode) => {
 | |
|         // skip namespaced elements
 | |
|         if (node.name.includes(':')) {
 | |
|           return;
 | |
|         }
 | |
|         // skip visiting foreignObject subtree
 | |
|         if (node.name === 'foreignObject') {
 | |
|           return visitSkip;
 | |
|         }
 | |
| 
 | |
|         // remove unknown element's content
 | |
|         if (unknownContent && parentNode.type === 'element') {
 | |
|           const allowedChildren = allowedChildrenPerElement.get(
 | |
|             parentNode.name
 | |
|           );
 | |
|           if (allowedChildren == null || allowedChildren.size === 0) {
 | |
|             // remove unknown elements
 | |
|             if (allowedChildrenPerElement.get(node.name) == null) {
 | |
|               detachNodeFromParent(node, parentNode);
 | |
|               return;
 | |
|             }
 | |
|           } else {
 | |
|             // remove not allowed children
 | |
|             if (allowedChildren.has(node.name) === false) {
 | |
|               detachNodeFromParent(node, parentNode);
 | |
|               return;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         const allowedAttributes = allowedAttributesPerElement.get(node.name);
 | |
|         const attributesDefaults = attributesDefaultsPerElement.get(node.name);
 | |
|         const computedParentStyle =
 | |
|           parentNode.type === 'element'
 | |
|             ? computeStyle(stylesheet, parentNode)
 | |
|             : null;
 | |
| 
 | |
|         // remove element's unknown attrs and attrs with default values
 | |
|         for (const [name, value] of Object.entries(node.attributes)) {
 | |
|           if (keepDataAttrs && name.startsWith('data-')) {
 | |
|             continue;
 | |
|           }
 | |
|           if (keepAriaAttrs && name.startsWith('aria-')) {
 | |
|             continue;
 | |
|           }
 | |
|           if (keepRoleAttr && name === 'role') {
 | |
|             continue;
 | |
|           }
 | |
|           // skip xmlns attribute
 | |
|           if (name === 'xmlns') {
 | |
|             continue;
 | |
|           }
 | |
|           // skip namespaced attributes except xml:* and xlink:*
 | |
|           if (name.includes(':')) {
 | |
|             const [prefix] = name.split(':');
 | |
|             if (prefix !== 'xml' && prefix !== 'xlink') {
 | |
|               continue;
 | |
|             }
 | |
|           }
 | |
| 
 | |
|           if (
 | |
|             unknownAttrs &&
 | |
|             allowedAttributes &&
 | |
|             allowedAttributes.has(name) === false
 | |
|           ) {
 | |
|             delete node.attributes[name];
 | |
|           }
 | |
|           if (
 | |
|             defaultAttrs &&
 | |
|             node.attributes.id == null &&
 | |
|             attributesDefaults &&
 | |
|             attributesDefaults.get(name) === value
 | |
|           ) {
 | |
|             // keep defaults if parent has own or inherited style
 | |
|             if (
 | |
|               computedParentStyle == null ||
 | |
|               computedParentStyle[name] == null
 | |
|             ) {
 | |
|               delete node.attributes[name];
 | |
|             }
 | |
|           }
 | |
|           if (uselessOverrides && node.attributes.id == null) {
 | |
|             const style =
 | |
|               computedParentStyle == null ? null : computedParentStyle[name];
 | |
|             if (
 | |
|               presentationNonInheritableGroupAttrs.includes(name) === false &&
 | |
|               style != null &&
 | |
|               style.type === 'static' &&
 | |
|               style.value === value
 | |
|             ) {
 | |
|               delete node.attributes[name];
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       },
 | |
|     },
 | |
|   };
 | |
| };
 |