htmlnano 2.0.2 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/docs/docs/010-introduction.md +4 -4
- package/docs/docs/020-usage.md +63 -23
- package/docs/docs/030-config.md +1 -1
- package/docs/docs/050-modules.md +500 -483
- package/docs/package-lock.json +289 -95
- package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
- package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
- package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
- package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
- package/index.d.ts +93 -0
- package/lib/helpers.js +4 -11
- package/lib/htmlnano.js +11 -36
- package/lib/modules/collapseAttributeWhitespace.js +11 -12
- package/lib/modules/collapseBooleanAttributes.js +33 -9
- package/lib/modules/collapseWhitespace.js +17 -19
- package/lib/modules/custom.js +0 -3
- package/lib/modules/deduplicateAttributeValues.js +3 -5
- package/lib/modules/mergeScripts.js +0 -11
- package/lib/modules/mergeStyles.js +2 -8
- package/lib/modules/minifyConditionalComments.js +4 -15
- package/lib/modules/minifyCss.js +5 -16
- package/lib/modules/minifyJs.js +8 -28
- package/lib/modules/minifyJson.js +2 -3
- package/lib/modules/minifySvg.js +13 -5
- package/lib/modules/minifyUrls.js +18 -34
- package/lib/modules/normalizeAttributeValues.js +85 -2
- package/lib/modules/removeAttributeQuotes.js +0 -2
- package/lib/modules/removeComments.js +10 -28
- package/lib/modules/removeEmptyAttributes.js +6 -6
- package/lib/modules/removeOptionalTags.js +9 -46
- package/lib/modules/removeRedundantAttributes.js +20 -68
- package/lib/modules/removeUnusedCss.js +7 -18
- package/lib/modules/sortAttributes.js +10 -25
- package/lib/modules/sortAttributesWithLists.js +7 -29
- package/lib/presets/ampSafe.js +2 -5
- package/lib/presets/max.js +2 -5
- package/lib/presets/safe.js +32 -15
- package/package.json +9 -15
- 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:
|
|
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
|
-
|
|
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-->';
|
|
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
|
-
|
|
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
|
-
}
|
|
62
|
-
|
|
51
|
+
}
|
|
63
52
|
|
|
53
|
+
// https://en.wikipedia.org/wiki/Conditional_comment
|
|
64
54
|
if ((0, _helpers.isConditionalComment)(text)) {
|
|
65
55
|
return false;
|
|
66
|
-
}
|
|
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 = {
|
|
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 === ''
|
|
66
|
-
|
|
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
|
-
}
|
|
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;
|
|
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
|
|
13
|
+
const missingValueDefaultAttributes = {
|
|
12
14
|
'form': {
|
|
13
15
|
'method': 'get'
|
|
14
16
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
input: {
|
|
18
|
+
type: 'text'
|
|
17
19
|
},
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
}
|