htmlnano 1.0.0 → 2.0.0
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 +55 -2
- package/README.md +12 -893
- package/docs/README.md +33 -0
- package/docs/babel.config.js +3 -0
- package/docs/docs/010-introduction.md +22 -0
- package/docs/docs/020-usage.md +77 -0
- package/docs/docs/030-config.md +21 -0
- package/docs/docs/040-presets.md +75 -0
- package/docs/docs/050-modules.md +838 -0
- package/docs/docs/060-contribute.md +16 -0
- package/docs/docusaurus.config.js +60 -0
- package/docs/netlify.toml +4 -0
- package/docs/package-lock.json +27251 -0
- package/docs/package.json +39 -0
- package/docs/sidebars.js +26 -0
- package/docs/versioned_docs/version-1.1.1/010-introduction.md +22 -0
- package/docs/versioned_docs/version-1.1.1/020-usage.md +77 -0
- package/docs/versioned_docs/version-1.1.1/030-config.md +21 -0
- package/docs/versioned_docs/version-1.1.1/040-presets.md +75 -0
- package/docs/versioned_docs/version-1.1.1/050-modules.md +786 -0
- package/docs/versioned_docs/version-1.1.1/060-contribute.md +16 -0
- package/docs/versioned_docs/version-2.0.0/010-introduction.md +22 -0
- package/docs/versioned_docs/version-2.0.0/020-usage.md +77 -0
- package/docs/versioned_docs/version-2.0.0/030-config.md +21 -0
- package/docs/versioned_docs/version-2.0.0/040-presets.md +75 -0
- package/docs/versioned_docs/version-2.0.0/050-modules.md +838 -0
- package/docs/versioned_docs/version-2.0.0/060-contribute.md +16 -0
- package/docs/versioned_sidebars/version-1.1.1-sidebars.json +8 -0
- package/docs/versioned_sidebars/version-2.0.0-sidebars.json +8 -0
- package/docs/versions.json +4 -0
- package/lib/helpers.js +19 -1
- package/lib/htmlnano.js +67 -6
- package/lib/modules/collapseAttributeWhitespace.js +63 -7
- package/lib/modules/collapseWhitespace.js +42 -17
- package/lib/modules/minifyCss.js +8 -8
- package/lib/modules/minifyJs.js +9 -10
- package/lib/modules/minifySvg.js +5 -2
- package/lib/modules/minifyUrls.js +71 -29
- package/lib/modules/normalizeAttributeValues.js +48 -0
- package/lib/modules/removeComments.js +25 -1
- package/lib/modules/removeEmptyAttributes.js +53 -8
- package/lib/modules/removeRedundantAttributes.js +69 -14
- package/lib/modules/removeUnusedCss.js +10 -10
- package/lib/presets/max.js +2 -0
- package/lib/presets/safe.js +13 -13
- package/package.json +58 -21
- package/test.js +25 -16
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Contribute
|
|
2
|
+
|
|
3
|
+
Since the minifier is modular, it's very easy to add new modules:
|
|
4
|
+
|
|
5
|
+
1. Create a ES6-file inside `lib/modules/` with a function that does some minification. For example you can check [`lib/modules/example.es6`](https://github.com/posthtml/htmlnano/blob/master/lib/modules/example.es6).
|
|
6
|
+
|
|
7
|
+
2. Add the module's name into one of those [presets](https://github.com/posthtml/htmlnano/tree/master/lib/presets). You can choose either `ampSafe`, `max`, or `safe`.
|
|
8
|
+
|
|
9
|
+
3. Create a JS-file inside `test/modules/` with some unit-tests.
|
|
10
|
+
|
|
11
|
+
4. Describe your module in the section "[Modules](https://github.com/posthtml/htmlnano/blob/master/README.md#modules)".
|
|
12
|
+
|
|
13
|
+
5. Send me a pull request.
|
|
14
|
+
|
|
15
|
+
Other types of contribution (bug fixes, documentation improves, etc) are also welcome!
|
|
16
|
+
Would like to contribute, but don't have any ideas what to do? Check out [our issues](https://github.com/posthtml/htmlnano/labels/help%20wanted).
|
package/lib/helpers.js
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.extractCssFromStyleNode = extractCssFromStyleNode;
|
|
6
7
|
exports.isAmpBoilerplate = isAmpBoilerplate;
|
|
7
8
|
exports.isComment = isComment;
|
|
8
9
|
exports.isConditionalComment = isConditionalComment;
|
|
10
|
+
exports.isEventHandler = isEventHandler;
|
|
9
11
|
exports.isStyleNode = isStyleNode;
|
|
10
|
-
exports.
|
|
12
|
+
exports.optionalRequire = optionalRequire;
|
|
11
13
|
const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
|
|
12
14
|
|
|
13
15
|
function isAmpBoilerplate(node) {
|
|
@@ -38,4 +40,20 @@ function isStyleNode(node) {
|
|
|
38
40
|
|
|
39
41
|
function extractCssFromStyleNode(node) {
|
|
40
42
|
return Array.isArray(node.content) ? node.content.join(' ') : node.content;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isEventHandler(attributeName) {
|
|
46
|
+
return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function optionalRequire(moduleName) {
|
|
50
|
+
try {
|
|
51
|
+
return require(moduleName);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
41
59
|
}
|
package/lib/htmlnano.js
CHANGED
|
@@ -4,18 +4,66 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
7
8
|
|
|
8
9
|
var _posthtml = _interopRequireDefault(require("posthtml"));
|
|
9
10
|
|
|
11
|
+
var _cosmiconfig = require("cosmiconfig");
|
|
12
|
+
|
|
10
13
|
var _safe = _interopRequireDefault(require("./presets/safe"));
|
|
11
14
|
|
|
12
15
|
var _ampSafe = _interopRequireDefault(require("./presets/ampSafe"));
|
|
13
16
|
|
|
14
17
|
var _max = _interopRequireDefault(require("./presets/max"));
|
|
15
18
|
|
|
19
|
+
var _package = _interopRequireDefault(require("../package.json"));
|
|
20
|
+
|
|
16
21
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
const presets = {
|
|
24
|
+
safe: _safe.default,
|
|
25
|
+
ampSafe: _ampSafe.default,
|
|
26
|
+
max: _max.default
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function loadConfig(options, preset, configPath) {
|
|
30
|
+
var _options;
|
|
31
|
+
|
|
32
|
+
if (!((_options = options) !== null && _options !== void 0 && _options.skipConfigLoading)) {
|
|
33
|
+
const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
|
|
34
|
+
const rc = configPath ? explorer.load(configPath) : explorer.search();
|
|
35
|
+
|
|
36
|
+
if (rc) {
|
|
37
|
+
const {
|
|
38
|
+
preset: presetName
|
|
39
|
+
} = rc.config;
|
|
40
|
+
|
|
41
|
+
if (presetName) {
|
|
42
|
+
if (!preset && presets[presetName]) {
|
|
43
|
+
preset = presets[presetName];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
delete rc.config.preset;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!options) {
|
|
50
|
+
options = rc.config;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [options || {}, preset || _safe.default];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const optionalDependencies = {
|
|
59
|
+
minifyCss: ['cssnano', 'postcss'],
|
|
60
|
+
minifyJs: ['terser'],
|
|
61
|
+
minifyUrl: ['relateurl', 'srcset', 'terser'],
|
|
62
|
+
minifySvg: ['svgo']
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function htmlnano(optionsRun, presetRun) {
|
|
66
|
+
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
19
67
|
return function minifier(tree) {
|
|
20
68
|
options = { ...preset,
|
|
21
69
|
...options
|
|
@@ -32,6 +80,18 @@ function htmlnano(options = {}, preset = _safe.default) {
|
|
|
32
80
|
throw new Error('Module "' + moduleName + '" is not defined');
|
|
33
81
|
}
|
|
34
82
|
|
|
83
|
+
(optionalDependencies[moduleName] || []).forEach(dependency => {
|
|
84
|
+
try {
|
|
85
|
+
require(dependency);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
88
|
+
console.warn(`You have to install "${dependency}" in order to use htmlnano's "${moduleName}" module`);
|
|
89
|
+
} else {
|
|
90
|
+
throw e;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
35
95
|
let module = require('./modules/' + moduleName);
|
|
36
96
|
|
|
37
97
|
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
@@ -41,14 +101,15 @@ function htmlnano(options = {}, preset = _safe.default) {
|
|
|
41
101
|
};
|
|
42
102
|
}
|
|
43
103
|
|
|
104
|
+
htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
|
|
105
|
+
const [options] = loadConfig(optionsRun, presetRun);
|
|
106
|
+
return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
|
|
107
|
+
};
|
|
108
|
+
|
|
44
109
|
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
45
110
|
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
46
111
|
};
|
|
47
112
|
|
|
48
|
-
htmlnano.presets =
|
|
49
|
-
safe: _safe.default,
|
|
50
|
-
ampSafe: _ampSafe.default,
|
|
51
|
-
max: _max.default
|
|
52
|
-
};
|
|
113
|
+
htmlnano.presets = presets;
|
|
53
114
|
var _default = htmlnano;
|
|
54
115
|
exports.default = _default;
|
|
@@ -3,12 +3,60 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.default = collapseAttributeWhitespace;
|
|
7
6
|
exports.attributesWithLists = void 0;
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
exports.default = collapseAttributeWhitespace;
|
|
8
|
+
|
|
9
|
+
var _helpers = require("../helpers");
|
|
10
|
+
|
|
11
|
+
const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, link
|
|
12
|
+
'ping', // a, area
|
|
13
|
+
'sandbox', // iframe
|
|
14
|
+
'sizes', // link
|
|
15
|
+
'headers' // td, th
|
|
16
|
+
]);
|
|
17
|
+
/** @type Record<string, string[] | null> */
|
|
10
18
|
|
|
11
19
|
exports.attributesWithLists = attributesWithLists;
|
|
20
|
+
const attributesWithSingleValue = {
|
|
21
|
+
accept: ['input'],
|
|
22
|
+
action: ['form'],
|
|
23
|
+
accesskey: null,
|
|
24
|
+
'accept-charset': ['form'],
|
|
25
|
+
cite: ['blockquote', 'del', 'ins', 'q'],
|
|
26
|
+
cols: ['textarea'],
|
|
27
|
+
colspan: ['td', 'th'],
|
|
28
|
+
data: ['object'],
|
|
29
|
+
dropzone: null,
|
|
30
|
+
formaction: ['button', 'input'],
|
|
31
|
+
height: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video'],
|
|
32
|
+
high: ['meter'],
|
|
33
|
+
href: ['a', 'area', 'base', 'link'],
|
|
34
|
+
itemid: null,
|
|
35
|
+
low: ['meter'],
|
|
36
|
+
manifest: ['html'],
|
|
37
|
+
max: ['meter', 'progress'],
|
|
38
|
+
maxlength: ['input', 'textarea'],
|
|
39
|
+
media: ['source'],
|
|
40
|
+
min: ['meter'],
|
|
41
|
+
minlength: ['input', 'textarea'],
|
|
42
|
+
optimum: ['meter'],
|
|
43
|
+
ping: ['a', 'area'],
|
|
44
|
+
poster: ['video'],
|
|
45
|
+
profile: ['head'],
|
|
46
|
+
rows: ['textarea'],
|
|
47
|
+
rowspan: ['td', 'th'],
|
|
48
|
+
size: ['input', 'select'],
|
|
49
|
+
span: ['col', 'colgroup'],
|
|
50
|
+
src: ['audio', 'embed', 'iframe', 'img', 'input', 'script', 'source', 'track', 'video'],
|
|
51
|
+
start: ['ol'],
|
|
52
|
+
step: ['input'],
|
|
53
|
+
style: null,
|
|
54
|
+
tabindex: null,
|
|
55
|
+
usemap: ['img', 'object'],
|
|
56
|
+
value: ['li', 'meter', 'progress'],
|
|
57
|
+
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
|
|
58
|
+
};
|
|
59
|
+
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
12
60
|
|
|
13
61
|
function collapseAttributeWhitespace(tree) {
|
|
14
62
|
tree.walk(node => {
|
|
@@ -19,14 +67,22 @@ function collapseAttributeWhitespace(tree) {
|
|
|
19
67
|
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
20
68
|
const attrNameLower = attrName.toLowerCase();
|
|
21
69
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
70
|
+
if (attributesWithLists.has(attrNameLower)) {
|
|
71
|
+
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
72
|
+
node.attrs[attrName] = newAttrValue;
|
|
73
|
+
return node;
|
|
24
74
|
}
|
|
25
75
|
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
if ((0, _helpers.isEventHandler)(attrNameLower) || Object.hasOwnProperty.call(attributesWithSingleValue, attrNameLower) && (attributesWithSingleValue[attrNameLower] === null || attributesWithSingleValue[attrNameLower].includes(node.tag))) {
|
|
77
|
+
node.attrs[attrName] = minifySingleAttributeValue(attrValue);
|
|
78
|
+
return node;
|
|
79
|
+
}
|
|
28
80
|
});
|
|
29
81
|
return node;
|
|
30
82
|
});
|
|
31
83
|
return tree;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function minifySingleAttributeValue(value) {
|
|
87
|
+
return typeof value === 'string' ? String(value).trim() : value;
|
|
32
88
|
}
|
|
@@ -13,34 +13,40 @@ const noTrimWhitespacesArroundElements = new Set([// non-empty tags that will ma
|
|
|
13
13
|
'comment', 'img', 'input', 'wbr']);
|
|
14
14
|
const noTrimWhitespacesInsideElements = new Set([// non-empty tags that will maintain whitespace within them
|
|
15
15
|
'a', 'abbr', 'acronym', 'b', 'big', 'del', 'em', 'font', 'i', 'ins', 'kbd', 'mark', 'nobr', 'rp', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'time', 'tt', 'u', 'var']);
|
|
16
|
-
const
|
|
16
|
+
const startsWithWhitespacePattern = /^\s/;
|
|
17
|
+
const endsWithWhitespacePattern = /\s$/;
|
|
18
|
+
const multipleWhitespacePattern = /\s+/g;
|
|
17
19
|
const NONE = '';
|
|
18
20
|
const SINGLE_SPACE = ' ';
|
|
19
21
|
const validOptions = ['all', 'aggressive', 'conservative'];
|
|
20
22
|
/** Collapses redundant whitespaces */
|
|
21
23
|
|
|
22
|
-
function collapseWhitespace(tree, options, collapseType,
|
|
24
|
+
function collapseWhitespace(tree, options, collapseType, parent) {
|
|
23
25
|
collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
|
|
24
26
|
tree.forEach((node, index) => {
|
|
27
|
+
const prevNode = tree[index - 1];
|
|
28
|
+
const nextNode = tree[index + 1];
|
|
29
|
+
|
|
25
30
|
if (typeof node === 'string') {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const prevNodeTag = prevNode && prevNode.tag;
|
|
29
|
-
const nextNodeTag = nextNode && nextNode.tag;
|
|
30
|
-
const isTopLevel = !tag || tag === 'html' || tag === 'head';
|
|
31
|
+
const parentNodeTag = parent && parent.node && parent.node.tag;
|
|
32
|
+
const isTopLevel = !parentNodeTag || parentNodeTag === 'html' || parentNodeTag === 'head';
|
|
31
33
|
const shouldTrim = collapseType === 'all' || isTopLevel ||
|
|
32
34
|
/*
|
|
33
35
|
* When collapseType is set to 'aggressive', and the tag is not inside 'noTrimWhitespacesInsideElements'.
|
|
34
36
|
* the first & last space inside the tag will be trimmed
|
|
35
37
|
*/
|
|
36
|
-
collapseType === 'aggressive'
|
|
37
|
-
node = collapseRedundantWhitespaces(node, collapseType, shouldTrim,
|
|
38
|
+
collapseType === 'aggressive';
|
|
39
|
+
node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, parent, prevNode, nextNode);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
|
|
41
43
|
|
|
42
44
|
if (node.content && node.content.length && isAllowCollapseWhitespace) {
|
|
43
|
-
node.content = collapseWhitespace(node.content, options, collapseType,
|
|
45
|
+
node.content = collapseWhitespace(node.content, options, collapseType, {
|
|
46
|
+
node,
|
|
47
|
+
prevNode,
|
|
48
|
+
nextNode
|
|
49
|
+
});
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
tree[index] = node;
|
|
@@ -48,23 +54,42 @@ function collapseWhitespace(tree, options, collapseType, tag) {
|
|
|
48
54
|
return tree;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false,
|
|
57
|
+
function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, parent, prevNode, nextNode) {
|
|
52
58
|
if (!text || text.length === 0) {
|
|
53
59
|
return NONE;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
if (!(0, _helpers.isComment)(text)) {
|
|
57
|
-
text = text.replace(
|
|
63
|
+
text = text.replace(multipleWhitespacePattern, SINGLE_SPACE);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
if (shouldTrim) {
|
|
61
67
|
if (collapseType === 'aggressive') {
|
|
62
|
-
if (!
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if (!noTrimWhitespacesInsideElements.has(parent && parent.node && parent.node.tag)) {
|
|
69
|
+
if (!noTrimWhitespacesArroundElements.has(prevNode && prevNode.tag)) {
|
|
70
|
+
text = text.trimStart();
|
|
71
|
+
} else {
|
|
72
|
+
// previous node is a "no trim whitespaces arround element"
|
|
73
|
+
if ( // but previous node ends with a whitespace
|
|
74
|
+
prevNode && prevNode.content && prevNode.content.length && endsWithWhitespacePattern.test(prevNode.content[prevNode.content.length - 1]) && (!nextNode // either the current node is the last child of the parent
|
|
75
|
+
|| // or the next node starts with a white space
|
|
76
|
+
nextNode && nextNode.content && nextNode.content.length && !startsWithWhitespacePattern.test(nextNode.content[0]))) {
|
|
77
|
+
text = text.trimStart();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
if (!noTrimWhitespacesArroundElements.has(nextNode && nextNode.tag)) {
|
|
82
|
+
text = text.trimEnd();
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// now it is a textNode inside a "no trim whitespaces inside elements" node
|
|
86
|
+
if (!prevNode // it the textnode is the first child of the node
|
|
87
|
+
&& startsWithWhitespacePattern.test(text[0]) // it starts with white space
|
|
88
|
+
&& typeof parent.prevNode === 'string' // the prev of the node is a textNode as well
|
|
89
|
+
&& endsWithWhitespacePattern.test(parent.prevNode[parent.prevNode.length - 1]) // that prev is ends with a white
|
|
90
|
+
) {
|
|
91
|
+
text = text.trimStart();
|
|
92
|
+
}
|
|
68
93
|
}
|
|
69
94
|
} else {
|
|
70
95
|
// collapseType is 'all', trim spaces
|
package/lib/modules/minifyCss.js
CHANGED
|
@@ -7,12 +7,8 @@ exports.default = minifyCss;
|
|
|
7
7
|
|
|
8
8
|
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
var _cssnano = _interopRequireDefault(require("cssnano"));
|
|
13
|
-
|
|
14
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
15
|
-
|
|
10
|
+
const cssnano = (0, _helpers.optionalRequire)('cssnano');
|
|
11
|
+
const postcss = (0, _helpers.optionalRequire)('postcss');
|
|
16
12
|
const postcssOptions = {
|
|
17
13
|
// Prevent the following warning from being shown:
|
|
18
14
|
// > Without `from` option PostCSS could generate wrong source map and will not find Browserslist config.
|
|
@@ -22,6 +18,10 @@ const postcssOptions = {
|
|
|
22
18
|
/** Minify CSS with cssnano */
|
|
23
19
|
|
|
24
20
|
function minifyCss(tree, options, cssnanoOptions) {
|
|
21
|
+
if (!cssnano || !postcss) {
|
|
22
|
+
return tree;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
25
|
let promises = [];
|
|
26
26
|
tree.walk(node => {
|
|
27
27
|
if ((0, _helpers.isStyleNode)(node)) {
|
|
@@ -46,7 +46,7 @@ function processStyleNode(styleNode, cssnanoOptions) {
|
|
|
46
46
|
css = strippedCss;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
return (
|
|
49
|
+
return postcss([cssnano(cssnanoOptions)]).process(css, postcssOptions).then(result => {
|
|
50
50
|
if (isCdataWrapped) {
|
|
51
51
|
return styleNode.content = ['<![CDATA[' + result + ']]>'];
|
|
52
52
|
}
|
|
@@ -61,7 +61,7 @@ function processStyleAttr(node, cssnanoOptions) {
|
|
|
61
61
|
const wrapperStart = 'a{';
|
|
62
62
|
const wrapperEnd = '}';
|
|
63
63
|
const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
|
|
64
|
-
return (
|
|
64
|
+
return postcss([cssnano(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
|
|
65
65
|
const minifiedCss = result.css; // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
|
|
66
66
|
|
|
67
67
|
node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
|
package/lib/modules/minifyJs.js
CHANGED
|
@@ -5,14 +5,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyJs;
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
10
|
var _removeRedundantAttributes = require("./removeRedundantAttributes");
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const terser = (0, _helpers.optionalRequire)('terser');
|
|
14
13
|
/** Minify JS with Terser */
|
|
14
|
+
|
|
15
15
|
function minifyJs(tree, options, terserOptions) {
|
|
16
|
+
if (!terser) return tree;
|
|
16
17
|
let promises = [];
|
|
17
18
|
tree.walk(node => {
|
|
18
19
|
if (node.tag && node.tag === 'script') {
|
|
@@ -60,7 +61,7 @@ function processScriptNode(scriptNode, terserOptions) {
|
|
|
60
61
|
js = strippedJs;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
return
|
|
64
|
+
return terser.minify(js, terserOptions).then(result => {
|
|
64
65
|
if (result.error) {
|
|
65
66
|
throw new Error(result.error);
|
|
66
67
|
}
|
|
@@ -80,12 +81,12 @@ function processScriptNode(scriptNode, terserOptions) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function processNodeWithOnAttrs(node, terserOptions) {
|
|
83
|
-
const jsWrapperStart = 'function
|
|
84
|
-
const jsWrapperEnd = '}';
|
|
84
|
+
const jsWrapperStart = 'function a(){';
|
|
85
|
+
const jsWrapperEnd = '}a();';
|
|
85
86
|
const promises = [];
|
|
86
87
|
|
|
87
88
|
for (const attrName of Object.keys(node.attrs || {})) {
|
|
88
|
-
if (!
|
|
89
|
+
if (!(0, _helpers.isEventHandler)(attrName)) {
|
|
89
90
|
continue;
|
|
90
91
|
} // For example onclick="return false" is valid,
|
|
91
92
|
// but "return false;" is invalid (error: 'return' outside of function)
|
|
@@ -94,14 +95,12 @@ function processNodeWithOnAttrs(node, terserOptions) {
|
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
let wrappedJs = jsWrapperStart + node.attrs[attrName] + jsWrapperEnd;
|
|
97
|
-
|
|
98
|
-
let promise = _terser.default.minify(wrappedJs, terserOptions).then(({
|
|
98
|
+
let promise = terser.minify(wrappedJs, terserOptions).then(({
|
|
99
99
|
code
|
|
100
100
|
}) => {
|
|
101
101
|
let minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
|
|
102
102
|
node.attrs[attrName] = minifiedJs;
|
|
103
103
|
});
|
|
104
|
-
|
|
105
104
|
promises.push(promise);
|
|
106
105
|
}
|
|
107
106
|
|
package/lib/modules/minifySvg.js
CHANGED
|
@@ -5,10 +5,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifySvg;
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
+
const svgo = (0, _helpers.optionalRequire)('svgo');
|
|
10
11
|
/** Minify SVG with SVGO */
|
|
12
|
+
|
|
11
13
|
function minifySvg(tree, options, svgoOptions = {}) {
|
|
14
|
+
if (!svgo) return tree;
|
|
12
15
|
tree.match({
|
|
13
16
|
tag: 'svg'
|
|
14
17
|
}, node => {
|
|
@@ -16,7 +19,7 @@ function minifySvg(tree, options, svgoOptions = {}) {
|
|
|
16
19
|
closingSingleTag: 'slash',
|
|
17
20
|
quoteAllAttributes: true
|
|
18
21
|
});
|
|
19
|
-
const result =
|
|
22
|
+
const result = svgo.optimize(svgStr, svgoOptions);
|
|
20
23
|
node.tag = false;
|
|
21
24
|
node.attrs = {};
|
|
22
25
|
node.content = result.data;
|
|
@@ -5,27 +5,28 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifyUrls;
|
|
7
7
|
|
|
8
|
-
var
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const RelateUrl = (0, _helpers.optionalRequire)('relateurl');
|
|
11
|
+
const srcset = (0, _helpers.optionalRequire)('srcset');
|
|
12
|
+
const terser = (0, _helpers.optionalRequire)('terser'); // Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
15
|
-
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'img', 'object', 'q', 'blockquote', 'ins', 'form', 'input', 'head', 'script']);
|
|
14
|
+
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
|
|
16
15
|
const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
|
|
17
16
|
const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
|
|
18
17
|
const attributesOfObjectTagHasUriValues = new Set(['classid', 'codebase', 'data', 'usemap']);
|
|
18
|
+
const tagsHasCiteAttributes = new Set(['blockquote', 'q', 'ins', 'del']);
|
|
19
|
+
const tagsHasSrcAttributes = new Set(['audio', 'embed', 'iframe', 'img', 'input', 'script', 'track', 'video',
|
|
20
|
+
/**
|
|
21
|
+
* https://html.spec.whatwg.org/#attr-source-src
|
|
22
|
+
*
|
|
23
|
+
* Although most of browsers recommend not to use "src" in <source>,
|
|
24
|
+
* but technically it does comply with HTML Standard.
|
|
25
|
+
*/
|
|
26
|
+
'source']);
|
|
19
27
|
|
|
20
28
|
const isUriTypeAttribute = (tag, attr) => {
|
|
21
|
-
return tagsHasHrefAttributes.has(tag) && attr === 'href' || tag === 'img' && attributesOfImgTagHasUriValues.has(attr) || tag === 'object' && attributesOfObjectTagHasUriValues.has(attr) ||
|
|
22
|
-
/**
|
|
23
|
-
* https://html.spec.whatwg.org/#attr-source-src
|
|
24
|
-
*
|
|
25
|
-
* Although most of browsers recommend not to use "src" in <source>,
|
|
26
|
-
* but technically it does comply with HTML Standard.
|
|
27
|
-
*/
|
|
28
|
-
tag === 'source' && attr === 'src';
|
|
29
|
+
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';
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const isSrcsetAttribute = (tag, attr) => {
|
|
@@ -55,11 +56,13 @@ const isLinkRelCanonical = ({
|
|
|
55
56
|
return false;
|
|
56
57
|
};
|
|
57
58
|
|
|
59
|
+
const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
|
|
58
60
|
let relateUrlInstance;
|
|
59
61
|
let STORED_URL_BASE;
|
|
60
62
|
/** Convert absolute url into relative url */
|
|
61
63
|
|
|
62
64
|
function minifyUrls(tree, options, moduleOptions) {
|
|
65
|
+
let promises = [];
|
|
63
66
|
const urlBase = processModuleOptions(moduleOptions); // Invalid configuration, return tree directly
|
|
64
67
|
|
|
65
68
|
if (!urlBase) return tree;
|
|
@@ -71,7 +74,10 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
71
74
|
*/
|
|
72
75
|
|
|
73
76
|
if (!relateUrlInstance || STORED_URL_BASE !== urlBase) {
|
|
74
|
-
|
|
77
|
+
if (RelateUrl) {
|
|
78
|
+
relateUrlInstance = new RelateUrl(urlBase);
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
STORED_URL_BASE = urlBase;
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -87,23 +93,34 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
87
93
|
const attrNameLower = attrName.toLowerCase();
|
|
88
94
|
|
|
89
95
|
if (isUriTypeAttribute(node.tag, attrNameLower)) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
if (isJavaScriptUrl(attrValue)) {
|
|
97
|
+
promises.push(minifyJavaScriptUrl(node, attrName));
|
|
98
|
+
} else {
|
|
99
|
+
if (relateUrlInstance) {
|
|
100
|
+
// FIXME!
|
|
101
|
+
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
|
|
102
|
+
// the WHATWG URL API is very strict while attrValue might not be a valid URL
|
|
103
|
+
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
|
|
104
|
+
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
continue;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
if (isSrcsetAttribute(node.tag, attrNameLower)) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
if (srcset) {
|
|
113
|
+
try {
|
|
114
|
+
const parsedSrcset = srcset.parse(attrValue);
|
|
115
|
+
node.attrs[attrName] = srcset.stringify(parsedSrcset.map(srcset => {
|
|
116
|
+
if (relateUrlInstance) {
|
|
117
|
+
srcset.url = relateUrlInstance.relate(srcset.url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return srcset;
|
|
121
|
+
}));
|
|
122
|
+
} catch (e) {// srcset will throw an Error for invalid srcset.
|
|
123
|
+
}
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
continue;
|
|
@@ -112,5 +129,30 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
112
129
|
|
|
113
130
|
return node;
|
|
114
131
|
});
|
|
115
|
-
return tree;
|
|
132
|
+
if (promises.length > 0) return Promise.all(promises).then(() => tree);
|
|
133
|
+
return Promise.resolve(tree);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isJavaScriptUrl(url) {
|
|
137
|
+
return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function minifyJavaScriptUrl(node, attrName) {
|
|
141
|
+
if (!terser) return Promise.resolve();
|
|
142
|
+
const jsWrapperStart = 'function a(){';
|
|
143
|
+
const jsWrapperEnd = '}a();';
|
|
144
|
+
let result = node.attrs[attrName];
|
|
145
|
+
|
|
146
|
+
if (result) {
|
|
147
|
+
result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
|
|
148
|
+
return terser.minify(result, {}) // Default Option is good enough
|
|
149
|
+
.then(({
|
|
150
|
+
code
|
|
151
|
+
}) => {
|
|
152
|
+
const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
|
|
153
|
+
node.attrs[attrName] = minifiedJs;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Promise.resolve();
|
|
116
158
|
}
|