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.
Files changed (115) hide show
  1. package/README.md +18 -17
  2. package/dist/_modules/collapseAttributeWhitespace.d.mts +41 -5
  3. package/dist/_modules/collapseAttributeWhitespace.d.ts +41 -5
  4. package/dist/_modules/collapseAttributeWhitespace.js +69 -16
  5. package/dist/_modules/collapseAttributeWhitespace.mjs +67 -17
  6. package/dist/_modules/collapseBooleanAttributes.d.mts +36 -3
  7. package/dist/_modules/collapseBooleanAttributes.d.ts +36 -3
  8. package/dist/_modules/collapseBooleanAttributes.js +18 -11
  9. package/dist/_modules/collapseBooleanAttributes.mjs +18 -11
  10. package/dist/_modules/collapseWhitespace.d.mts +36 -3
  11. package/dist/_modules/collapseWhitespace.d.ts +36 -3
  12. package/dist/_modules/collapseWhitespace.js +25 -2
  13. package/dist/_modules/collapseWhitespace.mjs +25 -2
  14. package/dist/_modules/custom.d.mts +36 -3
  15. package/dist/_modules/custom.d.ts +36 -3
  16. package/dist/_modules/deduplicateAttributeValues.d.mts +36 -3
  17. package/dist/_modules/deduplicateAttributeValues.d.ts +36 -3
  18. package/dist/_modules/deduplicateAttributeValues.js +20 -5
  19. package/dist/_modules/deduplicateAttributeValues.mjs +21 -6
  20. package/dist/_modules/example.d.mts +36 -3
  21. package/dist/_modules/example.d.ts +36 -3
  22. package/dist/_modules/mergeScripts.d.mts +36 -3
  23. package/dist/_modules/mergeScripts.d.ts +36 -3
  24. package/dist/_modules/mergeScripts.js +99 -24
  25. package/dist/_modules/mergeScripts.mjs +99 -24
  26. package/dist/_modules/mergeStyles.d.mts +36 -3
  27. package/dist/_modules/mergeStyles.d.ts +36 -3
  28. package/dist/_modules/mergeStyles.js +56 -4
  29. package/dist/_modules/mergeStyles.mjs +56 -4
  30. package/dist/_modules/minifyAttributes.d.mts +95 -0
  31. package/dist/_modules/minifyAttributes.d.ts +95 -0
  32. package/dist/_modules/minifyAttributes.js +159 -0
  33. package/dist/_modules/minifyAttributes.mjs +157 -0
  34. package/dist/_modules/minifyConditionalComments.d.mts +36 -3
  35. package/dist/_modules/minifyConditionalComments.d.ts +36 -3
  36. package/dist/_modules/minifyConditionalComments.js +37 -19
  37. package/dist/_modules/minifyConditionalComments.mjs +37 -19
  38. package/dist/_modules/minifyCss.d.mts +36 -3
  39. package/dist/_modules/minifyCss.d.ts +36 -3
  40. package/dist/_modules/minifyCss.js +13 -27
  41. package/dist/_modules/minifyCss.mjs +14 -28
  42. package/dist/_modules/minifyHtmlTemplate.d.mts +91 -0
  43. package/dist/_modules/minifyHtmlTemplate.d.ts +91 -0
  44. package/dist/_modules/minifyHtmlTemplate.js +231 -0
  45. package/dist/_modules/minifyHtmlTemplate.mjs +228 -0
  46. package/dist/_modules/minifyJs.d.mts +36 -3
  47. package/dist/_modules/minifyJs.d.ts +36 -3
  48. package/dist/_modules/minifyJs.js +94 -5
  49. package/dist/_modules/minifyJs.mjs +95 -6
  50. package/dist/_modules/minifyJson.d.mts +36 -3
  51. package/dist/_modules/minifyJson.d.ts +36 -3
  52. package/dist/_modules/minifyJson.js +8 -11
  53. package/dist/_modules/minifyJson.mjs +8 -11
  54. package/dist/_modules/minifySvg.d.mts +36 -3
  55. package/dist/_modules/minifySvg.d.ts +36 -3
  56. package/dist/_modules/minifySvg.js +35 -4
  57. package/dist/_modules/minifySvg.mjs +35 -4
  58. package/dist/_modules/minifyUrls.d.mts +37 -4
  59. package/dist/_modules/minifyUrls.d.ts +37 -4
  60. package/dist/_modules/minifyUrls.js +52 -27
  61. package/dist/_modules/minifyUrls.mjs +52 -27
  62. package/dist/_modules/normalizeAttributeValues.d.mts +36 -3
  63. package/dist/_modules/normalizeAttributeValues.d.ts +36 -3
  64. package/dist/_modules/normalizeAttributeValues.js +10 -8
  65. package/dist/_modules/normalizeAttributeValues.mjs +10 -8
  66. package/dist/_modules/removeAttributeQuotes.d.mts +40 -4
  67. package/dist/_modules/removeAttributeQuotes.d.ts +40 -4
  68. package/dist/_modules/removeAttributeQuotes.js +9 -4
  69. package/dist/_modules/removeAttributeQuotes.mjs +9 -4
  70. package/dist/_modules/removeComments.d.mts +37 -4
  71. package/dist/_modules/removeComments.d.ts +37 -4
  72. package/dist/_modules/removeComments.js +44 -12
  73. package/dist/_modules/removeComments.mjs +44 -12
  74. package/dist/_modules/removeEmptyAttributes.d.mts +36 -3
  75. package/dist/_modules/removeEmptyAttributes.d.ts +36 -3
  76. package/dist/_modules/removeEmptyAttributes.js +37 -16
  77. package/dist/_modules/removeEmptyAttributes.mjs +37 -16
  78. package/dist/_modules/removeEmptyElements.d.mts +95 -0
  79. package/dist/_modules/removeEmptyElements.d.ts +95 -0
  80. package/dist/_modules/removeEmptyElements.js +90 -0
  81. package/dist/_modules/removeEmptyElements.mjs +88 -0
  82. package/dist/_modules/removeOptionalTags.d.mts +36 -3
  83. package/dist/_modules/removeOptionalTags.d.ts +36 -3
  84. package/dist/_modules/removeOptionalTags.js +39 -28
  85. package/dist/_modules/removeOptionalTags.mjs +39 -28
  86. package/dist/_modules/removeRedundantAttributes.d.mts +36 -3
  87. package/dist/_modules/removeRedundantAttributes.d.ts +36 -3
  88. package/dist/_modules/removeRedundantAttributes.js +43 -28
  89. package/dist/_modules/removeRedundantAttributes.mjs +43 -28
  90. package/dist/_modules/removeUnusedCss.d.mts +37 -3
  91. package/dist/_modules/removeUnusedCss.d.ts +37 -3
  92. package/dist/_modules/removeUnusedCss.js +38 -13
  93. package/dist/_modules/removeUnusedCss.mjs +39 -14
  94. package/dist/_modules/sortAttributes.d.mts +36 -3
  95. package/dist/_modules/sortAttributes.d.ts +36 -3
  96. package/dist/_modules/sortAttributes.js +23 -5
  97. package/dist/_modules/sortAttributes.mjs +23 -5
  98. package/dist/_modules/sortAttributesWithLists.d.mts +36 -3
  99. package/dist/_modules/sortAttributesWithLists.d.ts +36 -3
  100. package/dist/_modules/sortAttributesWithLists.js +30 -8
  101. package/dist/_modules/sortAttributesWithLists.mjs +31 -9
  102. package/dist/helpers.d.ts +8 -1
  103. package/dist/helpers.js +48 -0
  104. package/dist/helpers.mjs +45 -1
  105. package/dist/index.d.ts +37 -4
  106. package/dist/index.js +13 -0
  107. package/dist/index.mjs +13 -0
  108. package/dist/presets/ampSafe.d.ts +36 -3
  109. package/dist/presets/max.d.ts +36 -3
  110. package/dist/presets/max.js +4 -0
  111. package/dist/presets/max.mjs +4 -0
  112. package/dist/presets/safe.d.ts +36 -3
  113. package/dist/presets/safe.js +6 -0
  114. package/dist/presets/safe.mjs +6 -0
  115. package/package.json +21 -13
package/README.md CHANGED
@@ -1,28 +1,29 @@
1
- # htmlnano
1
+ <h1><img src="docs/static/logo.png" alt="htmlnano logo" width="90" align="absmiddle">&nbsp;htmlnano</h1>
2
+
2
3
  [![npm version](https://badge.fury.io/js/htmlnano.svg)](http://badge.fury.io/js/htmlnano)
3
- ![CI](https://github.com/posthtml/htmlnano/actions/workflows/ci.yml/badge.svg)
4
+ ![CI](https://github.com/maltsev/htmlnano/actions/workflows/ci.yml/badge.svg)
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/1.4.0
11
- [htmlnano]: https://www.npmjs.com/package/htmlnano/v/2.1.3
12
- [minify]: https://www.npmjs.com/package/@tdewolff/minify/v/2.24.2
13
- [minify-html]: https://www.npmjs.com/package/@minify-html/node/v/0.16.4
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 | Source (KB) | [html-minifier-terser] | [html-minifier-next] | [htmlnano] | [minify] | [minify-html] |
16
- | ----------------------------------------------------------- | ----------: | ---------------------: | -------------------: | ---------: | -------: | ------------: |
17
- | [stackoverflow.blog](https://stackoverflow.blog/) | 166 | 3.3% | 3.3% | 8.3% | 4.6% | 4.0% |
18
- | [github.com](https://github.com/) | 541 | 3.7% | 3.7% | 18.1% | 7.9% | 6.2% |
19
- | [en.wikipedia.org](https://en.wikipedia.org/wiki/Main_Page) | 220 | 4.6% | 4.6% | 4.9% | 6.2% | 2.9% |
20
- | [npmjs.com](https://www.npmjs.com/package/eslint) | 460 | 0.5% | 0.5% | 0.9% | 3.6% | 0.7% |
21
- | [tc39.es](https://tc39.es/ecma262/) | 7198 | 8.5% | 8.5% | 8.7% | 9.5% | 9.1% |
22
- | [apple.com](https://www.apple.com/) | 190 | 7.6% | 7.6% | 12.1% | 10.5% | 8.1% |
23
- | [w3.org](https://www.w3.org/) | 49 | 18.9% | 18.9% | 23.0% | 24.1% | 19.9% |
24
- | [weather.com](https://weather.com) | 1770 | 0.2% | 0.2% | 12.1% | 11.9% | 0.6% |
25
- | **Avg. minify rate** | | **5.9%** | **5.9%** | **11.0%** | **9.8%** | **6.4%** |
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
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
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
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
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 Set([
6
- 'class',
7
- 'dropzone',
8
- 'rel',
9
- 'ping',
10
- 'sandbox',
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/posthtml/htmlnano/issues/180
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> will only have one entry in most cases.
16
- */ // 'sizes', // link
17
- 'headers' // td, th
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
- if (attributesWithLists.has(attrName)) {
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 (node.tag && attributesWithSingleValue.has(attrName)) {
284
- const tagSet = attributesWithSingleValue.get(attrName);
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 Set([
4
- 'class',
5
- 'dropzone',
6
- 'rel',
7
- 'ping',
8
- 'sandbox',
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/posthtml/htmlnano/issues/180
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> will only have one entry in most cases.
14
- */ // 'sizes', // link
15
- 'headers' // td, th
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
- if (attributesWithLists.has(attrName)) {
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 (node.tag && attributesWithSingleValue.has(attrName)) {
282
- const tagSet = attributesWithSingleValue.get(attrName);
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
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
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
- removeComments?: boolean | 'safe' | 'all' | RegExp | ((comment: string) => boolean);
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
- if (tagsHasMissingValueDefaultEmptyStringAttributes.has(node.tag)) {
123
- const tagAttributesCanBeReplacedWithEmptyString = missingValueDefaultEmptyStringAttributes[node.tag];
124
- for (const attributesCanBeReplacedWithEmptyString of Object.keys(tagAttributesCanBeReplacedWithEmptyString)){
125
- if (attributesCanBeReplacedWithEmptyString in attrs && attributesCanBeReplacedWithEmptyString in tagAttributesCanBeReplacedWithEmptyString && attrs[attributesCanBeReplacedWithEmptyString] === tagAttributesCanBeReplacedWithEmptyString[attributesCanBeReplacedWithEmptyString]) {
126
- attrs[attributesCanBeReplacedWithEmptyString] = true;
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
- if (attrName === 'visible' && node.tag.startsWith('a-')) {
132
+ const attrNameLower = attrName.toLowerCase();
133
+ if (attrNameLower === 'visible' && tagName.startsWith('a-')) {
132
134
  continue;
133
135
  }
134
- if (htmlBooleanAttributes.has(attrName)) {
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
- if (typeof newAttrs[attrName] !== 'string') continue;
140
- if (moduleOptions.amphtml && amphtmlBooleanAttributes.has(attrName) && attrs[attrName] === '') {
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 (attrs[attrName] === '') {
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 (attrName.toLowerCase() === 'crossorigin' && attrs[attrName] === 'anonymous') {
157
+ if (attrNameLower === 'crossorigin' && attrValueLower === 'anonymous') {
151
158
  newAttrs[attrName] = true;
152
159
  }
153
160
  }