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,21 @@
|
|
|
1
|
+
const rNodeAttrsTypeJson = /(\/|\+)json/;
|
|
2
|
+
|
|
3
|
+
export function onContent() {
|
|
4
|
+
return (content, node) => {
|
|
5
|
+
// Skip SRI, reasons are documented in "minifyJs" module
|
|
6
|
+
if (node.attrs && 'integrity' in node.attrs) {
|
|
7
|
+
return content;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
|
|
11
|
+
try {
|
|
12
|
+
// cast minified JSON to an array
|
|
13
|
+
return [JSON.stringify(JSON.parse((content || []).join('')))];
|
|
14
|
+
} catch (error) {
|
|
15
|
+
// Invalid JSON
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return content;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -4,11 +4,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifySvg;
|
|
7
|
-
var _helpers = require("../helpers");
|
|
8
|
-
const svgo = (0, _helpers.optionalRequire)('svgo');
|
|
9
|
-
|
|
7
|
+
var _helpers = require("../helpers.cjs");
|
|
10
8
|
/** Minify SVG with SVGO */
|
|
11
|
-
function minifySvg(tree, options, svgoOptions = {}) {
|
|
9
|
+
async function minifySvg(tree, options, svgoOptions = {}) {
|
|
10
|
+
const svgo = await (0, _helpers.optionalImport)('svgo');
|
|
12
11
|
if (!svgo) return tree;
|
|
13
12
|
tree.match({
|
|
14
13
|
tag: 'svg'
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { optionalImport } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
/** Minify SVG with SVGO */
|
|
4
|
+
export default async function minifySvg(tree, options, svgoOptions = {}) {
|
|
5
|
+
const svgo = await optionalImport('svgo');
|
|
6
|
+
|
|
7
|
+
if (!svgo) return tree;
|
|
8
|
+
|
|
9
|
+
tree.match({tag: 'svg'}, node => {
|
|
10
|
+
let svgStr = tree.render(node, { closingSingleTag: 'slash', quoteAllAttributes: true });
|
|
11
|
+
try {
|
|
12
|
+
const result = svgo.optimize(svgStr, svgoOptions);
|
|
13
|
+
node.tag = false;
|
|
14
|
+
node.attrs = {};
|
|
15
|
+
// result.data is a string, we need to cast it to an array
|
|
16
|
+
node.content = [result.data];
|
|
17
|
+
return node;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('htmlnano fails to minify the svg:');
|
|
20
|
+
console.error(error);
|
|
21
|
+
if (error.name === 'SvgoParserError') {
|
|
22
|
+
console.error(error.toString());
|
|
23
|
+
}
|
|
24
|
+
// We return the node as-is
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return tree;
|
|
30
|
+
}
|
|
@@ -4,11 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyUrls;
|
|
7
|
-
var _helpers = require("../helpers");
|
|
8
|
-
const RelateUrl = (0, _helpers.optionalRequire)('relateurl');
|
|
9
|
-
const srcset = (0, _helpers.optionalRequire)('srcset');
|
|
10
|
-
const terser = (0, _helpers.optionalRequire)('terser');
|
|
11
|
-
|
|
7
|
+
var _helpers = require("../helpers.cjs");
|
|
12
8
|
// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
13
9
|
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
|
|
14
10
|
const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
|
|
@@ -53,7 +49,10 @@ let relateUrlInstance;
|
|
|
53
49
|
let STORED_URL_BASE;
|
|
54
50
|
|
|
55
51
|
/** Convert absolute url into relative url */
|
|
56
|
-
function minifyUrls(tree, options, moduleOptions) {
|
|
52
|
+
async function minifyUrls(tree, options, moduleOptions) {
|
|
53
|
+
const RelateUrl = await (0, _helpers.optionalImport)('relateurl');
|
|
54
|
+
const srcset = await (0, _helpers.optionalImport)('srcset');
|
|
55
|
+
const terser = await (0, _helpers.optionalImport)('terser');
|
|
57
56
|
let promises = [];
|
|
58
57
|
const urlBase = processModuleOptions(moduleOptions);
|
|
59
58
|
|
|
@@ -84,7 +83,7 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
84
83
|
const attrNameLower = attrName.toLowerCase();
|
|
85
84
|
if (isUriTypeAttribute(node.tag, attrNameLower)) {
|
|
86
85
|
if (isJavaScriptUrl(attrValue)) {
|
|
87
|
-
promises.push(minifyJavaScriptUrl(node, attrName));
|
|
86
|
+
promises.push(minifyJavaScriptUrl(node, attrName, terser));
|
|
88
87
|
} else {
|
|
89
88
|
if (relateUrlInstance) {
|
|
90
89
|
// FIXME!
|
|
@@ -99,14 +98,14 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
99
98
|
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
100
99
|
if (srcset) {
|
|
101
100
|
try {
|
|
102
|
-
const parsedSrcset = srcset.
|
|
101
|
+
const parsedSrcset = srcset.parseSrcset(attrValue, {
|
|
103
102
|
strict: true
|
|
104
103
|
});
|
|
105
|
-
node.attrs[attrName] = srcset.
|
|
104
|
+
node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map(item => {
|
|
106
105
|
if (relateUrlInstance) {
|
|
107
|
-
|
|
106
|
+
item.url = relateUrlInstance.relate(item.url);
|
|
108
107
|
}
|
|
109
|
-
return
|
|
108
|
+
return item;
|
|
110
109
|
}));
|
|
111
110
|
} catch (e) {
|
|
112
111
|
// srcset will throw an Error for invalid srcset.
|
|
@@ -125,7 +124,7 @@ function isJavaScriptUrl(url) {
|
|
|
125
124
|
}
|
|
126
125
|
const jsWrapperStart = 'function a(){';
|
|
127
126
|
const jsWrapperEnd = '}a();';
|
|
128
|
-
function minifyJavaScriptUrl(node, attrName) {
|
|
127
|
+
function minifyJavaScriptUrl(node, attrName, terser) {
|
|
129
128
|
if (!terser) return Promise.resolve();
|
|
130
129
|
let result = node.attrs[attrName];
|
|
131
130
|
if (result) {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { optionalImport } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
4
|
+
const tagsHaveUriValuesForAttributes = new Set([
|
|
5
|
+
'a',
|
|
6
|
+
'area',
|
|
7
|
+
'link',
|
|
8
|
+
'base',
|
|
9
|
+
'object',
|
|
10
|
+
'blockquote',
|
|
11
|
+
'q',
|
|
12
|
+
'del',
|
|
13
|
+
'ins',
|
|
14
|
+
'form',
|
|
15
|
+
'input',
|
|
16
|
+
'head',
|
|
17
|
+
'audio',
|
|
18
|
+
'embed',
|
|
19
|
+
'iframe',
|
|
20
|
+
'img',
|
|
21
|
+
'script',
|
|
22
|
+
'track',
|
|
23
|
+
'video',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const tagsHasHrefAttributes = new Set([
|
|
27
|
+
'a',
|
|
28
|
+
'area',
|
|
29
|
+
'link',
|
|
30
|
+
'base'
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const attributesOfImgTagHasUriValues = new Set([
|
|
34
|
+
'src',
|
|
35
|
+
'longdesc',
|
|
36
|
+
'usemap'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const attributesOfObjectTagHasUriValues = new Set([
|
|
40
|
+
'classid',
|
|
41
|
+
'codebase',
|
|
42
|
+
'data',
|
|
43
|
+
'usemap'
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const tagsHasCiteAttributes = new Set([
|
|
47
|
+
'blockquote',
|
|
48
|
+
'q',
|
|
49
|
+
'ins',
|
|
50
|
+
'del'
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const tagsHasSrcAttributes = new Set([
|
|
54
|
+
'audio',
|
|
55
|
+
'embed',
|
|
56
|
+
'iframe',
|
|
57
|
+
'img',
|
|
58
|
+
'input',
|
|
59
|
+
'script',
|
|
60
|
+
'track',
|
|
61
|
+
'video',
|
|
62
|
+
/**
|
|
63
|
+
* https://html.spec.whatwg.org/#attr-source-src
|
|
64
|
+
*
|
|
65
|
+
* Although most of browsers recommend not to use "src" in <source>,
|
|
66
|
+
* but technically it does comply with HTML Standard.
|
|
67
|
+
*/
|
|
68
|
+
'source'
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const isUriTypeAttribute = (tag, attr) => {
|
|
72
|
+
return (
|
|
73
|
+
tagsHasHrefAttributes.has(tag) && attr === 'href' ||
|
|
74
|
+
tag === 'img' && attributesOfImgTagHasUriValues.has(attr) ||
|
|
75
|
+
tag === 'object' && attributesOfObjectTagHasUriValues.has(attr) ||
|
|
76
|
+
tagsHasCiteAttributes.has(tag) && attr === 'cite' ||
|
|
77
|
+
tag === 'form' && attr === 'action' ||
|
|
78
|
+
tag === 'input' && attr === 'usemap' ||
|
|
79
|
+
tag === 'head' && attr === 'profile' ||
|
|
80
|
+
tag === 'script' && attr === 'for' ||
|
|
81
|
+
tagsHasSrcAttributes.has(tag) && attr === 'src'
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const isSrcsetAttribute = (tag, attr) => {
|
|
86
|
+
return (
|
|
87
|
+
tag === 'source' && attr === 'srcset' ||
|
|
88
|
+
tag === 'img' && attr === 'srcset' ||
|
|
89
|
+
tag === 'link' && attr === 'imagesrcset'
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const processModuleOptions = options => {
|
|
94
|
+
// FIXME!
|
|
95
|
+
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
|
|
96
|
+
// should convert input into URL instance after relateurl@1 is stable
|
|
97
|
+
if (typeof options === 'string') return options;
|
|
98
|
+
if (options instanceof URL) return options.toString();
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isLinkRelCanonical = ({ tag, attrs }) => {
|
|
104
|
+
// Return false early for non-"link" tag
|
|
105
|
+
if (tag !== 'link') return false;
|
|
106
|
+
|
|
107
|
+
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
108
|
+
if (attrName.toLowerCase() === 'rel' && attrValue === 'canonical') return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
|
|
115
|
+
|
|
116
|
+
let relateUrlInstance;
|
|
117
|
+
let STORED_URL_BASE;
|
|
118
|
+
|
|
119
|
+
/** Convert absolute url into relative url */
|
|
120
|
+
export default async function minifyUrls(tree, options, moduleOptions) {
|
|
121
|
+
const RelateUrl = await optionalImport('relateurl');
|
|
122
|
+
const srcset = await optionalImport('srcset');
|
|
123
|
+
const terser = await optionalImport('terser');
|
|
124
|
+
|
|
125
|
+
let promises = [];
|
|
126
|
+
|
|
127
|
+
const urlBase = processModuleOptions(moduleOptions);
|
|
128
|
+
|
|
129
|
+
// Invalid configuration, return tree directly
|
|
130
|
+
if (!urlBase) return tree;
|
|
131
|
+
|
|
132
|
+
/** Bring up a reusable RelateUrl instances (only once)
|
|
133
|
+
*
|
|
134
|
+
* STORED_URL_BASE is used to invalidate RelateUrl instances,
|
|
135
|
+
* avoiding require.cache acrossing multiple htmlnano instance with different configuration,
|
|
136
|
+
* e.g. unit tests cases.
|
|
137
|
+
*/
|
|
138
|
+
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
|
|
139
|
+
if (RelateUrl) {
|
|
140
|
+
relateUrlInstance = new RelateUrl(urlBase);
|
|
141
|
+
}
|
|
142
|
+
STORED_URL_BASE = urlBase;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
tree.walk(node => {
|
|
146
|
+
if (!node.attrs) return node;
|
|
147
|
+
|
|
148
|
+
if (!node.tag) return node;
|
|
149
|
+
|
|
150
|
+
if (!tagsHaveUriValuesForAttributes.has(node.tag)) return node;
|
|
151
|
+
|
|
152
|
+
// Prevent link[rel=canonical] being processed
|
|
153
|
+
// Can't be excluded by isUriTypeAttribute()
|
|
154
|
+
if (isLinkRelCanonical(node)) return node;
|
|
155
|
+
|
|
156
|
+
for (const [attrName, attrValue] of Object.entries(node.attrs)) {
|
|
157
|
+
const attrNameLower = attrName.toLowerCase();
|
|
158
|
+
|
|
159
|
+
if (isUriTypeAttribute(node.tag, attrNameLower)) {
|
|
160
|
+
if (isJavaScriptUrl(attrValue)) {
|
|
161
|
+
promises.push(minifyJavaScriptUrl(node, attrName, terser));
|
|
162
|
+
} else {
|
|
163
|
+
if (relateUrlInstance) {
|
|
164
|
+
// FIXME!
|
|
165
|
+
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
|
|
166
|
+
// the WHATWG URL API is very strict while attrValue might not be a valid URL
|
|
167
|
+
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
|
|
168
|
+
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
176
|
+
if (srcset) {
|
|
177
|
+
try {
|
|
178
|
+
const parsedSrcset = srcset.parseSrcset(attrValue, { strict: true });
|
|
179
|
+
|
|
180
|
+
node.attrs[attrName] = srcset.stringifySrcset(parsedSrcset.map(item => {
|
|
181
|
+
if (relateUrlInstance) {
|
|
182
|
+
item.url = relateUrlInstance.relate(item.url);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return item;
|
|
186
|
+
}));
|
|
187
|
+
} catch (e) {
|
|
188
|
+
// srcset will throw an Error for invalid srcset.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return node;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (promises.length > 0) return Promise.all(promises).then(() => tree);
|
|
200
|
+
return Promise.resolve(tree);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isJavaScriptUrl(url) {
|
|
204
|
+
return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const jsWrapperStart = 'function a(){';
|
|
208
|
+
const jsWrapperEnd = '}a();';
|
|
209
|
+
|
|
210
|
+
function minifyJavaScriptUrl(node, attrName, terser) {
|
|
211
|
+
if (!terser) return Promise.resolve();
|
|
212
|
+
|
|
213
|
+
let result = node.attrs[attrName];
|
|
214
|
+
if (result) {
|
|
215
|
+
result = jsWrapperStart + result.slice(JAVASCRIPT_URL_PROTOCOL.length) + jsWrapperEnd;
|
|
216
|
+
|
|
217
|
+
return terser
|
|
218
|
+
.minify(result, {}) // Default Option is good enough
|
|
219
|
+
.then(({ code }) => {
|
|
220
|
+
const minifiedJs = code.substring(
|
|
221
|
+
jsWrapperStart.length,
|
|
222
|
+
code.length - jsWrapperEnd.length
|
|
223
|
+
);
|
|
224
|
+
node.attrs[attrName] = JAVASCRIPT_URL_PROTOCOL + minifiedJs;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return Promise.resolve();
|
|
229
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const caseInsensitiveAttributes = {
|
|
2
|
+
autocomplete: ['form'],
|
|
3
|
+
charset: ['meta', 'script'],
|
|
4
|
+
contenteditable: null,
|
|
5
|
+
crossorigin: ['audio', 'img', 'link', 'script', 'video'],
|
|
6
|
+
dir: null,
|
|
7
|
+
draggable: null,
|
|
8
|
+
dropzone: null,
|
|
9
|
+
formmethod: ['button', 'input'],
|
|
10
|
+
inputmode: ['input', 'textarea'],
|
|
11
|
+
kind: ['track'],
|
|
12
|
+
method: ['form'],
|
|
13
|
+
preload: ['audio', 'video'],
|
|
14
|
+
referrerpolicy: null,
|
|
15
|
+
sandbox: ['iframe'],
|
|
16
|
+
spellcheck: null,
|
|
17
|
+
scope: ['th'],
|
|
18
|
+
shape: ['area'],
|
|
19
|
+
sizes: ['link'],
|
|
20
|
+
step: ['input'],
|
|
21
|
+
translate: null,
|
|
22
|
+
type: [
|
|
23
|
+
'a',
|
|
24
|
+
'link',
|
|
25
|
+
'button',
|
|
26
|
+
'embed',
|
|
27
|
+
'object',
|
|
28
|
+
'script',
|
|
29
|
+
'source',
|
|
30
|
+
'style',
|
|
31
|
+
'input',
|
|
32
|
+
'menu',
|
|
33
|
+
'menuitem'
|
|
34
|
+
],
|
|
35
|
+
wrap: ['textarea']
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// https://html.spec.whatwg.org/#invalid-value-default
|
|
39
|
+
/** @typedef { [key: string]: { tag: null | string[], default: string, valid: string[] } } */
|
|
40
|
+
const invalidValueDefault = {
|
|
41
|
+
crossorigin: {
|
|
42
|
+
tag: null,
|
|
43
|
+
default: 'anonymous',
|
|
44
|
+
valid: ['', 'anonymous', 'use-credentials']
|
|
45
|
+
},
|
|
46
|
+
// https://html.spec.whatwg.org/#referrer-policy-attributes
|
|
47
|
+
// The attribute's invalid value default and missing value default are both the empty string state.
|
|
48
|
+
referrerpolicy: {
|
|
49
|
+
tag: null,
|
|
50
|
+
default: '',
|
|
51
|
+
valid: ['', 'url', 'origin', 'no-referrer', 'no-referrer-when-downgrade', 'same-origin', 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url']
|
|
52
|
+
},
|
|
53
|
+
// https://html.spec.whatwg.org/#lazy-loading-attributes
|
|
54
|
+
loading: {
|
|
55
|
+
tag: ['img', 'iframe'],
|
|
56
|
+
default: 'eager',
|
|
57
|
+
valid: ['lazy', 'eager']
|
|
58
|
+
},
|
|
59
|
+
// https://html.spec.whatwg.org/#the-img-element
|
|
60
|
+
// https://html.spec.whatwg.org/#image-decoding-hint
|
|
61
|
+
decoding: {
|
|
62
|
+
tag: ['img'],
|
|
63
|
+
default: 'auto',
|
|
64
|
+
valid: ['auto', 'sync', 'async']
|
|
65
|
+
},
|
|
66
|
+
// https://html.spec.whatwg.org/#the-track-element
|
|
67
|
+
kind: {
|
|
68
|
+
tag: ['track'],
|
|
69
|
+
default: 'metadata',
|
|
70
|
+
valid: ['subtitles', 'captions', 'descriptions', 'chapters', 'metadata']
|
|
71
|
+
},
|
|
72
|
+
type: {
|
|
73
|
+
tag: ['button'],
|
|
74
|
+
default: 'submit',
|
|
75
|
+
valid: ['submit', 'reset', 'button']
|
|
76
|
+
},
|
|
77
|
+
wrap: {
|
|
78
|
+
tag: ['textarea'],
|
|
79
|
+
default: 'soft',
|
|
80
|
+
valid: ['soft', 'hard']
|
|
81
|
+
},
|
|
82
|
+
// https://html.spec.whatwg.org/#the-hidden-attribute
|
|
83
|
+
hidden: {
|
|
84
|
+
tag: null,
|
|
85
|
+
default: 'hidden',
|
|
86
|
+
valid: ['hidden', 'until-found']
|
|
87
|
+
},
|
|
88
|
+
// https://html.spec.whatwg.org/#autocapitalization
|
|
89
|
+
autocapitalize: {
|
|
90
|
+
tag: null,
|
|
91
|
+
default: 'sentences',
|
|
92
|
+
valid: ['none', 'off', 'on', 'sentences', 'words', 'characters']
|
|
93
|
+
},
|
|
94
|
+
// https://html.spec.whatwg.org/#the-marquee-element
|
|
95
|
+
behavior: {
|
|
96
|
+
tag: ['marquee'],
|
|
97
|
+
default: 'scroll',
|
|
98
|
+
valid: ['scroll', 'slide', 'alternate']
|
|
99
|
+
},
|
|
100
|
+
direction: {
|
|
101
|
+
tag: ['marquee'],
|
|
102
|
+
default: 'left',
|
|
103
|
+
valid: ['left', 'right', 'up', 'down']
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function onAttrs() {
|
|
108
|
+
return (attrs, node) => {
|
|
109
|
+
const newAttrs = attrs;
|
|
110
|
+
|
|
111
|
+
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
112
|
+
let newAttrValue = attrValue;
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
Object.hasOwnProperty.call(caseInsensitiveAttributes, attrName)
|
|
116
|
+
&& (
|
|
117
|
+
caseInsensitiveAttributes[attrName] === null
|
|
118
|
+
|| caseInsensitiveAttributes[attrName].includes(node.tag)
|
|
119
|
+
)
|
|
120
|
+
) {
|
|
121
|
+
newAttrValue = typeof attrValue.toLowerCase === 'function' ? attrValue.toLowerCase() : attrValue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
Object.hasOwnProperty.call(invalidValueDefault, attrName)
|
|
126
|
+
) {
|
|
127
|
+
const meta = invalidValueDefault[attrName];
|
|
128
|
+
if (meta.tag === null || (node && node.tag && meta.tag.includes(node.tag))) {
|
|
129
|
+
if (!meta.valid.includes(newAttrValue)) {
|
|
130
|
+
newAttrValue = meta.default;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
newAttrs[attrName] = newAttrValue;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return newAttrs;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Specification: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
|
|
2
|
+
// See also: https://github.com/posthtml/posthtml-render/pull/30
|
|
3
|
+
// See also: https://github.com/posthtml/htmlnano/issues/6#issuecomment-707105334
|
|
4
|
+
|
|
5
|
+
/** Disable quoteAllAttributes while not overriding the configuration */
|
|
6
|
+
export default function removeAttributeQuotes(tree) {
|
|
7
|
+
if (tree.options && typeof tree.options.quoteAllAttributes === 'undefined') {
|
|
8
|
+
tree.options.quoteAllAttributes = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return tree;
|
|
12
|
+
}
|
|
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.onContent = onContent;
|
|
7
7
|
exports.onNode = onNode;
|
|
8
|
-
var _helpers = require("../helpers");
|
|
8
|
+
var _helpers = require("../helpers.cjs");
|
|
9
9
|
const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
|
|
10
10
|
|
|
11
11
|
/** Removes HTML comments */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { isComment, isConditionalComment } from '../helpers.mjs';
|
|
2
|
+
|
|
3
|
+
const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
|
|
4
|
+
|
|
5
|
+
/** Removes HTML comments */
|
|
6
|
+
export function onNode(options, removeType) {
|
|
7
|
+
if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
|
|
8
|
+
removeType = 'safe';
|
|
9
|
+
}
|
|
10
|
+
return (node) => {
|
|
11
|
+
if (isCommentToRemove(node, removeType)) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
return node;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function onContent(options, removeType) {
|
|
19
|
+
if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
|
|
20
|
+
removeType = 'safe';
|
|
21
|
+
}
|
|
22
|
+
return (contents) => {
|
|
23
|
+
return contents.filter(content => ! isCommentToRemove(content, removeType));
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isCommentToRemove(text, removeType) {
|
|
28
|
+
if (typeof text !== 'string') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (! isComment(text)) {
|
|
33
|
+
// Not HTML comment
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (removeType === 'safe') {
|
|
38
|
+
const isNoindex = text === '<!--noindex-->' || text === '<!--/noindex-->';
|
|
39
|
+
// Don't remove noindex comments.
|
|
40
|
+
// See: https://yandex.com/support/webmaster/controlling-robot/html.xml
|
|
41
|
+
if (isNoindex) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isServerSideExclude = text === '<!--sse-->' || text === '<!--/sse-->';
|
|
46
|
+
// Don't remove sse comments.
|
|
47
|
+
// See: https://support.cloudflare.com/hc/en-us/articles/200170036-What-does-Server-Side-Excludes-SSE-do-
|
|
48
|
+
if (isServerSideExclude) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// https://en.wikipedia.org/wiki/Conditional_comment
|
|
53
|
+
if (isConditionalComment(text)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hexo: https://hexo.io/docs/tag-plugins#Post-Excerpt
|
|
58
|
+
// Hugo: https://gohugo.io/content-management/summaries/#manual-summary-splitting
|
|
59
|
+
// WordPress: https://wordpress.com/support/wordpress-editor/blocks/more-block/2/
|
|
60
|
+
// Jekyll: https://jekyllrb.com/docs/posts/#post-excerpts
|
|
61
|
+
const isCMSExcerptComment = MATCH_EXCERPT_REGEXP.test(text);
|
|
62
|
+
if (isCMSExcerptComment) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (isMatcher(removeType)) {
|
|
68
|
+
return isMatch(text, removeType);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isMatch(input, matcher) {
|
|
75
|
+
if (matcher instanceof RegExp) {
|
|
76
|
+
return matcher.test(input);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof matcher === 'function') {
|
|
80
|
+
return Boolean(matcher(input));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isMatcher(matcher) {
|
|
87
|
+
if (matcher instanceof RegExp || typeof matcher === 'function') {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return false;
|
|
92
|
+
}
|