htmlnano 3.0.0 → 3.1.0
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/README.md +18 -17
- package/dist/_modules/collapseAttributeWhitespace.d.mts +41 -5
- package/dist/_modules/collapseAttributeWhitespace.d.ts +41 -5
- package/dist/_modules/collapseAttributeWhitespace.js +69 -16
- package/dist/_modules/collapseAttributeWhitespace.mjs +67 -17
- package/dist/_modules/collapseBooleanAttributes.d.mts +36 -3
- package/dist/_modules/collapseBooleanAttributes.d.ts +36 -3
- package/dist/_modules/collapseBooleanAttributes.js +18 -11
- package/dist/_modules/collapseBooleanAttributes.mjs +18 -11
- package/dist/_modules/collapseWhitespace.d.mts +36 -3
- package/dist/_modules/collapseWhitespace.d.ts +36 -3
- package/dist/_modules/collapseWhitespace.js +25 -2
- package/dist/_modules/collapseWhitespace.mjs +25 -2
- package/dist/_modules/custom.d.mts +36 -3
- package/dist/_modules/custom.d.ts +36 -3
- package/dist/_modules/deduplicateAttributeValues.d.mts +36 -3
- package/dist/_modules/deduplicateAttributeValues.d.ts +36 -3
- package/dist/_modules/deduplicateAttributeValues.js +20 -5
- package/dist/_modules/deduplicateAttributeValues.mjs +21 -6
- package/dist/_modules/example.d.mts +36 -3
- package/dist/_modules/example.d.ts +36 -3
- package/dist/_modules/mergeScripts.d.mts +36 -3
- package/dist/_modules/mergeScripts.d.ts +36 -3
- package/dist/_modules/mergeScripts.js +99 -24
- package/dist/_modules/mergeScripts.mjs +99 -24
- package/dist/_modules/mergeStyles.d.mts +36 -3
- package/dist/_modules/mergeStyles.d.ts +36 -3
- package/dist/_modules/mergeStyles.js +56 -4
- package/dist/_modules/mergeStyles.mjs +56 -4
- package/dist/_modules/minifyAttributes.d.mts +95 -0
- package/dist/_modules/minifyAttributes.d.ts +95 -0
- package/dist/_modules/minifyAttributes.js +159 -0
- package/dist/_modules/minifyAttributes.mjs +157 -0
- package/dist/_modules/minifyConditionalComments.d.mts +36 -3
- package/dist/_modules/minifyConditionalComments.d.ts +36 -3
- package/dist/_modules/minifyConditionalComments.js +37 -19
- package/dist/_modules/minifyConditionalComments.mjs +37 -19
- package/dist/_modules/minifyCss.d.mts +36 -3
- package/dist/_modules/minifyCss.d.ts +36 -3
- package/dist/_modules/minifyCss.js +13 -27
- package/dist/_modules/minifyCss.mjs +14 -28
- package/dist/_modules/minifyHtmlTemplate.d.mts +91 -0
- package/dist/_modules/minifyHtmlTemplate.d.ts +91 -0
- package/dist/_modules/minifyHtmlTemplate.js +231 -0
- package/dist/_modules/minifyHtmlTemplate.mjs +228 -0
- package/dist/_modules/minifyJs.d.mts +36 -3
- package/dist/_modules/minifyJs.d.ts +36 -3
- package/dist/_modules/minifyJs.js +94 -5
- package/dist/_modules/minifyJs.mjs +95 -6
- package/dist/_modules/minifyJson.d.mts +36 -3
- package/dist/_modules/minifyJson.d.ts +36 -3
- package/dist/_modules/minifyJson.js +8 -11
- package/dist/_modules/minifyJson.mjs +8 -11
- package/dist/_modules/minifySvg.d.mts +36 -3
- package/dist/_modules/minifySvg.d.ts +36 -3
- package/dist/_modules/minifySvg.js +35 -4
- package/dist/_modules/minifySvg.mjs +35 -4
- package/dist/_modules/minifyUrls.d.mts +37 -4
- package/dist/_modules/minifyUrls.d.ts +37 -4
- package/dist/_modules/minifyUrls.js +52 -27
- package/dist/_modules/minifyUrls.mjs +52 -27
- package/dist/_modules/normalizeAttributeValues.d.mts +36 -3
- package/dist/_modules/normalizeAttributeValues.d.ts +36 -3
- package/dist/_modules/normalizeAttributeValues.js +10 -8
- package/dist/_modules/normalizeAttributeValues.mjs +10 -8
- package/dist/_modules/removeAttributeQuotes.d.mts +40 -4
- package/dist/_modules/removeAttributeQuotes.d.ts +40 -4
- package/dist/_modules/removeAttributeQuotes.js +9 -4
- package/dist/_modules/removeAttributeQuotes.mjs +9 -4
- package/dist/_modules/removeComments.d.mts +37 -4
- package/dist/_modules/removeComments.d.ts +37 -4
- package/dist/_modules/removeComments.js +44 -12
- package/dist/_modules/removeComments.mjs +44 -12
- package/dist/_modules/removeEmptyAttributes.d.mts +36 -3
- package/dist/_modules/removeEmptyAttributes.d.ts +36 -3
- package/dist/_modules/removeEmptyAttributes.js +37 -16
- package/dist/_modules/removeEmptyAttributes.mjs +37 -16
- package/dist/_modules/removeEmptyElements.d.mts +95 -0
- package/dist/_modules/removeEmptyElements.d.ts +95 -0
- package/dist/_modules/removeEmptyElements.js +90 -0
- package/dist/_modules/removeEmptyElements.mjs +88 -0
- package/dist/_modules/removeOptionalTags.d.mts +36 -3
- package/dist/_modules/removeOptionalTags.d.ts +36 -3
- package/dist/_modules/removeOptionalTags.js +39 -28
- package/dist/_modules/removeOptionalTags.mjs +39 -28
- package/dist/_modules/removeRedundantAttributes.d.mts +36 -3
- package/dist/_modules/removeRedundantAttributes.d.ts +36 -3
- package/dist/_modules/removeRedundantAttributes.js +43 -28
- package/dist/_modules/removeRedundantAttributes.mjs +43 -28
- package/dist/_modules/removeUnusedCss.d.mts +37 -3
- package/dist/_modules/removeUnusedCss.d.ts +37 -3
- package/dist/_modules/removeUnusedCss.js +38 -13
- package/dist/_modules/removeUnusedCss.mjs +39 -14
- package/dist/_modules/sortAttributes.d.mts +36 -3
- package/dist/_modules/sortAttributes.d.ts +36 -3
- package/dist/_modules/sortAttributes.js +23 -5
- package/dist/_modules/sortAttributes.mjs +23 -5
- package/dist/_modules/sortAttributesWithLists.d.mts +36 -3
- package/dist/_modules/sortAttributesWithLists.d.ts +36 -3
- package/dist/_modules/sortAttributesWithLists.js +30 -8
- package/dist/_modules/sortAttributesWithLists.mjs +31 -9
- package/dist/helpers.d.ts +8 -1
- package/dist/helpers.js +48 -0
- package/dist/helpers.mjs +45 -1
- package/dist/index.d.ts +37 -4
- package/dist/index.js +13 -0
- package/dist/index.mjs +13 -0
- package/dist/presets/ampSafe.d.ts +36 -3
- package/dist/presets/max.d.ts +36 -3
- package/dist/presets/max.js +4 -0
- package/dist/presets/max.mjs +4 -0
- package/dist/presets/safe.d.ts +36 -3
- package/dist/presets/safe.js +6 -0
- package/dist/presets/safe.mjs +6 -0
- package/package.json +21 -13
package/README.md
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
<h1><img src="docs/static/logo.png" alt="htmlnano logo" width="90" align="absmiddle"> htmlnano</h1>
|
|
2
|
+
|
|
2
3
|
[](http://badge.fury.io/js/htmlnano)
|
|
3
|
-

|
|
4
5
|
|
|
5
6
|
Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml). Inspired by [cssnano](https://github.com/cssnano/cssnano).
|
|
6
7
|
|
|
7
8
|
## Benchmarks
|
|
8
9
|
|
|
9
10
|
[html-minifier-terser]: https://www.npmjs.com/package/html-minifier-terser/v/7.2.0
|
|
10
|
-
[html-minifier-next]: https://www.npmjs.com/package/html-minifier-next/v/
|
|
11
|
-
[htmlnano]: https://www.npmjs.com/package/htmlnano/v/
|
|
12
|
-
[minify]: https://www.npmjs.com/package/@tdewolff/minify/v/2.24.
|
|
13
|
-
[minify-html]: https://www.npmjs.com/package/@minify-html/node/v/0.
|
|
11
|
+
[html-minifier-next]: https://www.npmjs.com/package/html-minifier-next/v/4.15.2
|
|
12
|
+
[htmlnano]: https://www.npmjs.com/package/htmlnano/v/3.1.0
|
|
13
|
+
[minify]: https://www.npmjs.com/package/@tdewolff/minify/v/2.24.8
|
|
14
|
+
[minify-html]: https://www.npmjs.com/package/@minify-html/node/v/0.18.1
|
|
14
15
|
|
|
15
|
-
| Website
|
|
16
|
-
|
|
|
17
|
-
| [stackoverflow.blog](https://stackoverflow.blog/)
|
|
18
|
-
| [github.com](https://github.com/)
|
|
19
|
-
| [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page)
|
|
20
|
-
| [
|
|
21
|
-
| [tc39.es](https://tc39.es/ecma262/)
|
|
22
|
-
| [apple.com](https://www.apple.com/)
|
|
23
|
-
| [w3.org](https://www.w3.org/)
|
|
24
|
-
| [weather.com](https://weather.com)
|
|
25
|
-
| **Avg. minify rate**
|
|
16
|
+
| Website | Source (KB) | [html-minifier-terser] | [html-minifier-next] | [htmlnano] | [minify] | [minify-html] |
|
|
17
|
+
| ------------------------------------------------------------- | ----------: | ---------------------: | -------------------: | ---------: | --------: | ------------: |
|
|
18
|
+
| [stackoverflow.blog](https://stackoverflow.blog/) | 142 | 3.7% | 32.3% | 6.8% | 4.5% | 4.6% |
|
|
19
|
+
| [github.com](https://github.com/) | 549 | 2.9% | 42.2% | 16.6% | 7.3% | 5.7% |
|
|
20
|
+
| [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 218 | 4.6% | 7.7% | 7.4% | 6.2% | 2.9% |
|
|
21
|
+
| [developer.mozilla.org](https://developer.mozilla.org/en-US/) | 109 | 37.9% | 42.0% | 52.7% | 40.1% | 39.9% |
|
|
22
|
+
| [tc39.es](https://tc39.es/ecma262/) | 7243 | 8.5% | 11.8% | 9.3% | 9.5% | 9.1% |
|
|
23
|
+
| [apple.com](https://www.apple.com/) | 210 | 9.2% | 14.4% | 11.2% | 10.3% | 9.8% |
|
|
24
|
+
| [w3.org](https://www.w3.org/) | 50 | 19.0% | 24.6% | 23.4% | 24.4% | 20.3% |
|
|
25
|
+
| [weather.com](https://weather.com) | 1960 | 0.4% | 11.2% | 19.8% | 11.6% | 0.6% |
|
|
26
|
+
| **Avg. minify rate** | | **10.8%** | **23.3%** | **18.4%** | **14.2%** | **11.6%** |
|
|
26
27
|
|
|
27
28
|
Latest benchmarks: https://github.com/maltsev/html-minifiers-benchmark (updated daily).
|
|
28
29
|
|
|
@@ -2,16 +2,24 @@ import PostHTML from 'posthtml';
|
|
|
2
2
|
import { MinifyOptions } from 'terser';
|
|
3
3
|
import { Options } from 'cssnano';
|
|
4
4
|
import { Config } from 'svgo';
|
|
5
|
+
import { UserDefinedOptions } from 'purgecss';
|
|
5
6
|
|
|
6
7
|
type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
|
|
7
8
|
options?: {
|
|
8
9
|
quoteAllAttributes?: boolean | undefined;
|
|
10
|
+
quoteStyle?: 0 | 1 | 2 | undefined;
|
|
11
|
+
replaceQuote?: boolean | undefined;
|
|
9
12
|
} | undefined;
|
|
10
13
|
render(): string;
|
|
11
14
|
render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
|
|
12
15
|
};
|
|
13
16
|
type MaybeArray<T> = T | Array<T>;
|
|
14
17
|
type PostHTMLNodeLike = PostHTML.Node | string;
|
|
18
|
+
type HtmlnanoTemplateRule = {
|
|
19
|
+
tag: string;
|
|
20
|
+
attrs?: Record<string, string | boolean | void>;
|
|
21
|
+
};
|
|
22
|
+
type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
|
|
15
23
|
interface HtmlnanoOptions {
|
|
16
24
|
skipConfigLoading?: boolean;
|
|
17
25
|
configPath?: string;
|
|
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
|
|
|
27
35
|
mergeStyles?: boolean;
|
|
28
36
|
mergeScripts?: boolean;
|
|
29
37
|
minifyCss?: Options | boolean;
|
|
38
|
+
minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
|
|
30
39
|
minifyConditionalComments?: boolean;
|
|
31
40
|
minifyJs?: MinifyOptions | boolean;
|
|
32
41
|
minifyJson?: boolean;
|
|
42
|
+
minifyAttributes?: boolean | {
|
|
43
|
+
metaContent?: boolean;
|
|
44
|
+
redundantWhitespaces?: 'safe' | 'agressive' | false;
|
|
45
|
+
};
|
|
33
46
|
minifySvg?: Config | boolean;
|
|
34
47
|
normalizeAttributeValues?: boolean;
|
|
35
|
-
removeAttributeQuotes?: boolean
|
|
36
|
-
|
|
48
|
+
removeAttributeQuotes?: boolean | {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
};
|
|
51
|
+
removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
|
|
37
52
|
removeEmptyAttributes?: boolean;
|
|
53
|
+
removeEmptyElements?: boolean | {
|
|
54
|
+
removeWithAttributes?: boolean;
|
|
55
|
+
};
|
|
38
56
|
removeRedundantAttributes?: boolean;
|
|
39
57
|
removeOptionalTags?: boolean;
|
|
40
|
-
removeUnusedCss?: boolean
|
|
58
|
+
removeUnusedCss?: boolean | ({
|
|
59
|
+
tool: 'purgeCSS';
|
|
60
|
+
} & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
|
|
61
|
+
banner?: boolean;
|
|
62
|
+
csspath?: string;
|
|
63
|
+
htmlroot?: string;
|
|
64
|
+
ignore?: (string | RegExp)[];
|
|
65
|
+
inject?: string;
|
|
66
|
+
jsdom?: object;
|
|
67
|
+
media?: string[];
|
|
68
|
+
report?: boolean;
|
|
69
|
+
strictSSL?: boolean;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
uncssrc?: string;
|
|
72
|
+
userAgent?: string;
|
|
73
|
+
};
|
|
41
74
|
sortAttributes?: boolean | 'alphabetical' | 'frequency';
|
|
42
75
|
sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
|
|
43
76
|
}
|
|
@@ -52,7 +85,10 @@ type HtmlnanoModule<Options = any> = {
|
|
|
52
85
|
default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
|
|
53
86
|
};
|
|
54
87
|
|
|
55
|
-
declare const attributesWithLists: Set<string
|
|
88
|
+
declare const attributesWithLists: Map<string, Set<string>>;
|
|
89
|
+
declare function isListAttribute(attrName: string, tagName?: string): boolean;
|
|
90
|
+
declare const attributesWithSingleValue: Map<string, Set<string>>;
|
|
91
|
+
declare function isSingleValueAttribute(attrName: string, tagName?: string): boolean;
|
|
56
92
|
declare const mod: HtmlnanoModule;
|
|
57
93
|
|
|
58
|
-
export { attributesWithLists, mod as default };
|
|
94
|
+
export { attributesWithLists, attributesWithSingleValue, mod as default, isListAttribute, isSingleValueAttribute };
|
|
@@ -2,16 +2,24 @@ import PostHTML from 'posthtml';
|
|
|
2
2
|
import { MinifyOptions } from 'terser';
|
|
3
3
|
import { Options } from 'cssnano';
|
|
4
4
|
import { Config } from 'svgo';
|
|
5
|
+
import { UserDefinedOptions } from 'purgecss';
|
|
5
6
|
|
|
6
7
|
type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
|
|
7
8
|
options?: {
|
|
8
9
|
quoteAllAttributes?: boolean | undefined;
|
|
10
|
+
quoteStyle?: 0 | 1 | 2 | undefined;
|
|
11
|
+
replaceQuote?: boolean | undefined;
|
|
9
12
|
} | undefined;
|
|
10
13
|
render(): string;
|
|
11
14
|
render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
|
|
12
15
|
};
|
|
13
16
|
type MaybeArray<T> = T | Array<T>;
|
|
14
17
|
type PostHTMLNodeLike = PostHTML.Node | string;
|
|
18
|
+
type HtmlnanoTemplateRule = {
|
|
19
|
+
tag: string;
|
|
20
|
+
attrs?: Record<string, string | boolean | void>;
|
|
21
|
+
};
|
|
22
|
+
type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
|
|
15
23
|
interface HtmlnanoOptions {
|
|
16
24
|
skipConfigLoading?: boolean;
|
|
17
25
|
configPath?: string;
|
|
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
|
|
|
27
35
|
mergeStyles?: boolean;
|
|
28
36
|
mergeScripts?: boolean;
|
|
29
37
|
minifyCss?: Options | boolean;
|
|
38
|
+
minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
|
|
30
39
|
minifyConditionalComments?: boolean;
|
|
31
40
|
minifyJs?: MinifyOptions | boolean;
|
|
32
41
|
minifyJson?: boolean;
|
|
42
|
+
minifyAttributes?: boolean | {
|
|
43
|
+
metaContent?: boolean;
|
|
44
|
+
redundantWhitespaces?: 'safe' | 'agressive' | false;
|
|
45
|
+
};
|
|
33
46
|
minifySvg?: Config | boolean;
|
|
34
47
|
normalizeAttributeValues?: boolean;
|
|
35
|
-
removeAttributeQuotes?: boolean
|
|
36
|
-
|
|
48
|
+
removeAttributeQuotes?: boolean | {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
};
|
|
51
|
+
removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
|
|
37
52
|
removeEmptyAttributes?: boolean;
|
|
53
|
+
removeEmptyElements?: boolean | {
|
|
54
|
+
removeWithAttributes?: boolean;
|
|
55
|
+
};
|
|
38
56
|
removeRedundantAttributes?: boolean;
|
|
39
57
|
removeOptionalTags?: boolean;
|
|
40
|
-
removeUnusedCss?: boolean
|
|
58
|
+
removeUnusedCss?: boolean | ({
|
|
59
|
+
tool: 'purgeCSS';
|
|
60
|
+
} & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
|
|
61
|
+
banner?: boolean;
|
|
62
|
+
csspath?: string;
|
|
63
|
+
htmlroot?: string;
|
|
64
|
+
ignore?: (string | RegExp)[];
|
|
65
|
+
inject?: string;
|
|
66
|
+
jsdom?: object;
|
|
67
|
+
media?: string[];
|
|
68
|
+
report?: boolean;
|
|
69
|
+
strictSSL?: boolean;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
uncssrc?: string;
|
|
72
|
+
userAgent?: string;
|
|
73
|
+
};
|
|
41
74
|
sortAttributes?: boolean | 'alphabetical' | 'frequency';
|
|
42
75
|
sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
|
|
43
76
|
}
|
|
@@ -52,7 +85,10 @@ type HtmlnanoModule<Options = any> = {
|
|
|
52
85
|
default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
|
|
53
86
|
};
|
|
54
87
|
|
|
55
|
-
declare const attributesWithLists: Set<string
|
|
88
|
+
declare const attributesWithLists: Map<string, Set<string>>;
|
|
89
|
+
declare function isListAttribute(attrName: string, tagName?: string): boolean;
|
|
90
|
+
declare const attributesWithSingleValue: Map<string, Set<string>>;
|
|
91
|
+
declare function isSingleValueAttribute(attrName: string, tagName?: string): boolean;
|
|
56
92
|
declare const mod: HtmlnanoModule;
|
|
57
93
|
|
|
58
|
-
export { attributesWithLists, mod as default };
|
|
94
|
+
export { attributesWithLists, attributesWithSingleValue, mod as default, isListAttribute, isSingleValueAttribute };
|
|
@@ -2,20 +2,57 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
2
2
|
|
|
3
3
|
var helpers_js = require('../helpers.js');
|
|
4
4
|
|
|
5
|
-
const attributesWithLists = new
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const attributesWithLists = new Map([
|
|
6
|
+
[
|
|
7
|
+
'class',
|
|
8
|
+
new Set()
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
'dropzone',
|
|
12
|
+
new Set()
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
'rel',
|
|
16
|
+
new Set()
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
'ping',
|
|
20
|
+
new Set()
|
|
21
|
+
],
|
|
22
|
+
[
|
|
23
|
+
'sandbox',
|
|
24
|
+
new Set()
|
|
25
|
+
],
|
|
11
26
|
/**
|
|
12
|
-
* https://github.com/
|
|
27
|
+
* https://github.com/maltsev/htmlnano/issues/180
|
|
13
28
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
14
29
|
*
|
|
15
|
-
* "sizes" of <img> should not be modified, while "sizes" of <link>
|
|
16
|
-
*/
|
|
17
|
-
|
|
30
|
+
* "sizes" of <img> should not be modified, while "sizes" of <link> is a list of tokens.
|
|
31
|
+
*/ [
|
|
32
|
+
'sizes',
|
|
33
|
+
new Set([
|
|
34
|
+
'link'
|
|
35
|
+
])
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
'headers',
|
|
39
|
+
new Set()
|
|
40
|
+
] // td, th
|
|
18
41
|
]);
|
|
42
|
+
function isListAttribute(attrName, tagName) {
|
|
43
|
+
const attrKey = attrName.toLowerCase();
|
|
44
|
+
const tagSet = attributesWithLists.get(attrKey);
|
|
45
|
+
if (!tagSet) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (tagSet.size === 0) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!tagName) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return tagSet.has(tagName.toLowerCase());
|
|
55
|
+
}
|
|
19
56
|
/** empty set means the attribute is alwasy trimmable */ const attributesWithSingleValue = new Map([
|
|
20
57
|
[
|
|
21
58
|
'accept',
|
|
@@ -268,23 +305,36 @@ const attributesWithLists = new Set([
|
|
|
268
305
|
])
|
|
269
306
|
]
|
|
270
307
|
]);
|
|
308
|
+
function isSingleValueAttribute(attrName, tagName) {
|
|
309
|
+
const attrKey = attrName.toLowerCase();
|
|
310
|
+
const tagSet = attributesWithSingleValue.get(attrKey);
|
|
311
|
+
if (!tagSet) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (!tagName) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
if (tagSet.size === 0) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return tagSet.has(tagName.toLowerCase());
|
|
321
|
+
}
|
|
271
322
|
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */ const mod = {
|
|
272
323
|
onAttrs () {
|
|
273
324
|
return (attrs, node)=>{
|
|
274
325
|
const newAttrs = attrs;
|
|
326
|
+
const tagName = node.tag ? node.tag.toLowerCase() : undefined;
|
|
275
327
|
Object.entries(attrs).forEach(([attrName, attrValue])=>{
|
|
276
328
|
if (typeof attrValue !== 'string') return;
|
|
277
|
-
|
|
329
|
+
const attrNameLower = attrName.toLowerCase();
|
|
330
|
+
if (isListAttribute(attrNameLower, tagName)) {
|
|
278
331
|
newAttrs[attrName] = attrValue.replace(/\s+/g, ' ').trim();
|
|
279
332
|
return;
|
|
280
333
|
}
|
|
281
334
|
if (helpers_js.isEventHandler(attrName)) {
|
|
282
335
|
newAttrs[attrName] = attrValue.trim();
|
|
283
|
-
} else if (
|
|
284
|
-
|
|
285
|
-
if (tagSet.size === 0 || tagSet.has(node.tag)) {
|
|
286
|
-
newAttrs[attrName] = attrValue.trim();
|
|
287
|
-
}
|
|
336
|
+
} else if (isSingleValueAttribute(attrNameLower, tagName)) {
|
|
337
|
+
newAttrs[attrName] = attrValue.trim();
|
|
288
338
|
}
|
|
289
339
|
});
|
|
290
340
|
return newAttrs;
|
|
@@ -293,4 +343,7 @@ const attributesWithLists = new Set([
|
|
|
293
343
|
};
|
|
294
344
|
|
|
295
345
|
exports.attributesWithLists = attributesWithLists;
|
|
346
|
+
exports.attributesWithSingleValue = attributesWithSingleValue;
|
|
296
347
|
exports.default = mod;
|
|
348
|
+
exports.isListAttribute = isListAttribute;
|
|
349
|
+
exports.isSingleValueAttribute = isSingleValueAttribute;
|
|
@@ -1,19 +1,56 @@
|
|
|
1
1
|
import { isEventHandler } from '../helpers.mjs';
|
|
2
2
|
|
|
3
|
-
const attributesWithLists = new
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
const attributesWithLists = new Map([
|
|
4
|
+
[
|
|
5
|
+
'class',
|
|
6
|
+
new Set()
|
|
7
|
+
],
|
|
8
|
+
[
|
|
9
|
+
'dropzone',
|
|
10
|
+
new Set()
|
|
11
|
+
],
|
|
12
|
+
[
|
|
13
|
+
'rel',
|
|
14
|
+
new Set()
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
'ping',
|
|
18
|
+
new Set()
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
'sandbox',
|
|
22
|
+
new Set()
|
|
23
|
+
],
|
|
9
24
|
/**
|
|
10
|
-
* https://github.com/
|
|
25
|
+
* https://github.com/maltsev/htmlnano/issues/180
|
|
11
26
|
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#attr-sizes
|
|
12
27
|
*
|
|
13
|
-
* "sizes" of <img> should not be modified, while "sizes" of <link>
|
|
14
|
-
*/
|
|
15
|
-
|
|
28
|
+
* "sizes" of <img> should not be modified, while "sizes" of <link> is a list of tokens.
|
|
29
|
+
*/ [
|
|
30
|
+
'sizes',
|
|
31
|
+
new Set([
|
|
32
|
+
'link'
|
|
33
|
+
])
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
'headers',
|
|
37
|
+
new Set()
|
|
38
|
+
] // td, th
|
|
16
39
|
]);
|
|
40
|
+
function isListAttribute(attrName, tagName) {
|
|
41
|
+
const attrKey = attrName.toLowerCase();
|
|
42
|
+
const tagSet = attributesWithLists.get(attrKey);
|
|
43
|
+
if (!tagSet) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (tagSet.size === 0) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (!tagName) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return tagSet.has(tagName.toLowerCase());
|
|
53
|
+
}
|
|
17
54
|
/** empty set means the attribute is alwasy trimmable */ const attributesWithSingleValue = new Map([
|
|
18
55
|
[
|
|
19
56
|
'accept',
|
|
@@ -266,23 +303,36 @@ const attributesWithLists = new Set([
|
|
|
266
303
|
])
|
|
267
304
|
]
|
|
268
305
|
]);
|
|
306
|
+
function isSingleValueAttribute(attrName, tagName) {
|
|
307
|
+
const attrKey = attrName.toLowerCase();
|
|
308
|
+
const tagSet = attributesWithSingleValue.get(attrKey);
|
|
309
|
+
if (!tagSet) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (!tagName) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
if (tagSet.size === 0) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
return tagSet.has(tagName.toLowerCase());
|
|
319
|
+
}
|
|
269
320
|
/** Collapse whitespaces inside list-like attributes (e.g. class, rel) */ const mod = {
|
|
270
321
|
onAttrs () {
|
|
271
322
|
return (attrs, node)=>{
|
|
272
323
|
const newAttrs = attrs;
|
|
324
|
+
const tagName = node.tag ? node.tag.toLowerCase() : undefined;
|
|
273
325
|
Object.entries(attrs).forEach(([attrName, attrValue])=>{
|
|
274
326
|
if (typeof attrValue !== 'string') return;
|
|
275
|
-
|
|
327
|
+
const attrNameLower = attrName.toLowerCase();
|
|
328
|
+
if (isListAttribute(attrNameLower, tagName)) {
|
|
276
329
|
newAttrs[attrName] = attrValue.replace(/\s+/g, ' ').trim();
|
|
277
330
|
return;
|
|
278
331
|
}
|
|
279
332
|
if (isEventHandler(attrName)) {
|
|
280
333
|
newAttrs[attrName] = attrValue.trim();
|
|
281
|
-
} else if (
|
|
282
|
-
|
|
283
|
-
if (tagSet.size === 0 || tagSet.has(node.tag)) {
|
|
284
|
-
newAttrs[attrName] = attrValue.trim();
|
|
285
|
-
}
|
|
334
|
+
} else if (isSingleValueAttribute(attrNameLower, tagName)) {
|
|
335
|
+
newAttrs[attrName] = attrValue.trim();
|
|
286
336
|
}
|
|
287
337
|
});
|
|
288
338
|
return newAttrs;
|
|
@@ -290,4 +340,4 @@ const attributesWithLists = new Set([
|
|
|
290
340
|
}
|
|
291
341
|
};
|
|
292
342
|
|
|
293
|
-
export { attributesWithLists, mod as default };
|
|
343
|
+
export { attributesWithLists, attributesWithSingleValue, mod as default, isListAttribute, isSingleValueAttribute };
|
|
@@ -2,16 +2,24 @@ import PostHTML from 'posthtml';
|
|
|
2
2
|
import { MinifyOptions } from 'terser';
|
|
3
3
|
import { Options } from 'cssnano';
|
|
4
4
|
import { Config } from 'svgo';
|
|
5
|
+
import { UserDefinedOptions } from 'purgecss';
|
|
5
6
|
|
|
6
7
|
type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
|
|
7
8
|
options?: {
|
|
8
9
|
quoteAllAttributes?: boolean | undefined;
|
|
10
|
+
quoteStyle?: 0 | 1 | 2 | undefined;
|
|
11
|
+
replaceQuote?: boolean | undefined;
|
|
9
12
|
} | undefined;
|
|
10
13
|
render(): string;
|
|
11
14
|
render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
|
|
12
15
|
};
|
|
13
16
|
type MaybeArray<T> = T | Array<T>;
|
|
14
17
|
type PostHTMLNodeLike = PostHTML.Node | string;
|
|
18
|
+
type HtmlnanoTemplateRule = {
|
|
19
|
+
tag: string;
|
|
20
|
+
attrs?: Record<string, string | boolean | void>;
|
|
21
|
+
};
|
|
22
|
+
type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
|
|
15
23
|
interface HtmlnanoOptions {
|
|
16
24
|
skipConfigLoading?: boolean;
|
|
17
25
|
configPath?: string;
|
|
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
|
|
|
27
35
|
mergeStyles?: boolean;
|
|
28
36
|
mergeScripts?: boolean;
|
|
29
37
|
minifyCss?: Options | boolean;
|
|
38
|
+
minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
|
|
30
39
|
minifyConditionalComments?: boolean;
|
|
31
40
|
minifyJs?: MinifyOptions | boolean;
|
|
32
41
|
minifyJson?: boolean;
|
|
42
|
+
minifyAttributes?: boolean | {
|
|
43
|
+
metaContent?: boolean;
|
|
44
|
+
redundantWhitespaces?: 'safe' | 'agressive' | false;
|
|
45
|
+
};
|
|
33
46
|
minifySvg?: Config | boolean;
|
|
34
47
|
normalizeAttributeValues?: boolean;
|
|
35
|
-
removeAttributeQuotes?: boolean
|
|
36
|
-
|
|
48
|
+
removeAttributeQuotes?: boolean | {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
};
|
|
51
|
+
removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
|
|
37
52
|
removeEmptyAttributes?: boolean;
|
|
53
|
+
removeEmptyElements?: boolean | {
|
|
54
|
+
removeWithAttributes?: boolean;
|
|
55
|
+
};
|
|
38
56
|
removeRedundantAttributes?: boolean;
|
|
39
57
|
removeOptionalTags?: boolean;
|
|
40
|
-
removeUnusedCss?: boolean
|
|
58
|
+
removeUnusedCss?: boolean | ({
|
|
59
|
+
tool: 'purgeCSS';
|
|
60
|
+
} & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
|
|
61
|
+
banner?: boolean;
|
|
62
|
+
csspath?: string;
|
|
63
|
+
htmlroot?: string;
|
|
64
|
+
ignore?: (string | RegExp)[];
|
|
65
|
+
inject?: string;
|
|
66
|
+
jsdom?: object;
|
|
67
|
+
media?: string[];
|
|
68
|
+
report?: boolean;
|
|
69
|
+
strictSSL?: boolean;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
uncssrc?: string;
|
|
72
|
+
userAgent?: string;
|
|
73
|
+
};
|
|
41
74
|
sortAttributes?: boolean | 'alphabetical' | 'frequency';
|
|
42
75
|
sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
|
|
43
76
|
}
|
|
@@ -2,16 +2,24 @@ import PostHTML from 'posthtml';
|
|
|
2
2
|
import { MinifyOptions } from 'terser';
|
|
3
3
|
import { Options } from 'cssnano';
|
|
4
4
|
import { Config } from 'svgo';
|
|
5
|
+
import { UserDefinedOptions } from 'purgecss';
|
|
5
6
|
|
|
6
7
|
type PostHTMLTreeLike = [PostHTML.Node] & PostHTML.NodeAPI & {
|
|
7
8
|
options?: {
|
|
8
9
|
quoteAllAttributes?: boolean | undefined;
|
|
10
|
+
quoteStyle?: 0 | 1 | 2 | undefined;
|
|
11
|
+
replaceQuote?: boolean | undefined;
|
|
9
12
|
} | undefined;
|
|
10
13
|
render(): string;
|
|
11
14
|
render(node: PostHTML.Node | PostHTMLTreeLike, renderOptions?: any): string;
|
|
12
15
|
};
|
|
13
16
|
type MaybeArray<T> = T | Array<T>;
|
|
14
17
|
type PostHTMLNodeLike = PostHTML.Node | string;
|
|
18
|
+
type HtmlnanoTemplateRule = {
|
|
19
|
+
tag: string;
|
|
20
|
+
attrs?: Record<string, string | boolean | void>;
|
|
21
|
+
};
|
|
22
|
+
type MinifyHtmlTemplateOptions = boolean | HtmlnanoTemplateRule[];
|
|
15
23
|
interface HtmlnanoOptions {
|
|
16
24
|
skipConfigLoading?: boolean;
|
|
17
25
|
configPath?: string;
|
|
@@ -27,17 +35,42 @@ interface HtmlnanoOptions {
|
|
|
27
35
|
mergeStyles?: boolean;
|
|
28
36
|
mergeScripts?: boolean;
|
|
29
37
|
minifyCss?: Options | boolean;
|
|
38
|
+
minifyHtmlTemplate?: MinifyHtmlTemplateOptions;
|
|
30
39
|
minifyConditionalComments?: boolean;
|
|
31
40
|
minifyJs?: MinifyOptions | boolean;
|
|
32
41
|
minifyJson?: boolean;
|
|
42
|
+
minifyAttributes?: boolean | {
|
|
43
|
+
metaContent?: boolean;
|
|
44
|
+
redundantWhitespaces?: 'safe' | 'agressive' | false;
|
|
45
|
+
};
|
|
33
46
|
minifySvg?: Config | boolean;
|
|
34
47
|
normalizeAttributeValues?: boolean;
|
|
35
|
-
removeAttributeQuotes?: boolean
|
|
36
|
-
|
|
48
|
+
removeAttributeQuotes?: boolean | {
|
|
49
|
+
force?: boolean;
|
|
50
|
+
};
|
|
51
|
+
removeComments?: boolean | RegExp | ((comment: string) => boolean) | string;
|
|
37
52
|
removeEmptyAttributes?: boolean;
|
|
53
|
+
removeEmptyElements?: boolean | {
|
|
54
|
+
removeWithAttributes?: boolean;
|
|
55
|
+
};
|
|
38
56
|
removeRedundantAttributes?: boolean;
|
|
39
57
|
removeOptionalTags?: boolean;
|
|
40
|
-
removeUnusedCss?: boolean
|
|
58
|
+
removeUnusedCss?: boolean | ({
|
|
59
|
+
tool: 'purgeCSS';
|
|
60
|
+
} & Omit<UserDefinedOptions, 'content' | 'css' | 'extractors'>) | {
|
|
61
|
+
banner?: boolean;
|
|
62
|
+
csspath?: string;
|
|
63
|
+
htmlroot?: string;
|
|
64
|
+
ignore?: (string | RegExp)[];
|
|
65
|
+
inject?: string;
|
|
66
|
+
jsdom?: object;
|
|
67
|
+
media?: string[];
|
|
68
|
+
report?: boolean;
|
|
69
|
+
strictSSL?: boolean;
|
|
70
|
+
timeout?: number;
|
|
71
|
+
uncssrc?: string;
|
|
72
|
+
userAgent?: string;
|
|
73
|
+
};
|
|
41
74
|
sortAttributes?: boolean | 'alphabetical' | 'frequency';
|
|
42
75
|
sortAttributesWithLists?: boolean | 'alphabetical' | 'frequency';
|
|
43
76
|
}
|
|
@@ -119,35 +119,42 @@ const mod = {
|
|
|
119
119
|
return (attrs, node)=>{
|
|
120
120
|
if (!node.tag) return attrs;
|
|
121
121
|
const newAttrs = attrs;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
const tagName = node.tag.toLowerCase();
|
|
123
|
+
if (tagsHasMissingValueDefaultEmptyStringAttributes.has(tagName)) {
|
|
124
|
+
const tagAttributesCanBeReplacedWithEmptyString = missingValueDefaultEmptyStringAttributes[tagName];
|
|
125
|
+
for (const attributeName of Object.keys(tagAttributesCanBeReplacedWithEmptyString)){
|
|
126
|
+
if (attributeName in attrs && typeof attrs[attributeName] === 'string' && attrs[attributeName].toLowerCase() === tagAttributesCanBeReplacedWithEmptyString[attributeName]) {
|
|
127
|
+
attrs[attributeName] = true;
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
for (const attrName of Object.keys(attrs)){
|
|
131
|
-
|
|
132
|
+
const attrNameLower = attrName.toLowerCase();
|
|
133
|
+
if (attrNameLower === 'visible' && tagName.startsWith('a-')) {
|
|
132
134
|
continue;
|
|
133
135
|
}
|
|
134
|
-
if (htmlBooleanAttributes.has(
|
|
136
|
+
if (htmlBooleanAttributes.has(attrNameLower)) {
|
|
135
137
|
newAttrs[attrName] = true;
|
|
138
|
+
continue;
|
|
136
139
|
}
|
|
137
140
|
// Fast path optimization.
|
|
138
141
|
// The rest of tranformations are only for string type attrValue.
|
|
139
|
-
|
|
140
|
-
if (
|
|
142
|
+
const attrValue = newAttrs[attrName];
|
|
143
|
+
if (typeof attrValue !== 'string') continue;
|
|
144
|
+
const attrValueLower = attrValue.toLowerCase();
|
|
145
|
+
if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrNameLower) && (attrValue === '' || attrValueLower === 'true' || attrValueLower === attrNameLower)) {
|
|
141
146
|
newAttrs[attrName] = true;
|
|
147
|
+
continue;
|
|
142
148
|
}
|
|
143
149
|
// https://html.spec.whatwg.org/#a-quick-introduction-to-html
|
|
144
150
|
// The value, along with the "=" character, can be omitted altogether if the value is the empty string.
|
|
145
|
-
if (
|
|
151
|
+
if (attrValue === '') {
|
|
146
152
|
newAttrs[attrName] = true;
|
|
153
|
+
continue;
|
|
147
154
|
}
|
|
148
155
|
// collapse crossorigin attributes
|
|
149
156
|
// Specification: https://html.spec.whatwg.org/multipage/urls-and-fetching.html#cors-settings-attributes
|
|
150
|
-
if (
|
|
157
|
+
if (attrNameLower === 'crossorigin' && attrValueLower === 'anonymous') {
|
|
151
158
|
newAttrs[attrName] = true;
|
|
152
159
|
}
|
|
153
160
|
}
|