htmlnano 2.0.2 → 2.0.3
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 +6 -0
- package/README.md +2 -2
- package/docs/docs/010-introduction.md +4 -4
- package/docs/docs/020-usage.md +63 -23
- package/docs/docs/030-config.md +1 -1
- package/docs/docs/050-modules.md +500 -483
- package/docs/package-lock.json +289 -95
- package/docs/versioned_docs/version-1.1.1/010-introduction.md +4 -4
- package/docs/versioned_docs/version-1.1.1/030-config.md +1 -1
- package/docs/versioned_docs/version-2.0.0/010-introduction.md +4 -4
- package/docs/versioned_docs/version-2.0.0/030-config.md +2 -2
- package/index.d.ts +93 -0
- package/lib/helpers.js +4 -11
- package/lib/htmlnano.js +11 -36
- package/lib/modules/collapseAttributeWhitespace.js +11 -12
- package/lib/modules/collapseBooleanAttributes.js +33 -9
- package/lib/modules/collapseWhitespace.js +17 -19
- package/lib/modules/custom.js +0 -3
- package/lib/modules/deduplicateAttributeValues.js +3 -5
- package/lib/modules/mergeScripts.js +0 -11
- package/lib/modules/mergeStyles.js +2 -8
- package/lib/modules/minifyConditionalComments.js +4 -15
- package/lib/modules/minifyCss.js +5 -16
- package/lib/modules/minifyJs.js +8 -28
- package/lib/modules/minifyJson.js +2 -3
- package/lib/modules/minifySvg.js +13 -5
- package/lib/modules/minifyUrls.js +18 -34
- package/lib/modules/normalizeAttributeValues.js +85 -2
- package/lib/modules/removeAttributeQuotes.js +0 -2
- package/lib/modules/removeComments.js +10 -28
- package/lib/modules/removeEmptyAttributes.js +6 -6
- package/lib/modules/removeOptionalTags.js +9 -46
- package/lib/modules/removeRedundantAttributes.js +20 -68
- package/lib/modules/removeUnusedCss.js +7 -18
- package/lib/modules/sortAttributes.js +10 -25
- package/lib/modules/sortAttributesWithLists.js +7 -29
- package/lib/presets/ampSafe.js +2 -5
- package/lib/presets/max.js +2 -5
- package/lib/presets/safe.js +32 -15
- package/package.json +9 -15
- package/test.js +0 -48
|
@@ -5,18 +5,18 @@ slug: /
|
|
|
5
5
|
|
|
6
6
|
# Introduction
|
|
7
7
|
|
|
8
|
-
Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
|
|
8
|
+
Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
|
|
9
9
|
Inspired by [cssnano](http://cssnano.co/).
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
## [Benchmark](https://github.com/maltsev/html-minifiers-benchmark/blob/master/README.md)
|
|
13
13
|
[html-minifier-terser@5.1.1]: https://www.npmjs.com/package/html-minifier-terser
|
|
14
|
-
[htmlnano@1.
|
|
14
|
+
[htmlnano@1.1.1]: https://www.npmjs.com/package/htmlnano
|
|
15
15
|
|
|
16
|
-
| Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@1.
|
|
16
|
+
| Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@1.1.1] |
|
|
17
17
|
|---------|------------:|----------------:|-----------:|
|
|
18
18
|
| [stackoverflow.blog](https://stackoverflow.blog/) | 95 | 87 | 82 |
|
|
19
19
|
| [github.com](https://github.com/) | 210 | 183 | 171 |
|
|
20
20
|
| [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 78 | 72 | 72 |
|
|
21
21
|
| [npmjs.com](https://www.npmjs.com/features) | 41 | 38 | 36 |
|
|
22
|
-
| **Avg. minify rate** | 0% | **9%** | **13%** |
|
|
22
|
+
| **Avg. minify rate** | 0% | **9%** | **13%** |
|
|
@@ -6,7 +6,7 @@ There are two main ways to configure htmlnano:
|
|
|
6
6
|
This is the way described above in the examples.
|
|
7
7
|
|
|
8
8
|
## Using configuration file
|
|
9
|
-
Alternatively, you might create a configuration file (e.g., `
|
|
9
|
+
Alternatively, you might create a configuration file (e.g., `htmlnanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
|
|
10
10
|
`htmlnano` uses `cosmiconfig`, so refer to [its documentation](https://github.com/davidtheclark/cosmiconfig/blob/main/README.md) for a more detailed description.
|
|
11
11
|
|
|
12
12
|
If you want to specify a preset that way, use `preset` key:
|
|
@@ -5,18 +5,18 @@ slug: /
|
|
|
5
5
|
|
|
6
6
|
# Introduction
|
|
7
7
|
|
|
8
|
-
Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
|
|
8
|
+
Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml).
|
|
9
9
|
Inspired by [cssnano](http://cssnano.co/).
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
## [Benchmark](https://github.com/maltsev/html-minifiers-benchmark/blob/master/README.md)
|
|
13
13
|
[html-minifier-terser@5.1.1]: https://www.npmjs.com/package/html-minifier-terser
|
|
14
|
-
[htmlnano@
|
|
14
|
+
[htmlnano@2.0.0]: https://www.npmjs.com/package/htmlnano
|
|
15
15
|
|
|
16
|
-
| Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@
|
|
16
|
+
| Website | Source (KB) | [html-minifier-terser@5.1.1] | [htmlnano@2.0.0] |
|
|
17
17
|
|---------|------------:|----------------:|-----------:|
|
|
18
18
|
| [stackoverflow.blog](https://stackoverflow.blog/) | 95 | 87 | 82 |
|
|
19
19
|
| [github.com](https://github.com/) | 210 | 183 | 171 |
|
|
20
20
|
| [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 78 | 72 | 72 |
|
|
21
21
|
| [npmjs.com](https://www.npmjs.com/features) | 41 | 38 | 36 |
|
|
22
|
-
| **Avg. minify rate** | 0% | **9%** | **13%** |
|
|
22
|
+
| **Avg. minify rate** | 0% | **9%** | **13%** |
|
|
@@ -6,7 +6,7 @@ There are two main ways to configure htmlnano:
|
|
|
6
6
|
This is the way described above in the examples.
|
|
7
7
|
|
|
8
8
|
## Using configuration file
|
|
9
|
-
Alternatively, you might create a configuration file (e.g., `
|
|
9
|
+
Alternatively, you might create a configuration file (e.g., `htmlnanorc.json` or `htmlnanorc.js`) or save options to `package.json` with `htmlnano` key.
|
|
10
10
|
`htmlnano` uses `cosmiconfig`, so refer to [its documentation](https://github.com/davidtheclark/cosmiconfig/blob/main/README.md) for a more detailed description.
|
|
11
11
|
|
|
12
12
|
If you want to specify a preset that way, use `preset` key:
|
|
@@ -18,4 +18,4 @@ If you want to specify a preset that way, use `preset` key:
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Configuration files have lower precedence than passing options to `htmlnano` directly.
|
|
21
|
-
So if you use both ways, then the configuration file would be ignored.
|
|
21
|
+
So if you use both ways, then the configuration file would be ignored.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type PostHTML from "posthtml";
|
|
2
|
+
import type terser from "terser";
|
|
3
|
+
import type { Options as CssNanoOptions } from "cssnano";
|
|
4
|
+
import type { OptimizeOptions as SvgoOptimizeOptions } from "svgo";
|
|
5
|
+
|
|
6
|
+
export interface HtmlnanoOptions {
|
|
7
|
+
skipConfigLoading?: boolean;
|
|
8
|
+
collapseAttributeWhitespace?: boolean;
|
|
9
|
+
collapseBooleanAttributes?: {
|
|
10
|
+
amphtml?: boolean;
|
|
11
|
+
};
|
|
12
|
+
collapseWhitespace?: "conservative" | "all" | "aggressive";
|
|
13
|
+
custom?: (tree: PostHTML.Node, options?: any) => PostHTML.Node;
|
|
14
|
+
deduplicateAttributeValues?: boolean;
|
|
15
|
+
minifyUrls?: URL | string | false;
|
|
16
|
+
mergeStyles?: boolean;
|
|
17
|
+
mergeScripts?: boolean;
|
|
18
|
+
minifyCss?: CssNanoOptions | boolean;
|
|
19
|
+
minifyConditionalComments?: boolean;
|
|
20
|
+
minifyJs?: terser.FormatOptions | boolean;
|
|
21
|
+
minifyJson?: boolean;
|
|
22
|
+
minifySvg?: SvgoOptimizeOptions | boolean;
|
|
23
|
+
normalizeAttributeValues?: boolean;
|
|
24
|
+
removeAttributeQuotes?: boolean;
|
|
25
|
+
removeComments?: boolean | "safe" | "all" | RegExp | (() => boolean);
|
|
26
|
+
removeEmptyAttributes?: boolean;
|
|
27
|
+
removeRedundantAttributes?: boolean;
|
|
28
|
+
removeOptionalTags?: boolean;
|
|
29
|
+
removeUnusedCss?: boolean;
|
|
30
|
+
sortAttributes?: boolean | "alphabetical" | "frequency";
|
|
31
|
+
sortAttributesWithLists?: boolean | "alphabetical" | "frequency";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HtmlnanoPreset extends Omit<HtmlnanoOptions, "skipConfigLoading"> {}
|
|
35
|
+
|
|
36
|
+
export interface Presets {
|
|
37
|
+
safe: HtmlnanoPreset;
|
|
38
|
+
ampSafe: HtmlnanoPreset;
|
|
39
|
+
max: HtmlnanoPreset;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Preset = Presets[keyof Presets];
|
|
43
|
+
|
|
44
|
+
export function loadConfig(
|
|
45
|
+
options?: HtmlnanoOptions,
|
|
46
|
+
preset?: Preset,
|
|
47
|
+
configPath?: string
|
|
48
|
+
): [HtmlnanoOptions | {}, Preset];
|
|
49
|
+
|
|
50
|
+
declare function htmlnano<TMessage>(
|
|
51
|
+
options?: HtmlnanoOptions,
|
|
52
|
+
preset?: Preset
|
|
53
|
+
): (tree: PostHTML.Node) => Promise<PostHTML.Result<TMessage>>;
|
|
54
|
+
|
|
55
|
+
interface PostHtmlOptions {
|
|
56
|
+
directives?: Array<{
|
|
57
|
+
name: string | RegExp;
|
|
58
|
+
start: string;
|
|
59
|
+
end: string;
|
|
60
|
+
}>;
|
|
61
|
+
sourceLocations?: boolean;
|
|
62
|
+
recognizeNoValueAttribute?: boolean;
|
|
63
|
+
xmlMode?: boolean;
|
|
64
|
+
decodeEntities?: boolean;
|
|
65
|
+
lowerCaseTags?: boolean;
|
|
66
|
+
lowerCaseAttributeNames?: boolean;
|
|
67
|
+
recognizeCDATA?: boolean;
|
|
68
|
+
recognizeSelfClosing?: boolean;
|
|
69
|
+
Tokenizer?: any;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
declare namespace htmlnano {
|
|
73
|
+
export function process<TMessage>(
|
|
74
|
+
html: string,
|
|
75
|
+
options?: HtmlnanoOptions,
|
|
76
|
+
preset?: Preset,
|
|
77
|
+
postHtmlOptions?: PostHtmlOptions
|
|
78
|
+
): Promise<PostHTML.Result<TMessage>>;
|
|
79
|
+
|
|
80
|
+
export function getRequiredOptionalDependencies(
|
|
81
|
+
optionsRun?: HtmlnanoOptions,
|
|
82
|
+
presetRun?: Preset
|
|
83
|
+
): string[];
|
|
84
|
+
|
|
85
|
+
export function htmlMinimizerWebpackPluginMinify(
|
|
86
|
+
input: { [file: string]: string },
|
|
87
|
+
minimizerOptions?: HtmlnanoOptions
|
|
88
|
+
): any;
|
|
89
|
+
|
|
90
|
+
export const presets: Presets;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default htmlnano;
|
package/lib/helpers.js
CHANGED
|
@@ -11,41 +11,35 @@ exports.isEventHandler = isEventHandler;
|
|
|
11
11
|
exports.isStyleNode = isStyleNode;
|
|
12
12
|
exports.optionalRequire = optionalRequire;
|
|
13
13
|
const ampBoilerplateAttributes = ['amp-boilerplate', 'amp4ads-boilerplate', 'amp4email-boilerplate'];
|
|
14
|
-
|
|
15
14
|
function isAmpBoilerplate(node) {
|
|
16
15
|
if (!node.attrs) {
|
|
17
16
|
return false;
|
|
18
17
|
}
|
|
19
|
-
|
|
20
18
|
for (const attr of ampBoilerplateAttributes) {
|
|
21
19
|
if (attr in node.attrs) {
|
|
22
20
|
return true;
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
|
-
|
|
26
23
|
return false;
|
|
27
24
|
}
|
|
28
|
-
|
|
29
25
|
function isComment(content) {
|
|
30
|
-
|
|
26
|
+
if (typeof content === 'string') {
|
|
27
|
+
return content.trim().startsWith('<!--');
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
31
30
|
}
|
|
32
|
-
|
|
33
31
|
function isConditionalComment(content) {
|
|
34
32
|
return (content || '').trim().startsWith('<!--[if');
|
|
35
33
|
}
|
|
36
|
-
|
|
37
34
|
function isStyleNode(node) {
|
|
38
35
|
return node.tag === 'style' && !isAmpBoilerplate(node) && 'content' in node && node.content.length > 0;
|
|
39
36
|
}
|
|
40
|
-
|
|
41
37
|
function extractCssFromStyleNode(node) {
|
|
42
38
|
return Array.isArray(node.content) ? node.content.join(' ') : node.content;
|
|
43
39
|
}
|
|
44
|
-
|
|
45
40
|
function isEventHandler(attributeName) {
|
|
46
41
|
return attributeName && attributeName.slice && attributeName.slice(0, 2).toLowerCase() === 'on' && attributeName.length >= 5;
|
|
47
42
|
}
|
|
48
|
-
|
|
49
43
|
function optionalRequire(moduleName) {
|
|
50
44
|
try {
|
|
51
45
|
return require(moduleName);
|
|
@@ -53,7 +47,6 @@ function optionalRequire(moduleName) {
|
|
|
53
47
|
if (e.code === 'MODULE_NOT_FOUND') {
|
|
54
48
|
return null;
|
|
55
49
|
}
|
|
56
|
-
|
|
57
50
|
throw e;
|
|
58
51
|
}
|
|
59
52
|
}
|
package/lib/htmlnano.js
CHANGED
|
@@ -5,84 +5,65 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
7
|
exports.loadConfig = loadConfig;
|
|
8
|
-
|
|
9
8
|
var _posthtml = _interopRequireDefault(require("posthtml"));
|
|
10
|
-
|
|
11
9
|
var _cosmiconfig = require("cosmiconfig");
|
|
12
|
-
|
|
13
10
|
var _safe = _interopRequireDefault(require("./presets/safe"));
|
|
14
|
-
|
|
15
11
|
var _ampSafe = _interopRequireDefault(require("./presets/ampSafe"));
|
|
16
|
-
|
|
17
12
|
var _max = _interopRequireDefault(require("./presets/max"));
|
|
18
|
-
|
|
19
13
|
var _package = _interopRequireDefault(require("../package.json"));
|
|
20
|
-
|
|
21
14
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
22
|
-
|
|
23
15
|
const presets = {
|
|
24
16
|
safe: _safe.default,
|
|
25
17
|
ampSafe: _ampSafe.default,
|
|
26
18
|
max: _max.default
|
|
27
19
|
};
|
|
28
|
-
|
|
29
20
|
function loadConfig(options, preset, configPath) {
|
|
30
21
|
var _options;
|
|
31
|
-
|
|
32
22
|
if (!((_options = options) !== null && _options !== void 0 && _options.skipConfigLoading)) {
|
|
33
23
|
const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
|
|
34
24
|
const rc = configPath ? explorer.load(configPath) : explorer.search();
|
|
35
|
-
|
|
36
25
|
if (rc) {
|
|
37
26
|
const {
|
|
38
27
|
preset: presetName
|
|
39
28
|
} = rc.config;
|
|
40
|
-
|
|
41
29
|
if (presetName) {
|
|
42
30
|
if (!preset && presets[presetName]) {
|
|
43
31
|
preset = presets[presetName];
|
|
44
32
|
}
|
|
45
|
-
|
|
46
33
|
delete rc.config.preset;
|
|
47
34
|
}
|
|
48
|
-
|
|
49
35
|
if (!options) {
|
|
50
36
|
options = rc.config;
|
|
51
37
|
}
|
|
52
38
|
}
|
|
53
39
|
}
|
|
54
|
-
|
|
55
40
|
return [options || {}, preset || _safe.default];
|
|
56
41
|
}
|
|
57
|
-
|
|
58
42
|
const optionalDependencies = {
|
|
59
43
|
minifyCss: ['cssnano', 'postcss'],
|
|
60
44
|
minifyJs: ['terser'],
|
|
61
45
|
minifyUrl: ['relateurl', 'srcset', 'terser'],
|
|
62
46
|
minifySvg: ['svgo']
|
|
63
47
|
};
|
|
64
|
-
|
|
65
48
|
function htmlnano(optionsRun, presetRun) {
|
|
66
49
|
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
67
50
|
return function minifier(tree) {
|
|
68
51
|
const nodeHandlers = [];
|
|
69
52
|
const attrsHandlers = [];
|
|
70
53
|
const contentsHandlers = [];
|
|
71
|
-
options = {
|
|
54
|
+
options = {
|
|
55
|
+
...preset,
|
|
72
56
|
...options
|
|
73
57
|
};
|
|
74
58
|
let promise = Promise.resolve(tree);
|
|
75
|
-
|
|
76
59
|
for (const [moduleName, moduleOptions] of Object.entries(options)) {
|
|
77
60
|
if (!moduleOptions) {
|
|
78
61
|
// The module is disabled
|
|
79
62
|
continue;
|
|
80
63
|
}
|
|
81
|
-
|
|
82
64
|
if (_safe.default[moduleName] === undefined) {
|
|
83
65
|
throw new Error('Module "' + moduleName + '" is not defined');
|
|
84
66
|
}
|
|
85
|
-
|
|
86
67
|
(optionalDependencies[moduleName] || []).forEach(dependency => {
|
|
87
68
|
try {
|
|
88
69
|
require(dependency);
|
|
@@ -94,30 +75,23 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
94
75
|
}
|
|
95
76
|
}
|
|
96
77
|
});
|
|
97
|
-
|
|
98
78
|
const module = require('./modules/' + moduleName);
|
|
99
|
-
|
|
100
79
|
if (typeof module.onAttrs === 'function') {
|
|
101
80
|
attrsHandlers.push(module.onAttrs(options, moduleOptions));
|
|
102
81
|
}
|
|
103
|
-
|
|
104
82
|
if (typeof module.onContent === 'function') {
|
|
105
83
|
contentsHandlers.push(module.onContent(options, moduleOptions));
|
|
106
84
|
}
|
|
107
|
-
|
|
108
85
|
if (typeof module.onNode === 'function') {
|
|
109
86
|
nodeHandlers.push(module.onNode(options, moduleOptions));
|
|
110
87
|
}
|
|
111
|
-
|
|
112
88
|
if (typeof module.default === 'function') {
|
|
113
89
|
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
114
90
|
}
|
|
115
91
|
}
|
|
116
|
-
|
|
117
92
|
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
|
|
118
93
|
return promise;
|
|
119
94
|
}
|
|
120
|
-
|
|
121
95
|
return promise.then(tree => {
|
|
122
96
|
tree.walk(node => {
|
|
123
97
|
if (node) {
|
|
@@ -127,17 +101,13 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
127
101
|
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
128
102
|
newAttrsObj[attrName.toLowerCase()] = attrValue;
|
|
129
103
|
});
|
|
130
|
-
|
|
131
104
|
for (const handler of attrsHandlers) {
|
|
132
105
|
newAttrsObj = handler(newAttrsObj, node);
|
|
133
106
|
}
|
|
134
|
-
|
|
135
107
|
node.attrs = newAttrsObj;
|
|
136
108
|
}
|
|
137
|
-
|
|
138
109
|
if (node.content) {
|
|
139
110
|
node.content = typeof node.content === 'string' ? [node.content] : node.content;
|
|
140
|
-
|
|
141
111
|
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
142
112
|
for (const handler of contentsHandlers) {
|
|
143
113
|
const result = handler(node.content, node);
|
|
@@ -145,28 +115,33 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
145
115
|
}
|
|
146
116
|
}
|
|
147
117
|
}
|
|
148
|
-
|
|
149
118
|
for (const handler of nodeHandlers) {
|
|
150
119
|
node = handler(node);
|
|
151
120
|
}
|
|
152
121
|
}
|
|
153
|
-
|
|
154
122
|
return node;
|
|
155
123
|
});
|
|
156
124
|
return tree;
|
|
157
125
|
});
|
|
158
126
|
};
|
|
159
127
|
}
|
|
160
|
-
|
|
161
128
|
htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
|
|
162
129
|
const [options] = loadConfig(optionsRun, presetRun);
|
|
163
130
|
return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
|
|
164
131
|
};
|
|
165
|
-
|
|
166
132
|
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
167
133
|
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
168
134
|
};
|
|
169
135
|
|
|
136
|
+
// https://github.com/webpack-contrib/html-minimizer-webpack-plugin/blob/faca00f2219514bc671c5942685721f0b5dbaa70/src/utils.js#L74
|
|
137
|
+
htmlnano.htmlMinimizerWebpackPluginMinify = function htmlNano(input, minimizerOptions = {}) {
|
|
138
|
+
const [[, code]] = Object.entries(input);
|
|
139
|
+
return htmlnano.process(code, minimizerOptions, presets.safe).then(result => {
|
|
140
|
+
return {
|
|
141
|
+
code: result.html
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
};
|
|
170
145
|
htmlnano.presets = presets;
|
|
171
146
|
var _default = htmlnano;
|
|
172
147
|
exports.default = _default;
|
|
@@ -5,13 +5,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.attributesWithLists = void 0;
|
|
7
7
|
exports.onAttrs = onAttrs;
|
|
8
|
-
|
|
9
8
|
var _helpers = require("../helpers");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'ping',
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const attributesWithLists = new Set(['class', 'dropzone', 'rel',
|
|
10
|
+
// a, area, link
|
|
11
|
+
'ping',
|
|
12
|
+
// a, area
|
|
13
|
+
'sandbox',
|
|
14
|
+
// iframe
|
|
15
15
|
/**
|
|
16
16
|
* https://github.com/posthtml/htmlnano/issues/180
|
|
17
17
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
@@ -21,8 +21,8 @@ const attributesWithLists = new Set(['class', 'dropzone', 'rel', // a, area, lin
|
|
|
21
21
|
// 'sizes', // link
|
|
22
22
|
'headers' // td, th
|
|
23
23
|
]);
|
|
24
|
-
/** @type Record<string, string[] | null> */
|
|
25
24
|
|
|
25
|
+
/** @type Record<string, string[] | null> */
|
|
26
26
|
exports.attributesWithLists = attributesWithLists;
|
|
27
27
|
const attributesWithSingleValue = {
|
|
28
28
|
accept: ['input'],
|
|
@@ -63,26 +63,25 @@ const attributesWithSingleValue = {
|
|
|
63
63
|
value: ['li', 'meter', 'progress'],
|
|
64
64
|
width: ['canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video']
|
|
65
65
|
};
|
|
66
|
-
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
67
66
|
|
|
67
|
+
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */
|
|
68
68
|
function onAttrs() {
|
|
69
69
|
return (attrs, node) => {
|
|
70
70
|
const newAttrs = attrs;
|
|
71
71
|
Object.entries(attrs).forEach(([attrName, attrValue]) => {
|
|
72
|
+
if (typeof attrValue !== 'string') return;
|
|
72
73
|
if (attributesWithLists.has(attrName)) {
|
|
73
74
|
const newAttrValue = attrValue.replace(/\s+/g, ' ').trim();
|
|
74
75
|
newAttrs[attrName] = newAttrValue;
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
if ((0, _helpers.isEventHandler)(attrName) || Object.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
|
|
78
|
+
if ((0, _helpers.isEventHandler)(attrName) || Object.prototype.hasOwnProperty.call(attributesWithSingleValue, attrName) && (attributesWithSingleValue[attrName] === null || attributesWithSingleValue[attrName].includes(node.tag))) {
|
|
79
79
|
newAttrs[attrName] = minifySingleAttributeValue(attrValue);
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
return newAttrs;
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
|
-
|
|
86
85
|
function minifySingleAttributeValue(value) {
|
|
87
|
-
return typeof value === 'string' ?
|
|
86
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
88
87
|
}
|
|
@@ -5,34 +5,58 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.onAttrs = onAttrs;
|
|
7
7
|
// Source: https://github.com/kangax/html-minifier/issues/63
|
|
8
|
-
|
|
8
|
+
// https://html.spec.whatwg.org/#boolean-attribute
|
|
9
|
+
// https://html.spec.whatwg.org/#attributes-1
|
|
10
|
+
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', 'nomodule', 'noresize', 'noshade', 'novalidate', 'nowrap', 'open', 'pauseonexit', 'playsinline', 'readonly', 'required', 'reversed', 'scoped', 'seamless', 'selected', 'sortable', 'truespeed', 'typemustmatch', 'visible']);
|
|
9
11
|
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']);
|
|
10
|
-
|
|
12
|
+
const missingValueDefaultEmptyStringAttributes = {
|
|
13
|
+
// https://html.spec.whatwg.org/#attr-media-preload
|
|
14
|
+
audio: {
|
|
15
|
+
preload: 'auto'
|
|
16
|
+
},
|
|
17
|
+
video: {
|
|
18
|
+
preload: 'auto'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const tagsHasMissingValueDefaultEmptyStringAttributes = new Set(Object.keys(missingValueDefaultEmptyStringAttributes));
|
|
11
22
|
function onAttrs(options, moduleOptions) {
|
|
12
23
|
return (attrs, node) => {
|
|
13
24
|
if (!node.tag) return attrs;
|
|
14
25
|
const newAttrs = attrs;
|
|
15
|
-
|
|
26
|
+
if (tagsHasMissingValueDefaultEmptyStringAttributes.has(node.tag)) {
|
|
27
|
+
const tagAttributesCanBeReplacedWithEmptyString = missingValueDefaultEmptyStringAttributes[node.tag];
|
|
28
|
+
for (const attributesCanBeReplacedWithEmptyString of Object.keys(tagAttributesCanBeReplacedWithEmptyString)) {
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(attrs, attributesCanBeReplacedWithEmptyString) && attrs[attributesCanBeReplacedWithEmptyString] === tagAttributesCanBeReplacedWithEmptyString[attributesCanBeReplacedWithEmptyString]) {
|
|
30
|
+
attrs[attributesCanBeReplacedWithEmptyString] = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
for (const attrName of Object.keys(attrs)) {
|
|
17
35
|
if (attrName === 'visible' && node.tag.startsWith('a-')) {
|
|
18
36
|
continue;
|
|
19
37
|
}
|
|
20
|
-
|
|
21
38
|
if (htmlBooleanAttributes.has(attrName)) {
|
|
22
39
|
newAttrs[attrName] = true;
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
// Fast path optimization.
|
|
43
|
+
// The rest of tranformations are only for string type attrValue.
|
|
44
|
+
if (typeof newAttrs[attrName] !== 'string') continue;
|
|
25
45
|
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
|
|
26
46
|
newAttrs[attrName] = true;
|
|
27
|
-
}
|
|
28
|
-
//
|
|
29
|
-
|
|
47
|
+
}
|
|
48
|
+
// https://html.spec.whatwg.org/#a-quick-introduction-to-html
|
|
49
|
+
// The value, along with the "=" character, can be omitted altogether if the value is the empty string.
|
|
50
|
+
if (attrs[attrName] === '') {
|
|
51
|
+
newAttrs[attrName] = true;
|
|
52
|
+
}
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
// collapse crossorigin attributes
|
|
55
|
+
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
56
|
+
if (attrName.toLowerCase() === 'crossorigin' && attrs[attrName] === 'anonymous') {
|
|
32
57
|
newAttrs[attrName] = true;
|
|
33
58
|
}
|
|
34
59
|
}
|
|
35
|
-
|
|
36
60
|
return newAttrs;
|
|
37
61
|
};
|
|
38
62
|
}
|
|
@@ -4,14 +4,15 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = collapseWhitespace;
|
|
7
|
-
|
|
8
7
|
var _helpers = require("../helpers");
|
|
9
|
-
|
|
10
8
|
const noWhitespaceCollapseElements = new Set(['script', 'style', 'pre', 'textarea']);
|
|
11
|
-
const noTrimWhitespacesArroundElements = new Set([
|
|
12
|
-
|
|
9
|
+
const noTrimWhitespacesArroundElements = new Set([
|
|
10
|
+
// non-empty tags that will maintain whitespace around them
|
|
11
|
+
'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',
|
|
12
|
+
// self-closing tags that will maintain whitespace around them
|
|
13
13
|
'comment', 'img', 'input', 'wbr']);
|
|
14
|
-
const noTrimWhitespacesInsideElements = new Set([
|
|
14
|
+
const noTrimWhitespacesInsideElements = new Set([
|
|
15
|
+
// non-empty tags that will maintain whitespace within them
|
|
15
16
|
'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
17
|
const startsWithWhitespacePattern = /^\s/;
|
|
17
18
|
const endsWithWhitespacePattern = /\s$/;
|
|
@@ -19,14 +20,13 @@ const multipleWhitespacePattern = /\s+/g;
|
|
|
19
20
|
const NONE = '';
|
|
20
21
|
const SINGLE_SPACE = ' ';
|
|
21
22
|
const validOptions = ['all', 'aggressive', 'conservative'];
|
|
22
|
-
/** Collapses redundant whitespaces */
|
|
23
23
|
|
|
24
|
+
/** Collapses redundant whitespaces */
|
|
24
25
|
function collapseWhitespace(tree, options, collapseType, parent) {
|
|
25
26
|
collapseType = validOptions.includes(collapseType) ? collapseType : 'conservative';
|
|
26
27
|
tree.forEach((node, index) => {
|
|
27
28
|
const prevNode = tree[index - 1];
|
|
28
29
|
const nextNode = tree[index + 1];
|
|
29
|
-
|
|
30
30
|
if (typeof node === 'string') {
|
|
31
31
|
const parentNodeTag = parent && parent.node && parent.node.tag;
|
|
32
32
|
const isTopLevel = !parentNodeTag || parentNodeTag === 'html' || parentNodeTag === 'head';
|
|
@@ -38,9 +38,7 @@ function collapseWhitespace(tree, options, collapseType, parent) {
|
|
|
38
38
|
collapseType === 'aggressive';
|
|
39
39
|
node = collapseRedundantWhitespaces(node, collapseType, shouldTrim, parent, prevNode, nextNode);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
41
|
const isAllowCollapseWhitespace = !noWhitespaceCollapseElements.has(node.tag);
|
|
43
|
-
|
|
44
42
|
if (node.content && node.content.length && isAllowCollapseWhitespace) {
|
|
45
43
|
node.content = collapseWhitespace(node.content, options, collapseType, {
|
|
46
44
|
node,
|
|
@@ -48,37 +46,38 @@ function collapseWhitespace(tree, options, collapseType, parent) {
|
|
|
48
46
|
nextNode
|
|
49
47
|
});
|
|
50
48
|
}
|
|
51
|
-
|
|
52
49
|
tree[index] = node;
|
|
53
50
|
});
|
|
54
51
|
return tree;
|
|
55
52
|
}
|
|
56
|
-
|
|
57
53
|
function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, parent, prevNode, nextNode) {
|
|
58
54
|
if (!text || text.length === 0) {
|
|
59
55
|
return NONE;
|
|
60
56
|
}
|
|
61
|
-
|
|
62
57
|
if (!(0, _helpers.isComment)(text)) {
|
|
63
58
|
text = text.replace(multipleWhitespacePattern, SINGLE_SPACE);
|
|
64
59
|
}
|
|
65
|
-
|
|
66
60
|
if (shouldTrim) {
|
|
67
61
|
if (collapseType === 'aggressive') {
|
|
68
62
|
if (!noTrimWhitespacesInsideElements.has(parent && parent.node && parent.node.tag)) {
|
|
69
|
-
if (
|
|
63
|
+
if (
|
|
64
|
+
// It is the first child node of the parent
|
|
65
|
+
!prevNode
|
|
66
|
+
// It is not the first child node, and prevNode not a text node, and prevNode is safe to trim around
|
|
67
|
+
|| prevNode && prevNode.tag && !noTrimWhitespacesArroundElements.has(prevNode.tag)) {
|
|
70
68
|
text = text.trimStart();
|
|
71
69
|
} else {
|
|
72
70
|
// previous node is a "no trim whitespaces arround element"
|
|
73
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
// but previous node ends with a whitespace
|
|
74
73
|
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
|
-
||
|
|
74
|
+
||
|
|
75
|
+
// or the next node starts with a white space
|
|
76
76
|
nextNode && nextNode.content && nextNode.content.length && !startsWithWhitespacePattern.test(nextNode.content[0]))) {
|
|
77
77
|
text = text.trimStart();
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
if (!noTrimWhitespacesArroundElements.has(nextNode && nextNode.tag)) {
|
|
80
|
+
if (!nextNode || nextNode && nextNode.tag && !noTrimWhitespacesArroundElements.has(nextNode.tag)) {
|
|
82
81
|
text = text.trimEnd();
|
|
83
82
|
}
|
|
84
83
|
} else {
|
|
@@ -96,6 +95,5 @@ function collapseRedundantWhitespaces(text, collapseType, shouldTrim = false, pa
|
|
|
96
95
|
text = text.trim();
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
|
-
|
|
100
98
|
return text;
|
|
101
99
|
}
|
package/lib/modules/custom.js
CHANGED
|
@@ -4,17 +4,14 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = custom;
|
|
7
|
-
|
|
8
7
|
/** Meta-module that runs custom modules */
|
|
9
8
|
function custom(tree, options, customModules) {
|
|
10
9
|
if (!customModules) {
|
|
11
10
|
return tree;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
12
|
if (!Array.isArray(customModules)) {
|
|
15
13
|
customModules = [customModules];
|
|
16
14
|
}
|
|
17
|
-
|
|
18
15
|
customModules.forEach(customModule => {
|
|
19
16
|
tree = customModule(tree, options);
|
|
20
17
|
});
|