htmlnano 0.2.4 → 0.2.8
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 +79 -10
- package/README.md +318 -18
- package/lib/helpers.js +16 -36
- package/lib/htmlnano.js +33 -73
- package/lib/modules/collapseAttributeWhitespace.js +21 -19
- package/lib/modules/collapseBooleanAttributes.js +34 -48
- package/lib/modules/collapseWhitespace.js +66 -32
- package/lib/modules/custom.js +12 -12
- package/lib/modules/deduplicateAttributeValues.js +34 -36
- package/lib/modules/mergeScripts.js +46 -63
- package/lib/modules/mergeStyles.js +33 -32
- package/lib/modules/minifyConditionalComments.js +52 -0
- package/lib/modules/minifyCss.js +33 -41
- package/lib/modules/minifyJs.js +72 -68
- package/lib/modules/minifyJson.js +23 -17
- package/lib/modules/minifySvg.js +19 -28
- package/lib/modules/minifyUrls.js +117 -0
- package/lib/modules/removeAttributeQuotes.js +19 -0
- package/lib/modules/removeComments.js +52 -34
- package/lib/modules/removeEmptyAttributes.js +19 -21
- package/lib/modules/removeOptionalTags.js +220 -0
- package/lib/modules/removeRedundantAttributes.js +82 -67
- package/lib/modules/removeUnusedCss.js +104 -52
- package/lib/modules/sortAttributes.js +120 -0
- package/lib/modules/sortAttributesWithLists.js +143 -0
- package/lib/presets/ampSafe.js +16 -17
- package/lib/presets/max.js +23 -21
- package/lib/presets/safe.js +38 -25
- package/package.json +29 -19
- package/test.js +29 -6
- package/lib/presets/hard.js +0 -26
package/lib/htmlnano.js
CHANGED
|
@@ -1,94 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.default = void 0;
|
|
6
7
|
|
|
7
|
-
var
|
|
8
|
+
var _posthtml = _interopRequireDefault(require("posthtml"));
|
|
8
9
|
|
|
9
|
-
var
|
|
10
|
+
var _safe = _interopRequireDefault(require("./presets/safe"));
|
|
10
11
|
|
|
11
|
-
var
|
|
12
|
+
var _ampSafe = _interopRequireDefault(require("./presets/ampSafe"));
|
|
12
13
|
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
var _safe = require('./presets/safe');
|
|
16
|
-
|
|
17
|
-
var _safe2 = _interopRequireDefault(_safe);
|
|
18
|
-
|
|
19
|
-
var _ampSafe = require('./presets/ampSafe');
|
|
20
|
-
|
|
21
|
-
var _ampSafe2 = _interopRequireDefault(_ampSafe);
|
|
22
|
-
|
|
23
|
-
var _max = require('./presets/max');
|
|
24
|
-
|
|
25
|
-
var _max2 = _interopRequireDefault(_max);
|
|
14
|
+
var _max = _interopRequireDefault(require("./presets/max"));
|
|
26
15
|
|
|
27
16
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
28
17
|
|
|
29
|
-
function htmlnano() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
var promise = Promise.resolve(tree);
|
|
36
|
-
|
|
37
|
-
var _loop = function _loop(moduleName) {
|
|
38
|
-
if (!options[moduleName]) {
|
|
39
|
-
// The module is disabled
|
|
40
|
-
return 'continue';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (_safe2.default[moduleName] === undefined) {
|
|
44
|
-
throw new Error('Module "' + moduleName + '" is not defined');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
var module = require('./modules/' + moduleName);
|
|
48
|
-
promise = promise.then(function (tree) {
|
|
49
|
-
return module.default(tree, options, options[moduleName]);
|
|
50
|
-
});
|
|
51
|
-
};
|
|
18
|
+
function htmlnano(options = {}, preset = _safe.default) {
|
|
19
|
+
return function minifier(tree) {
|
|
20
|
+
options = { ...preset,
|
|
21
|
+
...options
|
|
22
|
+
};
|
|
23
|
+
let promise = Promise.resolve(tree);
|
|
52
24
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
25
|
+
for (const [moduleName, moduleOptions] of Object.entries(options)) {
|
|
26
|
+
if (!moduleOptions) {
|
|
27
|
+
// The module is disabled
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
56
30
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
31
|
+
if (_safe.default[moduleName] === undefined) {
|
|
32
|
+
throw new Error('Module "' + moduleName + '" is not defined');
|
|
33
|
+
}
|
|
60
34
|
|
|
61
|
-
|
|
35
|
+
let module = require('./modules/' + moduleName);
|
|
62
36
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} catch (err) {
|
|
66
|
-
_didIteratorError = true;
|
|
67
|
-
_iteratorError = err;
|
|
68
|
-
} finally {
|
|
69
|
-
try {
|
|
70
|
-
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
71
|
-
_iterator.return();
|
|
72
|
-
}
|
|
73
|
-
} finally {
|
|
74
|
-
if (_didIteratorError) {
|
|
75
|
-
throw _iteratorError;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
37
|
+
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
38
|
+
}
|
|
79
39
|
|
|
80
|
-
|
|
81
|
-
|
|
40
|
+
return promise;
|
|
41
|
+
};
|
|
82
42
|
}
|
|
83
43
|
|
|
84
|
-
htmlnano.process = function (html, options, preset) {
|
|
85
|
-
|
|
44
|
+
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
45
|
+
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
86
46
|
};
|
|
87
47
|
|
|
88
48
|
htmlnano.presets = {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
49
|
+
safe: _safe.default,
|
|
50
|
+
ampSafe: _ampSafe.default,
|
|
51
|
+
max: _max.default
|
|
92
52
|
};
|
|
93
|
-
|
|
94
|
-
exports.default =
|
|
53
|
+
var _default = htmlnano;
|
|
54
|
+
exports.default = _default;
|
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseAttributeWhitespace;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
exports.attributesWithLists = void 0;
|
|
8
|
+
const attributesWithLists = new Set(['class', 'rel', 'ping']);
|
|
9
9
|
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
10
|
+
|
|
11
|
+
exports.attributesWithLists = attributesWithLists;
|
|
12
|
+
|
|
10
13
|
function collapseAttributeWhitespace(tree) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
tree.walk(node => {
|
|
15
|
+
if (!node.attrs) {
|
|
16
|
+
return node;
|
|
17
|
+
}
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!attributesWithLists.has(attrNameLower)) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
19
|
+
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
20
|
+
const attrNameLower = attrName.toLowerCase();
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (!attributesWithLists.has(attrNameLower)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
27
|
+
node.attrs[attrName] = newAttrValue;
|
|
27
28
|
});
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
return node;
|
|
30
|
+
});
|
|
31
|
+
return tree;
|
|
30
32
|
}
|
|
@@ -1,56 +1,42 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseBooleanAttributes;
|
|
7
7
|
// Source: https://github.com/kangax/html-minifier/issues/63
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
|
|
8
|
+
const htmlBooleanAttributes = new Set(['allowfullscreen', 'allowpaymentrequest', 'allowtransparency', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', 'default', 'defaultchecked', 'defaultmuted', 'defaultselected', 'defer', 'disabled', 'enabled', 'formnovalidate', 'hidden', 'indeterminate', 'inert', 'ismap', 'itemscope', 'loop', 'multiple', 'muted', 'nohref', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
|
|
9
|
+
const amphtmlBooleanAttributes = new Set(['⚡', 'amp', '⚡4ads', 'amp4ads', '⚡4email', 'amp4email', 'amp-custom', 'amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate', 'allow-blocked-ranges', 'amp-access-hide', 'amp-access-template', 'amp-keyframes', 'animate', 'arrows', 'data-block-on-consent', 'data-enable-refresh', 'data-multi-size', 'date-template', 'disable-double-tap', 'disable-session-states', 'disableremoteplayback', 'dots', 'expand-single-section', 'expanded', 'fallback', 'first', 'fullscreen', 'inline', 'lightbox', 'noaudio', 'noautoplay', 'noloading', 'once', 'open-after-clear', 'open-after-select', 'open-button', 'placeholder', 'preload', 'reset-on-refresh', 'reset-on-resize', 'resizable', 'rotate-to-fullscreen', 'second', 'standalone', 'stereo', 'submit-error', 'submit-success', 'submitting', 'subscriptions-actions', 'subscriptions-dialog']);
|
|
11
10
|
|
|
12
11
|
function collapseBooleanAttributes(tree, options, moduleOptions) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
_iterator.return();
|
|
44
|
-
}
|
|
45
|
-
} finally {
|
|
46
|
-
if (_didIteratorError) {
|
|
47
|
-
throw _iteratorError;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return node;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return tree;
|
|
12
|
+
tree.match({
|
|
13
|
+
attrs: true
|
|
14
|
+
}, node => {
|
|
15
|
+
for (const attrName of Object.keys(node.attrs)) {
|
|
16
|
+
if (!node.tag) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (node.tag.search('a-') === 0 && attrName === 'visible') {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (htmlBooleanAttributes.has(attrName)) {
|
|
25
|
+
node.attrs[attrName] = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (moduleOptions.amphtml && node.attrs[attrName] === '' && amphtmlBooleanAttributes.has(attrName)) {
|
|
29
|
+
node.attrs[attrName] = true;
|
|
30
|
+
} // collapse crossorigin attributes
|
|
31
|
+
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if (attrName.toLowerCase() === 'crossorigin' && (node.attrs[attrName] === 'anonymous' || node.attrs[attrName] === '')) {
|
|
35
|
+
node.attrs[attrName] = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return node;
|
|
40
|
+
});
|
|
41
|
+
return tree;
|
|
56
42
|
}
|
|
@@ -1,50 +1,84 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseWhitespace;
|
|
7
7
|
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
var _normalizeHtmlWhitespace2 = _interopRequireDefault(_normalizeHtmlWhitespace);
|
|
11
|
-
|
|
12
|
-
var _helpers = require('../helpers');
|
|
13
|
-
|
|
14
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
15
|
-
|
|
16
|
-
var noWhitespaceCollapseElements = new Set(['script', 'style', 'pre', 'textarea']);
|
|
8
|
+
var _helpers = require("../helpers");
|
|
17
9
|
|
|
10
|
+
const noWhitespaceCollapseElements = new Set(['script', 'style', 'pre', 'textarea']);
|
|
11
|
+
const noTrimWhitespacesArroundElements = new Set([// non-empty tags that will maintain whitespace around them
|
|
12
|
+
'a', 'abbr', 'acronym', 'b', 'bdi', 'bdo', 'big', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'ins', 'kbd', 'label', 'mark', 'math', 'nobr', 'object', 'q', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'svg', 'textarea', 'time', 'tt', 'u', 'var', // self-closing tags that will maintain whitespace around them
|
|
13
|
+
'comment', 'img', 'input', 'wbr']);
|
|
14
|
+
const noTrimWhitespacesInsideElements = new Set([// non-empty tags that will maintain whitespace within them
|
|
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 whitespacePattern = /\s{1,}/g;
|
|
17
|
+
const onlyWhitespacePattern = /^\s+$/;
|
|
18
|
+
const NONE = '';
|
|
19
|
+
const SINGLE_SPACE = ' ';
|
|
20
|
+
const validOptions = ['all', 'aggressive', 'conservative'];
|
|
18
21
|
/** Collapses redundant whitespaces */
|
|
22
|
+
|
|
19
23
|
function collapseWhitespace(tree, options, collapseType, tag) {
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
|
|
25
|
+
tree.forEach((node, index) => {
|
|
26
|
+
if (typeof node === 'string' && !(0, _helpers.isComment)(node)) {
|
|
27
|
+
const prevNode = tree[index - 1];
|
|
28
|
+
const nextNode = tree[index + 1];
|
|
29
|
+
const prevNodeTag = prevNode && prevNode.tag;
|
|
30
|
+
const nextNodeTag = nextNode && nextNode.tag;
|
|
31
|
+
const isTopLevel = !tag || tag === 'html' || tag === 'head';
|
|
32
|
+
const shouldTrim = collapseType === 'all' || isTopLevel ||
|
|
33
|
+
/*
|
|
34
|
+
* When collapseType is set to 'aggressive', and the tag is not inside 'noTrimWhitespacesInsideElements'.
|
|
35
|
+
* the first & last space inside the tag will be trimmed
|
|
36
|
+
*/
|
|
37
|
+
collapseType === 'aggressive' && !noTrimWhitespacesInsideElements.has(tag);
|
|
38
|
+
node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, tag, prevNodeTag, nextNodeTag);
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
if (typeof node === 'string' && !(0, _helpers.isComment)(node)) {
|
|
26
|
-
var isTopLevel = !tag || tag === 'html' || tag === 'head';
|
|
27
|
-
node = collapseRedundantWhitespaces(node, collapseType, isTopLevel);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
var isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
|
|
31
|
-
if (node.content && node.content.length && isAllowCollapseWhitespace) {
|
|
32
|
-
node.content = collapseWhitespace(node.content, options, collapseType, node.tag);
|
|
33
|
-
}
|
|
41
|
+
const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if (node.content && node.content.length && isAllowCollapseWhitespace) {
|
|
44
|
+
node.content = collapseWhitespace(node.content, options, collapseType, node.tag);
|
|
45
|
+
}
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
tree[index] = node;
|
|
48
|
+
});
|
|
49
|
+
return tree;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
function collapseRedundantWhitespaces(text, collapseType) {
|
|
42
|
-
|
|
52
|
+
function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, currentTag, prevNodeTag, nextNodeTag) {
|
|
53
|
+
if (!text || text.length === 0) {
|
|
54
|
+
return NONE;
|
|
55
|
+
}
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
text = text.replace(whitespacePattern, SINGLE_SPACE);
|
|
58
|
+
|
|
59
|
+
if (shouldTrim) {
|
|
60
|
+
if (collapseType === 'aggressive') {
|
|
61
|
+
if (onlyWhitespacePattern.test(text)) {
|
|
62
|
+
// "text" only contains whitespaces. Only trim when both prevNodeTag & nextNodeTag are not "noTrimWhitespacesArroundElement"
|
|
63
|
+
// Otherwise the required ONE whitespace will be trimmed
|
|
64
|
+
if (!noTrimWhitespacesArroundElements.has(prevNodeTag) || !noTrimWhitespacesArroundElements.has(nextNodeTag)) {
|
|
65
|
+
text = text.trim();
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// text contains whitespaces & non-whitespaces
|
|
69
|
+
if (!noTrimWhitespacesArroundElements.has(prevNodeTag)) {
|
|
70
|
+
text = text.trimStart();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!noTrimWhitespacesArroundElements.has(nextNodeTag)) {
|
|
74
|
+
text = text.trimEnd();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// collapseType is 'all', trim spaces
|
|
79
|
+
text = text.trim();
|
|
47
80
|
}
|
|
81
|
+
}
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
return text;
|
|
50
84
|
}
|
package/lib/modules/custom.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = custom;
|
|
7
|
+
|
|
7
8
|
/** Meta-module that runs custom modules */
|
|
8
9
|
function custom(tree, options, customModules) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!Array.isArray(customModules)) {
|
|
14
|
-
customModules = [customModules];
|
|
15
|
-
}
|
|
10
|
+
if (!customModules) {
|
|
11
|
+
return tree;
|
|
12
|
+
}
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
if (!Array.isArray(customModules)) {
|
|
15
|
+
customModules = [customModules];
|
|
16
|
+
}
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
customModules.forEach(customModule => {
|
|
19
|
+
tree = customModule(tree, options);
|
|
20
|
+
});
|
|
21
|
+
return tree;
|
|
22
22
|
}
|
|
@@ -1,48 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseAttributeWhitespace;
|
|
7
7
|
|
|
8
|
-
var _collapseAttributeWhitespace = require(
|
|
8
|
+
var _collapseAttributeWhitespace = require("./collapseAttributeWhitespace");
|
|
9
9
|
|
|
10
10
|
/** Deduplicate values inside list-like attributes (e.g. class, rel) */
|
|
11
11
|
function collapseAttributeWhitespace(tree) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
tree.walk(node => {
|
|
13
|
+
if (!node.attrs) {
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Object.keys(node.attrs).forEach(attrName => {
|
|
18
|
+
const attrNameLower = attrName.toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (!_collapseAttributeWhitespace.attributesWithLists.has(attrNameLower)) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const attrValues = node.attrs[attrName].split(/\s/);
|
|
25
|
+
const uniqeAttrValues = new Set();
|
|
26
|
+
let deduplicatedAttrValues = [];
|
|
27
|
+
attrValues.forEach(attrValue => {
|
|
28
|
+
if (!attrValue) {
|
|
29
|
+
// Keep whitespaces
|
|
30
|
+
deduplicatedAttrValues.push('');
|
|
31
|
+
return;
|
|
15
32
|
}
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
var attrValues = node.attrs[attrName].split(/\s/);
|
|
24
|
-
var uniqeAttrValues = new Set();
|
|
25
|
-
var deduplicatedAttrValues = [];
|
|
26
|
-
attrValues.forEach(function (attrValue) {
|
|
27
|
-
if (!attrValue) {
|
|
28
|
-
// Keep whitespaces
|
|
29
|
-
deduplicatedAttrValues.push('');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (uniqeAttrValues.has(attrValue)) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
deduplicatedAttrValues.push(attrValue);
|
|
38
|
-
uniqeAttrValues.add(attrValue);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
node.attrs[attrName] = deduplicatedAttrValues.join(' ');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
return node;
|
|
45
|
-
});
|
|
34
|
+
if (uniqeAttrValues.has(attrValue)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
deduplicatedAttrValues.push(attrValue);
|
|
39
|
+
uniqeAttrValues.add(attrValue);
|
|
40
|
+
});
|
|
41
|
+
node.attrs[attrName] = deduplicatedAttrValues.join(' ');
|
|
42
|
+
});
|
|
43
|
+
return node;
|
|
44
|
+
});
|
|
45
|
+
return tree;
|
|
48
46
|
}
|
|
@@ -1,80 +1,63 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
|
-
|
|
4
|
+
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = mergeScripts;
|
|
7
|
+
|
|
7
8
|
/* Merge multiple <script> into one */
|
|
8
9
|
function mergeScripts(tree) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
let scriptNodesIndex = {};
|
|
11
|
+
let scriptSrcIndex = 1;
|
|
12
|
+
tree.match({
|
|
13
|
+
tag: 'script'
|
|
14
|
+
}, node => {
|
|
15
|
+
const nodeAttrs = node.attrs || {};
|
|
16
|
+
|
|
17
|
+
if (nodeAttrs.src) {
|
|
18
|
+
scriptSrcIndex++;
|
|
19
|
+
return node;
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
if (scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
|
|
19
|
-
return node;
|
|
20
|
-
}
|
|
22
|
+
const scriptType = nodeAttrs.type || 'text/javascript';
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
type: scriptType,
|
|
26
|
-
defer: nodeAttrs.defer !== undefined,
|
|
27
|
-
async: nodeAttrs.async !== undefined
|
|
28
|
-
});
|
|
29
|
-
if (!scriptNodesIndex[scriptKey]) {
|
|
30
|
-
scriptNodesIndex[scriptKey] = [];
|
|
31
|
-
}
|
|
24
|
+
if (scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
const scriptKey = JSON.stringify({
|
|
29
|
+
id: nodeAttrs.id,
|
|
30
|
+
class: nodeAttrs.class,
|
|
31
|
+
type: scriptType,
|
|
32
|
+
defer: nodeAttrs.defer !== undefined,
|
|
33
|
+
async: nodeAttrs.async !== undefined,
|
|
34
|
+
index: scriptSrcIndex
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
scriptNodes.reverse().forEach(function (scriptNode) {
|
|
41
|
-
var scriptContent = (scriptNode.content || []).join(' ');
|
|
42
|
-
scriptContent = scriptContent.trim();
|
|
43
|
-
if (scriptContent.slice(-1) !== ';') {
|
|
44
|
-
scriptContent += ';';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
lastScriptNode.content.unshift(scriptContent);
|
|
37
|
+
if (!scriptNodesIndex[scriptKey]) {
|
|
38
|
+
scriptNodesIndex[scriptKey] = [];
|
|
39
|
+
}
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
41
|
+
scriptNodesIndex[scriptKey].push(node);
|
|
42
|
+
return node;
|
|
43
|
+
});
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
for (const scriptNodes of Object.values(scriptNodesIndex)) {
|
|
46
|
+
let lastScriptNode = scriptNodes.pop();
|
|
47
|
+
scriptNodes.reverse().forEach(scriptNode => {
|
|
48
|
+
let scriptContent = (scriptNode.content || []).join(' ');
|
|
49
|
+
scriptContent = scriptContent.trim();
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
if (scriptContent.slice(-1) !== ';') {
|
|
52
|
+
scriptContent += ';';
|
|
53
|
+
}
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
if (!_iteratorNormalCompletion && _iterator.return) {
|
|
70
|
-
_iterator.return();
|
|
71
|
-
}
|
|
72
|
-
} finally {
|
|
73
|
-
if (_didIteratorError) {
|
|
74
|
-
throw _iteratorError;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
55
|
+
lastScriptNode.content = lastScriptNode.content || [];
|
|
56
|
+
lastScriptNode.content.unshift(scriptContent);
|
|
57
|
+
scriptNode.tag = false;
|
|
58
|
+
scriptNode.content = [];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
78
61
|
|
|
79
|
-
|
|
62
|
+
return tree;
|
|
80
63
|
}
|