htmlnano 3.1.0 → 3.2.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 CHANGED
@@ -5,28 +5,6 @@
5
5
 
6
6
  Modular HTML minifier, built on top of the [PostHTML](https://github.com/posthtml/posthtml). Inspired by [cssnano](https://github.com/cssnano/cssnano).
7
7
 
8
- ## Benchmarks
9
-
10
- [html-minifier-terser]: https://www.npmjs.com/package/html-minifier-terser/v/7.2.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
15
-
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%** |
27
-
28
- Latest benchmarks: https://github.com/maltsev/html-minifiers-benchmark (updated daily).
29
-
30
8
  ## Documentation
31
9
  https://htmlnano.netlify.app
32
10
 
@@ -68,4 +46,40 @@ Also, you can use it as CLI tool:
68
46
  node_modules/.bin/htmlnano --help
69
47
  ```
70
48
 
71
- More usage examples (PostHTML, Gulp, Webpack): https://htmlnano.netlify.app/next/usage
49
+ More usage examples (PostHTML, CLI, Webpack): https://htmlnano.netlify.app/usage
50
+
51
+
52
+ ## Benchmarks
53
+
54
+ [html-minifier-terser]: https://www.npmjs.com/package/html-minifier-terser/v/7.2.0
55
+ [html-minifier-next]: https://www.npmjs.com/package/html-minifier-next/v/5.0.3
56
+ [htmlnano]: https://www.npmjs.com/package/htmlnano/v/3.1.0
57
+ [minify]: https://www.npmjs.com/package/@tdewolff/minify/v/2.24.8
58
+ [minify-html]: https://www.npmjs.com/package/@minify-html/node/v/0.18.1
59
+
60
+ | Website | Source (KB) | [html-minifier-terser] | [html-minifier-next] | [htmlnano] | [minify] | [minify-html] |
61
+ | --------------------------------------------------------------- | ----------: | ---------------------: | -------------------: | ---------: | --------: | ------------: |
62
+ | [alistapart.com](https://alistapart.com/) | 63 | 7.6% | 11.6% | 34.6% | 11.1% | 8.6% |
63
+ | [developer.mozilla.org](https://developer.mozilla.org/en-US/) | 109 | 38.0% | 41.7% | 52.8% | 40.1% | 39.9% |
64
+ | [css-tricks.com](https://css-tricks.com) | 11 | 8.2% | 34.1% | 37.2% | 18.8% | 8.5% |
65
+ | [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 224 | 4.5% | 7.4% | 7.2% | 60.6% | 2.9% |
66
+ | [github.com](https://github.com/) | 546 | 3.0% | 9.7% | 16.7% | 7.3% | 5.7% |
67
+ | [edri.org](https://edri.org) | 80 | 7.7% | 12.3% | 30.6% | 12.3% | 8.2% |
68
+ | [leanpub.com](https://leanpub.com) | 251 | 1.3% | 6.9% | 6.3% | 6.0% | 1.7% |
69
+ | [stackoverflow.blog](https://stackoverflow.blog/) | 139 | 3.9% | 5.7% | 7.0% | 4.6% | 4.7% |
70
+ | [html.spec.whatwg.org](https://html.spec.whatwg.org/multipage/) | 149 | -3.9% | 0.7% | -2.6% | 0.3% | 0.2% |
71
+ | [eff.org](https://eff.org) | 54 | 8.8% | 14.7% | 10.9% | 13.4% | 9.7% |
72
+ | [apple.com](https://apple.com/) | 229 | 8.9% | 12.5% | 11.5% | 10.4% | 9.5% |
73
+ | [w3.org](https://w3.org/) | 50 | 19.0% | 24.6% | 23.4% | 24.4% | 20.3% |
74
+ | [mastodon.social](https://mastodon.social/explore) | 37 | 3.4% | 6.8% | 14.6% | 5.9% | 3.6% |
75
+ | [bbc.co.uk](https://bbc.co.uk) | 694 | 0.8% | 6.3% | 5.9% | 4.7% | 1.2% |
76
+ | [un.org](https://un.org/en/) | 151 | 14.2% | 22.5% | 41.2% | 20.0% | 15.0% |
77
+ | [lafrenchtech.gouv.fr](https://lafrenchtech.gouv.fr/) | 152 | 13.2% | 17.9% | 64.1% | 17.0% | 13.8% |
78
+ | [sitepoint.com](https://sitepoint.com) | 497 | 0.8% | 7.4% | 12.9% | 6.1% | 0.9% |
79
+ | [faz.net](https://faz.net/aktuell/) | 1572 | 3.4% | 8.0% | 15.8% | 4.8% | 3.6% |
80
+ | [weather.com](https://weather.com) | 2395 | 0.3% | 11.4% | 18.1% | 11.0% | 0.6% |
81
+ | [tc39.es](https://tc39.es/ecma262/) | 7254 | 8.5% | 11.3% | 9.3% | 9.5% | 9.1% |
82
+ | [home.cern](https://home.cern) | 151 | 37.1% | 46.4% | 40.2% | 38.9% | 39.5% |
83
+ | **Avg. minify rate** | | **9.0%** | **15.2%** | **21.8%** | **15.6%** | **9.9%** |
84
+
85
+ Latest benchmarks: https://github.com/maltsev/html-minifiers-benchmark (updated daily).
@@ -2,6 +2,23 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var helpers_js = require('../helpers.js');
4
4
 
5
+ function normalizeAttrsForKey(attrs, config) {
6
+ const normalized = {
7
+ ...config.baseAttrs
8
+ };
9
+ for (const [key, value] of Object.entries(attrs || {})){
10
+ if (config.skippedAttrs.has(key) || value === undefined) {
11
+ continue;
12
+ }
13
+ if (config.booleanAttrs.has(key)) {
14
+ normalized[key] = true;
15
+ continue;
16
+ }
17
+ normalized[key] = value;
18
+ }
19
+ return normalized;
20
+ }
21
+
5
22
  function normalizeAsyncAttr(attrs) {
6
23
  if (!attrs) {
7
24
  return;
@@ -25,24 +42,19 @@ const booleanAttrs = new Set([
25
42
  'defer',
26
43
  'nomodule'
27
44
  ]);
45
+ const skippedAttrs = new Set([
46
+ 'src',
47
+ 'integrity',
48
+ 'type'
49
+ ]);
28
50
  function normalizeScriptAttrsForKey(attrs, scriptType) {
29
- const normalized = {
30
- type: scriptType
31
- };
32
- for (const [key, value] of Object.entries(attrs)){
33
- if (key === 'src' || key === 'integrity' || key === 'type') {
34
- continue;
35
- }
36
- if (value === undefined) {
37
- continue;
38
- }
39
- if (booleanAttrs.has(key)) {
40
- normalized[key] = true;
41
- continue;
42
- }
43
- normalized[key] = value;
44
- }
45
- return normalized;
51
+ return normalizeAttrsForKey(attrs, {
52
+ baseAttrs: {
53
+ type: scriptType
54
+ },
55
+ booleanAttrs,
56
+ skippedAttrs
57
+ });
46
58
  }
47
59
  function buildScriptKey(attrs, scriptType, scriptSrcIndex) {
48
60
  const normalizedAttrs = normalizeScriptAttrsForKey(attrs, scriptType);
@@ -1,5 +1,22 @@
1
1
  import { extractTextContentFromNode } from '../helpers.mjs';
2
2
 
3
+ function normalizeAttrsForKey(attrs, config) {
4
+ const normalized = {
5
+ ...config.baseAttrs
6
+ };
7
+ for (const [key, value] of Object.entries(attrs || {})){
8
+ if (config.skippedAttrs.has(key) || value === undefined) {
9
+ continue;
10
+ }
11
+ if (config.booleanAttrs.has(key)) {
12
+ normalized[key] = true;
13
+ continue;
14
+ }
15
+ normalized[key] = value;
16
+ }
17
+ return normalized;
18
+ }
19
+
3
20
  function normalizeAsyncAttr(attrs) {
4
21
  if (!attrs) {
5
22
  return;
@@ -23,24 +40,19 @@ const booleanAttrs = new Set([
23
40
  'defer',
24
41
  'nomodule'
25
42
  ]);
43
+ const skippedAttrs = new Set([
44
+ 'src',
45
+ 'integrity',
46
+ 'type'
47
+ ]);
26
48
  function normalizeScriptAttrsForKey(attrs, scriptType) {
27
- const normalized = {
28
- type: scriptType
29
- };
30
- for (const [key, value] of Object.entries(attrs)){
31
- if (key === 'src' || key === 'integrity' || key === 'type') {
32
- continue;
33
- }
34
- if (value === undefined) {
35
- continue;
36
- }
37
- if (booleanAttrs.has(key)) {
38
- normalized[key] = true;
39
- continue;
40
- }
41
- normalized[key] = value;
42
- }
43
- return normalized;
49
+ return normalizeAttrsForKey(attrs, {
50
+ baseAttrs: {
51
+ type: scriptType
52
+ },
53
+ booleanAttrs,
54
+ skippedAttrs
55
+ });
44
56
  }
45
57
  function buildScriptKey(attrs, scriptType, scriptSrcIndex) {
46
58
  const normalizedAttrs = normalizeScriptAttrsForKey(attrs, scriptType);
@@ -2,10 +2,31 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var helpers_js = require('../helpers.js');
4
4
 
5
+ function normalizeAttrsForKey(attrs, config) {
6
+ const normalized = {
7
+ ...config.baseAttrs
8
+ };
9
+ for (const [key, value] of Object.entries(attrs || {})){
10
+ if (config.skippedAttrs.has(key) || value === undefined) {
11
+ continue;
12
+ }
13
+ if (config.booleanAttrs.has(key)) {
14
+ normalized[key] = true;
15
+ continue;
16
+ }
17
+ normalized[key] = value;
18
+ }
19
+ return normalized;
20
+ }
21
+
5
22
  const booleanAttrs = new Set([
6
23
  'amp-custom',
7
24
  'disabled'
8
25
  ]);
26
+ const skippedAttrs = new Set([
27
+ 'type',
28
+ 'media'
29
+ ]);
9
30
  function normalizeStyleType(attrs) {
10
31
  if (!attrs || typeof attrs.type !== 'string') {
11
32
  return 'text/css';
@@ -21,21 +42,10 @@ function normalizeStyleMedia(attrs) {
21
42
  return media ? media.replace(/\s+/g, ' ').toLowerCase() : 'all';
22
43
  }
23
44
  function normalizeStyleAttrsForKey(attrs) {
24
- const normalized = {};
25
- for (const [key, value] of Object.entries(attrs || {})){
26
- if (key === 'type' || key === 'media') {
27
- continue;
28
- }
29
- if (value === undefined) {
30
- continue;
31
- }
32
- if (booleanAttrs.has(key)) {
33
- normalized[key] = true;
34
- continue;
35
- }
36
- normalized[key] = value;
37
- }
38
- return normalized;
45
+ return normalizeAttrsForKey(attrs, {
46
+ booleanAttrs,
47
+ skippedAttrs
48
+ });
39
49
  }
40
50
  function buildStyleKey(attrs) {
41
51
  const keyObject = {
@@ -1,9 +1,30 @@
1
1
  import { isAmpBoilerplate, extractTextContentFromNode } from '../helpers.mjs';
2
2
 
3
+ function normalizeAttrsForKey(attrs, config) {
4
+ const normalized = {
5
+ ...config.baseAttrs
6
+ };
7
+ for (const [key, value] of Object.entries(attrs || {})){
8
+ if (config.skippedAttrs.has(key) || value === undefined) {
9
+ continue;
10
+ }
11
+ if (config.booleanAttrs.has(key)) {
12
+ normalized[key] = true;
13
+ continue;
14
+ }
15
+ normalized[key] = value;
16
+ }
17
+ return normalized;
18
+ }
19
+
3
20
  const booleanAttrs = new Set([
4
21
  'amp-custom',
5
22
  'disabled'
6
23
  ]);
24
+ const skippedAttrs = new Set([
25
+ 'type',
26
+ 'media'
27
+ ]);
7
28
  function normalizeStyleType(attrs) {
8
29
  if (!attrs || typeof attrs.type !== 'string') {
9
30
  return 'text/css';
@@ -19,21 +40,10 @@ function normalizeStyleMedia(attrs) {
19
40
  return media ? media.replace(/\s+/g, ' ').toLowerCase() : 'all';
20
41
  }
21
42
  function normalizeStyleAttrsForKey(attrs) {
22
- const normalized = {};
23
- for (const [key, value] of Object.entries(attrs || {})){
24
- if (key === 'type' || key === 'media') {
25
- continue;
26
- }
27
- if (value === undefined) {
28
- continue;
29
- }
30
- if (booleanAttrs.has(key)) {
31
- normalized[key] = true;
32
- continue;
33
- }
34
- normalized[key] = value;
35
- }
36
- return normalized;
43
+ return normalizeAttrsForKey(attrs, {
44
+ booleanAttrs,
45
+ skippedAttrs
46
+ });
37
47
  }
38
48
  function buildStyleKey(attrs) {
39
49
  const keyObject = {
@@ -192,10 +192,22 @@ function processNodeWithOnAttrs(node, terserOptions, terser) {
192
192
  const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
193
193
  node.attrs[attrName] = minifiedJs;
194
194
  }
195
+ }).catch((error)=>{
196
+ // Skip invalid inline handler code and preserve the original value.
197
+ if (isTerserParseError(error)) {
198
+ return;
199
+ }
200
+ throw error;
195
201
  });
196
202
  promises.push(promise);
197
203
  }
198
204
  return promises;
199
205
  }
206
+ function isTerserParseError(error) {
207
+ if (!(error instanceof Error)) {
208
+ return false;
209
+ }
210
+ return error.name === 'SyntaxError' || error.message.includes('JS_Parse_Error');
211
+ }
200
212
 
201
213
  exports.default = mod;
@@ -190,10 +190,22 @@ function processNodeWithOnAttrs(node, terserOptions, terser) {
190
190
  const minifiedJs = code.substring(jsWrapperStart.length, code.length - jsWrapperEnd.length);
191
191
  node.attrs[attrName] = minifiedJs;
192
192
  }
193
+ }).catch((error)=>{
194
+ // Skip invalid inline handler code and preserve the original value.
195
+ if (isTerserParseError(error)) {
196
+ return;
197
+ }
198
+ throw error;
193
199
  });
194
200
  promises.push(promise);
195
201
  }
196
202
  return promises;
197
203
  }
204
+ function isTerserParseError(error) {
205
+ if (!(error instanceof Error)) {
206
+ return false;
207
+ }
208
+ return error.name === 'SyntaxError' || error.message.includes('JS_Parse_Error');
209
+ }
198
210
 
199
211
  export { mod as default };
@@ -112,13 +112,14 @@ function runPurgecss(tree, css, userOptions, purgecss, extractor) {
112
112
  }
113
113
  /** Remove unused CSS */ const mod = {
114
114
  async default (tree, options, userOptions) {
115
+ var _resolvedOptions_tool;
115
116
  const promises = [];
116
117
  let html;
117
118
  let extractor;
118
119
  const purgecss = await helpers_js.optionalImport('purgecss');
119
120
  const uncss = await helpers_js.optionalImport('uncss');
120
121
  const resolvedOptions = resolveUserOptions(userOptions);
121
- const tool = resolvedOptions.tool;
122
+ const tool = (_resolvedOptions_tool = resolvedOptions.tool) != null ? _resolvedOptions_tool : 'purgeCSS';
122
123
  const toolOptions = stripToolOption(resolvedOptions);
123
124
  tree.walk((node)=>{
124
125
  if (helpers_js.isStyleNode(node) && helpers_js.isCssStyleType(node)) {
@@ -127,7 +128,7 @@ function runPurgecss(tree, css, userOptions, purgecss, extractor) {
127
128
  extractor != null ? extractor : extractor = purgeFromHtml(tree);
128
129
  promises.push(processStyleNodePurgeCSS(tree, node, toolOptions, purgecss, extractor));
129
130
  }
130
- } else {
131
+ } else if (tool === 'uncss') {
131
132
  if (uncss) {
132
133
  html != null ? html : html = tree.render(tree);
133
134
  promises.push(processStyleNodeUnCSS(html, node, toolOptions, uncss));
@@ -110,13 +110,14 @@ function runPurgecss(tree, css, userOptions, purgecss, extractor) {
110
110
  }
111
111
  /** Remove unused CSS */ const mod = {
112
112
  async default (tree, options, userOptions) {
113
+ var _resolvedOptions_tool;
113
114
  const promises = [];
114
115
  let html;
115
116
  let extractor;
116
117
  const purgecss = await optionalImport('purgecss');
117
118
  const uncss = await optionalImport('uncss');
118
119
  const resolvedOptions = resolveUserOptions(userOptions);
119
- const tool = resolvedOptions.tool;
120
+ const tool = (_resolvedOptions_tool = resolvedOptions.tool) != null ? _resolvedOptions_tool : 'purgeCSS';
120
121
  const toolOptions = stripToolOption(resolvedOptions);
121
122
  tree.walk((node)=>{
122
123
  if (isStyleNode(node) && isCssStyleType(node)) {
@@ -125,7 +126,7 @@ function runPurgecss(tree, css, userOptions, purgecss, extractor) {
125
126
  extractor != null ? extractor : extractor = purgeFromHtml(tree);
126
127
  promises.push(processStyleNodePurgeCSS(tree, node, toolOptions, purgecss, extractor));
127
128
  }
128
- } else {
129
+ } else if (tool === 'uncss') {
129
130
  if (uncss) {
130
131
  html != null ? html : html = tree.render(tree);
131
132
  promises.push(processStyleNodeUnCSS(html, node, toolOptions, uncss));
@@ -85,7 +85,8 @@ type HtmlnanoModule<Options = any> = {
85
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
86
  };
87
87
 
88
- type ValidOptions = 'alphabetical' | 'frequency';
89
- declare const mod: HtmlnanoModule<boolean | ValidOptions>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
90
91
 
91
92
  export { mod as default };
@@ -85,7 +85,8 @@ type HtmlnanoModule<Options = any> = {
85
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
86
  };
87
87
 
88
- type ValidOptions = 'alphabetical' | 'frequency';
89
- declare const mod: HtmlnanoModule<boolean | ValidOptions>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
90
91
 
91
92
  export { mod as default };
@@ -4,11 +4,12 @@ const validOptions = new Set([
4
4
  'frequency',
5
5
  'alphabetical'
6
6
  ]);
7
- const processModuleOptions = (options)=>{
7
+ function resolveSortType(options) {
8
8
  if (options === true) return 'alphabetical';
9
9
  if (options === false) return false;
10
10
  return validOptions.has(options) ? options : false;
11
- };
11
+ }
12
+
12
13
  class AttributeTokenChain {
13
14
  addFromNodeAttrs(nodeAttrs) {
14
15
  Object.keys(nodeAttrs).forEach((attrName)=>{
@@ -70,7 +71,7 @@ class AttributeTokenChain {
70
71
  }
71
72
  /** Sort attibutes */ const mod = {
72
73
  default (tree, options, moduleOptions) {
73
- const sortType = processModuleOptions(moduleOptions);
74
+ const sortType = resolveSortType(moduleOptions);
74
75
  if (sortType === 'alphabetical') {
75
76
  return sortAttributesInAlphabeticalOrder(tree);
76
77
  }
@@ -2,11 +2,12 @@ const validOptions = new Set([
2
2
  'frequency',
3
3
  'alphabetical'
4
4
  ]);
5
- const processModuleOptions = (options)=>{
5
+ function resolveSortType(options) {
6
6
  if (options === true) return 'alphabetical';
7
7
  if (options === false) return false;
8
8
  return validOptions.has(options) ? options : false;
9
- };
9
+ }
10
+
10
11
  class AttributeTokenChain {
11
12
  addFromNodeAttrs(nodeAttrs) {
12
13
  Object.keys(nodeAttrs).forEach((attrName)=>{
@@ -68,7 +69,7 @@ class AttributeTokenChain {
68
69
  }
69
70
  /** Sort attibutes */ const mod = {
70
71
  default (tree, options, moduleOptions) {
71
- const sortType = processModuleOptions(moduleOptions);
72
+ const sortType = resolveSortType(moduleOptions);
72
73
  if (sortType === 'alphabetical') {
73
74
  return sortAttributesInAlphabeticalOrder(tree);
74
75
  }
@@ -85,6 +85,8 @@ type HtmlnanoModule<Options = any> = {
85
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
86
  };
87
87
 
88
- declare const mod: HtmlnanoModule<boolean | 'alphabetical' | 'frequency'>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
89
91
 
90
92
  export { mod as default };
@@ -85,6 +85,8 @@ type HtmlnanoModule<Options = any> = {
85
85
  default?: (tree: PostHTMLTreeLike, options: Partial<HtmlnanoOptions>, moduleOptions: OptionalOptions<Options>) => PostHTMLTreeLike | Promise<PostHTMLTreeLike>;
86
86
  };
87
87
 
88
- declare const mod: HtmlnanoModule<boolean | 'alphabetical' | 'frequency'>;
88
+ type SortAttributesOption = 'alphabetical' | 'frequency';
89
+
90
+ declare const mod: HtmlnanoModule<boolean | SortAttributesOption>;
89
91
 
90
92
  export { mod as default };
@@ -2,35 +2,36 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var collapseAttributeWhitespace_js = require('./collapseAttributeWhitespace.js');
4
4
 
5
- // class, rel, ping
6
5
  const validOptions = new Set([
7
6
  'frequency',
8
7
  'alphabetical'
9
8
  ]);
10
- const processModuleOptions = (options)=>{
9
+ function resolveSortType(options) {
11
10
  if (options === true) return 'alphabetical';
12
11
  if (options === false) return false;
13
12
  return validOptions.has(options) ? options : false;
14
- };
15
- class AttributeTokenChain {
13
+ }
14
+
15
+ // class, rel, ping
16
+ class ListAttributeTokenChain {
16
17
  addFromNodeAttrsArray(attrValuesArray) {
17
18
  attrValuesArray.forEach((attrValue)=>{
18
19
  if (!attrValue) {
19
20
  return;
20
21
  }
21
- if (this.freqData.has(attrValue)) {
22
- this.freqData.set(attrValue, this.freqData.get(attrValue) + 1);
22
+ if (this.tokenCounts.has(attrValue)) {
23
+ this.tokenCounts.set(attrValue, this.tokenCounts.get(attrValue) + 1);
23
24
  } else {
24
- this.freqData.set(attrValue, 1);
25
+ this.tokenCounts.set(attrValue, 1);
25
26
  }
26
27
  });
27
28
  }
28
29
  createSortOrder() {
29
- const _sortOrder = [
30
- ...this.freqData.entries()
30
+ const nextSortOrder = [
31
+ ...this.tokenCounts.entries()
31
32
  ];
32
- _sortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
33
- this.sortOrder = _sortOrder.map((i)=>i[0]);
33
+ nextSortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
34
+ this.sortedTokens = nextSortOrder.map((i)=>i[0]);
34
35
  }
35
36
  sortFromNodeAttrsArray(attrValuesArray) {
36
37
  const resultArray = [];
@@ -42,10 +43,10 @@ class AttributeTokenChain {
42
43
  }
43
44
  tokenCounts.set(attrValue, ((_tokenCounts_get = tokenCounts.get(attrValue)) != null ? _tokenCounts_get : 0) + 1);
44
45
  });
45
- if (!this.sortOrder) {
46
+ if (!this.sortedTokens) {
46
47
  this.createSortOrder();
47
48
  }
48
- this.sortOrder.forEach((k)=>{
49
+ this.sortedTokens.forEach((k)=>{
49
50
  const count = tokenCounts.get(k);
50
51
  if (!count) {
51
52
  return;
@@ -57,13 +58,13 @@ class AttributeTokenChain {
57
58
  return resultArray;
58
59
  }
59
60
  constructor(){
60
- /** <attr, frequency> */ this.freqData = new Map();
61
- this.sortOrder = null;
61
+ /** <attr, frequency> */ this.tokenCounts = new Map();
62
+ this.sortedTokens = null;
62
63
  }
63
64
  }
64
65
  /** Sort values inside list-like attributes (e.g. class, rel) */ const mod = {
65
66
  default (tree, options, moduleOptions) {
66
- const sortType = processModuleOptions(moduleOptions);
67
+ const sortType = resolveSortType(moduleOptions);
67
68
  if (sortType === 'alphabetical') {
68
69
  return sortAttributesWithListsInAlphabeticalOrder(tree);
69
70
  }
@@ -75,64 +76,50 @@ class AttributeTokenChain {
75
76
  }
76
77
  };
77
78
  const splitListAttributeValues = (attrValue)=>attrValue.split(/\s+/).filter(Boolean);
78
- function sortAttributesWithListsInAlphabeticalOrder(tree) {
79
+ function walkListAttributes(tree, walkFn) {
79
80
  tree.walk((node)=>{
80
81
  if (!node.attrs) {
81
82
  return node;
82
83
  }
83
84
  const tagName = node.tag ? node.tag.toLowerCase() : undefined;
84
- Object.keys(node.attrs).forEach((attrName)=>{
85
+ Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
85
86
  const attrNameLower = attrName.toLowerCase();
86
- if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName)) {
87
- return;
88
- }
89
- const attrValues = splitListAttributeValues(node.attrs[attrName]);
90
- if (attrValues.length < 2) {
87
+ if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName) || typeof attrValues !== 'string') {
91
88
  return;
92
89
  }
93
- node.attrs[attrName] = attrValues.sort((a, b)=>{
94
- // @ts-expect-error -- deliberately use minus operator to sort things
95
- return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
96
- }).join(' ');
90
+ walkFn(node.attrs, attrName, attrValues);
97
91
  });
98
92
  return node;
99
93
  });
94
+ }
95
+ function sortAttributesWithListsInAlphabeticalOrder(tree) {
96
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
97
+ const values = splitListAttributeValues(attrValues);
98
+ if (values.length < 2) {
99
+ return;
100
+ }
101
+ nodeAttrs[attrName] = values.sort((a, b)=>{
102
+ // @ts-expect-error -- deliberately use minus operator to sort things
103
+ return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
104
+ }).join(' ');
105
+ });
100
106
  return tree;
101
107
  }
102
108
  function sortAttributesWithListsByFrequency(tree) {
103
- const tokenChainObj = {}; // <attrNameLower: AttributeTokenChain>[]
109
+ const tokenChainObj = {};
104
110
  // Traverse through tree to get frequency
105
- tree.walk((node)=>{
106
- if (!node.attrs) {
107
- return node;
108
- }
109
- const tagName = node.tag ? node.tag.toLowerCase() : undefined;
110
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
111
- const attrNameLower = attrName.toLowerCase();
112
- if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName)) {
113
- return;
114
- }
115
- tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new AttributeTokenChain();
116
- tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
117
- });
118
- return node;
111
+ walkListAttributes(tree, (_nodeAttrs, attrName, attrValues)=>{
112
+ const attrNameLower = attrName.toLowerCase();
113
+ tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new ListAttributeTokenChain();
114
+ tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
119
115
  });
120
116
  // Traverse through tree again, this time sort the attribute values
121
- tree.walk((node)=>{
122
- if (!node.attrs) {
123
- return node;
117
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
118
+ const attrNameLower = attrName.toLowerCase();
119
+ if (!tokenChainObj[attrNameLower]) {
120
+ return;
124
121
  }
125
- const tagName = node.tag ? node.tag.toLowerCase() : undefined;
126
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
127
- const attrNameLower = attrName.toLowerCase();
128
- if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName)) {
129
- return;
130
- }
131
- if (tokenChainObj[attrNameLower]) {
132
- node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
133
- }
134
- });
135
- return node;
122
+ nodeAttrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
136
123
  });
137
124
  return tree;
138
125
  }
@@ -1,34 +1,35 @@
1
1
  import { isListAttribute } from './collapseAttributeWhitespace.mjs';
2
2
 
3
- // class, rel, ping
4
3
  const validOptions = new Set([
5
4
  'frequency',
6
5
  'alphabetical'
7
6
  ]);
8
- const processModuleOptions = (options)=>{
7
+ function resolveSortType(options) {
9
8
  if (options === true) return 'alphabetical';
10
9
  if (options === false) return false;
11
10
  return validOptions.has(options) ? options : false;
12
- };
13
- class AttributeTokenChain {
11
+ }
12
+
13
+ // class, rel, ping
14
+ class ListAttributeTokenChain {
14
15
  addFromNodeAttrsArray(attrValuesArray) {
15
16
  attrValuesArray.forEach((attrValue)=>{
16
17
  if (!attrValue) {
17
18
  return;
18
19
  }
19
- if (this.freqData.has(attrValue)) {
20
- this.freqData.set(attrValue, this.freqData.get(attrValue) + 1);
20
+ if (this.tokenCounts.has(attrValue)) {
21
+ this.tokenCounts.set(attrValue, this.tokenCounts.get(attrValue) + 1);
21
22
  } else {
22
- this.freqData.set(attrValue, 1);
23
+ this.tokenCounts.set(attrValue, 1);
23
24
  }
24
25
  });
25
26
  }
26
27
  createSortOrder() {
27
- const _sortOrder = [
28
- ...this.freqData.entries()
28
+ const nextSortOrder = [
29
+ ...this.tokenCounts.entries()
29
30
  ];
30
- _sortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
31
- this.sortOrder = _sortOrder.map((i)=>i[0]);
31
+ nextSortOrder.sort((a, b)=>b[1] - a[1] || a[0].localeCompare(b[0]));
32
+ this.sortedTokens = nextSortOrder.map((i)=>i[0]);
32
33
  }
33
34
  sortFromNodeAttrsArray(attrValuesArray) {
34
35
  const resultArray = [];
@@ -40,10 +41,10 @@ class AttributeTokenChain {
40
41
  }
41
42
  tokenCounts.set(attrValue, ((_tokenCounts_get = tokenCounts.get(attrValue)) != null ? _tokenCounts_get : 0) + 1);
42
43
  });
43
- if (!this.sortOrder) {
44
+ if (!this.sortedTokens) {
44
45
  this.createSortOrder();
45
46
  }
46
- this.sortOrder.forEach((k)=>{
47
+ this.sortedTokens.forEach((k)=>{
47
48
  const count = tokenCounts.get(k);
48
49
  if (!count) {
49
50
  return;
@@ -55,13 +56,13 @@ class AttributeTokenChain {
55
56
  return resultArray;
56
57
  }
57
58
  constructor(){
58
- /** <attr, frequency> */ this.freqData = new Map();
59
- this.sortOrder = null;
59
+ /** <attr, frequency> */ this.tokenCounts = new Map();
60
+ this.sortedTokens = null;
60
61
  }
61
62
  }
62
63
  /** Sort values inside list-like attributes (e.g. class, rel) */ const mod = {
63
64
  default (tree, options, moduleOptions) {
64
- const sortType = processModuleOptions(moduleOptions);
65
+ const sortType = resolveSortType(moduleOptions);
65
66
  if (sortType === 'alphabetical') {
66
67
  return sortAttributesWithListsInAlphabeticalOrder(tree);
67
68
  }
@@ -73,64 +74,50 @@ class AttributeTokenChain {
73
74
  }
74
75
  };
75
76
  const splitListAttributeValues = (attrValue)=>attrValue.split(/\s+/).filter(Boolean);
76
- function sortAttributesWithListsInAlphabeticalOrder(tree) {
77
+ function walkListAttributes(tree, walkFn) {
77
78
  tree.walk((node)=>{
78
79
  if (!node.attrs) {
79
80
  return node;
80
81
  }
81
82
  const tagName = node.tag ? node.tag.toLowerCase() : undefined;
82
- Object.keys(node.attrs).forEach((attrName)=>{
83
+ Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
83
84
  const attrNameLower = attrName.toLowerCase();
84
- if (!isListAttribute(attrNameLower, tagName)) {
85
- return;
86
- }
87
- const attrValues = splitListAttributeValues(node.attrs[attrName]);
88
- if (attrValues.length < 2) {
85
+ if (!isListAttribute(attrNameLower, tagName) || typeof attrValues !== 'string') {
89
86
  return;
90
87
  }
91
- node.attrs[attrName] = attrValues.sort((a, b)=>{
92
- // @ts-expect-error -- deliberately use minus operator to sort things
93
- return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
94
- }).join(' ');
88
+ walkFn(node.attrs, attrName, attrValues);
95
89
  });
96
90
  return node;
97
91
  });
92
+ }
93
+ function sortAttributesWithListsInAlphabeticalOrder(tree) {
94
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
95
+ const values = splitListAttributeValues(attrValues);
96
+ if (values.length < 2) {
97
+ return;
98
+ }
99
+ nodeAttrs[attrName] = values.sort((a, b)=>{
100
+ // @ts-expect-error -- deliberately use minus operator to sort things
101
+ return typeof a.localeCompare === 'function' ? a.localeCompare(b) : a - b;
102
+ }).join(' ');
103
+ });
98
104
  return tree;
99
105
  }
100
106
  function sortAttributesWithListsByFrequency(tree) {
101
- const tokenChainObj = {}; // <attrNameLower: AttributeTokenChain>[]
107
+ const tokenChainObj = {};
102
108
  // Traverse through tree to get frequency
103
- tree.walk((node)=>{
104
- if (!node.attrs) {
105
- return node;
106
- }
107
- const tagName = node.tag ? node.tag.toLowerCase() : undefined;
108
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
109
- const attrNameLower = attrName.toLowerCase();
110
- if (!isListAttribute(attrNameLower, tagName)) {
111
- return;
112
- }
113
- tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new AttributeTokenChain();
114
- tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
115
- });
116
- return node;
109
+ walkListAttributes(tree, (_nodeAttrs, attrName, attrValues)=>{
110
+ const attrNameLower = attrName.toLowerCase();
111
+ tokenChainObj[attrNameLower] = tokenChainObj[attrNameLower] || new ListAttributeTokenChain();
112
+ tokenChainObj[attrNameLower].addFromNodeAttrsArray(splitListAttributeValues(attrValues));
117
113
  });
118
114
  // Traverse through tree again, this time sort the attribute values
119
- tree.walk((node)=>{
120
- if (!node.attrs) {
121
- return node;
115
+ walkListAttributes(tree, (nodeAttrs, attrName, attrValues)=>{
116
+ const attrNameLower = attrName.toLowerCase();
117
+ if (!tokenChainObj[attrNameLower]) {
118
+ return;
122
119
  }
123
- const tagName = node.tag ? node.tag.toLowerCase() : undefined;
124
- Object.entries(node.attrs).forEach(([attrName, attrValues])=>{
125
- const attrNameLower = attrName.toLowerCase();
126
- if (!isListAttribute(attrNameLower, tagName)) {
127
- return;
128
- }
129
- if (tokenChainObj[attrNameLower]) {
130
- node.attrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
131
- }
132
- });
133
- return node;
120
+ nodeAttrs[attrName] = tokenChainObj[attrNameLower].sortFromNodeAttrsArray(splitListAttributeValues(attrValues)).join(' ');
134
121
  });
135
122
  return tree;
136
123
  }
@@ -10,23 +10,27 @@ var safePreset__default = /*#__PURE__*/_interopDefault(safePreset);
10
10
  * Maximal minification (might break some pages)
11
11
  */ var max = {
12
12
  ...safePreset__default.default,
13
+ removeRedundantAttributes: true,
14
+ sortAttributes: true,
13
15
  collapseWhitespace: 'all',
14
16
  removeComments: 'all',
17
+ removeEmptyElements: true,
18
+ minifyConditionalComments: true,
19
+ removeOptionalTags: true,
15
20
  removeAttributeQuotes: true,
16
- removeRedundantAttributes: true,
17
21
  minifyAttributes: {
18
22
  metaContent: true,
19
23
  redundantWhitespaces: 'agressive'
20
24
  },
21
25
  mergeScripts: true,
22
26
  mergeStyles: true,
23
- removeUnusedCss: {},
27
+ removeUnusedCss: {
28
+ tool: 'purgeCSS'
29
+ },
24
30
  minifyCss: {
25
31
  preset: 'default'
26
32
  },
27
- minifySvg: {},
28
- minifyConditionalComments: true,
29
- removeOptionalTags: true
33
+ minifySvg: {}
30
34
  };
31
35
 
32
36
  exports.default = max;
@@ -4,23 +4,27 @@ import safePreset from './safe.mjs';
4
4
  * Maximal minification (might break some pages)
5
5
  */ var max = {
6
6
  ...safePreset,
7
+ removeRedundantAttributes: true,
8
+ sortAttributes: true,
7
9
  collapseWhitespace: 'all',
8
10
  removeComments: 'all',
11
+ removeEmptyElements: true,
12
+ minifyConditionalComments: true,
13
+ removeOptionalTags: true,
9
14
  removeAttributeQuotes: true,
10
- removeRedundantAttributes: true,
11
15
  minifyAttributes: {
12
16
  metaContent: true,
13
17
  redundantWhitespaces: 'agressive'
14
18
  },
15
19
  mergeScripts: true,
16
20
  mergeStyles: true,
17
- removeUnusedCss: {},
21
+ removeUnusedCss: {
22
+ tool: 'purgeCSS'
23
+ },
18
24
  minifyCss: {
19
25
  preset: 'default'
20
26
  },
21
- minifySvg: {},
22
- minifyConditionalComments: true,
23
- removeOptionalTags: true
27
+ minifySvg: {}
24
28
  };
25
29
 
26
30
  export { max as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "htmlnano",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Modular HTML minifier, built on top of the PostHTML",
5
5
  "author": "Kirill Maltsev <maltsevkirill@gmail.com>",
6
6
  "license": "MIT",
@@ -9,7 +9,10 @@
9
9
  "build": "npm run clean && bunchee",
10
10
  "postbuild": "chmod +x dist/bin.js",
11
11
  "compile": "npm run build",
12
- "lint": "eslint --fix .",
12
+ "lint:eslint": "eslint --fix .",
13
+ "lint:duplicates": "jscpd",
14
+ "lint:knip": "knip",
15
+ "lint": "npm run lint:eslint && npm run lint:duplicates && npm run lint:knip",
13
16
  "test:mocha": "mocha --timeout 5000 --require @swc-node/register --recursive --check-leaks --globals addresses 'test/**/*.ts'",
14
17
  "pretest": "npm run lint && npm run compile",
15
18
  "test": "c8 -r text -r html npm run test:mocha",
@@ -69,15 +72,16 @@
69
72
  "@types/mocha": "^10.0.10",
70
73
  "@types/node": "^25.0.0",
71
74
  "bunchee": "^6.5.1",
72
- "c8": "^10.1.3",
75
+ "c8": "^11.0.0",
73
76
  "cssnano": "^7.0.0",
74
77
  "eslint": "^9.25.1",
75
78
  "eslint-config-flat-gitignore": "^2.1.0",
76
- "eslint-plugin-import": "^2.28.1",
77
79
  "eslint-plugin-import-x": "^4.11.0",
78
80
  "eslint-plugin-unused-imports": "^4.1.4",
79
81
  "expect": "^30.1.1",
80
82
  "globals": "^17.0.0",
83
+ "jscpd": "^4.0.8",
84
+ "knip": "^5.83.1",
81
85
  "mocha": "^11.0.1",
82
86
  "postcss": "^8.3.11",
83
87
  "purgecss": "^8.0.0",