htmlnano 2.0.0 → 2.0.2
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 +19 -0
- package/docs/package-lock.json +262 -2072
- package/lib/htmlnano.js +60 -3
- package/lib/modules/collapseAttributeWhitespace.js +20 -20
- package/lib/modules/collapseBooleanAttributes.js +13 -19
- package/lib/modules/deduplicateAttributeValues.js +10 -16
- package/lib/modules/minifyJson.js +12 -24
- package/lib/modules/minifySvg.js +3 -2
- package/lib/modules/normalizeAttributeValues.js +9 -15
- package/lib/modules/removeComments.js +17 -9
- package/lib/modules/removeEmptyAttributes.js +13 -17
- package/lib/modules/removeRedundantAttributes.js +19 -23
- package/package.json +3 -3
package/lib/htmlnano.js
CHANGED
|
@@ -65,6 +65,9 @@ const optionalDependencies = {
|
|
|
65
65
|
function htmlnano(optionsRun, presetRun) {
|
|
66
66
|
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
67
67
|
return function minifier(tree) {
|
|
68
|
+
const nodeHandlers = [];
|
|
69
|
+
const attrsHandlers = [];
|
|
70
|
+
const contentsHandlers = [];
|
|
68
71
|
options = { ...preset,
|
|
69
72
|
...options
|
|
70
73
|
};
|
|
@@ -92,12 +95,66 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
92
95
|
}
|
|
93
96
|
});
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
const module = require('./modules/' + moduleName);
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
if (typeof module.onAttrs === 'function') {
|
|
101
|
+
attrsHandlers.push(module.onAttrs(options, moduleOptions));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof module.onContent === 'function') {
|
|
105
|
+
contentsHandlers.push(module.onContent(options, moduleOptions));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof module.onNode === 'function') {
|
|
109
|
+
nodeHandlers.push(module.onNode(options, moduleOptions));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof module.default === 'function') {
|
|
113
|
+
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
|
|
118
|
+
return promise;
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
return promise
|
|
121
|
+
return promise.then(tree => {
|
|
122
|
+
tree.walk(node => {
|
|
123
|
+
if (node) {
|
|
124
|
+
if (node.attrs && typeof node.attrs === 'object') {
|
|
125
|
+
// Convert all attrs' key to lower case
|
|
126
|
+
let newAttrsObj = {};
|
|
127
|
+
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
128
|
+
newAttrsObj[attrName.toLowerCase()] = attrValue;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
for (const handler of attrsHandlers) {
|
|
132
|
+
newAttrsObj = handler(newAttrsObj, node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
node.attrs = newAttrsObj;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (node.content) {
|
|
139
|
+
node.content = typeof node.content === 'string' ? [node.content] : node.content;
|
|
140
|
+
|
|
141
|
+
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
142
|
+
for (const handler of contentsHandlers) {
|
|
143
|
+
const result = handler(node.content, node);
|
|
144
|
+
node.content = typeof result === 'string' ? [result] : result;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const handler of nodeHandlers) {
|
|
150
|
+
node = handler(node);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return node;
|
|
155
|
+
});
|
|
156
|
+
return tree;
|
|
157
|
+
});
|
|
101
158
|
};
|
|
102
159
|
}
|
|
103
160
|
|
|
@@ -4,14 +4,21 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.attributesWithLists = void 0;
|
|
7
|
-
exports.
|
|
7
|
+
exports.onAttrs = onAttrs;
|
|
8
8
|
|
|
9
9
|
var _helpers = require("../helpers");
|
|
10
10
|
|
|
11
11
|
const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
|
|
12
12
|
'ping', // a, area
|
|
13
13
|
'sandbox', // iframe
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* https://github.com/posthtml/htmlnano/issues/180
|
|
17
|
+
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
18
|
+
*
|
|
19
|
+
* "sizes" of <img> should not be modified, while "sizes" of <link> will only have one entry in most cases.
|
|
20
|
+
*/
|
|
21
|
+
// 'sizes', // link
|
|
15
22
|
'headers' // td, th
|
|
16
23
|
]);
|
|
17
24
|
/** @type Record<string, string[] | null> */
|
|
@@ -58,29 +65,22 @@ const attributesWithSingleValue = {
|
|
|
58
65
|
};
|
|
59
66
|
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
60
67
|
|
|
61
|
-
function
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
68
|
-
const attrNameLower = attrName.toLowerCase();
|
|
69
|
-
|
|
70
|
-
if (attributesWithLists.has(attrNameLower)) {
|
|
68
|
+
function onAttrs() {
|
|
69
|
+
return (attrs, node) => {
|
|
70
|
+
const newAttrs = attrs;
|
|
71
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
72
|
+
if (attributesWithLists.has(attrName)) {
|
|
71
73
|
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
+
newAttrs[attrName] = newAttrValue;
|
|
75
|
+
return;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
if ((0, _helpers.isEventHandler)(
|
|
77
|
-
|
|
78
|
-
return node;
|
|
78
|
+
if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
|
|
79
|
+
newAttrs[attrName] = minifySingleAttributeValue(attrValue);
|
|
79
80
|
}
|
|
80
81
|
});
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
return tree;
|
|
82
|
+
return newAttrs;
|
|
83
|
+
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function minifySingleAttributeValue(value) {
|
|
@@ -3,42 +3,36 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
// Source: https://github.com/kangax/html-minifier/issues/63
|
|
8
8
|
const htmlBooleanAttributes = new Set(['allowfullscreen', 'allowpaymentrequest', 'allowtransparency', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
|
|
9
9
|
const amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
|
|
10
10
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
if (!node.
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (!node.tag) {
|
|
18
|
-
return node;
|
|
19
|
-
}
|
|
11
|
+
function onAttrs(options, moduleOptions) {
|
|
12
|
+
return (attrs, node) => {
|
|
13
|
+
if (!node.tag) return attrs;
|
|
14
|
+
const newAttrs = attrs;
|
|
20
15
|
|
|
21
|
-
for (const attrName of Object.keys(
|
|
16
|
+
for (const attrName of Object.keys(attrs)) {
|
|
22
17
|
if (attrName === 'visible' && node.tag.startsWith('a-')) {
|
|
23
18
|
continue;
|
|
24
19
|
}
|
|
25
20
|
|
|
26
21
|
if (htmlBooleanAttributes.has(attrName)) {
|
|
27
|
-
|
|
22
|
+
newAttrs[attrName] = true;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) &&
|
|
31
|
-
|
|
25
|
+
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
|
|
26
|
+
newAttrs[attrName] = true;
|
|
32
27
|
} // collapse crossorigin attributes
|
|
33
28
|
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
34
29
|
|
|
35
30
|
|
|
36
|
-
if (attrName.toLowerCase() === 'crossorigin' && (
|
|
37
|
-
|
|
31
|
+
if (attrName.toLowerCase() === 'crossorigin' && (attrs[attrName] === 'anonymous' || attrs[attrName] === '')) {
|
|
32
|
+
newAttrs[attrName] = true;
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
return tree;
|
|
36
|
+
return newAttrs;
|
|
37
|
+
};
|
|
44
38
|
}
|
|
@@ -3,25 +3,20 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
|
|
8
8
|
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
|
|
9
9
|
|
|
10
10
|
/** Deduplicate values inside list-like attributes (e.g. class, rel) */
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Object.keys(node.attrs).forEach(attrName => {
|
|
18
|
-
const attrNameLower = attrName.toLowerCase();
|
|
19
|
-
|
|
20
|
-
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
|
|
11
|
+
function onAttrs() {
|
|
12
|
+
return attrs => {
|
|
13
|
+
const newAttrs = attrs;
|
|
14
|
+
Object.keys(attrs).forEach(attrName => {
|
|
15
|
+
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrName)) {
|
|
21
16
|
return;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
const attrValues =
|
|
19
|
+
const attrValues = attrs[attrName].split(/\s/);
|
|
25
20
|
const uniqeAttrValues = new Set();
|
|
26
21
|
const deduplicatedAttrValues = [];
|
|
27
22
|
attrValues.forEach(attrValue => {
|
|
@@ -38,9 +33,8 @@ function collapseAttributeWhitespace(tree) {
|
|
|
38
33
|
deduplicatedAttrValues.push(attrValue);
|
|
39
34
|
uniqeAttrValues.add(attrValue);
|
|
40
35
|
});
|
|
41
|
-
|
|
36
|
+
newAttrs[attrName] = deduplicatedAttrValues.join(' ');
|
|
42
37
|
});
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
return tree;
|
|
38
|
+
return newAttrs;
|
|
39
|
+
};
|
|
46
40
|
}
|
|
@@ -3,31 +3,19 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onContent = onContent;
|
|
7
|
+
const rNodeAttrsTypeJson = /(\/|\+)json/;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
function onContent() {
|
|
10
|
+
return (content, node) => {
|
|
11
|
+
if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
|
|
12
|
+
try {
|
|
13
|
+
// cast minified JSON to an array
|
|
14
|
+
return [JSON.stringify(JSON.parse((content || []).join('')))];
|
|
15
|
+
} catch (error) {// Invalid JSON
|
|
16
|
+
}
|
|
15
17
|
}
|
|
16
|
-
}, node => {
|
|
17
|
-
let content = (node.content || []).join('');
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
content = JSON.stringify(JSON.parse(content));
|
|
25
|
-
} catch (error) {
|
|
26
|
-
return node;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
node.content = [content];
|
|
30
|
-
return node;
|
|
31
|
-
});
|
|
32
|
-
return tree;
|
|
19
|
+
return content;
|
|
20
|
+
};
|
|
33
21
|
}
|
package/lib/modules/minifySvg.js
CHANGED
|
@@ -21,8 +21,9 @@ function minifySvg(tree, options, svgoOptions = {}) {
|
|
|
21
21
|
});
|
|
22
22
|
const result = svgo.optimize(svgStr, svgoOptions);
|
|
23
23
|
node.tag = false;
|
|
24
|
-
node.attrs = {};
|
|
25
|
-
|
|
24
|
+
node.attrs = {}; // result.data is a string, we need to cast it to an array
|
|
25
|
+
|
|
26
|
+
node.content = [result.data];
|
|
26
27
|
return node;
|
|
27
28
|
});
|
|
28
29
|
return tree;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
7
|
const caseInsensitiveAttributes = {
|
|
8
8
|
autocomplete: ['form'],
|
|
9
9
|
charset: ['meta', 'script'],
|
|
@@ -29,20 +29,14 @@ const caseInsensitiveAttributes = {
|
|
|
29
29
|
wrap: ['textarea']
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
39
|
-
const attrNameLower = attrName.toLowerCase();
|
|
40
|
-
|
|
41
|
-
if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower) && (caseInsensitiveAttributes[attrNameLower] === null || caseInsensitiveAttributes[attrNameLower].includes(node.tag))) {
|
|
42
|
-
node.attrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
|
|
32
|
+
function onAttrs() {
|
|
33
|
+
return (attrs, node) => {
|
|
34
|
+
const newAttrs = attrs;
|
|
35
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
36
|
+
if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName) && (caseInsensitiveAttributes[attrName] === null || caseInsensitiveAttributes[attrName].includes(node.tag))) {
|
|
37
|
+
newAttrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
|
|
43
38
|
}
|
|
44
39
|
});
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
return tree;
|
|
40
|
+
return newAttrs;
|
|
41
|
+
};
|
|
48
42
|
}
|
|
@@ -3,28 +3,36 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onContent = onContent;
|
|
7
|
+
exports.onNode = onNode;
|
|
7
8
|
|
|
8
9
|
var _helpers = require("../helpers");
|
|
9
10
|
|
|
10
11
|
const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
|
|
11
12
|
/** Removes HTML comments */
|
|
12
13
|
|
|
13
|
-
function
|
|
14
|
+
function onNode(options, removeType) {
|
|
14
15
|
if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
|
|
15
16
|
removeType = 'safe';
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
if (node
|
|
20
|
-
|
|
21
|
-
} else if (isCommentToRemove(node, removeType)) {
|
|
22
|
-
node = '';
|
|
19
|
+
return node => {
|
|
20
|
+
if (isCommentToRemove(node, removeType)) {
|
|
21
|
+
return '';
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
return node;
|
|
26
|
-
}
|
|
27
|
-
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function onContent(options, removeType) {
|
|
29
|
+
if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
|
|
30
|
+
removeType = 'safe';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return contents => {
|
|
34
|
+
return contents.filter(content => !isCommentToRemove(content, removeType));
|
|
35
|
+
};
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
function isCommentToRemove(text, removeType) {
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.onAttrs = onAttrs;
|
|
7
|
+
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
|
+
|
|
7
10
|
const safeToRemoveAttrs = {
|
|
8
11
|
id: null,
|
|
9
12
|
class: null,
|
|
@@ -52,25 +55,18 @@ const safeToRemoveAttrs = {
|
|
|
52
55
|
value: ['button', 'input', 'li'],
|
|
53
56
|
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
|
|
54
57
|
};
|
|
55
|
-
/** Removes empty attributes */
|
|
56
|
-
|
|
57
|
-
function removeEmptyAttributes(tree) {
|
|
58
|
-
tree.walk(node => {
|
|
59
|
-
if (!node.attrs) {
|
|
60
|
-
return node;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
64
|
-
const attrNameLower = attrName.toLowerCase();
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
function onAttrs() {
|
|
60
|
+
return (attrs, node) => {
|
|
61
|
+
const newAttrs = { ...attrs
|
|
62
|
+
};
|
|
63
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
64
|
+
if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(safeToRemoveAttrs, attrName) && (safeToRemoveAttrs[attrName] === null || safeToRemoveAttrs[attrName].includes(node.tag))) {
|
|
68
65
|
if (attrValue === '' || (attrValue || '').match(/^\s+$/)) {
|
|
69
|
-
delete
|
|
66
|
+
delete newAttrs[attrName];
|
|
70
67
|
}
|
|
71
68
|
}
|
|
72
69
|
});
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
return tree;
|
|
70
|
+
return newAttrs;
|
|
71
|
+
};
|
|
76
72
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
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']);
|
|
@@ -20,8 +20,8 @@ const redundantAttributes = {
|
|
|
20
20
|
},
|
|
21
21
|
'script': {
|
|
22
22
|
'language': 'javascript',
|
|
23
|
-
'type':
|
|
24
|
-
for (const [attrName, attrValue] of Object.entries(
|
|
23
|
+
'type': attrs => {
|
|
24
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
25
25
|
if (attrName.toLowerCase() !== 'type') {
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
@@ -32,10 +32,10 @@ const redundantAttributes = {
|
|
|
32
32
|
return false;
|
|
33
33
|
},
|
|
34
34
|
// Remove attribute if the function returns false
|
|
35
|
-
'charset':
|
|
35
|
+
'charset': attrs => {
|
|
36
36
|
// The charset attribute only really makes sense on “external” SCRIPT elements:
|
|
37
37
|
// http://perfectionkills.com/optimizing-html/#8_script_charset
|
|
38
|
-
return
|
|
38
|
+
return !attrs.src;
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
'style': {
|
|
@@ -44,13 +44,13 @@ const redundantAttributes = {
|
|
|
44
44
|
},
|
|
45
45
|
'link': {
|
|
46
46
|
'media': 'all',
|
|
47
|
-
'type':
|
|
47
|
+
'type': attrs => {
|
|
48
48
|
// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
|
|
49
49
|
let isRelStyleSheet = false;
|
|
50
50
|
let isTypeTextCSS = false;
|
|
51
51
|
|
|
52
|
-
if (
|
|
53
|
-
for (const [attrName, attrValue] of Object.entries(
|
|
52
|
+
if (attrs) {
|
|
53
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
54
54
|
if (attrName.toLowerCase() === 'rel' && attrValue === 'stylesheet') {
|
|
55
55
|
isRelStyleSheet = true;
|
|
56
56
|
}
|
|
@@ -115,13 +115,10 @@ const tagsHaveRedundantAttributes = new Set(Object.keys(redundantAttributes));
|
|
|
115
115
|
const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(canBeReplacedWithEmptyStringAttributes));
|
|
116
116
|
/** Removes redundant attributes */
|
|
117
117
|
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
if (!node.tag)
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
node.attrs = node.attrs || {};
|
|
118
|
+
function onAttrs() {
|
|
119
|
+
return (attrs, node) => {
|
|
120
|
+
if (!node.tag) return attrs;
|
|
121
|
+
const newAttrs = attrs;
|
|
125
122
|
|
|
126
123
|
if (tagsHaveRedundantAttributes.has(node.tag)) {
|
|
127
124
|
const tagRedundantAttributes = redundantAttributes[node.tag];
|
|
@@ -131,13 +128,13 @@ function removeRedundantAttributes(tree) {
|
|
|
131
128
|
let isRemove = false;
|
|
132
129
|
|
|
133
130
|
if (typeof tagRedundantAttributeValue === 'function') {
|
|
134
|
-
isRemove = tagRedundantAttributeValue(
|
|
135
|
-
} else if (
|
|
131
|
+
isRemove = tagRedundantAttributeValue(attrs);
|
|
132
|
+
} else if (attrs[redundantAttributeName] === tagRedundantAttributeValue) {
|
|
136
133
|
isRemove = true;
|
|
137
134
|
}
|
|
138
135
|
|
|
139
136
|
if (isRemove) {
|
|
140
|
-
delete
|
|
137
|
+
delete newAttrs[redundantAttributeName];
|
|
141
138
|
}
|
|
142
139
|
}
|
|
143
140
|
}
|
|
@@ -149,17 +146,16 @@ function removeRedundantAttributes(tree) {
|
|
|
149
146
|
let tagMissingValueDefaultAttribute = tagMissingValueDefaultAttributes[canBeReplacedWithEmptyStringAttributeName];
|
|
150
147
|
let isReplace = false;
|
|
151
148
|
|
|
152
|
-
if (
|
|
149
|
+
if (attrs[canBeReplacedWithEmptyStringAttributeName] === tagMissingValueDefaultAttribute) {
|
|
153
150
|
isReplace = true;
|
|
154
151
|
}
|
|
155
152
|
|
|
156
153
|
if (isReplace) {
|
|
157
|
-
|
|
154
|
+
newAttrs[canBeReplacedWithEmptyStringAttributeName] = '';
|
|
158
155
|
}
|
|
159
156
|
}
|
|
160
157
|
}
|
|
161
158
|
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
return tree;
|
|
159
|
+
return newAttrs;
|
|
160
|
+
};
|
|
165
161
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "htmlnano",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Modular HTML minifier, built on top of the PostHTML",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Kirill Maltsev <maltsevkirill@gmail.com>",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@babel/cli": "^7.15.7",
|
|
48
48
|
"@babel/core": "^7.15.5",
|
|
49
|
+
"@babel/eslint-parser": "^7.17.0",
|
|
49
50
|
"@babel/preset-env": "^7.15.6",
|
|
50
51
|
"@babel/register": "^7.15.3",
|
|
51
|
-
"babel-eslint": "^10.1.0",
|
|
52
52
|
"cssnano": "^5.0.11",
|
|
53
|
-
"eslint": "^
|
|
53
|
+
"eslint": "^8.12.0",
|
|
54
54
|
"expect": "^27.2.0",
|
|
55
55
|
"mocha": "^9.1.0",
|
|
56
56
|
"postcss": "^8.3.11",
|