htmlnano 0.2.9 → 1.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/CHANGELOG.md +57 -2
- package/README.md +15 -886
- 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 +786 -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 +11621 -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_sidebars/version-1.1.1-sidebars.json +8 -0
- package/docs/versions.json +3 -0
- package/lib/helpers.js +5 -0
- package/lib/htmlnano.js +43 -6
- package/lib/modules/collapseAttributeWhitespace.js +62 -6
- package/lib/modules/collapseWhitespace.js +42 -17
- package/lib/modules/minifyCss.js +4 -2
- package/lib/modules/minifyJs.js +5 -3
- package/lib/modules/minifySvg.js +6 -12
- package/lib/modules/minifyUrls.js +50 -15
- package/lib/modules/normalizeAttributeValues.js +48 -0
- package/lib/modules/removeComments.js +25 -1
- package/lib/modules/removeEmptyAttributes.js +52 -8
- package/lib/modules/removeRedundantAttributes.js +69 -14
- package/lib/presets/safe.js +9 -4
- package/package.json +18 -16
- package/test.js +25 -16
- package/uncss-fork.patch +13 -0
|
@@ -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
|
@@ -8,6 +8,7 @@ exports.isComment = isComment;
|
|
|
8
8
|
exports.isConditionalComment = isConditionalComment;
|
|
9
9
|
exports.isStyleNode = isStyleNode;
|
|
10
10
|
exports.extractCssFromStyleNode = extractCssFromStyleNode;
|
|
11
|
+
exports.isEventHandler = isEventHandler;
|
|
11
12
|
const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
|
|
12
13
|
|
|
13
14
|
function isAmpBoilerplate(node) {
|
|
@@ -38,4 +39,8 @@ function isStyleNode(node) {
|
|
|
38
39
|
|
|
39
40
|
function extractCssFromStyleNode(node) {
|
|
40
41
|
return Array.isArray(node.content) ? node.content.join(' ') : node.content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isEventHandler(attributeName) {
|
|
45
|
+
return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
|
|
41
46
|
}
|
package/lib/htmlnano.js
CHANGED
|
@@ -3,19 +3,60 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.loadConfig = loadConfig;
|
|
6
7
|
exports.default = void 0;
|
|
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
|
+
function htmlnano(optionsRun, presetRun) {
|
|
59
|
+
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
19
60
|
return function minifier(tree) {
|
|
20
61
|
options = { ...preset,
|
|
21
62
|
...options
|
|
@@ -45,10 +86,6 @@ htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
|
45
86
|
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
46
87
|
};
|
|
47
88
|
|
|
48
|
-
htmlnano.presets =
|
|
49
|
-
safe: _safe.default,
|
|
50
|
-
ampSafe: _ampSafe.default,
|
|
51
|
-
max: _max.default
|
|
52
|
-
};
|
|
89
|
+
htmlnano.presets = presets;
|
|
53
90
|
var _default = htmlnano;
|
|
54
91
|
exports.default = _default;
|
|
@@ -5,10 +5,58 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseAttributeWhitespace;
|
|
7
7
|
exports.attributesWithLists = void 0;
|
|
8
|
-
|
|
9
|
-
|
|
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,6 +7,8 @@ exports.default = minifyCss;
|
|
|
7
7
|
|
|
8
8
|
var _helpers = require("../helpers");
|
|
9
9
|
|
|
10
|
+
var _postcss = _interopRequireDefault(require("postcss"));
|
|
11
|
+
|
|
10
12
|
var _cssnano = _interopRequireDefault(require("cssnano"));
|
|
11
13
|
|
|
12
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
@@ -44,7 +46,7 @@ function processStyleNode(styleNode, cssnanoOptions) {
|
|
|
44
46
|
css = strippedCss;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
return _cssnano.default.process(css, postcssOptions
|
|
49
|
+
return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(css, postcssOptions).then(result => {
|
|
48
50
|
if (isCdataWrapped) {
|
|
49
51
|
return styleNode.content = ['<![CDATA[' + result + ']]>'];
|
|
50
52
|
}
|
|
@@ -59,7 +61,7 @@ function processStyleAttr(node, cssnanoOptions) {
|
|
|
59
61
|
const wrapperStart = 'a{';
|
|
60
62
|
const wrapperEnd = '}';
|
|
61
63
|
const wrappedStyle = wrapperStart + (node.attrs.style || '') + wrapperEnd;
|
|
62
|
-
return _cssnano.default.process(wrappedStyle, postcssOptions
|
|
64
|
+
return (0, _postcss.default)([(0, _cssnano.default)(cssnanoOptions)]).process(wrappedStyle, postcssOptions).then(result => {
|
|
63
65
|
const minifiedCss = result.css; // Remove wrapperStart at the start and wrapperEnd at the end of minifiedCss
|
|
64
66
|
|
|
65
67
|
node.attrs.style = minifiedCss.substring(wrapperStart.length, minifiedCss.length - wrapperEnd.length);
|
package/lib/modules/minifyJs.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.default = minifyJs;
|
|
|
7
7
|
|
|
8
8
|
var _terser = _interopRequireDefault(require("terser"));
|
|
9
9
|
|
|
10
|
+
var _helpers = require("../helpers");
|
|
11
|
+
|
|
10
12
|
var _removeRedundantAttributes = require("./removeRedundantAttributes");
|
|
11
13
|
|
|
12
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
@@ -80,12 +82,12 @@ function processScriptNode(scriptNode, terserOptions) {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
function processNodeWithOnAttrs(node, terserOptions) {
|
|
83
|
-
const jsWrapperStart = 'function
|
|
84
|
-
const jsWrapperEnd = '}';
|
|
85
|
+
const jsWrapperStart = 'function a(){';
|
|
86
|
+
const jsWrapperEnd = '}a();';
|
|
85
87
|
const promises = [];
|
|
86
88
|
|
|
87
89
|
for (const attrName of Object.keys(node.attrs || {})) {
|
|
88
|
-
if (!
|
|
90
|
+
if (!(0, _helpers.isEventHandler)(attrName)) {
|
|
89
91
|
continue;
|
|
90
92
|
} // For example onclick="return false" is valid,
|
|
91
93
|
// but "return false;" is invalid (error: 'return' outside of function)
|
package/lib/modules/minifySvg.js
CHANGED
|
@@ -5,14 +5,10 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = minifySvg;
|
|
7
7
|
|
|
8
|
-
var _svgo =
|
|
9
|
-
|
|
10
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
8
|
+
var _svgo = require("svgo");
|
|
11
9
|
|
|
12
10
|
/** Minify SVG with SVGO */
|
|
13
11
|
function minifySvg(tree, options, svgoOptions = {}) {
|
|
14
|
-
let promises = [];
|
|
15
|
-
const svgo = new _svgo.default(svgoOptions);
|
|
16
12
|
tree.match({
|
|
17
13
|
tag: 'svg'
|
|
18
14
|
}, node => {
|
|
@@ -20,13 +16,11 @@ function minifySvg(tree, options, svgoOptions = {}) {
|
|
|
20
16
|
closingSingleTag: 'slash',
|
|
21
17
|
quoteAllAttributes: true
|
|
22
18
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
promises.push(promise);
|
|
19
|
+
const result = (0, _svgo.optimize)(svgStr, svgoOptions);
|
|
20
|
+
node.tag = false;
|
|
21
|
+
node.attrs = {};
|
|
22
|
+
node.content = result.data;
|
|
29
23
|
return node;
|
|
30
24
|
});
|
|
31
|
-
return
|
|
25
|
+
return tree;
|
|
32
26
|
}
|
|
@@ -9,23 +9,27 @@ var _relateurl = _interopRequireDefault(require("relateurl"));
|
|
|
9
9
|
|
|
10
10
|
var _srcset = _interopRequireDefault(require("srcset"));
|
|
11
11
|
|
|
12
|
+
var _terser = _interopRequireDefault(require("terser"));
|
|
13
|
+
|
|
12
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
13
15
|
|
|
14
16
|
// Adopts from https://github.com/kangax/html-minifier/blob/51ce10f4daedb1de483ffbcccecc41be1c873da2/src/htmlminifier.js#L209-L221
|
|
15
|
-
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', '
|
|
17
|
+
const tagsHaveUriValuesForAttributes = new Set(['a', 'area', 'link', 'base', 'object', 'blockquote', 'q', 'del', 'ins', 'form', 'input', 'head', 'audio', 'embed', 'iframe', 'img', 'script', 'track', 'video']);
|
|
16
18
|
const tagsHasHrefAttributes = new Set(['a', 'area', 'link', 'base']);
|
|
17
19
|
const attributesOfImgTagHasUriValues = new Set(['src', 'longdesc', 'usemap']);
|
|
18
20
|
const attributesOfObjectTagHasUriValues = new Set(['classid', 'codebase', 'data', 'usemap']);
|
|
21
|
+
const tagsHasCiteAttributes = new Set(['blockquote', 'q', 'ins', 'del']);
|
|
22
|
+
const tagsHasSrcAttributes = new Set(['audio', 'embed', 'iframe', 'img', 'input', 'script', 'track', 'video',
|
|
23
|
+
/**
|
|
24
|
+
* https://html.spec.whatwg.org/#attr-source-src
|
|
25
|
+
*
|
|
26
|
+
* Although most of browsers recommend not to use "src" in <source>,
|
|
27
|
+
* but technically it does comply with HTML Standard.
|
|
28
|
+
*/
|
|
29
|
+
'source']);
|
|
19
30
|
|
|
20
31
|
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';
|
|
32
|
+
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
33
|
};
|
|
30
34
|
|
|
31
35
|
const isSrcsetAttribute = (tag, attr) => {
|
|
@@ -55,11 +59,13 @@ const isLinkRelCanonical = ({
|
|
|
55
59
|
return false;
|
|
56
60
|
};
|
|
57
61
|
|
|
62
|
+
const JAVASCRIPT_URL_PROTOCOL = 'javascript:';
|
|
58
63
|
let relateUrlInstance;
|
|
59
64
|
let STORED_URL_BASE;
|
|
60
65
|
/** Convert absolute url into relative url */
|
|
61
66
|
|
|
62
67
|
function minifyUrls(tree, options, moduleOptions) {
|
|
68
|
+
let promises = [];
|
|
63
69
|
const urlBase = processModuleOptions(moduleOptions); // Invalid configuration, return tree directly
|
|
64
70
|
|
|
65
71
|
if (!urlBase) return tree;
|
|
@@ -87,11 +93,16 @@ 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
|
+
// FIXME!
|
|
100
|
+
// relateurl@1.0.0-alpha only supports URL while stable version (0.2.7) only supports string
|
|
101
|
+
// the WHATWG URL API is very strict while attrValue might not be a valid URL
|
|
102
|
+
// new URL should be used, and relateUrl#relate should be wrapped in try...catch after relateurl@1 is stable
|
|
103
|
+
node.attrs[attrName] = relateUrlInstance.relate(attrValue);
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
continue;
|
|
96
107
|
}
|
|
97
108
|
|
|
@@ -112,5 +123,29 @@ function minifyUrls(tree, options, moduleOptions) {
|
|
|
112
123
|
|
|
113
124
|
return node;
|
|
114
125
|
});
|
|
115
|
-
return tree;
|
|
126
|
+
if (promises.length > 0) return Promise.all(promises).then(() => tree);
|
|
127
|
+
return Promise.resolve(tree);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isJavaScriptUrl(url) {
|
|
131
|
+
return typeof url === 'string' && url.toLowerCase().startsWith(JAVASCRIPT_URL_PROTOCOL);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function minifyJavaScriptUrl(node, attrName) {
|
|
135
|
+
const jsWrapperStart = 'function a(){';
|
|
136
|
+
const jsWrapperEnd = '}a();';
|
|
137
|
+
let result = node.attrs[attrName];
|
|
138
|
+
|
|
139
|
+
if (result) {
|
|
140
|
+
result = result.slice(JAVASCRIPT_URL_PROTOCOL.length);
|
|
141
|
+
return _terser.default.minify(result, {}) // Default Option is good enough
|
|
142
|
+
.then(({
|
|
143
|
+
code
|
|
144
|
+
}) => {
|
|
145
|
+
const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
|
|
146
|
+
node.attrs[attrName] = minifiedJs;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return Promise.resolve();
|
|
116
151
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = normalizeAttributeValues;
|
|
7
|
+
const caseInsensitiveAttributes = {
|
|
8
|
+
autocomplete: ['form'],
|
|
9
|
+
charset: ['meta', 'script'],
|
|
10
|
+
contenteditable: null,
|
|
11
|
+
crossorigin: ['audio', 'img', 'link', 'script', 'video'],
|
|
12
|
+
dir: null,
|
|
13
|
+
draggable: null,
|
|
14
|
+
dropzone: null,
|
|
15
|
+
formmethod: ['button', 'input'],
|
|
16
|
+
inputmode: ['input', 'textarea'],
|
|
17
|
+
kind: ['track'],
|
|
18
|
+
method: ['form'],
|
|
19
|
+
preload: ['audio', 'video'],
|
|
20
|
+
referrerpolicy: ['a', 'area', 'iframe', 'img', 'link'],
|
|
21
|
+
sandbox: ['iframe'],
|
|
22
|
+
spellcheck: null,
|
|
23
|
+
scope: ['th'],
|
|
24
|
+
shape: ['area'],
|
|
25
|
+
sizes: ['link'],
|
|
26
|
+
step: ['input'],
|
|
27
|
+
translate: null,
|
|
28
|
+
type: ['a', 'link', 'button', 'embed', 'object', 'script', 'source', 'style', 'input', 'menu', 'menuitem'],
|
|
29
|
+
wrap: ['textarea']
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalizeAttributeValues(tree) {
|
|
33
|
+
tree.walk(node => {
|
|
34
|
+
if (!node.attrs) {
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
39
|
+
const attrNameLower = attrName.toLowerCase();
|
|
40
|
+
|
|
41
|
+
if (Object.hasOwnProperty.call(caseInsensitiveAttributes, attrNameLower) && (caseInsensitiveAttributes[attrNameLower] === null || caseInsensitiveAttributes[attrNameLower].includes(node.tag))) {
|
|
42
|
+
node.attrs[attrName] = attrValue.toLowerCase ? attrValue.toLowerCase() : attrValue;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return node;
|
|
46
|
+
});
|
|
47
|
+
return tree;
|
|
48
|
+
}
|
|
@@ -11,7 +11,7 @@ const MATCH_EXCERPT_REGEXP = /<!-- ?more ?-->/i;
|
|
|
11
11
|
/** Removes HTML comments */
|
|
12
12
|
|
|
13
13
|
function removeComments(tree, options, removeType) {
|
|
14
|
-
if (removeType !== 'all' && removeType !== 'safe') {
|
|
14
|
+
if (removeType !== 'all' && removeType !== 'safe' && !isMatcher(removeType)) {
|
|
15
15
|
removeType = 'safe';
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -68,5 +68,29 @@ function isCommentToRemove(text, removeType) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
if (isMatcher(removeType)) {
|
|
72
|
+
return isMatch(text, removeType);
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMatch(input, matcher) {
|
|
79
|
+
if (matcher instanceof RegExp) {
|
|
80
|
+
return matcher.test(input);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof matcher === 'function') {
|
|
84
|
+
return Boolean(matcher(input));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isMatcher(matcher) {
|
|
91
|
+
if (matcher instanceof RegExp || typeof matcher === 'function') {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
72
96
|
}
|