htmlnano 2.0.2 → 2.0.4
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 +35 -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 +7763 -11645
- package/docs/package.json +3 -3
- 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 +6 -12
- package/lib/htmlnano.js +18 -40
- 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 +3 -12
- package/lib/modules/mergeStyles.js +5 -9
- package/lib/modules/minifyConditionalComments.js +4 -15
- package/lib/modules/minifyCss.js +9 -16
- package/lib/modules/minifyJs.js +25 -29
- package/lib/modules/minifyJson.js +6 -3
- package/lib/modules/minifySvg.js +17 -9
- package/lib/modules/minifyUrls.js +18 -34
- package/lib/modules/normalizeAttributeValues.js +80 -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 +15 -21
package/docs/package.json
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"write-heading-ids": "docusaurus write-heading-ids"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@docusaurus/core": "2.
|
|
17
|
-
"@docusaurus/preset-classic": "2.
|
|
16
|
+
"@docusaurus/core": "2.2.0",
|
|
17
|
+
"@docusaurus/preset-classic": "2.2.0",
|
|
18
18
|
"@mdx-js/react": "^1.6.21",
|
|
19
|
-
"@svgr/webpack": "^
|
|
19
|
+
"@svgr/webpack": "^6.5.1",
|
|
20
20
|
"clsx": "^1.1.1",
|
|
21
21
|
"file-loader": "^6.2.0",
|
|
22
22
|
"prism-react-renderer": "^1.2.1",
|
|
@@ -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,49 +11,43 @@ 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
|
+
const module = require(moduleName);
|
|
46
|
+
return module.default || module;
|
|
52
47
|
} catch (e) {
|
|
53
48
|
if (e.code === 'MODULE_NOT_FOUND') {
|
|
54
49
|
return null;
|
|
55
50
|
}
|
|
56
|
-
|
|
57
51
|
throw e;
|
|
58
52
|
}
|
|
59
53
|
}
|
package/lib/htmlnano.js
CHANGED
|
@@ -5,84 +5,68 @@ 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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
let {
|
|
22
|
+
skipConfigLoading = false,
|
|
23
|
+
...rest
|
|
24
|
+
} = options || {};
|
|
25
|
+
if (!skipConfigLoading) {
|
|
33
26
|
const explorer = (0, _cosmiconfig.cosmiconfigSync)(_package.default.name);
|
|
34
27
|
const rc = configPath ? explorer.load(configPath) : explorer.search();
|
|
35
|
-
|
|
36
28
|
if (rc) {
|
|
37
29
|
const {
|
|
38
30
|
preset: presetName
|
|
39
31
|
} = rc.config;
|
|
40
|
-
|
|
41
32
|
if (presetName) {
|
|
42
33
|
if (!preset && presets[presetName]) {
|
|
43
34
|
preset = presets[presetName];
|
|
44
35
|
}
|
|
45
|
-
|
|
46
36
|
delete rc.config.preset;
|
|
47
37
|
}
|
|
48
|
-
|
|
49
38
|
if (!options) {
|
|
50
|
-
|
|
39
|
+
rest = rc.config;
|
|
51
40
|
}
|
|
52
41
|
}
|
|
53
42
|
}
|
|
54
|
-
|
|
55
|
-
return [options || {}, preset || _safe.default];
|
|
43
|
+
return [rest || {}, preset || _safe.default];
|
|
56
44
|
}
|
|
57
|
-
|
|
58
45
|
const optionalDependencies = {
|
|
59
46
|
minifyCss: ['cssnano', 'postcss'],
|
|
60
47
|
minifyJs: ['terser'],
|
|
61
48
|
minifyUrl: ['relateurl', 'srcset', 'terser'],
|
|
62
49
|
minifySvg: ['svgo']
|
|
63
50
|
};
|
|
64
|
-
|
|
65
51
|
function htmlnano(optionsRun, presetRun) {
|
|
66
52
|
let [options, preset] = loadConfig(optionsRun, presetRun);
|
|
67
53
|
return function minifier(tree) {
|
|
68
54
|
const nodeHandlers = [];
|
|
69
55
|
const attrsHandlers = [];
|
|
70
56
|
const contentsHandlers = [];
|
|
71
|
-
options = {
|
|
57
|
+
options = {
|
|
58
|
+
...preset,
|
|
72
59
|
...options
|
|
73
60
|
};
|
|
74
61
|
let promise = Promise.resolve(tree);
|
|
75
|
-
|
|
76
62
|
for (const [moduleName, moduleOptions] of Object.entries(options)) {
|
|
77
63
|
if (!moduleOptions) {
|
|
78
64
|
// The module is disabled
|
|
79
65
|
continue;
|
|
80
66
|
}
|
|
81
|
-
|
|
82
67
|
if (_safe.default[moduleName] === undefined) {
|
|
83
68
|
throw new Error('Module "' + moduleName + '" is not defined');
|
|
84
69
|
}
|
|
85
|
-
|
|
86
70
|
(optionalDependencies[moduleName] || []).forEach(dependency => {
|
|
87
71
|
try {
|
|
88
72
|
require(dependency);
|
|
@@ -94,30 +78,23 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
94
78
|
}
|
|
95
79
|
}
|
|
96
80
|
});
|
|
97
|
-
|
|
98
81
|
const module = require('./modules/' + moduleName);
|
|
99
|
-
|
|
100
82
|
if (typeof module.onAttrs === 'function') {
|
|
101
83
|
attrsHandlers.push(module.onAttrs(options, moduleOptions));
|
|
102
84
|
}
|
|
103
|
-
|
|
104
85
|
if (typeof module.onContent === 'function') {
|
|
105
86
|
contentsHandlers.push(module.onContent(options, moduleOptions));
|
|
106
87
|
}
|
|
107
|
-
|
|
108
88
|
if (typeof module.onNode === 'function') {
|
|
109
89
|
nodeHandlers.push(module.onNode(options, moduleOptions));
|
|
110
90
|
}
|
|
111
|
-
|
|
112
91
|
if (typeof module.default === 'function') {
|
|
113
92
|
promise = promise.then(tree => module.default(tree, options, moduleOptions));
|
|
114
93
|
}
|
|
115
94
|
}
|
|
116
|
-
|
|
117
95
|
if (attrsHandlers.length + contentsHandlers.length + nodeHandlers.length === 0) {
|
|
118
96
|
return promise;
|
|
119
97
|
}
|
|
120
|
-
|
|
121
98
|
return promise.then(tree => {
|
|
122
99
|
tree.walk(node => {
|
|
123
100
|
if (node) {
|
|
@@ -127,17 +104,13 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
127
104
|
Object.entries(node.attrs).forEach(([attrName, attrValue]) => {
|
|
128
105
|
newAttrsObj[attrName.toLowerCase()] = attrValue;
|
|
129
106
|
});
|
|
130
|
-
|
|
131
107
|
for (const handler of attrsHandlers) {
|
|
132
108
|
newAttrsObj = handler(newAttrsObj, node);
|
|
133
109
|
}
|
|
134
|
-
|
|
135
110
|
node.attrs = newAttrsObj;
|
|
136
111
|
}
|
|
137
|
-
|
|
138
112
|
if (node.content) {
|
|
139
113
|
node.content = typeof node.content === 'string' ? [node.content] : node.content;
|
|
140
|
-
|
|
141
114
|
if (Array.isArray(node.content) && node.content.length > 0) {
|
|
142
115
|
for (const handler of contentsHandlers) {
|
|
143
116
|
const result = handler(node.content, node);
|
|
@@ -145,28 +118,33 @@ function htmlnano(optionsRun, presetRun) {
|
|
|
145
118
|
}
|
|
146
119
|
}
|
|
147
120
|
}
|
|
148
|
-
|
|
149
121
|
for (const handler of nodeHandlers) {
|
|
150
122
|
node = handler(node);
|
|
151
123
|
}
|
|
152
124
|
}
|
|
153
|
-
|
|
154
125
|
return node;
|
|
155
126
|
});
|
|
156
127
|
return tree;
|
|
157
128
|
});
|
|
158
129
|
};
|
|
159
130
|
}
|
|
160
|
-
|
|
161
131
|
htmlnano.getRequiredOptionalDependencies = function (optionsRun, presetRun) {
|
|
162
132
|
const [options] = loadConfig(optionsRun, presetRun);
|
|
163
133
|
return [...new Set(Object.keys(options).filter(moduleName => options[moduleName]).map(moduleName => optionalDependencies[moduleName]).flat())];
|
|
164
134
|
};
|
|
165
|
-
|
|
166
135
|
htmlnano.process = function (html, options, preset, postHtmlOptions) {
|
|
167
136
|
return (0, _posthtml.default)([htmlnano(options, preset)]).process(html, postHtmlOptions);
|
|
168
137
|
};
|
|
169
138
|
|
|
139
|
+
// https://github.com/webpack-contrib/html-minimizer-webpack-plugin/blob/faca00f2219514bc671c5942685721f0b5dbaa70/src/utils.js#L74
|
|
140
|
+
htmlnano.htmlMinimizerWebpackPluginMinify = function htmlNano(input, minimizerOptions = {}) {
|
|
141
|
+
const [[, code]] = Object.entries(input);
|
|
142
|
+
return htmlnano.process(code, minimizerOptions, presets.safe).then(result => {
|
|
143
|
+
return {
|
|
144
|
+
code: result.html
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
};
|
|
170
148
|
htmlnano.presets = presets;
|
|
171
149
|
var _default = htmlnano;
|
|
172
150
|
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
|
}
|