htmlnano 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/docs/docs/010-introduction.md +4 -4
  4. package/docs/docs/020-usage.md +63 -23
  5. package/docs/docs/030-config.md +1 -1
  6. package/docs/docs/050-modules.md +500 -483
  7. package/docs/package-lock.json +289 -95
  8. package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
  9. package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
  10. package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
  11. package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
  12. package/index.d.ts +93 -0
  13. package/lib/helpers.js +4 -11
  14. package/lib/htmlnano.js +37 -55
  15. package/lib/modules/collapseAttributeWhitespace.js +11 -12
  16. package/lib/modules/collapseBooleanAttributes.js +33 -9
  17. package/lib/modules/collapseWhitespace.js +17 -19
  18. package/lib/modules/custom.js +0 -3
  19. package/lib/modules/deduplicateAttributeValues.js +3 -5
  20. package/lib/modules/mergeScripts.js +0 -11
  21. package/lib/modules/mergeStyles.js +2 -8
  22. package/lib/modules/minifyConditionalComments.js +4 -15
  23. package/lib/modules/minifyCss.js +5 -16
  24. package/lib/modules/minifyJs.js +8 -28
  25. package/lib/modules/minifyJson.js +5 -7
  26. package/lib/modules/minifySvg.js +13 -4
  27. package/lib/modules/minifyUrls.js +18 -34
  28. package/lib/modules/normalizeAttributeValues.js +85 -2
  29. package/lib/modules/removeAttributeQuotes.js +0 -2
  30. package/lib/modules/removeComments.js +10 -28
  31. package/lib/modules/removeEmptyAttributes.js +6 -6
  32. package/lib/modules/removeOptionalTags.js +9 -46
  33. package/lib/modules/removeRedundantAttributes.js +20 -68
  34. package/lib/modules/removeUnusedCss.js +7 -18
  35. package/lib/modules/sortAttributes.js +10 -25
  36. package/lib/modules/sortAttributesWithLists.js +7 -29
  37. package/lib/presets/ampSafe.js +2 -5
  38. package/lib/presets/max.js +2 -5
  39. package/lib/presets/safe.js +32 -15
  40. package/package.json +9 -15
  41. package/test.js +0 -48
@@ -17,7 +17,7 @@ const caseInsensitiveAttributes = {
17
17
  kind: ['track'],
18
18
  method: ['form'],
19
19
  preload: ['audio', 'video'],
20
- referrerpolicy: ['a', 'area', 'iframe', 'img', 'link'],
20
+ referrerpolicy: null,
21
21
  sandbox: ['iframe'],
22
22
  spellcheck: null,
23
23
  scope: ['th'],
@@ -29,13 +29,96 @@ const caseInsensitiveAttributes = {
29
29
  wrap: ['textarea']
30
30
  };
31
31
 
32
+ // https://html.spec.whatwg.org/#invalid-value-default
33
+ /** @typedef { [key: string]: { tag: null | string[], default: string, valid: string[] } } */
34
+ const invalidValueDefault = {
35
+ crossorigin: {
36
+ tag: null,
37
+ default: 'anonymous',
38
+ valid: ['', 'anonymous', 'use-credentials']
39
+ },
40
+ // https://html.spec.whatwg.org/#referrer-policy-attributes
41
+ // The attribute's invalid value default and missing value default are both the empty string state.
42
+ referrerpolicy: {
43
+ tag: null,
44
+ default: '',
45
+ valid: ['', 'url', 'origin', 'no-referrer', 'no-referrer-when-downgrade', 'same-origin', 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url']
46
+ },
47
+ // https://html.spec.whatwg.org/#lazy-loading-attributes
48
+ loading: {
49
+ tag: ['img', 'iframe'],
50
+ default: 'eager',
51
+ valid: ['lazy', 'eager']
52
+ },
53
+ // https://html.spec.whatwg.org/#the-img-element
54
+ // https://html.spec.whatwg.org/#image-decoding-hint
55
+ decoding: {
56
+ tag: ['img'],
57
+ default: 'auto',
58
+ valid: ['auto', 'sync', 'async']
59
+ },
60
+ // https://html.spec.whatwg.org/#the-track-element
61
+ kind: {
62
+ tag: ['track'],
63
+ default: 'metadata',
64
+ valid: ['subtitles', 'captions', 'descriptions', 'chapters', 'metadata']
65
+ },
66
+ autocomplete: {
67
+ tag: null,
68
+ default: 'on',
69
+ valid: ['on', 'off']
70
+ },
71
+ type: {
72
+ tag: ['button'],
73
+ default: 'submit',
74
+ valid: ['submit', 'reset', 'button']
75
+ },
76
+ wrap: {
77
+ tag: ['textarea'],
78
+ default: 'soft',
79
+ valid: ['soft', 'hard']
80
+ },
81
+ // https://html.spec.whatwg.org/#the-hidden-attribute
82
+ hidden: {
83
+ tag: null,
84
+ default: 'hidden',
85
+ valid: ['hidden', 'until-found']
86
+ },
87
+ // https://html.spec.whatwg.org/#autocapitalization
88
+ autocapitalize: {
89
+ tag: null,
90
+ default: 'sentences',
91
+ valid: ['none', 'off', 'on', 'sentences', 'words', 'characters']
92
+ },
93
+ // https://html.spec.whatwg.org/#the-marquee-element
94
+ behavior: {
95
+ tag: ['marquee'],
96
+ default: 'scroll',
97
+ valid: ['scroll', 'slide', 'alternate']
98
+ },
99
+ direction: {
100
+ tag: ['marquee'],
101
+ default: 'left',
102
+ valid: ['left', 'right', 'up', 'down']
103
+ }
104
+ };
32
105
  function onAttrs() {
33
106
  return (attrs, node) => {
34
107
  const newAttrs = attrs;
35
108
  Object.entries(attrs).forEach(([attrName, attrValue]) => {
109
+ let newAttrValue = attrValue;
36
110
  if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || caseInsensitiveAttributes[attrName].includes(node.tag))) {
37
- newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
111
+ newAttrValue = typeof attrValue.toLowerCase === 'function' ? attrValue.toLowerCase() : attrValue;
112
+ }
113
+ if (Object.hasOwnProperty.call(invalidValueDefault, attrName)) {
114
+ const meta = invalidValueDefault[attrName];
115
+ if (meta.tag === null || node && node.tag && meta.tag.includes(node.tag)) {
116
+ if (!meta.valid.includes(newAttrValue)) {
117
+ newAttrValue = meta.default;
118
+ }
119
+ }
38
120
  }
121
+ newAttrs[attrName] = newAttrValue;
39
122
  });
40
123
  return newAttrs;
41
124
  };
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = removeAttributeQuotes;
7
-
8
7
  // Specification: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
9
8
  // See also: https://github.com/posthtml/posthtml-render/pull/30
10
9
  // See also: https://github.com/posthtml/htmlnano/issues/6#issuecomment-707105334
@@ -14,6 +13,5 @@ function removeAttributeQuotes(tree) {
14
13
  if (tree.options && typeof tree.options.quoteAllAttributes === 'undefined') {
15
14
  tree.options.quoteAllAttributes = false;
16
15
  }
17
-
18
16
  return tree;
19
17
  }
@@ -5,100 +5,82 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.onContent = onContent;
7
7
  exports.onNode = onNode;
8
-
9
8
  var _helpers = require("../helpers");
10
-
11
9
  const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
12
- /** Removes HTML comments */
13
10
 
11
+ /** Removes HTML comments */
14
12
  function onNode(options, removeType) {
15
13
  if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
16
14
  removeType = 'safe';
17
15
  }
18
-
19
16
  return node => {
20
17
  if (isCommentToRemove(node, removeType)) {
21
18
  return '';
22
19
  }
23
-
24
20
  return node;
25
21
  };
26
22
  }
27
-
28
23
  function onContent(options, removeType) {
29
24
  if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
30
25
  removeType = 'safe';
31
26
  }
32
-
33
27
  return contents => {
34
28
  return contents.filter(content => !isCommentToRemove(content, removeType));
35
29
  };
36
30
  }
37
-
38
31
  function isCommentToRemove(text, removeType) {
39
32
  if (typeof text !== 'string') {
40
33
  return false;
41
34
  }
42
-
43
35
  if (!(0, _helpers.isComment)(text)) {
44
36
  // Not HTML comment
45
37
  return false;
46
38
  }
47
-
48
39
  if (removeType === 'safe') {
49
- const isNoindex = text === '<!--noindex-->' || text === '<!--/noindex-->'; // Don't remove noindex comments.
40
+ const isNoindex = text === '<!--noindex-->' || text === '<!--/noindex-->';
41
+ // Don't remove noindex comments.
50
42
  // See: https://yandex.com/support/webmaster/controlling-robot/html.xml
51
-
52
43
  if (isNoindex) {
53
44
  return false;
54
45
  }
55
-
56
- const isServerSideExclude = text === '<!--sse-->' || text === '<!--/sse-->'; // Don't remove sse comments.
46
+ const isServerSideExclude = text === '<!--sse-->' || text === '<!--/sse-->';
47
+ // Don't remove sse comments.
57
48
  // See: https://support.cloudflare.com/hc/en-us/articles/200170036-What-does-Server-Side-Excludes-SSE-do-
58
-
59
49
  if (isServerSideExclude) {
60
50
  return false;
61
- } // https://en.wikipedia.org/wiki/Conditional_comment
62
-
51
+ }
63
52
 
53
+ // https://en.wikipedia.org/wiki/Conditional_comment
64
54
  if ((0, _helpers.isConditionalComment)(text)) {
65
55
  return false;
66
- } // Hexo: https://hexo.io/docs/tag-plugins#Post-Excerpt
56
+ }
57
+
58
+ // Hexo: https://hexo.io/docs/tag-plugins#Post-Excerpt
67
59
  // Hugo: https://gohugo.io/content-management/summaries/#manual-summary-splitting
68
60
  // WordPress: https://wordpress.com/support/wordpress-editor/blocks/more-block/2/
69
61
  // Jekyll: https://jekyllrb.com/docs/posts/#post-excerpts
70
-
71
-
72
62
  const isCMSExcerptComment = MATCH_EXCERPT_REGEXP.test(text);
73
-
74
63
  if (isCMSExcerptComment) {
75
64
  return false;
76
65
  }
77
66
  }
78
-
79
67
  if (isMatcher(removeType)) {
80
68
  return isMatch(text, removeType);
81
69
  }
82
-
83
70
  return true;
84
71
  }
85
-
86
72
  function isMatch(input, matcher) {
87
73
  if (matcher instanceof RegExp) {
88
74
  return matcher.test(input);
89
75
  }
90
-
91
76
  if (typeof matcher === 'function') {
92
77
  return Boolean(matcher(input));
93
78
  }
94
-
95
79
  return false;
96
80
  }
97
-
98
81
  function isMatcher(matcher) {
99
82
  if (matcher instanceof RegExp || typeof matcher === 'function') {
100
83
  return true;
101
84
  }
102
-
103
85
  return false;
104
86
  }
@@ -4,9 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.onAttrs = onAttrs;
7
-
8
7
  var _helpers = require("../helpers");
9
-
10
8
  const safeToRemoveAttrs = {
11
9
  id: null,
12
10
  class: null,
@@ -55,15 +53,17 @@ const safeToRemoveAttrs = {
55
53
  value: ['button', 'input', 'li'],
56
54
  width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
57
55
  };
58
-
59
56
  function onAttrs() {
60
57
  return (attrs, node) => {
61
- const newAttrs = { ...attrs
58
+ const newAttrs = {
59
+ ...attrs
62
60
  };
63
61
  Object.entries(attrs).forEach(([attrName, attrValue]) => {
64
62
  if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(safeToRemoveAttrs, attrName) && (safeToRemoveAttrs[attrName] === null || safeToRemoveAttrs[attrName].includes(node.tag))) {
65
- if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
66
- delete newAttrs[attrName];
63
+ if (typeof attrValue === 'string') {
64
+ if (attrValue === '' || attrValue.trim() === '') {
65
+ delete newAttrs[attrName];
66
+ }
67
67
  }
68
68
  }
69
69
  });
@@ -4,34 +4,26 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = removeOptionalTags;
7
-
8
7
  var _helpers = require("../helpers");
9
-
10
8
  const startWithWhitespacePattern = /^\s+/;
11
9
  const bodyStartTagCantBeOmittedWithFirstChildTags = new Set(['meta', 'link', 'script', 'style']);
12
10
  const tbodyStartTagCantBeOmittedWithPrecededTags = new Set(['tbody', 'thead', 'tfoot']);
13
11
  const tbodyEndTagCantBeOmittedWithFollowedTags = new Set(['tbody', 'tfoot']);
14
-
15
12
  function isEmptyTextNode(node) {
16
13
  if (typeof node === 'string' && node.trim() === '') {
17
14
  return true;
18
15
  }
19
-
20
16
  return false;
21
17
  }
22
-
23
18
  function isEmptyNode(node) {
24
19
  if (!node.content) {
25
20
  return true;
26
21
  }
27
-
28
22
  if (node.content.length) {
29
23
  return !node.content.filter(n => typeof n === 'string' && isEmptyTextNode(n) ? false : true).length;
30
24
  }
31
-
32
25
  return true;
33
26
  }
34
-
35
27
  function getFirstChildTag(node, nonEmpty = true) {
36
28
  if (node.content && node.content.length) {
37
29
  if (nonEmpty) {
@@ -43,10 +35,8 @@ function getFirstChildTag(node, nonEmpty = true) {
43
35
  return node.content[0] || null;
44
36
  }
45
37
  }
46
-
47
38
  return null;
48
39
  }
49
-
50
40
  function getPrevNode(tree, currentNodeIndex, nonEmpty = false) {
51
41
  if (nonEmpty) {
52
42
  for (let i = currentNodeIndex - 1; i >= 0; i--) {
@@ -57,10 +47,8 @@ function getPrevNode(tree, currentNodeIndex, nonEmpty = false) {
57
47
  } else {
58
48
  return tree[currentNodeIndex - 1] || null;
59
49
  }
60
-
61
50
  return null;
62
51
  }
63
-
64
52
  function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
65
53
  if (nonEmpty) {
66
54
  for (let i = currentNodeIndex + 1; i < tree.length; i++) {
@@ -71,149 +59,124 @@ function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
71
59
  } else {
72
60
  return tree[currentNodeIndex + 1] || null;
73
61
  }
74
-
75
62
  return null;
76
- } // Specification https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
63
+ }
77
64
 
65
+ // Specification https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
78
66
  /** Remove optional tag in the DOM */
79
-
80
-
81
67
  function removeOptionalTags(tree) {
82
68
  tree.forEach((node, index) => {
83
69
  if (!node.tag) return node;
84
- if (node.attrs && Object.keys(node.attrs).length) return node; // const prevNode = getPrevNode(tree, index);
70
+ if (node.attrs && Object.keys(node.attrs).length) return node;
85
71
 
72
+ // const prevNode = getPrevNode(tree, index);
86
73
  const prevNonEmptyNode = getPrevNode(tree, index, true);
87
74
  const nextNode = getNextNode(tree, index);
88
75
  const nextNonEmptyNode = getNextNode(tree, index, true);
89
76
  const firstChildNode = getFirstChildTag(node, false);
90
77
  const firstNonEmptyChildNode = getFirstChildTag(node);
78
+
91
79
  /**
92
80
  * An "html" element's start tag may be omitted if the first thing inside the "html" element is not a comment.
93
81
  * An "html" element's end tag may be omitted if the "html" element is not IMMEDIATELY followed by a comment.
94
82
  */
95
-
96
83
  if (node.tag === 'html') {
97
84
  let isHtmlStartTagCanBeOmitted = true;
98
85
  let isHtmlEndTagCanBeOmitted = true;
99
-
100
86
  if (typeof firstNonEmptyChildNode === 'string' && (0, _helpers.isComment)(firstNonEmptyChildNode)) {
101
87
  isHtmlStartTagCanBeOmitted = false;
102
88
  }
103
-
104
89
  if (typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNonEmptyNode)) {
105
90
  isHtmlEndTagCanBeOmitted = false;
106
91
  }
107
-
108
92
  if (isHtmlStartTagCanBeOmitted && isHtmlEndTagCanBeOmitted) {
109
93
  node.tag = false;
110
94
  }
111
95
  }
96
+
112
97
  /**
113
98
  * A "head" element's start tag may be omitted if the element is empty, or if the first thing inside the "head" element is an element.
114
99
  * A "head" element's end tag may be omitted if the "head" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
115
100
  */
116
-
117
-
118
101
  if (node.tag === 'head') {
119
102
  let isHeadStartTagCanBeOmitted = false;
120
103
  let isHeadEndTagCanBeOmitted = true;
121
-
122
104
  if (isEmptyNode(node) || firstNonEmptyChildNode && firstNonEmptyChildNode.tag) {
123
105
  isHeadStartTagCanBeOmitted = true;
124
106
  }
125
-
126
107
  if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNode)) {
127
108
  isHeadEndTagCanBeOmitted = false;
128
109
  }
129
-
130
110
  if (isHeadStartTagCanBeOmitted && isHeadEndTagCanBeOmitted) {
131
111
  node.tag = false;
132
112
  }
133
113
  }
114
+
134
115
  /**
135
116
  * A "body" element's start tag may be omitted if the element is empty, or if the first thing inside the "body" element is not ASCII whitespace or a comment, except if the first thing inside the "body" element is a "meta", "link", "script", "style", or "template" element.
136
117
  * A "body" element's end tag may be omitted if the "body" element is not IMMEDIATELY followed by a comment.
137
118
  */
138
-
139
-
140
119
  if (node.tag === 'body') {
141
120
  let isBodyStartTagCanBeOmitted = true;
142
121
  let isBodyEndTagCanBeOmitted = true;
143
-
144
122
  if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstNonEmptyChildNode === 'string' && (0, _helpers.isComment)(firstNonEmptyChildNode)) {
145
123
  isBodyStartTagCanBeOmitted = false;
146
124
  }
147
-
148
125
  if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstNonEmptyChildNode.tag)) {
149
126
  isBodyStartTagCanBeOmitted = false;
150
127
  }
151
-
152
128
  if (nextNode && typeof nextNode === 'string' && (0, _helpers.isComment)(nextNode)) {
153
129
  isBodyEndTagCanBeOmitted = false;
154
130
  }
155
-
156
131
  if (isBodyStartTagCanBeOmitted && isBodyEndTagCanBeOmitted) {
157
132
  node.tag = false;
158
133
  }
159
134
  }
135
+
160
136
  /**
161
137
  * A "colgroup" element's start tag may be omitted if the first thing inside the "colgroup" element is a "col" element, and if the element is not IMMEDIATELY preceded by another "colgroup" element. It can't be omitted if the element is empty.
162
138
  * A "colgroup" element's end tag may be omitted if the "colgroup" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
163
139
  */
164
-
165
-
166
140
  if (node.tag === 'colgroup') {
167
141
  let isColgroupStartTagCanBeOmitted = false;
168
142
  let isColgroupEndTagCanBeOmitted = true;
169
-
170
143
  if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'col') {
171
144
  isColgroupStartTagCanBeOmitted = true;
172
145
  }
173
-
174
146
  if (prevNonEmptyNode && prevNonEmptyNode.tag && prevNonEmptyNode.tag === 'colgroup') {
175
147
  isColgroupStartTagCanBeOmitted = false;
176
148
  }
177
-
178
149
  if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNonEmptyNode)) {
179
150
  isColgroupEndTagCanBeOmitted = false;
180
151
  }
181
-
182
152
  if (isColgroupStartTagCanBeOmitted && isColgroupEndTagCanBeOmitted) {
183
153
  node.tag = false;
184
154
  }
185
155
  }
156
+
186
157
  /**
187
158
  * A "tbody" element's start tag may be omitted if the first thing inside the "tbody" element is a "tr" element, and if the element is not immediately preceded by another "tbody", "thead" or "tfoot" element. It can't be omitted if the element is empty.
188
159
  * A "tbody" element's end tag may be omitted if the "tbody" element is not IMMEDIATELY followed by a "tbody" or "tfoot" element.
189
160
  */
190
-
191
-
192
161
  if (node.tag === 'tbody') {
193
162
  let isTbodyStartTagCanBeOmitted = false;
194
163
  let isTbodyEndTagCanBeOmitted = true;
195
-
196
164
  if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'tr') {
197
165
  isTbodyStartTagCanBeOmitted = true;
198
166
  }
199
-
200
167
  if (prevNonEmptyNode && prevNonEmptyNode.tag && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevNonEmptyNode.tag)) {
201
168
  isTbodyStartTagCanBeOmitted = false;
202
169
  }
203
-
204
170
  if (nextNonEmptyNode && nextNonEmptyNode.tag && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextNonEmptyNode.tag)) {
205
171
  isTbodyEndTagCanBeOmitted = false;
206
172
  }
207
-
208
173
  if (isTbodyStartTagCanBeOmitted && isTbodyEndTagCanBeOmitted) {
209
174
  node.tag = false;
210
175
  }
211
176
  }
212
-
213
177
  if (node.content && node.content.length) {
214
178
  removeOptionalTags(node.content);
215
179
  }
216
-
217
180
  return node;
218
181
  });
219
182
  return tree;
@@ -7,16 +7,19 @@ exports.onAttrs = onAttrs;
7
7
  exports.redundantScriptTypes = void 0;
8
8
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#JavaScript_types
9
9
  const redundantScriptTypes = new Set(['application/javascript', 'application/ecmascript', 'application/x-ecmascript', 'application/x-javascript', 'text/javascript', 'text/ecmascript', 'text/javascript1.0', 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4', 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript']);
10
+
11
+ // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default
10
12
  exports.redundantScriptTypes = redundantScriptTypes;
11
- const redundantAttributes = {
13
+ const missingValueDefaultAttributes = {
12
14
  'form': {
13
15
  'method': 'get'
14
16
  },
15
- 'input': {
16
- 'type': 'text'
17
+ input: {
18
+ type: 'text'
17
19
  },
18
- 'button': {
19
- 'type': 'submit'
20
+ button: {
21
+ // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
22
+ type: 'submit'
20
23
  },
21
24
  'script': {
22
25
  'language': 'javascript',
@@ -25,10 +28,8 @@ const redundantAttributes = {
25
28
  if (attrName.toLowerCase() !== 'type') {
26
29
  continue;
27
30
  }
28
-
29
31
  return redundantScriptTypes.has(attrValue);
30
32
  }
31
-
32
33
  return false;
33
34
  },
34
35
  // Remove attribute if the function returns false
@@ -43,55 +44,37 @@ const redundantAttributes = {
43
44
  'type': 'text/css'
44
45
  },
45
46
  'link': {
46
- 'media': 'all',
47
+ media: 'all',
47
48
  'type': attrs => {
48
49
  // https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
49
50
  let isRelStyleSheet = false;
50
51
  let isTypeTextCSS = false;
51
-
52
52
  if (attrs) {
53
53
  for (const [attrName, attrValue] of Object.entries(attrs)) {
54
54
  if (attrName.toLowerCase() === 'rel' && attrValue === 'stylesheet') {
55
55
  isRelStyleSheet = true;
56
56
  }
57
-
58
57
  if (attrName.toLowerCase() === 'type' && attrValue === 'text/css') {
59
58
  isTypeTextCSS = true;
60
59
  }
61
60
  }
62
- } // Only "text/css" is redudant for link[rel=stylesheet]. Otherwise "type" shouldn't be removed
63
-
61
+ }
64
62
 
63
+ // Only "text/css" is redudant for link[rel=stylesheet]. Otherwise "type" shouldn't be removed
65
64
  return isRelStyleSheet && isTypeTextCSS;
66
65
  }
67
66
  },
68
67
  // See: https://html.spec.whatwg.org/#lazy-loading-attributes
69
- 'img': {
70
- 'loading': 'eager'
71
- },
72
- 'iframe': {
73
- 'loading': 'eager'
74
- }
75
- }; // See: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default
76
-
77
- const canBeReplacedWithEmptyStringAttributes = {
78
- audio: {
79
- // https://html.spec.whatwg.org/#attr-media-preload
80
- preload: 'auto'
81
- },
82
- video: {
83
- preload: 'auto'
84
- },
85
- form: {
86
- // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
87
- autocomplete: 'on'
88
- },
89
68
  img: {
69
+ 'loading': 'eager',
90
70
  // https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decoding
91
71
  decoding: 'auto'
92
72
  },
73
+ iframe: {
74
+ 'loading': 'eager'
75
+ },
76
+ // https://html.spec.whatwg.org/multipage/media.html#htmltrackelement
93
77
  track: {
94
- // https://html.spec.whatwg.org/multipage/media.html#htmltrackelement
95
78
  kind: 'subtitles'
96
79
  },
97
80
  textarea: {
@@ -101,61 +84,30 @@ const canBeReplacedWithEmptyStringAttributes = {
101
84
  area: {
102
85
  // https://html.spec.whatwg.org/multipage/image-maps.html#attr-area-shape
103
86
  shape: 'rect'
104
- },
105
- button: {
106
- // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
107
- type: 'submit'
108
- },
109
- input: {
110
- // https://html.spec.whatwg.org/multipage/input.html#states-of-the-type-attribute
111
- type: 'text'
112
87
  }
113
88
  };
114
- const tagsHaveRedundantAttributes = new Set(Object.keys(redundantAttributes));
115
- const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(canBeReplacedWithEmptyStringAttributes));
116
- /** Removes redundant attributes */
89
+ const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(missingValueDefaultAttributes));
117
90
 
91
+ /** Removes redundant attributes */
118
92
  function onAttrs() {
119
93
  return (attrs, node) => {
120
94
  if (!node.tag) return attrs;
121
95
  const newAttrs = attrs;
122
-
123
- if (tagsHaveRedundantAttributes.has(node.tag)) {
124
- const tagRedundantAttributes = redundantAttributes[node.tag];
125
-
96
+ if (tagsHaveMissingValueDefaultAttributes.has(node.tag)) {
97
+ const tagRedundantAttributes = missingValueDefaultAttributes[node.tag];
126
98
  for (const redundantAttributeName of Object.keys(tagRedundantAttributes)) {
127
99
  let tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
128
100
  let isRemove = false;
129
-
130
101
  if (typeof tagRedundantAttributeValue === 'function') {
131
102
  isRemove = tagRedundantAttributeValue(attrs);
132
103
  } else if (attrs[redundantAttributeName] === tagRedundantAttributeValue) {
133
104
  isRemove = true;
134
105
  }
135
-
136
106
  if (isRemove) {
137
107
  delete newAttrs[redundantAttributeName];
138
108
  }
139
109
  }
140
110
  }
141
-
142
- if (tagsHaveMissingValueDefaultAttributes.has(node.tag)) {
143
- const tagMissingValueDefaultAttributes = canBeReplacedWithEmptyStringAttributes[node.tag];
144
-
145
- for (const canBeReplacedWithEmptyStringAttributeName of Object.keys(tagMissingValueDefaultAttributes)) {
146
- let tagMissingValueDefaultAttribute = tagMissingValueDefaultAttributes[canBeReplacedWithEmptyStringAttributeName];
147
- let isReplace = false;
148
-
149
- if (attrs[canBeReplacedWithEmptyStringAttributeName] === tagMissingValueDefaultAttribute) {
150
- isReplace = true;
151
- }
152
-
153
- if (isReplace) {
154
- newAttrs[canBeReplacedWithEmptyStringAttributeName] = '';
155
- }
156
- }
157
- }
158
-
159
111
  return newAttrs;
160
112
  };
161
113
  }