htmlnano 2.0.2 → 2.0.4
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 +35 -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 +7763 -11645
- package/docs/package.json +3 -3
- 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 +6 -12
- package/lib/htmlnano.js +18 -40
- 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 +3 -12
- package/lib/modules/mergeStyles.js +5 -9
- package/lib/modules/minifyConditionalComments.js +4 -15
- package/lib/modules/minifyCss.js +9 -16
- package/lib/modules/minifyJs.js +25 -29
- package/lib/modules/minifyJson.js +6 -3
- package/lib/modules/minifySvg.js +17 -9
- package/lib/modules/minifyUrls.js +18 -34
- package/lib/modules/normalizeAttributeValues.js +80 -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 +15 -21
package/lib/modules/custom.js
CHANGED
|
@@ -4,17 +4,14 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = custom;
|
|
7
|
-
|
|
8
7
|
/** Meta-module that runs custom modules */
|
|
9
8
|
function custom(tree, options, customModules) {
|
|
10
9
|
if (!customModules) {
|
|
11
10
|
return tree;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
12
|
if (!Array.isArray(customModules)) {
|
|
15
13
|
customModules = [customModules];
|
|
16
14
|
}
|
|
17
|
-
|
|
18
15
|
customModules.forEach(customModule => {
|
|
19
16
|
tree = customModule(tree, options);
|
|
20
17
|
});
|
|
@@ -4,9 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.onAttrs = onAttrs;
|
|
7
|
-
|
|
8
7
|
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
|
|
9
|
-
|
|
10
8
|
/** Deduplicate values inside list-like attributes (e.g. class, rel) */
|
|
11
9
|
function onAttrs() {
|
|
12
10
|
return attrs => {
|
|
@@ -15,7 +13,9 @@ function onAttrs() {
|
|
|
15
13
|
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrName)) {
|
|
16
14
|
return;
|
|
17
15
|
}
|
|
18
|
-
|
|
16
|
+
if (typeof attrs[attrName] !== 'string') {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
19
|
const attrValues = attrs[attrName].split(/\s/);
|
|
20
20
|
const uniqeAttrValues = new Set();
|
|
21
21
|
const deduplicatedAttrValues = [];
|
|
@@ -25,11 +25,9 @@ function onAttrs() {
|
|
|
25
25
|
deduplicatedAttrValues.push('');
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
|
|
29
28
|
if (uniqeAttrValues.has(attrValue)) {
|
|
30
29
|
return;
|
|
31
30
|
}
|
|
32
|
-
|
|
33
31
|
deduplicatedAttrValues.push(attrValue);
|
|
34
32
|
uniqeAttrValues.add(attrValue);
|
|
35
33
|
});
|
|
@@ -4,7 +4,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = mergeScripts;
|
|
7
|
-
|
|
8
7
|
/* Merge multiple <script> into one */
|
|
9
8
|
function mergeScripts(tree) {
|
|
10
9
|
let scriptNodesIndex = {};
|
|
@@ -13,18 +12,16 @@ function mergeScripts(tree) {
|
|
|
13
12
|
tag: 'script'
|
|
14
13
|
}, node => {
|
|
15
14
|
const nodeAttrs = node.attrs || {};
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if ('src' in nodeAttrs
|
|
16
|
+
// Skip SRI, reasons are documented in "minifyJs" module
|
|
17
|
+
|| 'integrity' in nodeAttrs) {
|
|
18
18
|
scriptSrcIndex++;
|
|
19
19
|
return node;
|
|
20
20
|
}
|
|
21
|
-
|
|
22
21
|
const scriptType = nodeAttrs.type || 'text/javascript';
|
|
23
|
-
|
|
24
22
|
if (scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
|
|
25
23
|
return node;
|
|
26
24
|
}
|
|
27
|
-
|
|
28
25
|
const scriptKey = JSON.stringify({
|
|
29
26
|
id: nodeAttrs.id,
|
|
30
27
|
class: nodeAttrs.class,
|
|
@@ -33,31 +30,25 @@ function mergeScripts(tree) {
|
|
|
33
30
|
async: nodeAttrs.async !== undefined,
|
|
34
31
|
index: scriptSrcIndex
|
|
35
32
|
});
|
|
36
|
-
|
|
37
33
|
if (!scriptNodesIndex[scriptKey]) {
|
|
38
34
|
scriptNodesIndex[scriptKey] = [];
|
|
39
35
|
}
|
|
40
|
-
|
|
41
36
|
scriptNodesIndex[scriptKey].push(node);
|
|
42
37
|
return node;
|
|
43
38
|
});
|
|
44
|
-
|
|
45
39
|
for (const scriptNodes of Object.values(scriptNodesIndex)) {
|
|
46
40
|
let lastScriptNode = scriptNodes.pop();
|
|
47
41
|
scriptNodes.reverse().forEach(scriptNode => {
|
|
48
42
|
let scriptContent = (scriptNode.content || []).join(' ');
|
|
49
43
|
scriptContent = scriptContent.trim();
|
|
50
|
-
|
|
51
44
|
if (scriptContent.slice(-1) !== ';') {
|
|
52
45
|
scriptContent += ';';
|
|
53
46
|
}
|
|
54
|
-
|
|
55
47
|
lastScriptNode.content = lastScriptNode.content || [];
|
|
56
48
|
lastScriptNode.content.unshift(scriptContent);
|
|
57
49
|
scriptNode.tag = false;
|
|
58
50
|
scriptNode.content = [];
|
|
59
51
|
});
|
|
60
52
|
}
|
|
61
|
-
|
|
62
53
|
return tree;
|
|
63
54
|
}
|
|
@@ -4,36 +4,32 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = mergeStyles;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
/* Merge multiple <style> into one */
|
|
11
9
|
function mergeStyles(tree) {
|
|
12
10
|
const styleNodes = {};
|
|
13
11
|
tree.match({
|
|
14
12
|
tag: 'style'
|
|
15
13
|
}, node => {
|
|
16
|
-
const nodeAttrs = node.attrs || {};
|
|
14
|
+
const nodeAttrs = node.attrs || {};
|
|
15
|
+
// Skip <style scoped></style>
|
|
17
16
|
// https://developer.mozilla.org/en/docs/Web/HTML/Element/style
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
//
|
|
18
|
+
// Also skip SRI, reasons are documented in "minifyJs" module
|
|
19
|
+
if ('scoped' in nodeAttrs || 'integrity' in nodeAttrs) {
|
|
20
20
|
return node;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
22
|
if ((0, _helpers.isAmpBoilerplate)(node)) {
|
|
24
23
|
return node;
|
|
25
24
|
}
|
|
26
|
-
|
|
27
25
|
const styleType = nodeAttrs.type || 'text/css';
|
|
28
26
|
const styleMedia = nodeAttrs.media || 'all';
|
|
29
27
|
const styleKey = styleType + '_' + styleMedia;
|
|
30
|
-
|
|
31
28
|
if (styleNodes[styleKey]) {
|
|
32
29
|
const styleContent = (node.content || []).join(' ');
|
|
33
30
|
styleNodes[styleKey].content.push(' ' + styleContent);
|
|
34
31
|
return '';
|
|
35
32
|
}
|
|
36
|
-
|
|
37
33
|
node.content = node.content || [];
|
|
38
34
|
styleNodes[styleKey] = node;
|
|
39
35
|
return node;
|
|
@@ -4,55 +4,44 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyConditionalComments;
|
|
7
|
-
|
|
8
7
|
var _htmlnano = _interopRequireDefault(require("../htmlnano"));
|
|
9
|
-
|
|
10
8
|
var _helpers = require("../helpers");
|
|
11
|
-
|
|
12
9
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
|
-
|
|
14
10
|
// Spec: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/compatibility/ms537512(v=vs.85)
|
|
15
11
|
const CONDITIONAL_COMMENT_REGEXP = /(<!--\[if\s+?[^<>[\]]+?]>)([\s\S]+?)(<!\[endif\]-->)/gm;
|
|
16
|
-
/** Minify content inside conditional comments */
|
|
17
12
|
|
|
13
|
+
/** Minify content inside conditional comments */
|
|
18
14
|
async function minifyConditionalComments(tree, htmlnanoOptions) {
|
|
19
15
|
// forEach, tree.walk, tree.match just don't support Promise.
|
|
20
16
|
for (let i = 0, len = tree.length; i < len; i++) {
|
|
21
17
|
const node = tree[i];
|
|
22
|
-
|
|
23
18
|
if (typeof node === 'string' && (0, _helpers.isConditionalComment)(node)) {
|
|
24
19
|
tree[i] = await minifycontentInsideConditionalComments(node, htmlnanoOptions);
|
|
25
20
|
}
|
|
26
|
-
|
|
27
21
|
if (node.content && node.content.length) {
|
|
28
22
|
tree[i].content = await minifyConditionalComments(node.content, htmlnanoOptions);
|
|
29
23
|
}
|
|
30
24
|
}
|
|
31
|
-
|
|
32
25
|
return tree;
|
|
33
26
|
}
|
|
34
|
-
|
|
35
27
|
async function minifycontentInsideConditionalComments(text, htmlnanoOptions) {
|
|
36
28
|
let match;
|
|
37
|
-
const matches = [];
|
|
38
|
-
// String#matchAll is supported since Node.js 12
|
|
29
|
+
const matches = [];
|
|
39
30
|
|
|
31
|
+
// FIXME!
|
|
32
|
+
// String#matchAll is supported since Node.js 12
|
|
40
33
|
while ((match = CONDITIONAL_COMMENT_REGEXP.exec(text)) !== null) {
|
|
41
34
|
matches.push([match[1], match[2], match[3]]);
|
|
42
35
|
}
|
|
43
|
-
|
|
44
36
|
if (!matches.length) {
|
|
45
37
|
return Promise.resolve(text);
|
|
46
38
|
}
|
|
47
|
-
|
|
48
39
|
return Promise.all(matches.map(async match => {
|
|
49
40
|
const result = await _htmlnano.default.process(match[1], htmlnanoOptions, {}, {});
|
|
50
41
|
let minified = result.html;
|
|
51
|
-
|
|
52
42
|
if (match[1].includes('<html') && minified.includes('</html>')) {
|
|
53
43
|
minified = minified.replace('</html>', '');
|
|
54
44
|
}
|
|
55
|
-
|
|
56
45
|
return match[0] + minified + match[2];
|
|
57
46
|
}));
|
|
58
47
|
}
|
package/lib/modules/minifyCss.js
CHANGED
|
@@ -4,9 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyCss;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
const cssnano = (0, _helpers.optionalRequire)('cssnano');
|
|
11
9
|
const postcss = (0, _helpers.optionalRequire)('postcss');
|
|
12
10
|
const postcssOptions = {
|
|
@@ -15,46 +13,44 @@ const postcssOptions = {
|
|
|
15
13
|
// > Set it to CSS file path or to `undefined` to prevent this warning.
|
|
16
14
|
from: undefined
|
|
17
15
|
};
|
|
18
|
-
/** Minify CSS with cssnano */
|
|
19
16
|
|
|
17
|
+
/** Minify CSS with cssnano */
|
|
20
18
|
function minifyCss(tree, options, cssnanoOptions) {
|
|
21
19
|
if (!cssnano || !postcss) {
|
|
22
20
|
return tree;
|
|
23
21
|
}
|
|
24
|
-
|
|
25
22
|
let promises = [];
|
|
26
23
|
tree.walk(node => {
|
|
24
|
+
// Skip SRI, reasons are documented in "minifyJs" module
|
|
25
|
+
if (node.attrs && 'integrity' in node.attrs) {
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
27
28
|
if ((0, _helpers.isStyleNode)(node)) {
|
|
28
29
|
promises.push(processStyleNode(node, cssnanoOptions));
|
|
29
30
|
} else if (node.attrs && node.attrs.style) {
|
|
30
31
|
promises.push(processStyleAttr(node, cssnanoOptions));
|
|
31
32
|
}
|
|
32
|
-
|
|
33
33
|
return node;
|
|
34
34
|
});
|
|
35
35
|
return Promise.all(promises).then(() => tree);
|
|
36
36
|
}
|
|
37
|
-
|
|
38
37
|
function processStyleNode(styleNode, cssnanoOptions) {
|
|
39
|
-
let css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
38
|
+
let css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
40
39
|
|
|
40
|
+
// Improve performance by avoiding calling stripCdata again and again
|
|
41
41
|
let isCdataWrapped = false;
|
|
42
|
-
|
|
43
42
|
if (css.includes('CDATA')) {
|
|
44
43
|
const strippedCss = stripCdata(css);
|
|
45
44
|
isCdataWrapped = css !== strippedCss;
|
|
46
45
|
css = strippedCss;
|
|
47
46
|
}
|
|
48
|
-
|
|
49
47
|
return postcss([cssnano(cssnanoOptions)]).process(css, postcssOptions).then(result => {
|
|
50
48
|
if (isCdataWrapped) {
|
|
51
49
|
return styleNode.content = ['<![CDATA[' + result + ']]>'];
|
|
52
50
|
}
|
|
53
|
-
|
|
54
51
|
return styleNode.content = [result.css];
|
|
55
52
|
});
|
|
56
53
|
}
|
|
57
|
-
|
|
58
54
|
function processStyleAttr(node, cssnanoOptions) {
|
|
59
55
|
// CSS "color: red;" is invalid. Therefore it should be wrapped inside some selector:
|
|
60
56
|
// a{color: red;}
|
|
@@ -62,19 +58,16 @@ function processStyleAttr(node, cssnanoOptions) {
|
|
|
62
58
|
const wrapperEnd = '}';
|
|
63
59
|
const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
|
|
64
60
|
return postcss([cssnano(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
|
|
65
|
-
const minifiedCss = result.css;
|
|
66
|
-
|
|
61
|
+
const minifiedCss = result.css;
|
|
62
|
+
// Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
|
|
67
63
|
node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
|
|
68
64
|
});
|
|
69
65
|
}
|
|
70
|
-
|
|
71
66
|
function stripCdata(css) {
|
|
72
67
|
const leftStrippedCss = css.replace('<![CDATA[', '');
|
|
73
|
-
|
|
74
68
|
if (leftStrippedCss === css) {
|
|
75
69
|
return css;
|
|
76
70
|
}
|
|
77
|
-
|
|
78
71
|
const strippedCss = leftStrippedCss.replace(']]>', '');
|
|
79
72
|
return leftStrippedCss === strippedCss ? css : strippedCss;
|
|
80
73
|
}
|
package/lib/modules/minifyJs.js
CHANGED
|
@@ -4,96 +4,93 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyJs;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
var _removeRedundantAttributes = require("./removeRedundantAttributes");
|
|
11
|
-
|
|
12
9
|
const terser = (0, _helpers.optionalRequire)('terser');
|
|
13
|
-
/** Minify JS with Terser */
|
|
14
10
|
|
|
11
|
+
/** Minify JS with Terser */
|
|
15
12
|
function minifyJs(tree, options, terserOptions) {
|
|
16
13
|
if (!terser) return tree;
|
|
17
14
|
let promises = [];
|
|
18
15
|
tree.walk(node => {
|
|
16
|
+
const nodeAttrs = node.attrs || {};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Skip SRI
|
|
20
|
+
*
|
|
21
|
+
* If the input <script /> has an SRI attribute, it means that the original <script /> could be trusted,
|
|
22
|
+
* and should not be altered anymore.
|
|
23
|
+
*
|
|
24
|
+
* htmlnano is exactly an MITM that SRI is designed to protect from. If htmlnano or its dependencies get
|
|
25
|
+
* compromised and introduces malicious code, then it is up to the original SRI to protect the end user.
|
|
26
|
+
*
|
|
27
|
+
* So htmlnano will simply skip <script /> that has SRI.
|
|
28
|
+
* If developers do trust htmlnano, they should generate SRI after htmlnano modify the <script />.
|
|
29
|
+
*/
|
|
30
|
+
if ('integrity' in nodeAttrs) {
|
|
31
|
+
return node;
|
|
32
|
+
}
|
|
19
33
|
if (node.tag && node.tag === 'script') {
|
|
20
|
-
const nodeAttrs = node.attrs || {};
|
|
21
34
|
const mimeType = nodeAttrs.type || 'text/javascript';
|
|
22
|
-
|
|
23
35
|
if (_removeRedundantAttributes.redundantScriptTypes.has(mimeType) || mimeType === 'module') {
|
|
24
36
|
promises.push(processScriptNode(node, terserOptions));
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
|
-
|
|
28
39
|
if (node.attrs) {
|
|
29
40
|
promises = promises.concat(processNodeWithOnAttrs(node, terserOptions));
|
|
30
41
|
}
|
|
31
|
-
|
|
32
42
|
return node;
|
|
33
43
|
});
|
|
34
44
|
return Promise.all(promises).then(() => tree);
|
|
35
45
|
}
|
|
36
|
-
|
|
37
46
|
function stripCdata(js) {
|
|
38
47
|
const leftStrippedJs = js.replace(/\/\/\s*<!\[CDATA\[/, '').replace(/\/\*\s*<!\[CDATA\[\s*\*\//, '');
|
|
39
|
-
|
|
40
48
|
if (leftStrippedJs === js) {
|
|
41
49
|
return js;
|
|
42
50
|
}
|
|
43
|
-
|
|
44
51
|
const strippedJs = leftStrippedJs.replace(/\/\/\s*\]\]>/, '').replace(/\/\*\s*\]\]>\s*\*\//, '');
|
|
45
52
|
return leftStrippedJs === strippedJs ? js : strippedJs;
|
|
46
53
|
}
|
|
47
|
-
|
|
48
54
|
function processScriptNode(scriptNode, terserOptions) {
|
|
49
55
|
let js = (scriptNode.content || []).join('').trim();
|
|
50
|
-
|
|
51
56
|
if (!js) {
|
|
52
57
|
return scriptNode;
|
|
53
|
-
}
|
|
54
|
-
|
|
58
|
+
}
|
|
55
59
|
|
|
60
|
+
// Improve performance by avoiding calling stripCdata again and again
|
|
56
61
|
let isCdataWrapped = false;
|
|
57
|
-
|
|
58
62
|
if (js.includes('CDATA')) {
|
|
59
63
|
const strippedJs = stripCdata(js);
|
|
60
64
|
isCdataWrapped = js !== strippedJs;
|
|
61
65
|
js = strippedJs;
|
|
62
66
|
}
|
|
63
|
-
|
|
64
67
|
return terser.minify(js, terserOptions).then(result => {
|
|
65
68
|
if (result.error) {
|
|
66
69
|
throw new Error(result.error);
|
|
67
70
|
}
|
|
68
|
-
|
|
69
71
|
if (result.code === undefined) {
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
|
-
|
|
73
74
|
let content = result.code;
|
|
74
|
-
|
|
75
75
|
if (isCdataWrapped) {
|
|
76
76
|
content = '/*<![CDATA[*/' + content + '/*]]>*/';
|
|
77
77
|
}
|
|
78
|
-
|
|
79
78
|
scriptNode.content = [content];
|
|
80
79
|
});
|
|
81
80
|
}
|
|
82
|
-
|
|
83
81
|
function processNodeWithOnAttrs(node, terserOptions) {
|
|
84
|
-
const jsWrapperStart = 'function
|
|
85
|
-
const jsWrapperEnd = '}a();';
|
|
82
|
+
const jsWrapperStart = 'a=function(){';
|
|
83
|
+
const jsWrapperEnd = '};a();';
|
|
86
84
|
const promises = [];
|
|
87
|
-
|
|
88
85
|
for (const attrName of Object.keys(node.attrs || {})) {
|
|
89
86
|
if (!(0, _helpers.isEventHandler)(attrName)) {
|
|
90
87
|
continue;
|
|
91
|
-
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// For example onclick="return false" is valid,
|
|
92
91
|
// but "return false;" is invalid (error: 'return' outside of function)
|
|
93
92
|
// Therefore the attribute's code should be wrapped inside function:
|
|
94
93
|
// "function _(){return false;}"
|
|
95
|
-
|
|
96
|
-
|
|
97
94
|
let wrappedJs = jsWrapperStart + node.attrs[attrName] + jsWrapperEnd;
|
|
98
95
|
let promise = terser.minify(wrappedJs, terserOptions).then(({
|
|
99
96
|
code
|
|
@@ -103,6 +100,5 @@ function processNodeWithOnAttrs(node, terserOptions) {
|
|
|
103
100
|
});
|
|
104
101
|
promises.push(promise);
|
|
105
102
|
}
|
|
106
|
-
|
|
107
103
|
return promises;
|
|
108
104
|
}
|
|
@@ -5,17 +5,20 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.onContent = onContent;
|
|
7
7
|
const rNodeAttrsTypeJson = /(\/|\+)json/;
|
|
8
|
-
|
|
9
8
|
function onContent() {
|
|
10
9
|
return (content, node) => {
|
|
10
|
+
// Skip SRI, reasons are documented in "minifyJs" module
|
|
11
|
+
if (node.attrs && 'integrity' in node.attrs) {
|
|
12
|
+
return content;
|
|
13
|
+
}
|
|
11
14
|
if (node.attrs && node.attrs.type && rNodeAttrsTypeJson.test(node.attrs.type)) {
|
|
12
15
|
try {
|
|
13
16
|
// cast minified JSON to an array
|
|
14
17
|
return [JSON.stringify(JSON.parse((content || []).join('')))];
|
|
15
|
-
} catch (error) {
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// Invalid JSON
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
|
-
|
|
19
22
|
return content;
|
|
20
23
|
};
|
|
21
24
|
}
|
package/lib/modules/minifySvg.js
CHANGED
|
@@ -4,12 +4,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifySvg;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
const svgo = (0, _helpers.optionalRequire)('svgo');
|
|
11
|
-
/** Minify SVG with SVGO */
|
|
12
9
|
|
|
10
|
+
/** Minify SVG with SVGO */
|
|
13
11
|
function minifySvg(tree, options, svgoOptions = {}) {
|
|
14
12
|
if (!svgo) return tree;
|
|
15
13
|
tree.match({
|
|
@@ -19,12 +17,22 @@ function minifySvg(tree, options, svgoOptions = {}) {
|
|
|
19
17
|
closingSingleTag: 'slash',
|
|
20
18
|
quoteAllAttributes: true
|
|
21
19
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
try {
|
|
21
|
+
const result = svgo.optimize(svgStr, svgoOptions);
|
|
22
|
+
node.tag = false;
|
|
23
|
+
node.attrs = {};
|
|
24
|
+
// result.data is a string, we need to cast it to an array
|
|
25
|
+
node.content = [result.data];
|
|
26
|
+
return node;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('htmlnano fails to minify the svg:');
|
|
29
|
+
console.error(error);
|
|
30
|
+
if (error.name === 'SvgoParserError') {
|
|
31
|
+
console.error(error.toString());
|
|
32
|
+
}
|
|
33
|
+
// We return the node as-is
|
|
34
|
+
return node;
|
|
35
|
+
}
|
|
28
36
|
});
|
|
29
37
|
return tree;
|
|
30
38
|
}
|
|
@@ -4,13 +4,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyUrls;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
const RelateUrl = (0, _helpers.optionalRequire)('relateurl');
|
|
11
9
|
const srcset = (0, _helpers.optionalRequire)('srcset');
|
|
12
|
-
const terser = (0, _helpers.optionalRequire)('terser');
|
|
10
|
+
const terser = (0, _helpers.optionalRequire)('terser');
|
|
13
11
|
|
|
12
|
+
// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
14
13
|
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
|
|
15
14
|
const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
|
|
16
15
|
const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
|
|
@@ -24,15 +23,12 @@ const tagsHasSrcAttributes = new Set(['audio', 'embed', 'iframe', 'img', 'input'
|
|
|
24
23
|
* but technically it does comply with HTML Standard.
|
|
25
24
|
*/
|
|
26
25
|
'source']);
|
|
27
|
-
|
|
28
26
|
const isUriTypeAttribute = (tag, attr) => {
|
|
29
27
|
return tagsHasHrefAttributes.has(tag) && attr === 'href' || tag === 'img' && attributesOfImgTagHasUriValues.has(attr) || tag === 'object' && attributesOfObjectTagHasUriValues.has(attr) || tagsHasCiteAttributes.has(tag) && attr === 'cite' || tag === 'form' && attr === 'action' || tag === 'input' && attr === 'usemap' || tag === 'head' && attr === 'profile' || tag === 'script' && attr === 'for' || tagsHasSrcAttributes.has(tag) && attr === 'src';
|
|
30
28
|
};
|
|
31
|
-
|
|
32
29
|
const isSrcsetAttribute = (tag, attr) => {
|
|
33
30
|
return tag === 'source' && attr === 'srcset' || tag === 'img' && attr === 'srcset' || tag === 'link' && attr === 'imagesrcset';
|
|
34
31
|
};
|
|
35
|
-
|
|
36
32
|
const processModuleOptions = options => {
|
|
37
33
|
// FIXME!
|
|
38
34
|
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
|
|
@@ -41,57 +37,51 @@ const processModuleOptions = options => {
|
|
|
41
37
|
if (options instanceof URL) return options.toString();
|
|
42
38
|
return false;
|
|
43
39
|
};
|
|
44
|
-
|
|
45
40
|
const isLinkRelCanonical = ({
|
|
46
41
|
tag,
|
|
47
42
|
attrs
|
|
48
43
|
}) => {
|
|
49
44
|
// Return false early for non-"link" tag
|
|
50
45
|
if (tag !== 'link') return false;
|
|
51
|
-
|
|
52
46
|
for (const [attrName, attrValue] of Object.entries(attrs)) {
|
|
53
47
|
if (attrName.toLowerCase() === 'rel' && attrValue === 'canonical') return true;
|
|
54
48
|
}
|
|
55
|
-
|
|
56
49
|
return false;
|
|
57
50
|
};
|
|
58
|
-
|
|
59
51
|
const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
|
|
60
52
|
let relateUrlInstance;
|
|
61
53
|
let STORED_URL_BASE;
|
|
62
|
-
/** Convert absolute url into relative url */
|
|
63
54
|
|
|
55
|
+
/** Convert absolute url into relative url */
|
|
64
56
|
function minifyUrls(tree, options, moduleOptions) {
|
|
65
57
|
let promises = [];
|
|
66
|
-
const urlBase = processModuleOptions(moduleOptions);
|
|
58
|
+
const urlBase = processModuleOptions(moduleOptions);
|
|
67
59
|
|
|
60
|
+
// Invalid configuration, return tree directly
|
|
68
61
|
if (!urlBase) return tree;
|
|
62
|
+
|
|
69
63
|
/** Bring up a reusable RelateUrl instances (only once)
|
|
70
64
|
*
|
|
71
65
|
* STORED_URL_BASE is used to invalidate RelateUrl instances,
|
|
72
66
|
* avoiding require.cache acrossing multiple htmlnano instance with different configuration,
|
|
73
67
|
* e.g. unit tests cases.
|
|
74
68
|
*/
|
|
75
|
-
|
|
76
69
|
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
|
|
77
70
|
if (RelateUrl) {
|
|
78
71
|
relateUrlInstance = new RelateUrl(urlBase);
|
|
79
72
|
}
|
|
80
|
-
|
|
81
73
|
STORED_URL_BASE = urlBase;
|
|
82
74
|
}
|
|
83
|
-
|
|
84
75
|
tree.walk(node => {
|
|
85
76
|
if (!node.attrs) return node;
|
|
86
77
|
if (!node.tag) return node;
|
|
87
|
-
if (!tagsHaveUriValuesForAttributes.has(node.tag)) return node;
|
|
88
|
-
// Can't be excluded by isUriTypeAttribute()
|
|
78
|
+
if (!tagsHaveUriValuesForAttributes.has(node.tag)) return node;
|
|
89
79
|
|
|
80
|
+
// Prevent link[rel=canonical] being processed
|
|
81
|
+
// Can't be excluded by isUriTypeAttribute()
|
|
90
82
|
if (isLinkRelCanonical(node)) return node;
|
|
91
|
-
|
|
92
83
|
for (const [attrName, attrValue] of Object.entries(node.attrs)) {
|
|
93
84
|
const attrNameLower = attrName.toLowerCase();
|
|
94
|
-
|
|
95
85
|
if (isUriTypeAttribute(node.tag, attrNameLower)) {
|
|
96
86
|
if (isJavaScriptUrl(attrValue)) {
|
|
97
87
|
promises.push(minifyJavaScriptUrl(node, attrName));
|
|
@@ -104,55 +94,49 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
104
94
|
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
|
|
105
95
|
}
|
|
106
96
|
}
|
|
107
|
-
|
|
108
97
|
continue;
|
|
109
98
|
}
|
|
110
|
-
|
|
111
99
|
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
112
100
|
if (srcset) {
|
|
113
101
|
try {
|
|
114
|
-
const parsedSrcset = srcset.parse(attrValue
|
|
102
|
+
const parsedSrcset = srcset.parse(attrValue, {
|
|
103
|
+
strict: true
|
|
104
|
+
});
|
|
115
105
|
node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
|
|
116
106
|
if (relateUrlInstance) {
|
|
117
107
|
srcset.url = relateUrlInstance.relate(srcset.url);
|
|
118
108
|
}
|
|
119
|
-
|
|
120
109
|
return srcset;
|
|
121
110
|
}));
|
|
122
|
-
} catch (e) {
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// srcset will throw an Error for invalid srcset.
|
|
123
113
|
}
|
|
124
114
|
}
|
|
125
|
-
|
|
126
115
|
continue;
|
|
127
116
|
}
|
|
128
117
|
}
|
|
129
|
-
|
|
130
118
|
return node;
|
|
131
119
|
});
|
|
132
120
|
if (promises.length > 0) return Promise.all(promises).then(() => tree);
|
|
133
121
|
return Promise.resolve(tree);
|
|
134
122
|
}
|
|
135
|
-
|
|
136
123
|
function isJavaScriptUrl(url) {
|
|
137
124
|
return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
|
|
138
125
|
}
|
|
139
|
-
|
|
126
|
+
const jsWrapperStart = 'function a(){';
|
|
127
|
+
const jsWrapperEnd = '}a();';
|
|
140
128
|
function minifyJavaScriptUrl(node, attrName) {
|
|
141
129
|
if (!terser) return Promise.resolve();
|
|
142
|
-
const jsWrapperStart = 'function a(){';
|
|
143
|
-
const jsWrapperEnd = '}a();';
|
|
144
130
|
let result = node.attrs[attrName];
|
|
145
|
-
|
|
146
131
|
if (result) {
|
|
147
|
-
result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
|
|
132
|
+
result = jsWrapperStart + result.slice(JAVASCRIPT_URL_PROTOCOL.length) + jsWrapperEnd;
|
|
148
133
|
return terser.minify(result, {}) // Default Option is good enough
|
|
149
134
|
.then(({
|
|
150
135
|
code
|
|
151
136
|
}) => {
|
|
152
137
|
const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
|
|
153
|
-
node.attrs[attrName] = minifiedJs;
|
|
138
|
+
node.attrs[attrName] = JAVASCRIPT_URL_PROTOCOL + minifiedJs;
|
|
154
139
|
});
|
|
155
140
|
}
|
|
156
|
-
|
|
157
141
|
return Promise.resolve();
|
|
158
142
|
}
|