htmlnano 0.2.7 → 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 +25 -0
- package/README.md +128 -0
- package/lib/helpers.js +4 -22
- package/lib/htmlnano.js +17 -47
- package/lib/modules/collapseAttributeWhitespace.js +6 -23
- package/lib/modules/collapseBooleanAttributes.js +5 -7
- package/lib/modules/collapseWhitespace.js +19 -24
- package/lib/modules/custom.js +2 -2
- package/lib/modules/deduplicateAttributeValues.js +8 -8
- package/lib/modules/mergeScripts.js +12 -17
- package/lib/modules/mergeStyles.js +8 -8
- package/lib/modules/minifyConditionalComments.js +52 -0
- package/lib/modules/minifyCss.js +13 -17
- package/lib/modules/minifyJs.js +19 -21
- package/lib/modules/minifyJson.js +3 -3
- package/lib/modules/minifySvg.js +9 -12
- package/lib/modules/minifyUrls.js +58 -55
- package/lib/modules/removeAttributeQuotes.js +1 -1
- package/lib/modules/removeComments.js +7 -9
- package/lib/modules/removeEmptyAttributes.js +5 -22
- package/lib/modules/removeOptionalTags.js +220 -0
- package/lib/modules/removeRedundantAttributes.js +33 -32
- package/lib/modules/removeUnusedCss.js +28 -52
- package/lib/modules/sortAttributes.js +120 -0
- package/lib/modules/sortAttributesWithLists.js +120 -7
- package/lib/presets/ampSafe.js +5 -55
- package/lib/presets/max.js +8 -56
- package/lib/presets/safe.js +7 -4
- package/package.json +16 -7
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = removeOptionalTags;
|
|
7
|
+
|
|
8
|
+
var _helpers = require("../helpers");
|
|
9
|
+
|
|
10
|
+
const startWithWhitespacePattern = /^\s+/;
|
|
11
|
+
const bodyStartTagCantBeOmittedWithFirstChildTags = new Set(['meta', 'link', 'script', 'style']);
|
|
12
|
+
const tbodyStartTagCantBeOmittedWithPrecededTags = new Set(['tbody', 'thead', 'tfoot']);
|
|
13
|
+
const tbodyEndTagCantBeOmittedWithFollowedTags = new Set(['tbody', 'tfoot']);
|
|
14
|
+
|
|
15
|
+
function isEmptyTextNode(node) {
|
|
16
|
+
if (typeof node === 'string' && node.trim() === '') {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isEmptyNode(node) {
|
|
24
|
+
if (!node.content) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (node.content.length) {
|
|
29
|
+
return !node.content.filter(n => typeof n === 'string' && isEmptyTextNode(n) ? false : true).length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getFirstChildTag(node, nonEmpty = true) {
|
|
36
|
+
if (node.content && node.content.length) {
|
|
37
|
+
if (nonEmpty) {
|
|
38
|
+
for (const childNode of node.content) {
|
|
39
|
+
if (childNode.tag) return childNode;
|
|
40
|
+
if (typeof childNode === 'string' && !isEmptyTextNode(childNode)) return childNode;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
return node.content[0] || null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPrevNode(tree, currentNodeIndex, nonEmpty = false) {
|
|
51
|
+
if (nonEmpty) {
|
|
52
|
+
for (let i = currentNodeIndex - 1; i >= 0; i--) {
|
|
53
|
+
const node = tree[i];
|
|
54
|
+
if (node.tag) return node;
|
|
55
|
+
if (typeof node === 'string' && !isEmptyTextNode(node)) return node;
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
return tree[currentNodeIndex - 1] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getNextNode(tree, currentNodeIndex, nonEmpty = false) {
|
|
65
|
+
if (nonEmpty) {
|
|
66
|
+
for (let i = currentNodeIndex + 1; i < tree.length; i++) {
|
|
67
|
+
const node = tree[i];
|
|
68
|
+
if (node.tag) return node;
|
|
69
|
+
if (typeof node === 'string' && !isEmptyTextNode(node)) return node;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
return tree[currentNodeIndex + 1] || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
} // Specification https://html.spec.whatwg.org/multipage/syntax.html#optional-tags
|
|
77
|
+
|
|
78
|
+
/** Remove optional tag in the DOM */
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
function removeOptionalTags(tree) {
|
|
82
|
+
tree.forEach((node, index) => {
|
|
83
|
+
if (!node.tag) return node;
|
|
84
|
+
if (node.attrs && Object.keys(node.attrs).length) return node; // const prevNode = getPrevNode(tree, index);
|
|
85
|
+
|
|
86
|
+
const prevNonEmptyNode = getPrevNode(tree, index, true);
|
|
87
|
+
const nextNode = getNextNode(tree, index);
|
|
88
|
+
const nextNonEmptyNode = getNextNode(tree, index, true);
|
|
89
|
+
const firstChildNode = getFirstChildTag(node, false);
|
|
90
|
+
const firstNonEmptyChildNode = getFirstChildTag(node);
|
|
91
|
+
/**
|
|
92
|
+
* An "html" element's start tag may be omitted if the first thing inside the "html" element is not a comment.
|
|
93
|
+
* An "html" element's end tag may be omitted if the "html" element is not IMMEDIATELY followed by a comment.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
if (node.tag === 'html') {
|
|
97
|
+
let isHtmlStartTagCanBeOmitted = true;
|
|
98
|
+
let isHtmlEndTagCanBeOmitted = true;
|
|
99
|
+
|
|
100
|
+
if (typeof firstNonEmptyChildNode === 'string' && (0, _helpers.isComment)(firstNonEmptyChildNode)) {
|
|
101
|
+
isHtmlStartTagCanBeOmitted = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNonEmptyNode)) {
|
|
105
|
+
isHtmlEndTagCanBeOmitted = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (isHtmlStartTagCanBeOmitted && isHtmlEndTagCanBeOmitted) {
|
|
109
|
+
node.tag = false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A "head" element's start tag may be omitted if the element is empty, or if the first thing inside the "head" element is an element.
|
|
114
|
+
* A "head" element's end tag may be omitted if the "head" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if (node.tag === 'head') {
|
|
119
|
+
let isHeadStartTagCanBeOmitted = false;
|
|
120
|
+
let isHeadEndTagCanBeOmitted = true;
|
|
121
|
+
|
|
122
|
+
if (isEmptyNode(node) || firstNonEmptyChildNode && firstNonEmptyChildNode.tag) {
|
|
123
|
+
isHeadStartTagCanBeOmitted = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNode)) {
|
|
127
|
+
isHeadEndTagCanBeOmitted = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isHeadStartTagCanBeOmitted && isHeadEndTagCanBeOmitted) {
|
|
131
|
+
node.tag = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A "body" element's start tag may be omitted if the element is empty, or if the first thing inside the "body" element is not ASCII whitespace or a comment, except if the first thing inside the "body" element is a "meta", "link", "script", "style", or "template" element.
|
|
136
|
+
* A "body" element's end tag may be omitted if the "body" element is not IMMEDIATELY followed by a comment.
|
|
137
|
+
*/
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if (node.tag === 'body') {
|
|
141
|
+
let isBodyStartTagCanBeOmitted = true;
|
|
142
|
+
let isBodyEndTagCanBeOmitted = true;
|
|
143
|
+
|
|
144
|
+
if (typeof firstChildNode === 'string' && startWithWhitespacePattern.test(firstChildNode) || typeof firstNonEmptyChildNode === 'string' && (0, _helpers.isComment)(firstNonEmptyChildNode)) {
|
|
145
|
+
isBodyStartTagCanBeOmitted = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && bodyStartTagCantBeOmittedWithFirstChildTags.has(firstNonEmptyChildNode.tag)) {
|
|
149
|
+
isBodyStartTagCanBeOmitted = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (nextNode && typeof nextNode === 'string' && (0, _helpers.isComment)(nextNode)) {
|
|
153
|
+
isBodyEndTagCanBeOmitted = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isBodyStartTagCanBeOmitted && isBodyEndTagCanBeOmitted) {
|
|
157
|
+
node.tag = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A "colgroup" element's start tag may be omitted if the first thing inside the "colgroup" element is a "col" element, and if the element is not IMMEDIATELY preceded by another "colgroup" element. It can't be omitted if the element is empty.
|
|
162
|
+
* A "colgroup" element's end tag may be omitted if the "colgroup" element is not IMMEDIATELY followed by ASCII whitespace or a comment.
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if (node.tag === 'colgroup') {
|
|
167
|
+
let isColgroupStartTagCanBeOmitted = false;
|
|
168
|
+
let isColgroupEndTagCanBeOmitted = true;
|
|
169
|
+
|
|
170
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'col') {
|
|
171
|
+
isColgroupStartTagCanBeOmitted = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (prevNonEmptyNode && prevNonEmptyNode.tag && prevNonEmptyNode.tag === 'colgroup') {
|
|
175
|
+
isColgroupStartTagCanBeOmitted = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (nextNode && typeof nextNode === 'string' && startWithWhitespacePattern.test(nextNode) || nextNonEmptyNode && typeof nextNonEmptyNode === 'string' && (0, _helpers.isComment)(nextNonEmptyNode)) {
|
|
179
|
+
isColgroupEndTagCanBeOmitted = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (isColgroupStartTagCanBeOmitted && isColgroupEndTagCanBeOmitted) {
|
|
183
|
+
node.tag = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A "tbody" element's start tag may be omitted if the first thing inside the "tbody" element is a "tr" element, and if the element is not immediately preceded by another "tbody", "thead" or "tfoot" element. It can't be omitted if the element is empty.
|
|
188
|
+
* A "tbody" element's end tag may be omitted if the "tbody" element is not IMMEDIATELY followed by a "tbody" or "tfoot" element.
|
|
189
|
+
*/
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if (node.tag === 'tbody') {
|
|
193
|
+
let isTbodyStartTagCanBeOmitted = false;
|
|
194
|
+
let isTbodyEndTagCanBeOmitted = true;
|
|
195
|
+
|
|
196
|
+
if (firstNonEmptyChildNode && firstNonEmptyChildNode.tag && firstNonEmptyChildNode.tag === 'tr') {
|
|
197
|
+
isTbodyStartTagCanBeOmitted = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (prevNonEmptyNode && prevNonEmptyNode.tag && tbodyStartTagCantBeOmittedWithPrecededTags.has(prevNonEmptyNode.tag)) {
|
|
201
|
+
isTbodyStartTagCanBeOmitted = false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (nextNonEmptyNode && nextNonEmptyNode.tag && tbodyEndTagCantBeOmittedWithFollowedTags.has(nextNonEmptyNode.tag)) {
|
|
205
|
+
isTbodyEndTagCanBeOmitted = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isTbodyStartTagCanBeOmitted && isTbodyEndTagCanBeOmitted) {
|
|
209
|
+
node.tag = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (node.content && node.content.length) {
|
|
214
|
+
removeOptionalTags(node.content);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return node;
|
|
218
|
+
});
|
|
219
|
+
return tree;
|
|
220
|
+
}
|
|
@@ -3,21 +3,10 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
11
|
-
|
|
12
|
-
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
13
|
-
|
|
14
|
-
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
15
|
-
|
|
16
|
-
function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
|
|
17
|
-
|
|
18
|
-
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
|
|
19
|
-
|
|
20
|
-
var redundantAttributes = {
|
|
6
|
+
exports.default = removeRedundantAttributes;
|
|
7
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#JavaScript_types
|
|
8
|
+
const redundantScriptTypes = new Set(['application/javascript', 'application/ecmascript', 'application/x-ecmascript', 'application/x-javascript', 'text/javascript', 'text/ecmascript', 'text/javascript1.0', 'text/javascript1.1', 'text/javascript1.2', 'text/javascript1.3', 'text/javascript1.4', 'text/javascript1.5', 'text/jscript', 'text/livescript', 'text/x-ecmascript', 'text/x-javascript']);
|
|
9
|
+
const redundantAttributes = {
|
|
21
10
|
'form': {
|
|
22
11
|
'method': 'get'
|
|
23
12
|
},
|
|
@@ -29,9 +18,19 @@ var redundantAttributes = {
|
|
|
29
18
|
},
|
|
30
19
|
'script': {
|
|
31
20
|
'language': 'javascript',
|
|
32
|
-
'type':
|
|
21
|
+
'type': node => {
|
|
22
|
+
for (const [attrName, attrValue] of Object.entries(node.attrs)) {
|
|
23
|
+
if (attrName.toLowerCase() !== 'type') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return redundantScriptTypes.has(attrValue);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
33
32
|
// Remove attribute if the function returns false
|
|
34
|
-
'charset':
|
|
33
|
+
'charset': node => {
|
|
35
34
|
// The charset attribute only really makes sense on “external” SCRIPT elements:
|
|
36
35
|
// http://perfectionkills.com/optimizing-html/#8_script_charset
|
|
37
36
|
return node.attrs && !node.attrs.src;
|
|
@@ -43,17 +42,13 @@ var redundantAttributes = {
|
|
|
43
42
|
},
|
|
44
43
|
'link': {
|
|
45
44
|
'media': 'all',
|
|
46
|
-
'type':
|
|
45
|
+
'type': node => {
|
|
47
46
|
// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
let isRelStyleSheet = false;
|
|
48
|
+
let isTypeTextCSS = false;
|
|
50
49
|
|
|
51
50
|
if (node.attrs) {
|
|
52
|
-
for (
|
|
53
|
-
var _Object$entries$_i = _slicedToArray(_Object$entries[_i], 2),
|
|
54
|
-
attrName = _Object$entries$_i[0],
|
|
55
|
-
attrValue = _Object$entries$_i[1];
|
|
56
|
-
|
|
51
|
+
for (const [attrName, attrValue] of Object.entries(node.attrs)) {
|
|
57
52
|
if (attrName.toLowerCase() === 'rel' && attrValue === 'stylesheet') {
|
|
58
53
|
isRelStyleSheet = true;
|
|
59
54
|
}
|
|
@@ -67,22 +62,28 @@ var redundantAttributes = {
|
|
|
67
62
|
|
|
68
63
|
return isRelStyleSheet && isTypeTextCSS;
|
|
69
64
|
}
|
|
65
|
+
},
|
|
66
|
+
// See: https://html.spec.whatwg.org/#lazy-loading-attributes
|
|
67
|
+
'img': {
|
|
68
|
+
'loading': 'eager'
|
|
69
|
+
},
|
|
70
|
+
'iframe': {
|
|
71
|
+
'loading': 'eager'
|
|
70
72
|
}
|
|
71
73
|
};
|
|
72
|
-
|
|
74
|
+
const TAG_MATCH_REGEXP = new RegExp('^(' + Object.keys(redundantAttributes).join('|') + ')$');
|
|
73
75
|
/** Removes redundant attributes */
|
|
74
76
|
|
|
75
77
|
function removeRedundantAttributes(tree) {
|
|
76
78
|
tree.match({
|
|
77
79
|
tag: TAG_MATCH_REGEXP
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
+
}, node => {
|
|
81
|
+
const tagRedundantAttributes = redundantAttributes[node.tag];
|
|
80
82
|
node.attrs = node.attrs || {};
|
|
81
83
|
|
|
82
|
-
for (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
var isRemove = false;
|
|
84
|
+
for (const redundantAttributeName of Object.keys(tagRedundantAttributes)) {
|
|
85
|
+
let tagRedundantAttributeValue = tagRedundantAttributes[redundantAttributeName];
|
|
86
|
+
let isRemove = false;
|
|
86
87
|
|
|
87
88
|
if (typeof tagRedundantAttributeValue === 'function') {
|
|
88
89
|
isRemove = tagRedundantAttributeValue(node);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports
|
|
6
|
+
exports.default = removeUnusedCss;
|
|
7
7
|
|
|
8
8
|
var _helpers = require("../helpers");
|
|
9
9
|
|
|
@@ -13,37 +13,17 @@ var _purgecss = _interopRequireDefault(require("purgecss"));
|
|
|
13
13
|
|
|
14
14
|
var _posthtmlRender = _interopRequireDefault(require("posthtml-render"));
|
|
15
15
|
|
|
16
|
-
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {
|
|
17
|
-
|
|
18
|
-
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }
|
|
19
|
-
|
|
20
|
-
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
21
|
-
|
|
22
|
-
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
|
|
23
|
-
|
|
24
|
-
function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }
|
|
25
|
-
|
|
26
|
-
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
|
|
27
|
-
|
|
28
|
-
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
|
|
29
|
-
|
|
30
|
-
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
|
31
|
-
|
|
32
|
-
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
|
33
|
-
|
|
34
|
-
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
35
|
-
|
|
36
|
-
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
|
|
16
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
37
17
|
|
|
38
18
|
// These options must be set and shouldn't be overriden to ensure uncss doesn't look at linked stylesheets.
|
|
39
|
-
|
|
19
|
+
const uncssOptions = {
|
|
40
20
|
ignoreSheets: [/\s*/],
|
|
41
21
|
stylesheets: []
|
|
42
22
|
};
|
|
43
23
|
|
|
44
24
|
function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
|
|
45
|
-
|
|
46
|
-
return runUncss(html, css, uncssOptions).then(
|
|
25
|
+
const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
26
|
+
return runUncss(html, css, uncssOptions).then(css => {
|
|
47
27
|
// uncss may have left some style tags empty
|
|
48
28
|
if (css.trim().length === 0) {
|
|
49
29
|
styleNode.tag = false;
|
|
@@ -56,15 +36,16 @@ function processStyleNodeUnCSS(html, styleNode, uncssOptions) {
|
|
|
56
36
|
}
|
|
57
37
|
|
|
58
38
|
function runUncss(html, css, userOptions) {
|
|
59
|
-
if (
|
|
39
|
+
if (typeof userOptions !== 'object') {
|
|
60
40
|
userOptions = {};
|
|
61
41
|
}
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
const options = { ...userOptions,
|
|
44
|
+
...uncssOptions
|
|
45
|
+
};
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
66
47
|
options.raw = css;
|
|
67
|
-
(0, _uncss
|
|
48
|
+
(0, _uncss.default)(html, options, (error, output) => {
|
|
68
49
|
if (error) {
|
|
69
50
|
reject(error);
|
|
70
51
|
return;
|
|
@@ -75,25 +56,23 @@ function runUncss(html, css, userOptions) {
|
|
|
75
56
|
});
|
|
76
57
|
}
|
|
77
58
|
|
|
78
|
-
|
|
59
|
+
const purgeFromHtml = function (tree) {
|
|
79
60
|
// content is not used as we can directly used the parsed HTML,
|
|
80
61
|
// making the process faster
|
|
81
|
-
|
|
82
|
-
tree.walk(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
selectors.push
|
|
62
|
+
const selectors = [];
|
|
63
|
+
tree.walk(node => {
|
|
64
|
+
const classes = node.attrs && node.attrs.class && node.attrs.class.split(' ') || [];
|
|
65
|
+
const ids = node.attrs && node.attrs.id && node.attrs.id.split(' ') || [];
|
|
66
|
+
selectors.push(...classes, ...ids);
|
|
86
67
|
node.tag && selectors.push(node.tag);
|
|
87
68
|
return node;
|
|
88
69
|
});
|
|
89
|
-
return
|
|
90
|
-
return selectors;
|
|
91
|
-
};
|
|
70
|
+
return () => selectors;
|
|
92
71
|
};
|
|
93
72
|
|
|
94
73
|
function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
|
|
95
|
-
|
|
96
|
-
return runPurgecss(tree, css, purgecssOptions).then(
|
|
74
|
+
const css = (0, _helpers.extractCssFromStyleNode)(styleNode);
|
|
75
|
+
return runPurgecss(tree, css, purgecssOptions).then(css => {
|
|
97
76
|
if (css.trim().length === 0) {
|
|
98
77
|
styleNode.tag = false;
|
|
99
78
|
styleNode.content = [];
|
|
@@ -105,11 +84,11 @@ function processStyleNodePurgeCSS(tree, styleNode, purgecssOptions) {
|
|
|
105
84
|
}
|
|
106
85
|
|
|
107
86
|
function runPurgecss(tree, css, userOptions) {
|
|
108
|
-
if (
|
|
87
|
+
if (typeof userOptions !== 'object') {
|
|
109
88
|
userOptions = {};
|
|
110
89
|
}
|
|
111
90
|
|
|
112
|
-
|
|
91
|
+
const options = { ...userOptions,
|
|
113
92
|
content: [{
|
|
114
93
|
raw: tree,
|
|
115
94
|
extension: 'html'
|
|
@@ -122,9 +101,8 @@ function runPurgecss(tree, css, userOptions) {
|
|
|
122
101
|
extractor: purgeFromHtml(tree),
|
|
123
102
|
extensions: ['html']
|
|
124
103
|
}]
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return new _purgecss["default"]().purge(options).then(function (result) {
|
|
104
|
+
};
|
|
105
|
+
return new _purgecss.default().purge(options).then(result => {
|
|
128
106
|
return result[0].css;
|
|
129
107
|
});
|
|
130
108
|
}
|
|
@@ -132,9 +110,9 @@ function runPurgecss(tree, css, userOptions) {
|
|
|
132
110
|
|
|
133
111
|
|
|
134
112
|
function removeUnusedCss(tree, options, userOptions) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
tree.walk(
|
|
113
|
+
const promises = [];
|
|
114
|
+
const html = userOptions.tool !== 'purgeCSS' && (0, _posthtmlRender.default)(tree);
|
|
115
|
+
tree.walk(node => {
|
|
138
116
|
if ((0, _helpers.isStyleNode)(node)) {
|
|
139
117
|
if (userOptions.tool === 'purgeCSS') {
|
|
140
118
|
promises.push(processStyleNodePurgeCSS(tree, node, userOptions));
|
|
@@ -145,7 +123,5 @@ function removeUnusedCss(tree, options, userOptions) {
|
|
|
145
123
|
|
|
146
124
|
return node;
|
|
147
125
|
});
|
|
148
|
-
return Promise.all(promises).then(
|
|
149
|
-
return tree;
|
|
150
|
-
});
|
|
126
|
+
return Promise.all(promises).then(() => tree);
|
|
151
127
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = sortAttributes;
|
|
7
|
+
|
|
8
|
+
var _timsort = require("timsort");
|
|
9
|
+
|
|
10
|
+
const validOptions = new Set(['frequency', 'alphabetical']);
|
|
11
|
+
|
|
12
|
+
const processModuleOptions = options => {
|
|
13
|
+
if (options === true) return 'alphabetical';
|
|
14
|
+
return validOptions.has(options) ? options : false;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class AttributeTokenChain {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.freqData = new Map(); // <attr, frequency>[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
addFromNodeAttrs(nodeAttrs) {
|
|
23
|
+
Object.keys(nodeAttrs).forEach(attrName => {
|
|
24
|
+
const attrNameLower = attrName.toLowerCase();
|
|
25
|
+
|
|
26
|
+
if (this.freqData.has(attrNameLower)) {
|
|
27
|
+
this.freqData.set(attrNameLower, this.freqData.get(attrNameLower) + 1);
|
|
28
|
+
} else {
|
|
29
|
+
this.freqData.set(attrNameLower, 1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createSortOrder() {
|
|
35
|
+
let _sortOrder = [];
|
|
36
|
+
|
|
37
|
+
for (const item of this.freqData.entries()) {
|
|
38
|
+
_sortOrder.push(item);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(0, _timsort.sort)(_sortOrder, (a, b) => b[1] - a[1]);
|
|
42
|
+
this.sortOrder = _sortOrder.map(i => i[0]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
sortFromNodeAttrs(nodeAttrs) {
|
|
46
|
+
const newAttrs = {}; // Convert node.attrs attrName into lower case.
|
|
47
|
+
|
|
48
|
+
const loweredNodeAttrs = {};
|
|
49
|
+
Object.entries(nodeAttrs).forEach(([attrName, attrValue]) => {
|
|
50
|
+
loweredNodeAttrs[attrName.toLowerCase()] = attrValue;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!this.sortOrder) {
|
|
54
|
+
this.createSortOrder();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.sortOrder.forEach(attrNameLower => {
|
|
58
|
+
// The attrName inside "sortOrder" has been lowered
|
|
59
|
+
if (loweredNodeAttrs[attrNameLower]) {
|
|
60
|
+
newAttrs[attrNameLower] = loweredNodeAttrs[attrNameLower];
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return newAttrs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
/** Sort attibutes */
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function sortAttributes(tree, options, moduleOptions) {
|
|
71
|
+
const sortType = processModuleOptions(moduleOptions);
|
|
72
|
+
|
|
73
|
+
if (sortType === 'alphabetical') {
|
|
74
|
+
return sortAttributesInAlphabeticalOrder(tree);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sortType === 'frequency') {
|
|
78
|
+
return sortAttributesByFrequency(tree);
|
|
79
|
+
} // Invalid configuration
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sortAttributesInAlphabeticalOrder(tree) {
|
|
86
|
+
tree.walk(node => {
|
|
87
|
+
if (!node.attrs) {
|
|
88
|
+
return node;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const newAttrs = {};
|
|
92
|
+
Object.keys(node.attrs).sort((a, b) => typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b).forEach(attr => newAttrs[attr] = node.attrs[attr]);
|
|
93
|
+
node.attrs = newAttrs;
|
|
94
|
+
return node;
|
|
95
|
+
});
|
|
96
|
+
return tree;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function sortAttributesByFrequency(tree) {
|
|
100
|
+
const tokenchain = new AttributeTokenChain(); // Traverse through tree to get frequency
|
|
101
|
+
|
|
102
|
+
tree.walk(node => {
|
|
103
|
+
if (!node.attrs) {
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
tokenchain.addFromNodeAttrs(node.attrs);
|
|
108
|
+
return node;
|
|
109
|
+
}); // Traverse through tree again, this time sort the attributes
|
|
110
|
+
|
|
111
|
+
tree.walk(node => {
|
|
112
|
+
if (!node.attrs) {
|
|
113
|
+
return node;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
node.attrs = tokenchain.sortFromNodeAttrs(node.attrs);
|
|
117
|
+
return node;
|
|
118
|
+
});
|
|
119
|
+
return tree;
|
|
120
|
+
}
|