htmlnano 3.0.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.
Files changed (115) hide show
  1. package/README.md +40 -25
  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 +111 -24
  25. package/dist/_modules/mergeScripts.mjs +111 -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 +66 -4
  29. package/dist/_modules/mergeStyles.mjs +66 -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 +106 -5
  49. package/dist/_modules/minifyJs.mjs +107 -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 +40 -14
  93. package/dist/_modules/removeUnusedCss.mjs +41 -15
  94. package/dist/_modules/sortAttributes.d.mts +39 -5
  95. package/dist/_modules/sortAttributes.d.ts +39 -5
  96. package/dist/_modules/sortAttributes.js +27 -8
  97. package/dist/_modules/sortAttributes.mjs +27 -8
  98. package/dist/_modules/sortAttributesWithLists.d.mts +39 -4
  99. package/dist/_modules/sortAttributesWithLists.d.ts +39 -4
  100. package/dist/_modules/sortAttributesWithLists.js +61 -52
  101. package/dist/_modules/sortAttributesWithLists.mjs +62 -53
  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 +13 -5
  111. package/dist/presets/max.mjs +13 -5
  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 +27 -15
@@ -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,19 +2,33 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  var collapseAttributeWhitespace_js = require('./collapseAttributeWhitespace.js');
4
4
 
5
+ const caseInsensitiveListAttributes = new Set([
6
+ 'rel',
7
+ 'sandbox',
8
+ 'dropzone',
9
+ 'sizes'
10
+ ]);
11
+ function getDeduplicationKey(attrNameLower, attrValue) {
12
+ if (!caseInsensitiveListAttributes.has(attrNameLower)) {
13
+ return attrValue;
14
+ }
15
+ return attrValue.toLowerCase();
16
+ }
5
17
  /** Deduplicate values inside list-like attributes (e.g. class, rel) */ const mod = {
6
18
  onAttrs () {
7
- return (attrs)=>{
19
+ return (attrs, node)=>{
8
20
  const newAttrs = attrs;
21
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
9
22
  Object.keys(attrs).forEach((attrName)=>{
10
- if (!collapseAttributeWhitespace_js.attributesWithLists.has(attrName)) {
23
+ const attrNameLower = attrName.toLowerCase();
24
+ if (!collapseAttributeWhitespace_js.isListAttribute(attrNameLower, tagName)) {
11
25
  return;
12
26
  }
13
27
  if (typeof attrs[attrName] !== 'string') {
14
28
  return;
15
29
  }
16
30
  const attrValues = attrs[attrName].split(/\s/);
17
- const uniqeAttrValues = new Set();
31
+ const uniqueAttrValues = new Set();
18
32
  const deduplicatedAttrValues = [];
19
33
  attrValues.forEach((attrValue)=>{
20
34
  if (!attrValue) {
@@ -22,11 +36,12 @@ var collapseAttributeWhitespace_js = require('./collapseAttributeWhitespace.js')
22
36
  deduplicatedAttrValues.push('');
23
37
  return;
24
38
  }
25
- if (uniqeAttrValues.has(attrValue)) {
39
+ const deduplicationKey = getDeduplicationKey(attrNameLower, attrValue);
40
+ if (uniqueAttrValues.has(deduplicationKey)) {
26
41
  return;
27
42
  }
28
43
  deduplicatedAttrValues.push(attrValue);
29
- uniqeAttrValues.add(attrValue);
44
+ uniqueAttrValues.add(deduplicationKey);
30
45
  });
31
46
  newAttrs[attrName] = deduplicatedAttrValues.join(' ');
32
47
  });
@@ -1,18 +1,32 @@
1
- import { attributesWithLists } from './collapseAttributeWhitespace.mjs';
1
+ import { isListAttribute } from './collapseAttributeWhitespace.mjs';
2
2
 
3
+ const caseInsensitiveListAttributes = new Set([
4
+ 'rel',
5
+ 'sandbox',
6
+ 'dropzone',
7
+ 'sizes'
8
+ ]);
9
+ function getDeduplicationKey(attrNameLower, attrValue) {
10
+ if (!caseInsensitiveListAttributes.has(attrNameLower)) {
11
+ return attrValue;
12
+ }
13
+ return attrValue.toLowerCase();
14
+ }
3
15
  /** Deduplicate values inside list-like attributes (e.g. class, rel) */ const mod = {
4
16
  onAttrs () {
5
- return (attrs)=>{
17
+ return (attrs, node)=>{
6
18
  const newAttrs = attrs;
19
+ const tagName = node.tag ? node.tag.toLowerCase() : undefined;
7
20
  Object.keys(attrs).forEach((attrName)=>{
8
- if (!attributesWithLists.has(attrName)) {
21
+ const attrNameLower = attrName.toLowerCase();
22
+ if (!isListAttribute(attrNameLower, tagName)) {
9
23
  return;
10
24
  }
11
25
  if (typeof attrs[attrName] !== 'string') {
12
26
  return;
13
27
  }
14
28
  const attrValues = attrs[attrName].split(/\s/);
15
- const uniqeAttrValues = new Set();
29
+ const uniqueAttrValues = new Set();
16
30
  const deduplicatedAttrValues = [];
17
31
  attrValues.forEach((attrValue)=>{
18
32
  if (!attrValue) {
@@ -20,11 +34,12 @@ import { attributesWithLists } from './collapseAttributeWhitespace.mjs';
20
34
  deduplicatedAttrValues.push('');
21
35
  return;
22
36
  }
23
- if (uniqeAttrValues.has(attrValue)) {
37
+ const deduplicationKey = getDeduplicationKey(attrNameLower, attrValue);
38
+ if (uniqueAttrValues.has(deduplicationKey)) {
24
39
  return;
25
40
  }
26
41
  deduplicatedAttrValues.push(attrValue);
27
- uniqeAttrValues.add(attrValue);
42
+ uniqueAttrValues.add(deduplicationKey);
28
43
  });
29
44
  newAttrs[attrName] = deduplicatedAttrValues.join(' ');
30
45
  });
@@ -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
  }
@@ -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
  }
@@ -2,50 +2,137 @@ 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
+
22
+ function normalizeAsyncAttr(attrs) {
23
+ if (!attrs) {
24
+ return;
25
+ }
26
+ if (attrs.async === '') {
27
+ attrs.async = true;
28
+ }
29
+ if (attrs.nomodule === '') {
30
+ attrs.nomodule = true;
31
+ }
32
+ }
33
+ function getScriptType(attrs) {
34
+ const type = attrs.type || 'text/javascript';
35
+ return typeof type === 'string' ? type.toLowerCase() : 'text/javascript';
36
+ }
37
+ function isMergeableScriptType(type) {
38
+ return type === 'text/javascript' || type === 'application/javascript';
39
+ }
40
+ const booleanAttrs = new Set([
41
+ 'async',
42
+ 'defer',
43
+ 'nomodule'
44
+ ]);
45
+ const skippedAttrs = new Set([
46
+ 'src',
47
+ 'integrity',
48
+ 'type'
49
+ ]);
50
+ function normalizeScriptAttrsForKey(attrs, scriptType) {
51
+ return normalizeAttrsForKey(attrs, {
52
+ baseAttrs: {
53
+ type: scriptType
54
+ },
55
+ booleanAttrs,
56
+ skippedAttrs
57
+ });
58
+ }
59
+ function buildScriptKey(attrs, scriptType, scriptSrcIndex) {
60
+ const normalizedAttrs = normalizeScriptAttrsForKey(attrs, scriptType);
61
+ const keyObject = {
62
+ index: scriptSrcIndex,
63
+ ...normalizedAttrs
64
+ };
65
+ const sortedKeys = Object.keys(keyObject).sort();
66
+ const sortedKeyObject = {};
67
+ for (const key of sortedKeys){
68
+ sortedKeyObject[key] = keyObject[key];
69
+ }
70
+ return JSON.stringify(sortedKeyObject);
71
+ }
72
+ function endsWithLineComment(scriptContent) {
73
+ const lastNewlineIndex = Math.max(scriptContent.lastIndexOf('\n'), scriptContent.lastIndexOf('\r'));
74
+ const lastLine = lastNewlineIndex === -1 ? scriptContent : scriptContent.slice(lastNewlineIndex + 1);
75
+ return /\/\/.*$/.test(lastLine);
76
+ }
77
+ function mergeScriptNodes(scriptNodesIndex, tracking) {
78
+ for (const scriptNodes of Object.values(scriptNodesIndex)){
79
+ if (scriptNodes.length < 2) {
80
+ continue;
81
+ }
82
+ const lastScriptNode = scriptNodes.pop();
83
+ tracking.mergedScriptNodes.add(lastScriptNode);
84
+ scriptNodes.reverse().forEach((scriptNode)=>{
85
+ let scriptContent = helpers_js.extractTextContentFromNode(scriptNode).trim();
86
+ if (!scriptContent) {
87
+ tracking.removedScriptNodes.add(scriptNode);
88
+ // @ts-expect-error -- remove node
89
+ scriptNode.tag = false;
90
+ scriptNode.content = [];
91
+ return;
92
+ }
93
+ if (endsWithLineComment(scriptContent)) {
94
+ scriptContent += '\n;';
95
+ } else if (scriptContent.slice(-1) !== ';') {
96
+ scriptContent += ';';
97
+ }
98
+ lastScriptNode.content = lastScriptNode.content || [];
99
+ lastScriptNode.content.unshift(scriptContent);
100
+ tracking.removedScriptNodes.add(scriptNode);
101
+ // @ts-expect-error -- remove node
102
+ scriptNode.tag = false;
103
+ scriptNode.content = [];
104
+ });
105
+ }
106
+ }
5
107
  /* Merge multiple <script> into one */ const mod = {
6
108
  default (tree) {
7
109
  const scriptNodesIndex = {};
110
+ const tracking = {
111
+ mergedScriptNodes: new WeakSet(),
112
+ removedScriptNodes: new WeakSet()
113
+ };
8
114
  let scriptSrcIndex = 1;
9
115
  tree.match({
10
116
  tag: 'script'
11
117
  }, (node)=>{
12
118
  const nodeAttrs = node.attrs || {};
119
+ normalizeAsyncAttr(nodeAttrs);
13
120
  if ('src' in nodeAttrs || 'integrity' in nodeAttrs) {
14
121
  scriptSrcIndex++;
15
122
  return node;
16
123
  }
17
- const scriptType = nodeAttrs.type || 'text/javascript';
18
- if (scriptType !== 'text/javascript' && scriptType !== 'application/javascript') {
124
+ const scriptType = getScriptType(nodeAttrs);
125
+ if (!isMergeableScriptType(scriptType)) {
19
126
  return node;
20
127
  }
21
- const scriptKey = JSON.stringify({
22
- id: nodeAttrs.id,
23
- class: nodeAttrs.class,
24
- type: scriptType,
25
- defer: nodeAttrs.defer !== undefined,
26
- async: nodeAttrs.async !== undefined,
27
- index: scriptSrcIndex
28
- });
128
+ const scriptKey = buildScriptKey(nodeAttrs, scriptType, scriptSrcIndex);
29
129
  if (!scriptNodesIndex[scriptKey]) {
30
130
  scriptNodesIndex[scriptKey] = [];
31
131
  }
32
132
  scriptNodesIndex[scriptKey].push(node);
33
133
  return node;
34
134
  });
35
- for (const scriptNodes of Object.values(scriptNodesIndex)){
36
- const lastScriptNode = scriptNodes.pop();
37
- scriptNodes.reverse().forEach((scriptNode)=>{
38
- let scriptContent = helpers_js.extractTextContentFromNode(scriptNode).trim();
39
- if (scriptContent.slice(-1) !== ';') {
40
- scriptContent += ';';
41
- }
42
- lastScriptNode.content = lastScriptNode.content || [];
43
- lastScriptNode.content.unshift(scriptContent);
44
- // @ts-expect-error -- remove node
45
- scriptNode.tag = false;
46
- scriptNode.content = [];
47
- });
48
- }
135
+ mergeScriptNodes(scriptNodesIndex, tracking);
49
136
  return tree;
50
137
  }
51
138
  };