htmlnano 2.0.4 → 2.1.1
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/.eslintignore +3 -2
- package/CHANGELOG.md +19 -0
- package/docs/docs/040-presets.md +4 -4
- package/docs/docs/050-modules.md +1 -1
- package/docs/docs/060-contribute.md +1 -1
- package/docs/docusaurus.config.js +5 -0
- package/docs/package-lock.json +563 -326
- package/docs/package.json +1 -0
- package/docs/versioned_docs/version-1.1.1/040-presets.md +4 -4
- package/docs/versioned_docs/version-1.1.1/050-modules.md +1 -2
- package/docs/versioned_docs/version-1.1.1/060-contribute.md +1 -1
- package/docs/versioned_docs/version-2.0.0/040-presets.md +4 -4
- package/docs/versioned_docs/version-2.0.0/050-modules.md +1 -1
- package/docs/versioned_docs/version-2.0.0/060-contribute.md +1 -1
- package/index.cjs +11 -0
- package/index.d.cts +3 -0
- package/index.d.mts +3 -0
- package/index.d.ts +3 -3
- package/index.mjs +2 -0
- package/lib/helpers.cjs +79 -0
- package/lib/helpers.mjs +53 -0
- package/lib/htmlnano.cjs +200 -0
- package/lib/htmlnano.mjs +196 -0
- package/lib/modules/{collapseAttributeWhitespace.js → collapseAttributeWhitespace.cjs} +2 -3
- package/lib/modules/collapseAttributeWhitespace.mjs +104 -0
- package/lib/modules/collapseBooleanAttributes.mjs +175 -0
- package/lib/modules/{collapseWhitespace.js → collapseWhitespace.cjs} +3 -2
- package/lib/modules/collapseWhitespace.mjs +132 -0
- package/lib/modules/custom.mjs +16 -0
- package/lib/modules/{deduplicateAttributeValues.js → deduplicateAttributeValues.cjs} +1 -1
- package/lib/modules/deduplicateAttributeValues.mjs +40 -0
- package/lib/modules/example.cjs +85 -0
- package/lib/modules/example.mjs +75 -0
- package/lib/modules/mergeScripts.mjs +56 -0
- package/lib/modules/{mergeStyles.js → mergeStyles.cjs} +1 -1
- package/lib/modules/mergeStyles.mjs +36 -0
- package/lib/modules/{minifyConditionalComments.js → minifyConditionalComments.cjs} +2 -2
- package/lib/modules/minifyConditionalComments.mjs +49 -0
- package/lib/modules/{minifyCss.js → minifyCss.cjs} +8 -8
- package/lib/modules/minifyCss.mjs +88 -0
- package/lib/modules/{minifyJs.js → minifyJs.cjs} +8 -9
- package/lib/modules/minifyJs.mjs +121 -0
- package/lib/modules/minifyJson.mjs +21 -0
- package/lib/modules/{minifySvg.js → minifySvg.cjs} +3 -4
- package/lib/modules/minifySvg.mjs +30 -0
- package/lib/modules/{minifyUrls.js → minifyUrls.cjs} +11 -12
- package/lib/modules/minifyUrls.mjs +229 -0
- package/lib/modules/normalizeAttributeValues.mjs +140 -0
- package/lib/modules/removeAttributeQuotes.mjs +12 -0
- package/lib/modules/{removeComments.js → removeComments.cjs} +1 -1
- package/lib/modules/removeComments.mjs +92 -0
- package/lib/modules/{removeEmptyAttributes.js → removeEmptyAttributes.cjs} +1 -1
- package/lib/modules/removeEmptyAttributes.mjs +121 -0
- package/lib/modules/{removeOptionalTags.js → removeOptionalTags.cjs} +1 -1
- package/lib/modules/removeOptionalTags.mjs +225 -0
- package/lib/modules/{removeRedundantAttributes.js → removeRedundantAttributes.cjs} +1 -2
- package/lib/modules/removeRedundantAttributes.mjs +141 -0
- package/lib/modules/{removeUnusedCss.js → removeUnusedCss.cjs} +12 -13
- package/lib/modules/removeUnusedCss.mjs +122 -0
- package/lib/modules/{sortAttributes.js → sortAttributes.cjs} +0 -1
- package/lib/modules/sortAttributes.mjs +121 -0
- package/lib/modules/{sortAttributesWithLists.js → sortAttributesWithLists.cjs} +1 -2
- package/lib/modules/sortAttributesWithLists.mjs +135 -0
- package/lib/presets/{ampSafe.js → ampSafe.cjs} +4 -9
- package/lib/presets/ampSafe.mjs +11 -0
- package/lib/presets/{max.js → max.cjs} +4 -9
- package/lib/presets/max.mjs +20 -0
- package/lib/presets/{safe.js → safe.cjs} +2 -3
- package/lib/presets/safe.mjs +65 -0
- package/package.json +40 -12
- package/index.js +0 -1
- package/lib/helpers.js +0 -53
- package/lib/htmlnano.js +0 -150
- /package/lib/modules/{collapseBooleanAttributes.js → collapseBooleanAttributes.cjs} +0 -0
- /package/lib/modules/{custom.js → custom.cjs} +0 -0
- /package/lib/modules/{mergeScripts.js → mergeScripts.cjs} +0 -0
- /package/lib/modules/{minifyJson.js → minifyJson.cjs} +0 -0
- /package/lib/modules/{normalizeAttributeValues.js → normalizeAttributeValues.cjs} +0 -0
- /package/lib/modules/{removeAttributeQuotes.js → removeAttributeQuotes.cjs} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { isEventHandler } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
const safeToRemoveAttrs = {
|
|
4
|
+
id: null,
|
|
5
|
+
class: null,
|
|
6
|
+
style: null,
|
|
7
|
+
title: null,
|
|
8
|
+
lang: null,
|
|
9
|
+
dir: null,
|
|
10
|
+
abbr: ['th'],
|
|
11
|
+
accept: ['input'],
|
|
12
|
+
'accept-charset': ['form'],
|
|
13
|
+
charset: ['meta', 'script'],
|
|
14
|
+
action: ['form'],
|
|
15
|
+
cols: ['textarea'],
|
|
16
|
+
colspan: ['td', 'th'],
|
|
17
|
+
coords: ['area'],
|
|
18
|
+
dirname: ['input', 'textarea'],
|
|
19
|
+
dropzone: null,
|
|
20
|
+
headers: ['td', 'th'],
|
|
21
|
+
form: [
|
|
22
|
+
'button',
|
|
23
|
+
'fieldset',
|
|
24
|
+
'input',
|
|
25
|
+
'keygen',
|
|
26
|
+
'object',
|
|
27
|
+
'output',
|
|
28
|
+
'select',
|
|
29
|
+
'textarea'
|
|
30
|
+
],
|
|
31
|
+
formaction: ['button', 'input'],
|
|
32
|
+
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
33
|
+
high: 'meter',
|
|
34
|
+
href: 'link',
|
|
35
|
+
list: 'input',
|
|
36
|
+
low: 'meter',
|
|
37
|
+
manifest: 'html',
|
|
38
|
+
max: ['meter', 'progress'],
|
|
39
|
+
maxLength: ['input', 'textarea'],
|
|
40
|
+
menu: 'button',
|
|
41
|
+
min: 'meter',
|
|
42
|
+
minLength: ['input', 'textarea'],
|
|
43
|
+
name: [
|
|
44
|
+
'button',
|
|
45
|
+
'fieldset',
|
|
46
|
+
'input',
|
|
47
|
+
'keygen',
|
|
48
|
+
'output',
|
|
49
|
+
'select',
|
|
50
|
+
'textarea',
|
|
51
|
+
'form',
|
|
52
|
+
'map',
|
|
53
|
+
'meta',
|
|
54
|
+
'param',
|
|
55
|
+
'slot'
|
|
56
|
+
],
|
|
57
|
+
pattern: ['input'],
|
|
58
|
+
ping: ['a', 'area'],
|
|
59
|
+
placeholder: ['input', 'textarea'],
|
|
60
|
+
poster: ['video'],
|
|
61
|
+
rel: ['a', 'area', 'link'],
|
|
62
|
+
rows: 'textarea',
|
|
63
|
+
rowspan: ['td', 'th'],
|
|
64
|
+
size: ['input', 'select'],
|
|
65
|
+
span: ['col', 'colgroup'],
|
|
66
|
+
src: [
|
|
67
|
+
'audio',
|
|
68
|
+
'embed',
|
|
69
|
+
'iframe',
|
|
70
|
+
'img',
|
|
71
|
+
'input',
|
|
72
|
+
'script',
|
|
73
|
+
'source',
|
|
74
|
+
'track',
|
|
75
|
+
'video'
|
|
76
|
+
],
|
|
77
|
+
start: 'ol',
|
|
78
|
+
tabindex: null,
|
|
79
|
+
type: [
|
|
80
|
+
'a',
|
|
81
|
+
'link',
|
|
82
|
+
'button',
|
|
83
|
+
'embed',
|
|
84
|
+
'object',
|
|
85
|
+
'script',
|
|
86
|
+
'source',
|
|
87
|
+
'style',
|
|
88
|
+
'input',
|
|
89
|
+
'menu',
|
|
90
|
+
'menuitem',
|
|
91
|
+
'ol'
|
|
92
|
+
],
|
|
93
|
+
value: ['button', 'input', 'li'],
|
|
94
|
+
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function onAttrs() {
|
|
98
|
+
return (attrs, node) => {
|
|
99
|
+
const newAttrs = { ...attrs };
|
|
100
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
101
|
+
if (
|
|
102
|
+
isEventHandler(attrName)
|
|
103
|
+
|| (
|
|
104
|
+
Object.hasOwnProperty.call(safeToRemoveAttrs, attrName)
|
|
105
|
+
&& (
|
|
106
|
+
safeToRemoveAttrs[attrName] === null
|
|
107
|
+
|| safeToRemoveAttrs[attrName].includes(node.tag)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
) {
|
|
111
|
+
if (typeof attrValue === 'string') {
|
|
112
|
+
if (attrValue === '' || attrValue.trim() === '') {
|
|
113
|
+
delete newAttrs[attrName];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return newAttrs;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -4,7 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = removeOptionalTags;
|
|
7
|
-
var _helpers = require("../helpers");
|
|
7
|
+
var _helpers = require("../helpers.cjs");
|
|
8
8
|
const startWithWhitespacePattern = /^\s+/;
|
|
9
9
|
const bodyStartTagCantBeOmittedWithFirstChildTags = new Set(['meta', 'link', 'script', 'style']);
|
|
10
10
|
const tbodyStartTagCantBeOmittedWithPrecededTags = new Set(['tbody', 'thead', 'tfoot']);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { isComment } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
const startWithWhitespacePattern = /^\s+/;
|
|
4
|
+
|
|
5
|
+
const bodyStartTagCantBeOmittedWithFirstChildTags = new Set(['meta', 'link', 'script', 'style']);
|
|
6
|
+
const tbodyStartTagCantBeOmittedWithPrecededTags = new Set(['tbody', 'thead', 'tfoot']);
|
|
7
|
+
const tbodyEndTagCantBeOmittedWithFollowedTags = new Set(['tbody', 'tfoot']);
|
|
8
|
+
|
|
9
|
+
function isEmptyTextNode(node) {
|
|
10
|
+
if (typeof node === 'string' && node.trim() === '') {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isEmptyNode(node) {
|
|
18
|
+
if (!node.content) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (node.content.length) {
|
|
23
|
+
return !node.content.filter(n => typeof n === 'string' && isEmptyTextNode(n) ? false : true).length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFirstChildTag(node, nonEmpty = true) {
|
|
30
|
+
if (node.content && node.content.length) {
|
|
31
|
+
if (nonEmpty) {
|
|
32
|
+
for (const childNode of node.content) {
|
|
33
|
+
if (childNode.tag) return childNode;
|
|
34
|
+
if (typeof childNode === 'string' && !isEmptyTextNode(childNode)) return childNode;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
return node.content[0] || null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getPrevNode(tree, currentNodeIndex, nonEmpty = false) {
|
|
45
|
+
if (nonEmpty) {
|
|
46
|
+
for (let i = currentNodeIndex - 1; i >= 0; i--) {
|
|
47
|
+
const node = tree[i];
|
|
48
|
+
if (node.tag) return node;
|
|
49
|
+
if (typeof node === 'string' && !isEmptyTextNode(node)) return node;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
return tree[currentNodeIndex - 1] || null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
|
|
59
|
+
if (nonEmpty) {
|
|
60
|
+
for (let i = currentNodeIndex + 1; i < tree.length; i++) {
|
|
61
|
+
const node = tree[i];
|
|
62
|
+
if (node.tag) return node;
|
|
63
|
+
if (typeof node === 'string' && !isEmptyTextNode(node)) return node;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
return tree[currentNodeIndex + 1] || null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Specification https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
73
|
+
/** Remove optional tag in the DOM */
|
|
74
|
+
export default function removeOptionalTags(tree) {
|
|
75
|
+
tree.forEach((node, index) => {
|
|
76
|
+
if (!node.tag) return node;
|
|
77
|
+
|
|
78
|
+
if (node.attrs && Object.keys(node.attrs).length) return node;
|
|
79
|
+
|
|
80
|
+
// const prevNode = getPrevNode(tree, index);
|
|
81
|
+
const prevNonEmptyNode = getPrevNode(tree, index, true);
|
|
82
|
+
const nextNode = getNextNode(tree, index);
|
|
83
|
+
const nextNonEmptyNode = getNextNode(tree, index, true);
|
|
84
|
+
const firstChildNode = getFirstChildTag(node, false);
|
|
85
|
+
const firstNonEmptyChildNode = getFirstChildTag(node);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* An "html" element's start tag may be omitted if the first thing inside the "html" element is not a comment.
|
|
89
|
+
* An "html" element's end tag may be omitted if the "html" element is not IMMEDIATELY followed by a comment.
|
|
90
|
+
*/
|
|
91
|
+
if (node.tag === 'html') {
|
|
92
|
+
let isHtmlStartTagCanBeOmitted = true;
|
|
93
|
+
let isHtmlEndTagCanBeOmitted = true;
|
|
94
|
+
|
|
95
|
+
if (typeof firstNonEmptyChildNode === 'string' && isComment(firstNonEmptyChildNode)) {
|
|
96
|
+
isHtmlStartTagCanBeOmitted = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof nextNonEmptyNode === 'string' && isComment(nextNonEmptyNode)) {
|
|
100
|
+
isHtmlEndTagCanBeOmitted = false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isHtmlStartTagCanBeOmitted && isHtmlEndTagCanBeOmitted) {
|
|
104
|
+
node.tag = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 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.
|
|
110
|
+
* A "head" element's end tag may be omitted if the "head" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
|
|
111
|
+
*/
|
|
112
|
+
if (node.tag === 'head') {
|
|
113
|
+
let isHeadStartTagCanBeOmitted = false;
|
|
114
|
+
let isHeadEndTagCanBeOmitted = true;
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
isEmptyNode(node) ||
|
|
118
|
+
firstNonEmptyChildNode && firstNonEmptyChildNode.tag
|
|
119
|
+
) {
|
|
120
|
+
isHeadStartTagCanBeOmitted = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
(nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode)) ||
|
|
125
|
+
(nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && isComment(nextNode))
|
|
126
|
+
) {
|
|
127
|
+
isHeadEndTagCanBeOmitted = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isHeadStartTagCanBeOmitted && isHeadEndTagCanBeOmitted) {
|
|
131
|
+
node.tag = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 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.
|
|
138
|
+
* A "body" element's end tag may be omitted if the "body" element is not IMMEDIATELY followed by a comment.
|
|
139
|
+
*/
|
|
140
|
+
if (node.tag === 'body') {
|
|
141
|
+
let isBodyStartTagCanBeOmitted = true;
|
|
142
|
+
let isBodyEndTagCanBeOmitted = true;
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
(typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode)) ||
|
|
146
|
+
(typeof firstNonEmptyChildNode === 'string' && isComment(firstNonEmptyChildNode))
|
|
147
|
+
) {
|
|
148
|
+
isBodyStartTagCanBeOmitted = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstNonEmptyChildNode.tag)) {
|
|
152
|
+
isBodyStartTagCanBeOmitted = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (nextNode && typeof nextNode === 'string' && isComment(nextNode)) {
|
|
156
|
+
isBodyEndTagCanBeOmitted = false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isBodyStartTagCanBeOmitted && isBodyEndTagCanBeOmitted) {
|
|
160
|
+
node.tag = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 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.
|
|
166
|
+
* A "colgroup" element's end tag may be omitted if the "colgroup" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
|
|
167
|
+
*/
|
|
168
|
+
if (node.tag === 'colgroup') {
|
|
169
|
+
let isColgroupStartTagCanBeOmitted = false;
|
|
170
|
+
let isColgroupEndTagCanBeOmitted = true;
|
|
171
|
+
|
|
172
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'col') {
|
|
173
|
+
isColgroupStartTagCanBeOmitted = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (prevNonEmptyNode && prevNonEmptyNode.tag && prevNonEmptyNode.tag === 'colgroup') {
|
|
177
|
+
isColgroupStartTagCanBeOmitted = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
(nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode)) ||
|
|
182
|
+
(nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && isComment(nextNonEmptyNode))
|
|
183
|
+
) {
|
|
184
|
+
isColgroupEndTagCanBeOmitted = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (isColgroupStartTagCanBeOmitted && isColgroupEndTagCanBeOmitted) {
|
|
188
|
+
node.tag = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 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.
|
|
194
|
+
* A "tbody" element's end tag may be omitted if the "tbody" element is not IMMEDIATELY followed by a "tbody" or "tfoot" element.
|
|
195
|
+
*/
|
|
196
|
+
if (node.tag === 'tbody') {
|
|
197
|
+
let isTbodyStartTagCanBeOmitted = false;
|
|
198
|
+
let isTbodyEndTagCanBeOmitted = true;
|
|
199
|
+
|
|
200
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'tr') {
|
|
201
|
+
isTbodyStartTagCanBeOmitted = true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (prevNonEmptyNode && prevNonEmptyNode.tag && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevNonEmptyNode.tag)) {
|
|
205
|
+
isTbodyStartTagCanBeOmitted = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (nextNonEmptyNode && nextNonEmptyNode.tag && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextNonEmptyNode.tag)) {
|
|
209
|
+
isTbodyEndTagCanBeOmitted = false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (isTbodyStartTagCanBeOmitted && isTbodyEndTagCanBeOmitted) {
|
|
213
|
+
node.tag = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (node.content && node.content.length) {
|
|
218
|
+
removeOptionalTags(node.content);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return node;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return tree;
|
|
225
|
+
}
|
|
@@ -6,10 +6,9 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
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
|
-
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']);
|
|
9
|
+
const redundantScriptTypes = exports.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
10
|
|
|
11
11
|
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default
|
|
12
|
-
exports.redundantScriptTypes = redundantScriptTypes;
|
|
13
12
|
const missingValueDefaultAttributes = {
|
|
14
13
|
'form': {
|
|
15
14
|
'method': 'get'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#JavaScript_types
|
|
2
|
+
export const redundantScriptTypes = new Set([
|
|
3
|
+
'application/javascript',
|
|
4
|
+
'application/ecmascript',
|
|
5
|
+
'application/x-ecmascript',
|
|
6
|
+
'application/x-javascript',
|
|
7
|
+
'text/javascript',
|
|
8
|
+
'text/ecmascript',
|
|
9
|
+
'text/javascript1.0',
|
|
10
|
+
'text/javascript1.1',
|
|
11
|
+
'text/javascript1.2',
|
|
12
|
+
'text/javascript1.3',
|
|
13
|
+
'text/javascript1.4',
|
|
14
|
+
'text/javascript1.5',
|
|
15
|
+
'text/jscript',
|
|
16
|
+
'text/livescript',
|
|
17
|
+
'text/x-ecmascript',
|
|
18
|
+
'text/x-javascript'
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#missing-value-default
|
|
22
|
+
const missingValueDefaultAttributes = {
|
|
23
|
+
'form': {
|
|
24
|
+
'method': 'get'
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
input: {
|
|
28
|
+
type: 'text'
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
button: {
|
|
32
|
+
// https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
|
|
33
|
+
type: 'submit'
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
'script': {
|
|
37
|
+
'language': 'javascript',
|
|
38
|
+
'type': attrs => {
|
|
39
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
40
|
+
if (attrName.toLowerCase() !== 'type') {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return redundantScriptTypes.has(attrValue);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
},
|
|
49
|
+
// Remove attribute if the function returns false
|
|
50
|
+
'charset': attrs => {
|
|
51
|
+
// The charset attribute only really makes sense on “external” SCRIPT elements:
|
|
52
|
+
// http://perfectionkills.com/optimizing-html/#8_script_charset
|
|
53
|
+
return !attrs.src;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
'style': {
|
|
58
|
+
'media': 'all',
|
|
59
|
+
'type': 'text/css'
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
'link': {
|
|
63
|
+
media: 'all',
|
|
64
|
+
'type': attrs => {
|
|
65
|
+
// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
|
|
66
|
+
let isRelStyleSheet = false;
|
|
67
|
+
let isTypeTextCSS = false;
|
|
68
|
+
|
|
69
|
+
if (attrs) {
|
|
70
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
71
|
+
if (attrName.toLowerCase() === 'rel' && attrValue === 'stylesheet') {
|
|
72
|
+
isRelStyleSheet = true;
|
|
73
|
+
}
|
|
74
|
+
if (attrName.toLowerCase() === 'type' && attrValue === 'text/css') {
|
|
75
|
+
isTypeTextCSS = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Only "text/css" is redudant for link[rel=stylesheet]. Otherwise "type" shouldn't be removed
|
|
81
|
+
return isRelStyleSheet && isTypeTextCSS;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// See: https://html.spec.whatwg.org/#lazy-loading-attributes
|
|
86
|
+
img: {
|
|
87
|
+
'loading': 'eager',
|
|
88
|
+
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-decoding
|
|
89
|
+
decoding: 'auto'
|
|
90
|
+
},
|
|
91
|
+
iframe: {
|
|
92
|
+
'loading': 'eager'
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// https://html.spec.whatwg.org/multipage/media.html#htmltrackelement
|
|
96
|
+
track: {
|
|
97
|
+
kind: 'subtitles'
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
textarea: {
|
|
101
|
+
// https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-wrap
|
|
102
|
+
wrap: 'soft'
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
area: {
|
|
106
|
+
// https://html.spec.whatwg.org/multipage/image-maps.html#attr-area-shape
|
|
107
|
+
shape: 'rect'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const tagsHaveMissingValueDefaultAttributes = new Set(Object.keys(missingValueDefaultAttributes));
|
|
112
|
+
|
|
113
|
+
/** Removes redundant attributes */
|
|
114
|
+
export function onAttrs() {
|
|
115
|
+
return (attrs, node) => {
|
|
116
|
+
if (!node.tag) return attrs;
|
|
117
|
+
|
|
118
|
+
const newAttrs = attrs;
|
|
119
|
+
|
|
120
|
+
if (tagsHaveMissingValueDefaultAttributes.has(node.tag)) {
|
|
121
|
+
const tagRedundantAttributes = missingValueDefaultAttributes[node.tag];
|
|
122
|
+
|
|
123
|
+
for (const redundantAttributeName of Object.keys(tagRedundantAttributes)) {
|
|
124
|
+
let tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
|
|
125
|
+
let isRemove = false;
|
|
126
|
+
|
|
127
|
+
if (typeof tagRedundantAttributeValue === 'function') {
|
|
128
|
+
isRemove = tagRedundantAttributeValue(attrs);
|
|
129
|
+
} else if (attrs[redundantAttributeName] === tagRedundantAttributeValue) {
|
|
130
|
+
isRemove = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (isRemove) {
|
|
134
|
+
delete newAttrs[redundantAttributeName];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return newAttrs;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -4,18 +4,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = removeUnusedCss;
|
|
7
|
-
var _helpers = require("../helpers");
|
|
8
|
-
const uncss = (0, _helpers.optionalRequire)('uncss');
|
|
9
|
-
const purgecss = (0, _helpers.optionalRequire)('purgecss');
|
|
10
|
-
|
|
7
|
+
var _helpers = require("../helpers.cjs");
|
|
11
8
|
// These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
|
|
12
9
|
const uncssOptions = {
|
|
13
10
|
ignoreSheets: [/\s*/],
|
|
14
11
|
stylesheets: []
|
|
15
12
|
};
|
|
16
|
-
function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
|
|
13
|
+
function processStyleNodeUnCSS(html, styleNode, uncssOptions, uncss) {
|
|
17
14
|
const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
18
|
-
return runUncss(html, css, uncssOptions).then(css => {
|
|
15
|
+
return runUncss(html, css, uncssOptions, uncss).then(css => {
|
|
19
16
|
// uncss may have left some style tags empty
|
|
20
17
|
if (css.trim().length === 0) {
|
|
21
18
|
styleNode.tag = false;
|
|
@@ -25,7 +22,7 @@ function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
|
|
|
25
22
|
styleNode.content = [css];
|
|
26
23
|
});
|
|
27
24
|
}
|
|
28
|
-
function runUncss(html, css, userOptions) {
|
|
25
|
+
function runUncss(html, css, userOptions, uncss) {
|
|
29
26
|
if (typeof userOptions !== 'object') {
|
|
30
27
|
userOptions = {};
|
|
31
28
|
}
|
|
@@ -57,9 +54,9 @@ const purgeFromHtml = function (tree) {
|
|
|
57
54
|
});
|
|
58
55
|
return () => selectors;
|
|
59
56
|
};
|
|
60
|
-
function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
|
|
57
|
+
function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions, purgecss) {
|
|
61
58
|
const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
62
|
-
return runPurgecss(tree, css, purgecssOptions).then(css => {
|
|
59
|
+
return runPurgecss(tree, css, purgecssOptions, purgecss).then(css => {
|
|
63
60
|
if (css.trim().length === 0) {
|
|
64
61
|
styleNode.tag = false;
|
|
65
62
|
styleNode.content = [];
|
|
@@ -68,7 +65,7 @@ function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
|
|
|
68
65
|
styleNode.content = [css];
|
|
69
66
|
});
|
|
70
67
|
}
|
|
71
|
-
function runPurgecss(tree, css, userOptions) {
|
|
68
|
+
function runPurgecss(tree, css, userOptions, purgecss) {
|
|
72
69
|
if (typeof userOptions !== 'object') {
|
|
73
70
|
userOptions = {};
|
|
74
71
|
}
|
|
@@ -93,18 +90,20 @@ function runPurgecss(tree, css, userOptions) {
|
|
|
93
90
|
}
|
|
94
91
|
|
|
95
92
|
/** Remove unused CSS */
|
|
96
|
-
function removeUnusedCss(tree, options, userOptions) {
|
|
93
|
+
async function removeUnusedCss(tree, options, userOptions) {
|
|
97
94
|
const promises = [];
|
|
98
95
|
const html = userOptions.tool !== 'purgeCSS' && tree.render(tree);
|
|
96
|
+
const purgecss = await (0, _helpers.optionalImport)('purgecss');
|
|
97
|
+
const uncss = await (0, _helpers.optionalImport)('uncss');
|
|
99
98
|
tree.walk(node => {
|
|
100
99
|
if ((0, _helpers.isStyleNode)(node)) {
|
|
101
100
|
if (userOptions.tool === 'purgeCSS') {
|
|
102
101
|
if (purgecss) {
|
|
103
|
-
promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
|
|
102
|
+
promises.push(processStyleNodePurgeCSS(tree, node, userOptions, purgecss));
|
|
104
103
|
}
|
|
105
104
|
} else {
|
|
106
105
|
if (uncss) {
|
|
107
|
-
promises.push(processStyleNodeUnCSS(html, node, userOptions));
|
|
106
|
+
promises.push(processStyleNodeUnCSS(html, node, userOptions, uncss));
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
}
|