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