'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>} */ const allowedChildrenPerElement = new Map(); /** * @type {Map>} */ const allowedAttributesPerElement = new Map(); /** * @type {Map>} */ const attributesDefaultsPerElement = new Map(); for (const [name, config] of Object.entries(elems)) { /** * @type {Set} */ 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} */ const allowedAttributes = new Set(); if (config.attrs) { for (const attrName of config.attrs) { allowedAttributes.add(attrName); } } /** * @type {Map} */ 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]; } } } }, }, }; };